From d034ba749196bfd7d9c6b55e554a0527d119ffbf Mon Sep 17 00:00:00 2001 From: Dotta <34892728+cryppadotta@users.noreply.github.com> Date: Fri, 2 Oct 2026 14:21:11 -0500 Subject: [PATCH] fix(interactions): derive question storage from canonical forms (#14946) ## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - Agents request human input through durable issue interactions. > - A question form has a canonical presentation and a compatibility storage format. > - The creation API required agents to write both formats. > - Tool guidance told agents to split text and choice questions across those formats. > - This pull request accepts one complete canonical form and derives storage fields on the server. > - The benefit is a complete question card with stable answer and retry behavior. ## Linked Issues or Issue Description Related work: Refs #13630 and #14430. PR #13630 addresses the display of historical partial forms. This change fixes creation and keeps the check that rejects conflicting new forms. **What happened?** A question save supplied three compatibility questions and one canonical text question. The API correctly rejected the incomplete canonical form. The Runner's tool description encouraged this split. Sending only a complete canonical form also failed because the API required compatibility questions. **Expected behavior** An agent sends one complete `payload.questionSet` with every text and choice question. Paperclip derives `payload.questions` for storage and answer compatibility. Existing legacy requests remain valid. Explicitly conflicting dual forms remain invalid. **Steps to reproduce** 1. Call `paperclip_request_human_input` with `interactionKind: "questions"`. 2. Send `payload: { version: 1, questionSet: ... }` with a required text question and a required choice question. 3. The old API rejects the missing compatibility questions. With this change, it stores both questions and preserves the canonical form. 4. Retry with the same idempotency key. Confirm that only one interaction exists. 5. Submit both answers. Confirm that the normal resolver and continuation rules apply. **Paperclip version or commit** The branch is based on `cf8ad63c8`. The problem affects the native Runner and the interaction creation API. **Deployment mode** Server deployment with the native Paperclip Runner. Integration tests use the real interaction service and an embedded test database. ## What Changed - Add one shared canonical-to-storage projection. Reuse it for native harness question requests. - Accept canonical-only question creation at the shared validator and server boundary. - Export the input type and update the plugin SDK and its RPC contract. - Advertise a typed, complete question form in the live and scenario tool schemas. - Enforce canonical text and custom-answer constraints before ordinary or native resolution. Preserve harmless display whitespace. - Run regex matching in isolated workers with a deadline and resource limits. Both answer paths await the result before persistence. Saved native delivery uses the validated answer without taking another worker slot. - Update agent guidance and generated Runner contracts. - Test mixed forms, option-ID collisions, retries, answers, legacy requests, and conflicting forms. ## Verification - Interaction service, HTTP route, native bridge, and Runner authority suites: 221 tests passed after correcting an obsolete tool-description assertion. - Shared validator, plugin SDK, CLI, and UI compatibility suites: 67 tests passed. - Runner core tool-contract suite: 20 tests passed. AJV validates live and scenario schemas. - Final review regressions: 172 shared, service, native bridge, and authority tests passed. These cover text length, pattern, numeric limits, whitespace, custom option IDs, and historical pending cards. - Runner session suites: 67 tests passed. Published example tests: 4 tests passed. - Server typecheck and the shared/server builds passed after the compatibility fixes. - Final delivery verification: 35 response-delivery tests passed. The native delivery regression proves saved answers do not enter pattern workers; server typecheck and build passed. - Pattern security and answer-flow verification: 205 tests passed after repairing the child fixture loader. These cover pathological matching, event-loop responsiveness, worker concurrency, slot cleanup, HTTP routes, native delivery, and the full helper in a child process. - `pnpm -r typecheck` passed on the bounded-worker revision. - `pnpm build` passed on the bounded-worker revision. - All 55 GitHub checks passed on `fe457af`; four optional jobs were skipped. An unchanged Cursor adapter test timed out once in CI, passed locally, and passed on one failed-job rerun. - Reviewers can send the canonical-only mixed form above and verify that the saved interaction contains both canonical and compatibility questions. ## Risks - The creation API accepts a new input shape. Stored rows and answer contracts keep the existing shape. - The shared projection must preserve synthetic free-text option IDs. Collision and native round-trip tests cover this behavior. - Historical partial rows remain readable. New conflicting dual forms, including written-answer mismatches, remain rejected. - Existing pending cards retain the written-answer paths offered by their stored options. Canonical text constraints still apply. - Ordinary answers now enforce declared canonical constraints before persistence. Invalid answers leave the card pending. - Regex validation has a one-second deadline and a four-worker capacity limit. A complex pattern or capacity error leaves the card pending with a validation error. - No database migration or change to company authorization is required. ## Model Used - OpenAI GPT-6 through Codex. The session exposes the GPT-6 model family; its exact runtime model identifier and context window size are not exposed. Used reasoning, tool use, and code execution. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip --- packages/paperclip-runner/README.md | 12 + .../docs/capability-contract.md | 86 +++--- .../generated/capability/capabilities.yaml | 176 ++++++------ .../capability/capability-contract.md | 2 +- .../capability/semantic-tool-contracts.json | 2 +- .../evals/native-execution-seeded.json | 2 +- .../paperclip-runner/protocol/manifest.json | 2 +- .../spec/capability/capabilities.yaml | 258 +++++++++--------- .../src/live/clean-room-server.test.ts | 2 +- .../src/live/live-session.test.ts | 2 +- .../protocol-actions/core-contracts.test.ts | 16 ++ .../question-payload-schema.ts | 66 +++++ .../protocol-actions/request-human-input.ts | 18 +- packages/plugins/sdk/src/protocol.ts | 4 +- packages/plugins/sdk/src/testing.ts | 6 +- packages/plugins/sdk/src/types.ts | 6 +- .../plugins/sdk/tests/testing-actions.test.ts | 12 + packages/shared/src/index.ts | 2 + .../src/issue-thread-interactions.test.ts | 53 ++++ packages/shared/src/question-set.ts | 51 ++++ packages/shared/src/validators/index.ts | 1 + packages/shared/src/validators/issue.ts | 49 +++- .../hiring-operational-examples.test.ts | 7 +- .../issue-thread-interaction-routes.test.ts | 17 ++ .../issue-thread-interactions-service.test.ts | 83 ++++++ .../question-response-delivery.test.ts | 1 + server/src/routes/issues.ts | 2 +- server/src/services/chat-channels.ts | 2 +- .../src/services/issue-thread-interactions.ts | 14 +- .../native-question-bridge.test.ts | 36 ++- .../native-runtime/native-question-bridge.ts | 108 +------- .../paperclip-runner-tool-authority.test.ts | 35 +-- .../native-runtime/runner-api-reference.ts | 60 ++-- .../question-interaction-answers.test.ts | 30 ++ .../services/question-interaction-answers.ts | 86 ++++++ .../question-pattern-validation.test.ts | 31 +++ .../services/question-pattern-validation.ts | 53 ++++ skills/paperclip/SKILL.md | 5 +- skills/paperclip/references/api-reference.md | 37 ++- 39 files changed, 954 insertions(+), 481 deletions(-) create mode 100644 packages/paperclip-runner/src/protocol-actions/question-payload-schema.ts create mode 100644 packages/shared/src/question-set.ts create mode 100644 server/src/services/question-interaction-answers.test.ts create mode 100644 server/src/services/question-interaction-answers.ts create mode 100644 server/src/services/question-pattern-validation.test.ts create mode 100644 server/src/services/question-pattern-validation.ts diff --git a/packages/paperclip-runner/README.md b/packages/paperclip-runner/README.md index 9443b19b77a..b6a4d365481 100644 --- a/packages/paperclip-runner/README.md +++ b/packages/paperclip-runner/README.md @@ -147,6 +147,18 @@ reject or rewrite execution arguments. Diagnostic detection requires explicit credential fields/assignments or recognizable key, Bearer, JWT, or PEM formats, not ordinary prose such as "credential handling" or dotted filenames. +Human question tools accept one complete `payload.questionSet` for text and +choice questions. The control plane generates legacy `questions` entries with +stable free-text option IDs. Legacy callers remain supported. Calls that supply +both forms must describe the same complete form; partial forms remain invalid. +The native recovery bridge uses the same projection for answer delivery. + +The server validates canonical answer constraints before persistence. Regex +matching runs in isolated workers with a one-second deadline and at most four +active workers. A timeout or capacity error leaves the question pending. The +ordinary and native answer paths both await this validation before persistence. +Saved native answer delivery does not repeat regex matching. + Validated ACPX runtime events normalize into the same provider-neutral activity families as the direct Codex transport. Reasoning contents stay private. Tool targets are resolved within the workspace under the provider host's path diff --git a/packages/paperclip-runner/docs/capability-contract.md b/packages/paperclip-runner/docs/capability-contract.md index 7a6fb336df4..15e6d7d4c1b 100644 --- a/packages/paperclip-runner/docs/capability-contract.md +++ b/packages/paperclip-runner/docs/capability-contract.md @@ -47,31 +47,31 @@ The skill/reference inventory and eval cases are the only normative behavior sou | 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:197 | always_agent_tool | skills/paperclip/SKILL.md:197 | -| skill:skills/paperclip/SKILL.md:status-quick-guide:245 | control_plane_owned | skills/paperclip/SKILL.md:245 | -| skill:skills/paperclip/SKILL.md:monitors-and-watchers-say-only-what-you-actually-scheduled:255 | optional_agent_tool | skills/paperclip/SKILL.md:255 | -| skill:skills/paperclip/SKILL.md:delegating-review-tasks:268 | always_agent_tool | skills/paperclip/SKILL.md:268 | -| skill:skills/paperclip/SKILL.md:managing-a-user-s-inbox:279 | control_plane_owned | skills/paperclip/SKILL.md:279 | -| skill:skills/paperclip/SKILL.md:issue-dependencies-blockers:287 | control_plane_owned | skills/paperclip/SKILL.md:287 | -| skill:skills/paperclip/SKILL.md:requesting-board-approval:312 | optional_agent_tool | skills/paperclip/SKILL.md:312 | -| skill:skills/paperclip/SKILL.md:issue-thread-interactions:333 | optional_agent_tool | skills/paperclip/SKILL.md:333 | -| skill:skills/paperclip/SKILL.md:standalone-decisions:362 | optional_agent_tool | skills/paperclip/SKILL.md:362 | -| skill:skills/paperclip/SKILL.md:mcp-tool-approval-gates:466 | optional_agent_tool | skills/paperclip/SKILL.md:466 | -| skill:skills/paperclip/SKILL.md:niche-workflow-pointers:508 | optional_agent_tool | skills/paperclip/SKILL.md:508 | -| skill:skills/paperclip/SKILL.md:cases:518 | optional_agent_tool | skills/paperclip/SKILL.md:518 | -| skill:skills/paperclip/SKILL.md:company-skills-workflow:523 | optional_agent_tool | skills/paperclip/SKILL.md:523 | -| skill:skills/paperclip/SKILL.md:routines:534 | optional_agent_tool | skills/paperclip/SKILL.md:534 | -| skill:skills/paperclip/SKILL.md:issue-workspace-runtime-controls:545 | optional_agent_tool | skills/paperclip/SKILL.md:545 | -| skill:skills/paperclip/SKILL.md:proposing-credentials-safely:552 | optional_agent_tool | skills/paperclip/SKILL.md:552 | -| skill:skills/paperclip/SKILL.md:reading-granted-secrets:559 | optional_agent_tool | skills/paperclip/SKILL.md:559 | -| skill:skills/paperclip/SKILL.md:critical-rules:585 | optional_agent_tool | skills/paperclip/SKILL.md:585 | -| skill:skills/paperclip/SKILL.md:comment-style-required:606 | always_agent_tool | skills/paperclip/SKILL.md:606 | -| skill:skills/paperclip/SKILL.md:update:638 | optional_agent_tool | skills/paperclip/SKILL.md:638 | -| skill:skills/paperclip/SKILL.md:planning-required-when-planning-requested:648 | optional_agent_tool | skills/paperclip/SKILL.md:648 | -| skill:skills/paperclip/SKILL.md:key-endpoints-hot-routes:681 | optional_agent_tool | skills/paperclip/SKILL.md:681 | -| skill:skills/paperclip/SKILL.md:searching-issues:710 | optional_agent_tool | skills/paperclip/SKILL.md:710 | -| skill:skills/paperclip/SKILL.md:full-reference:720 | optional_agent_tool | skills/paperclip/SKILL.md:720 | -| skill:skills/paperclip/SKILL.md:conversational-confirmation-answers:724 | always_agent_tool | skills/paperclip/SKILL.md:724 | +| skill:skills/paperclip/SKILL.md:generated-artifacts-and-work-products:196 | always_agent_tool | skills/paperclip/SKILL.md:196 | +| skill:skills/paperclip/SKILL.md:status-quick-guide:244 | control_plane_owned | skills/paperclip/SKILL.md:244 | +| skill:skills/paperclip/SKILL.md:monitors-and-watchers-say-only-what-you-actually-scheduled:254 | optional_agent_tool | skills/paperclip/SKILL.md:254 | +| skill:skills/paperclip/SKILL.md:delegating-review-tasks:267 | always_agent_tool | skills/paperclip/SKILL.md:267 | +| skill:skills/paperclip/SKILL.md:managing-a-user-s-inbox:278 | control_plane_owned | skills/paperclip/SKILL.md:278 | +| skill:skills/paperclip/SKILL.md:issue-dependencies-blockers:286 | control_plane_owned | skills/paperclip/SKILL.md:286 | +| skill:skills/paperclip/SKILL.md:requesting-board-approval:311 | optional_agent_tool | skills/paperclip/SKILL.md:311 | +| skill:skills/paperclip/SKILL.md:issue-thread-interactions:332 | optional_agent_tool | skills/paperclip/SKILL.md:332 | +| skill:skills/paperclip/SKILL.md:standalone-decisions:361 | optional_agent_tool | skills/paperclip/SKILL.md:361 | +| skill:skills/paperclip/SKILL.md:mcp-tool-approval-gates:465 | optional_agent_tool | skills/paperclip/SKILL.md:465 | +| skill:skills/paperclip/SKILL.md:niche-workflow-pointers:507 | optional_agent_tool | skills/paperclip/SKILL.md:507 | +| skill:skills/paperclip/SKILL.md:cases:517 | optional_agent_tool | skills/paperclip/SKILL.md:517 | +| skill:skills/paperclip/SKILL.md:company-skills-workflow:522 | optional_agent_tool | skills/paperclip/SKILL.md:522 | +| skill:skills/paperclip/SKILL.md:routines:533 | optional_agent_tool | skills/paperclip/SKILL.md:533 | +| skill:skills/paperclip/SKILL.md:issue-workspace-runtime-controls:544 | optional_agent_tool | skills/paperclip/SKILL.md:544 | +| skill:skills/paperclip/SKILL.md:proposing-credentials-safely:551 | optional_agent_tool | skills/paperclip/SKILL.md:551 | +| skill:skills/paperclip/SKILL.md:reading-granted-secrets:558 | optional_agent_tool | skills/paperclip/SKILL.md:558 | +| skill:skills/paperclip/SKILL.md:critical-rules:584 | optional_agent_tool | skills/paperclip/SKILL.md:584 | +| skill:skills/paperclip/SKILL.md:comment-style-required:605 | always_agent_tool | skills/paperclip/SKILL.md:605 | +| skill:skills/paperclip/SKILL.md:update:637 | optional_agent_tool | skills/paperclip/SKILL.md:637 | +| skill:skills/paperclip/SKILL.md:planning-required-when-planning-requested:647 | optional_agent_tool | skills/paperclip/SKILL.md:647 | +| skill:skills/paperclip/SKILL.md:key-endpoints-hot-routes:680 | optional_agent_tool | skills/paperclip/SKILL.md:680 | +| skill:skills/paperclip/SKILL.md:searching-issues:709 | optional_agent_tool | skills/paperclip/SKILL.md:709 | +| skill:skills/paperclip/SKILL.md:full-reference:719 | optional_agent_tool | skills/paperclip/SKILL.md:719 | +| skill:skills/paperclip/SKILL.md:conversational-confirmation-answers:723 | always_agent_tool | skills/paperclip/SKILL.md:723 | | 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 | @@ -180,24 +180,24 @@ The skill/reference inventory and eval cases are the only normative behavior sou | skill:skills/paperclip/references/api-reference.md:requesting-a-hire-management-only:847 | optional_agent_tool | skills/paperclip/references/api-reference.md:847 | | skill:skills/paperclip/references/api-reference.md:ceo-strategy-approval:912 | optional_agent_tool | skills/paperclip/references/api-reference.md:912 | | skill:skills/paperclip/references/api-reference.md:questions-and-waiting-for-human-input:921 | always_agent_tool | skills/paperclip/references/api-reference.md:921 | -| skill:skills/paperclip/references/api-reference.md:issue-thread-confirmations:1028 | always_agent_tool | skills/paperclip/references/api-reference.md:1028 | -| skill:skills/paperclip/references/api-reference.md:conversational-confirmation-answers:1086 | always_agent_tool | skills/paperclip/references/api-reference.md:1086 | -| skill:skills/paperclip/references/api-reference.md:checkbox-confirmations:1092 | always_agent_tool | skills/paperclip/references/api-reference.md:1092 | -| skill:skills/paperclip/references/api-reference.md:item-verdict-requests:1207 | optional_agent_tool | skills/paperclip/references/api-reference.md:1207 | -| skill:skills/paperclip/references/api-reference.md:checking-approval-status:1317 | optional_agent_tool | skills/paperclip/references/api-reference.md:1317 | -| skill:skills/paperclip/references/api-reference.md:approval-follow-up-requesting-agent:1323 | always_agent_tool | skills/paperclip/references/api-reference.md:1323 | -| skill:skills/paperclip/references/api-reference.md:issue-lifecycle:1341 | always_agent_tool | skills/paperclip/references/api-reference.md:1341 | -| skill:skills/paperclip/references/api-reference.md:error-handling:1371 | control_plane_owned | skills/paperclip/references/api-reference.md:1371 | -| skill:skills/paperclip/references/api-reference.md:full-api-reference:1385 | optional_agent_tool | skills/paperclip/references/api-reference.md:1385 | -| skill:skills/paperclip/references/api-reference.md:agents:1387 | optional_agent_tool | skills/paperclip/references/api-reference.md:1387 | -| skill:skills/paperclip/references/api-reference.md:issues-tasks:1408 | optional_agent_tool | skills/paperclip/references/api-reference.md:1408 | -| skill:skills/paperclip/references/api-reference.md:companies-projects-goals:1449 | optional_agent_tool | skills/paperclip/references/api-reference.md:1449 | -| skill:skills/paperclip/references/api-reference.md:routines:1473 | optional_agent_tool | skills/paperclip/references/api-reference.md:1473 | -| skill:skills/paperclip/references/api-reference.md:approvals-costs-activity-dashboard:1489 | optional_agent_tool | skills/paperclip/references/api-reference.md:1489 | -| skill:skills/paperclip/references/api-reference.md:secrets:1511 | optional_agent_tool | skills/paperclip/references/api-reference.md:1511 | -| skill:skills/paperclip/references/api-reference.md:agent-secret-proposals:1524 | optional_agent_tool | skills/paperclip/references/api-reference.md:1524 | -| skill:skills/paperclip/references/api-reference.md:agent-secret-access:1624 | optional_agent_tool | skills/paperclip/references/api-reference.md:1624 | -| skill:skills/paperclip/references/api-reference.md:common-mistakes:1664 | optional_agent_tool | skills/paperclip/references/api-reference.md:1664 | +| skill:skills/paperclip/references/api-reference.md:issue-thread-confirmations:1025 | always_agent_tool | skills/paperclip/references/api-reference.md:1025 | +| skill:skills/paperclip/references/api-reference.md:conversational-confirmation-answers:1083 | always_agent_tool | skills/paperclip/references/api-reference.md:1083 | +| skill:skills/paperclip/references/api-reference.md:checkbox-confirmations:1089 | always_agent_tool | skills/paperclip/references/api-reference.md:1089 | +| skill:skills/paperclip/references/api-reference.md:item-verdict-requests:1204 | optional_agent_tool | skills/paperclip/references/api-reference.md:1204 | +| skill:skills/paperclip/references/api-reference.md:checking-approval-status:1314 | optional_agent_tool | skills/paperclip/references/api-reference.md:1314 | +| skill:skills/paperclip/references/api-reference.md:approval-follow-up-requesting-agent:1320 | always_agent_tool | skills/paperclip/references/api-reference.md:1320 | +| skill:skills/paperclip/references/api-reference.md:issue-lifecycle:1338 | always_agent_tool | skills/paperclip/references/api-reference.md:1338 | +| skill:skills/paperclip/references/api-reference.md:error-handling:1368 | control_plane_owned | skills/paperclip/references/api-reference.md:1368 | +| skill:skills/paperclip/references/api-reference.md:full-api-reference:1382 | optional_agent_tool | skills/paperclip/references/api-reference.md:1382 | +| skill:skills/paperclip/references/api-reference.md:agents:1384 | optional_agent_tool | skills/paperclip/references/api-reference.md:1384 | +| skill:skills/paperclip/references/api-reference.md:issues-tasks:1405 | optional_agent_tool | skills/paperclip/references/api-reference.md:1405 | +| skill:skills/paperclip/references/api-reference.md:companies-projects-goals:1446 | optional_agent_tool | skills/paperclip/references/api-reference.md:1446 | +| skill:skills/paperclip/references/api-reference.md:routines:1470 | optional_agent_tool | skills/paperclip/references/api-reference.md:1470 | +| skill:skills/paperclip/references/api-reference.md:approvals-costs-activity-dashboard:1486 | optional_agent_tool | skills/paperclip/references/api-reference.md:1486 | +| skill:skills/paperclip/references/api-reference.md:secrets:1508 | optional_agent_tool | skills/paperclip/references/api-reference.md:1508 | +| skill:skills/paperclip/references/api-reference.md:agent-secret-proposals:1521 | optional_agent_tool | skills/paperclip/references/api-reference.md:1521 | +| skill:skills/paperclip/references/api-reference.md:agent-secret-access:1621 | optional_agent_tool | skills/paperclip/references/api-reference.md:1621 | +| skill:skills/paperclip/references/api-reference.md:common-mistakes:1661 | optional_agent_tool | skills/paperclip/references/api-reference.md:1661 | ## Legacy MCP Alias Index diff --git a/packages/paperclip-runner/generated/capability/capabilities.yaml b/packages/paperclip-runner/generated/capability/capabilities.yaml index 450446d4c76..61bba278fd3 100644 --- a/packages/paperclip-runner/generated/capability/capabilities.yaml +++ b/packages/paperclip-runner/generated/capability/capabilities.yaml @@ -56,225 +56,225 @@ "expectedMockState": "runtime_decision_record" }, { - "id": "skill:skills/paperclip/SKILL.md:197", + "id": "skill:skills/paperclip/SKILL.md:196", "kind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md#L197:generated-artifacts-and-work-products", + "sourceAnchor": "skills/paperclip/SKILL.md#L196: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:245", + "id": "skill:skills/paperclip/SKILL.md:244", "kind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md#L245:status-quick-guide", + "sourceAnchor": "skills/paperclip/SKILL.md#L244:status-quick-guide", "heading": "Status Quick Guide", "primaryDisposition": "control_plane_owned", "semanticOperation": "runtime_reconciliation", "expectedMockState": "runtime_decision_record" }, { - "id": "skill:skills/paperclip/SKILL.md:255", + "id": "skill:skills/paperclip/SKILL.md:254", "kind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md#L255:monitors-and-watchers-say-only-what-you-actually-scheduled", + "sourceAnchor": "skills/paperclip/SKILL.md#L254: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:268", + "id": "skill:skills/paperclip/SKILL.md:267", "kind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md#L268:delegating-review-tasks", + "sourceAnchor": "skills/paperclip/SKILL.md#L267:delegating-review-tasks", "heading": "Delegating review tasks", "primaryDisposition": "control_plane_owned", "semanticOperation": "runtime_reconciliation", "expectedMockState": "runtime_decision_record" }, { - "id": "skill:skills/paperclip/SKILL.md:279", + "id": "skill:skills/paperclip/SKILL.md:278", "kind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md#L279:managing-a-user-s-inbox", + "sourceAnchor": "skills/paperclip/SKILL.md#L278: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:287", + "id": "skill:skills/paperclip/SKILL.md:286", "kind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md#L287:issue-dependencies-blockers", + "sourceAnchor": "skills/paperclip/SKILL.md#L286:issue-dependencies-blockers", "heading": "Issue Dependencies (Blockers)", "primaryDisposition": "control_plane_owned", "semanticOperation": "runtime_reconciliation", "expectedMockState": "runtime_decision_record" }, { - "id": "skill:skills/paperclip/SKILL.md:312", + "id": "skill:skills/paperclip/SKILL.md:311", "kind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md#L312:requesting-board-approval", + "sourceAnchor": "skills/paperclip/SKILL.md#L311:requesting-board-approval", "heading": "Requesting Board Approval", "primaryDisposition": "optional_agent_tool", "semanticOperation": "scoped_discovery", "expectedMockState": "operation_result" }, { - "id": "skill:skills/paperclip/SKILL.md:333", + "id": "skill:skills/paperclip/SKILL.md:332", "kind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md#L333:issue-thread-interactions", + "sourceAnchor": "skills/paperclip/SKILL.md#L332:issue-thread-interactions", "heading": "Issue-Thread Interactions", "primaryDisposition": "always_agent_tool", "semanticOperation": "request_human_input", "expectedMockState": "operation_result" }, { - "id": "skill:skills/paperclip/SKILL.md:362", + "id": "skill:skills/paperclip/SKILL.md:361", "kind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md#L362:standalone-decisions", + "sourceAnchor": "skills/paperclip/SKILL.md#L361:standalone-decisions", "heading": "Standalone Decisions", "primaryDisposition": "control_plane_owned", "semanticOperation": "runtime_reconciliation", "expectedMockState": "runtime_decision_record" }, { - "id": "skill:skills/paperclip/SKILL.md:466", + "id": "skill:skills/paperclip/SKILL.md:465", "kind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md#L466:mcp-tool-approval-gates", + "sourceAnchor": "skills/paperclip/SKILL.md#L465: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:508", + "id": "skill:skills/paperclip/SKILL.md:507", "kind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md#L508:niche-workflow-pointers", + "sourceAnchor": "skills/paperclip/SKILL.md#L507:niche-workflow-pointers", "heading": "Niche Workflow Pointers", "primaryDisposition": "control_plane_owned", "semanticOperation": "runtime_reconciliation", "expectedMockState": "runtime_decision_record" }, { - "id": "skill:skills/paperclip/SKILL.md:518", + "id": "skill:skills/paperclip/SKILL.md:517", "kind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md#L518:cases", + "sourceAnchor": "skills/paperclip/SKILL.md#L517:cases", "heading": "Cases", "primaryDisposition": "optional_agent_tool", "semanticOperation": "scoped_discovery", "expectedMockState": "operation_result" }, { - "id": "skill:skills/paperclip/SKILL.md:523", + "id": "skill:skills/paperclip/SKILL.md:522", "kind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md#L523:company-skills-workflow", + "sourceAnchor": "skills/paperclip/SKILL.md#L522:company-skills-workflow", "heading": "Company Skills Workflow", "primaryDisposition": "optional_agent_tool", "semanticOperation": "scoped_discovery", "expectedMockState": "operation_result" }, { - "id": "skill:skills/paperclip/SKILL.md:534", + "id": "skill:skills/paperclip/SKILL.md:533", "kind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md#L534:routines", + "sourceAnchor": "skills/paperclip/SKILL.md#L533:routines", "heading": "Routines", "primaryDisposition": "optional_agent_tool", "semanticOperation": "scoped_discovery", "expectedMockState": "operation_result" }, { - "id": "skill:skills/paperclip/SKILL.md:545", + "id": "skill:skills/paperclip/SKILL.md:544", "kind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md#L545:issue-workspace-runtime-controls", + "sourceAnchor": "skills/paperclip/SKILL.md#L544: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:552", + "id": "skill:skills/paperclip/SKILL.md:551", "kind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md#L552:proposing-credentials-safely", + "sourceAnchor": "skills/paperclip/SKILL.md#L551:proposing-credentials-safely", "heading": "Proposing Credentials Safely", "primaryDisposition": "control_plane_owned", "semanticOperation": "runtime_reconciliation", "expectedMockState": "runtime_decision_record" }, { - "id": "skill:skills/paperclip/SKILL.md:559", + "id": "skill:skills/paperclip/SKILL.md:558", "kind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md#L559:reading-granted-secrets", + "sourceAnchor": "skills/paperclip/SKILL.md#L558:reading-granted-secrets", "heading": "Reading Granted Secrets", "primaryDisposition": "optional_agent_tool", "semanticOperation": "scoped_discovery", "expectedMockState": "operation_result" }, { - "id": "skill:skills/paperclip/SKILL.md:585", + "id": "skill:skills/paperclip/SKILL.md:584", "kind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md#L585:critical-rules", + "sourceAnchor": "skills/paperclip/SKILL.md#L584:critical-rules", "heading": "Critical Rules", "primaryDisposition": "control_plane_owned", "semanticOperation": "runtime_reconciliation", "expectedMockState": "runtime_decision_record" }, { - "id": "skill:skills/paperclip/SKILL.md:606", + "id": "skill:skills/paperclip/SKILL.md:605", "kind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md#L606:comment-style-required", + "sourceAnchor": "skills/paperclip/SKILL.md#L605:comment-style-required", "heading": "Comment Style (Required)", "primaryDisposition": "always_agent_tool", "semanticOperation": "report_progress", "expectedMockState": "operation_result" }, { - "id": "skill:skills/paperclip/SKILL.md:638", + "id": "skill:skills/paperclip/SKILL.md:637", "kind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md#L638:update", + "sourceAnchor": "skills/paperclip/SKILL.md#L637:update", "heading": "Update", "primaryDisposition": "control_plane_owned", "semanticOperation": "runtime_reconciliation", "expectedMockState": "runtime_decision_record" }, { - "id": "skill:skills/paperclip/SKILL.md:648", + "id": "skill:skills/paperclip/SKILL.md:647", "kind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md#L648:planning-required-when-planning-requested", + "sourceAnchor": "skills/paperclip/SKILL.md#L647: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:681", + "id": "skill:skills/paperclip/SKILL.md:680", "kind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md#L681:key-endpoints-hot-routes", + "sourceAnchor": "skills/paperclip/SKILL.md#L680: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:710", + "id": "skill:skills/paperclip/SKILL.md:709", "kind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md#L710:searching-issues", + "sourceAnchor": "skills/paperclip/SKILL.md#L709:searching-issues", "heading": "Searching Issues", "primaryDisposition": "control_plane_owned", "semanticOperation": "runtime_reconciliation", "expectedMockState": "runtime_decision_record" }, { - "id": "skill:skills/paperclip/SKILL.md:720", + "id": "skill:skills/paperclip/SKILL.md:719", "kind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md#L720:full-reference", + "sourceAnchor": "skills/paperclip/SKILL.md#L719:full-reference", "heading": "Full Reference", "primaryDisposition": "control_plane_owned", "semanticOperation": "runtime_reconciliation", "expectedMockState": "runtime_decision_record" }, { - "id": "skill:skills/paperclip/SKILL.md:724", + "id": "skill:skills/paperclip/SKILL.md:723", "kind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md#L724:conversational-confirmation-answers", + "sourceAnchor": "skills/paperclip/SKILL.md#L723:conversational-confirmation-answers", "heading": "Conversational confirmation answers", "primaryDisposition": "always_agent_tool", "semanticOperation": "call_api", @@ -749,171 +749,171 @@ "expectedMockState": "operation_result" }, { - "id": "skill:skills/paperclip/references/api-reference.md:1028", + "id": "skill:skills/paperclip/references/api-reference.md:1025", "kind": "skill_heading", - "sourceAnchor": "skills/paperclip/references/api-reference.md#L1028:issue-thread-confirmations", + "sourceAnchor": "skills/paperclip/references/api-reference.md#L1025: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:1086", + "id": "skill:skills/paperclip/references/api-reference.md:1083", "kind": "skill_heading", - "sourceAnchor": "skills/paperclip/references/api-reference.md#L1086:conversational-confirmation-answers", + "sourceAnchor": "skills/paperclip/references/api-reference.md#L1083:conversational-confirmation-answers", "heading": "Conversational confirmation answers", "primaryDisposition": "always_agent_tool", "semanticOperation": "call_api", "expectedMockState": "operation_result" }, { - "id": "skill:skills/paperclip/references/api-reference.md:1092", + "id": "skill:skills/paperclip/references/api-reference.md:1089", "kind": "skill_heading", - "sourceAnchor": "skills/paperclip/references/api-reference.md#L1092:checkbox-confirmations", + "sourceAnchor": "skills/paperclip/references/api-reference.md#L1089:checkbox-confirmations", "heading": "Checkbox confirmations", "primaryDisposition": "optional_agent_tool", "semanticOperation": "scoped_discovery", "expectedMockState": "operation_result" }, { - "id": "skill:skills/paperclip/references/api-reference.md:1207", + "id": "skill:skills/paperclip/references/api-reference.md:1204", "kind": "skill_heading", - "sourceAnchor": "skills/paperclip/references/api-reference.md#L1207:item-verdict-requests", + "sourceAnchor": "skills/paperclip/references/api-reference.md#L1204: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:1317", + "id": "skill:skills/paperclip/references/api-reference.md:1314", "kind": "skill_heading", - "sourceAnchor": "skills/paperclip/references/api-reference.md#L1317:checking-approval-status", + "sourceAnchor": "skills/paperclip/references/api-reference.md#L1314: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:1323", + "id": "skill:skills/paperclip/references/api-reference.md:1320", "kind": "skill_heading", - "sourceAnchor": "skills/paperclip/references/api-reference.md#L1323:approval-follow-up-requesting-agent", + "sourceAnchor": "skills/paperclip/references/api-reference.md#L1320: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:1341", + "id": "skill:skills/paperclip/references/api-reference.md:1338", "kind": "skill_heading", - "sourceAnchor": "skills/paperclip/references/api-reference.md#L1341:issue-lifecycle", + "sourceAnchor": "skills/paperclip/references/api-reference.md#L1338:issue-lifecycle", "heading": "Issue Lifecycle", "primaryDisposition": "optional_agent_tool", "semanticOperation": "scoped_discovery", "expectedMockState": "operation_result" }, { - "id": "skill:skills/paperclip/references/api-reference.md:1371", + "id": "skill:skills/paperclip/references/api-reference.md:1368", "kind": "skill_heading", - "sourceAnchor": "skills/paperclip/references/api-reference.md#L1371:error-handling", + "sourceAnchor": "skills/paperclip/references/api-reference.md#L1368:error-handling", "heading": "Error Handling", "primaryDisposition": "control_plane_owned", "semanticOperation": "runtime_reconciliation", "expectedMockState": "runtime_decision_record" }, { - "id": "skill:skills/paperclip/references/api-reference.md:1385", + "id": "skill:skills/paperclip/references/api-reference.md:1382", "kind": "skill_heading", - "sourceAnchor": "skills/paperclip/references/api-reference.md#L1385:full-api-reference", + "sourceAnchor": "skills/paperclip/references/api-reference.md#L1382: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:1387", + "id": "skill:skills/paperclip/references/api-reference.md:1384", "kind": "skill_heading", - "sourceAnchor": "skills/paperclip/references/api-reference.md#L1387:agents", + "sourceAnchor": "skills/paperclip/references/api-reference.md#L1384:agents", "heading": "Agents", "primaryDisposition": "optional_agent_tool", "semanticOperation": "scoped_discovery", "expectedMockState": "operation_result" }, { - "id": "skill:skills/paperclip/references/api-reference.md:1408", + "id": "skill:skills/paperclip/references/api-reference.md:1405", "kind": "skill_heading", - "sourceAnchor": "skills/paperclip/references/api-reference.md#L1408:issues-tasks", + "sourceAnchor": "skills/paperclip/references/api-reference.md#L1405:issues-tasks", "heading": "Issues (Tasks)", "primaryDisposition": "optional_agent_tool", "semanticOperation": "scoped_discovery", "expectedMockState": "operation_result" }, { - "id": "skill:skills/paperclip/references/api-reference.md:1449", + "id": "skill:skills/paperclip/references/api-reference.md:1446", "kind": "skill_heading", - "sourceAnchor": "skills/paperclip/references/api-reference.md#L1449:companies-projects-goals", + "sourceAnchor": "skills/paperclip/references/api-reference.md#L1446: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:1473", + "id": "skill:skills/paperclip/references/api-reference.md:1470", "kind": "skill_heading", - "sourceAnchor": "skills/paperclip/references/api-reference.md#L1473:routines", + "sourceAnchor": "skills/paperclip/references/api-reference.md#L1470:routines", "heading": "Routines", "primaryDisposition": "optional_agent_tool", "semanticOperation": "scoped_discovery", "expectedMockState": "operation_result" }, { - "id": "skill:skills/paperclip/references/api-reference.md:1489", + "id": "skill:skills/paperclip/references/api-reference.md:1486", "kind": "skill_heading", - "sourceAnchor": "skills/paperclip/references/api-reference.md#L1489:approvals-costs-activity-dashboard", + "sourceAnchor": "skills/paperclip/references/api-reference.md#L1486: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:1511", + "id": "skill:skills/paperclip/references/api-reference.md:1508", "kind": "skill_heading", - "sourceAnchor": "skills/paperclip/references/api-reference.md#L1511:secrets", + "sourceAnchor": "skills/paperclip/references/api-reference.md#L1508:secrets", "heading": "Secrets", "primaryDisposition": "optional_agent_tool", "semanticOperation": "scoped_discovery", "expectedMockState": "operation_result" }, { - "id": "skill:skills/paperclip/references/api-reference.md:1524", + "id": "skill:skills/paperclip/references/api-reference.md:1521", "kind": "skill_heading", - "sourceAnchor": "skills/paperclip/references/api-reference.md#L1524:agent-secret-proposals", + "sourceAnchor": "skills/paperclip/references/api-reference.md#L1521: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:1577", + "id": "skill:skills/paperclip/references/api-reference.md:1574", "kind": "skill_heading", - "sourceAnchor": "skills/paperclip/references/api-reference.md#L1577:re-bind-an-existing-secret-under-a-new-path-no-secret-id", + "sourceAnchor": "skills/paperclip/references/api-reference.md#L1574: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:1624", + "id": "skill:skills/paperclip/references/api-reference.md:1621", "kind": "skill_heading", - "sourceAnchor": "skills/paperclip/references/api-reference.md#L1624:agent-secret-access", + "sourceAnchor": "skills/paperclip/references/api-reference.md#L1621: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:1664", + "id": "skill:skills/paperclip/references/api-reference.md:1661", "kind": "skill_heading", - "sourceAnchor": "skills/paperclip/references/api-reference.md#L1664:common-mistakes", + "sourceAnchor": "skills/paperclip/references/api-reference.md#L1661:common-mistakes", "heading": "Common Mistakes", "primaryDisposition": "optional_agent_tool", "semanticOperation": "scoped_discovery", diff --git a/packages/paperclip-runner/generated/capability/capability-contract.md b/packages/paperclip-runner/generated/capability/capability-contract.md index e49c3c958fc..907bf749bd5 100644 --- a/packages/paperclip-runner/generated/capability/capability-contract.md +++ b/packages/paperclip-runner/generated/capability/capability-contract.md @@ -5,6 +5,6 @@ Generated by `scripts/generate-capability-contract.mjs`; do not edit generated f - Skill/reference headings: 158 - Legacy MCP tools: 42 - Eval cases: 106 across 16 groups -- Deterministic content SHA-256: `1868c1fc065aa9c16a4ee97ab33fc75abb5d94a7bc49b95f768e1490c9cd0344` +- Deterministic content SHA-256: `2707de482cfbe6c7a483f6dee3bf4a2fc66e2b944bb50c3818ca6d5ef777b1de` Every row has exactly one primary disposition, a source anchor, a semantic operation, and a mock-state expectation. diff --git a/packages/paperclip-runner/generated/capability/semantic-tool-contracts.json b/packages/paperclip-runner/generated/capability/semantic-tool-contracts.json index 3103a31e4cb..4943b0a00a7 100644 --- a/packages/paperclip-runner/generated/capability/semantic-tool-contracts.json +++ b/packages/paperclip-runner/generated/capability/semantic-tool-contracts.json @@ -1 +1 @@ -[{"annotations":{"exposure":"always","operationId":"get_task_context","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Read the active task and actor, including the exact approved Markdown revision when this issue has an accepted plan.","inputSchema":{"additionalProperties":false,"properties":{},"required":[],"type":"object"},"name":"get_task_context","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"get_task_history","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Read bounded comments on the active task.","inputSchema":{"additionalProperties":false,"properties":{"limit":{"default":50,"maximum":200,"minimum":1,"type":"integer"}},"required":[],"type":"object"},"name":"get_task_history","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"list_documents","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"List revisioned documents on the active task.","inputSchema":{"additionalProperties":false,"properties":{},"required":[],"type":"object"},"name":"list_documents","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"read_document","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Read the current revision of one active-task document.","inputSchema":{"additionalProperties":false,"properties":{"key":{"description":"Stable issue-document key.","maxLength":120,"minLength":1,"type":"string"}},"required":["key"],"type":"object"},"name":"read_document","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"list_document_revisions","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Read bounded revision history for one active-task document.","inputSchema":{"additionalProperties":false,"properties":{"key":{"description":"Stable issue-document key.","maxLength":120,"minLength":1,"type":"string"},"limit":{"default":50,"maximum":200,"minimum":1,"type":"integer"}},"required":["key"],"type":"object"},"name":"list_document_revisions","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"report_progress","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Append a durable progress comment to the active task.","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Multiline progress update.","maxLength":20000,"minLength":1,"type":"string"},"idempotencyKey":{"description":"Caller-stable retry key.","maxLength":240,"minLength":1,"type":"string"}},"required":["idempotencyKey","body"],"type":"object"},"name":"report_progress","outputSchema":{"additionalProperties":false,"properties":{"commandId":{"description":"Stable command identifier.","maxLength":200,"minLength":1,"type":"string"},"disposition":{"enum":["applied","duplicate"]},"entityRefs":{"description":"Entities affected by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"scheduledWakeIds":{"description":"Wake identifiers scheduled by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"stateRevision":{"minimum":0,"type":"integer"}},"required":["commandId","disposition","stateRevision","entityRefs","scheduledWakeIds"],"type":"object"}},{"annotations":{"exposure":"always","operationId":"answer_status_question","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Append the answer to a status-only wake without changing task disposition.","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Concise status answer.","maxLength":20000,"minLength":1,"type":"string"},"idempotencyKey":{"description":"Caller-stable retry key.","maxLength":240,"minLength":1,"type":"string"}},"required":["idempotencyKey","body"],"type":"object"},"name":"answer_status_question","outputSchema":{"additionalProperties":false,"properties":{"commandId":{"description":"Stable command identifier.","maxLength":200,"minLength":1,"type":"string"},"disposition":{"enum":["applied","duplicate"]},"entityRefs":{"description":"Entities affected by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"scheduledWakeIds":{"description":"Wake identifiers scheduled by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"stateRevision":{"minimum":0,"type":"integer"}},"required":["commandId","disposition","stateRevision","entityRefs","scheduledWakeIds"],"type":"object"}},{"annotations":{"exposure":"always","operationId":"write_document","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Create or update an active-task document with optimistic revision safety.","inputSchema":{"additionalProperties":false,"properties":{"baseRevisionId":{"description":"Current revision id, or null when creating.","maxLength":20000,"type":["string","null"]},"body":{"description":"Markdown document body.","maxLength":200000,"minLength":1,"type":"string"},"changeSummary":{"description":"Optional revision summary.","maxLength":20000,"type":["string","null"]},"idempotencyKey":{"description":"Caller-stable retry key.","maxLength":240,"minLength":1,"type":"string"},"key":{"description":"Stable issue-document key.","maxLength":120,"minLength":1,"type":"string"},"title":{"description":"Document title.","maxLength":300,"minLength":1,"type":"string"}},"required":["idempotencyKey","key","title","body","baseRevisionId"],"type":"object"},"name":"write_document","outputSchema":{"additionalProperties":false,"properties":{"commandId":{"description":"Stable command identifier.","maxLength":200,"minLength":1,"type":"string"},"disposition":{"enum":["applied","duplicate"]},"entityRefs":{"description":"Entities affected by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"scheduledWakeIds":{"description":"Wake identifiers scheduled by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"stateRevision":{"minimum":0,"type":"integer"}},"required":["commandId","disposition","stateRevision","entityRefs","scheduledWakeIds"],"type":"object"}},{"annotations":{"exposure":"always","operationId":"request_human_input","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Create a durable human question or approval card on the current Paperclip task bound to this run; Paperclip renders it and authenticates the response. Use questions with continuationPolicy='wake_assignee' when an answer is needed, including otherwise tool-free chat turns. Supply a stable idempotencyKey and reuse it on retries. For one question at a time, ask only the next unanswered question and wait for its real answer. Never fabricate answers or treat ambiguous clarification as approval. For an ordinary confirmation or checkbox card, record a clear user chat answer with call_api POST /api/issues/{id}/interactions/{interactionId}/resolve-from-comment, using the source commentId and decision (accept/reject), plus explicit selectedOptionIds for checkbox acceptance. Existing resolver permissions still apply; governed tool, secret, and connection approvals are excluded. Question forms retain their dedicated answer workflow. Preserve existing review gates. Call this tool before claiming a question was asked; if creation fails, report the failure. Do not fabricate answer links or Markdown buttons, post duplicate cards, or use call_api to create the card. Use payload.questions for choices and payload.questionSet for text fields; see the payload schema for formats.","inputSchema":{"additionalProperties":false,"properties":{"continuationPolicy":{"enum":["none","wake_assignee","wake_assignee_on_accept"]},"idempotencyKey":{"description":"Caller-stable retry key.","maxLength":240,"minLength":1,"type":"string"},"interactionKind":{"enum":["confirmation","checkbox","questions","suggest_tasks","item_verdicts"]},"payload":{"additionalProperties":true,"description":"Kind-specific interaction data. For interactionKind='questions', use version:1 and questions:[{id,prompt,selectionMode:'single'|'multi',required?,options:[{id,label,description?,freeText?}]}]. Choice questions need at least two distinct meaningful options. For an open-ended text answer, ALSO include questionSet:{schema:'paperclip.question_set.v1',questions:[{id,prompt,answerMode:'text',required?}]} with no options or customAnswer in its text questions. Keep matching IDs/prompts in both arrays; the required compatibility questions entry uses selectionMode:'single' and options:[{id:'describe',label:'Your answer',freeText:true}]. Without questionSet this incorrectly renders as a one-option choice. Never use a lone Other or describe option as the presentation. Option keys are id/label, not value. For confirmation, payload may be {}. Keep IDs stable across retries.","type":"object"},"prompt":{"description":"Question or decision prompt.","maxLength":10000,"minLength":1,"type":"string"},"targetRevisionId":{"description":"Optional bound document revision.","maxLength":20000,"type":["string","null"]},"title":{"description":"Interaction card title.","maxLength":300,"minLength":1,"type":"string"}},"required":["idempotencyKey","interactionKind","title","prompt","continuationPolicy"],"type":"object"},"name":"request_human_input","outputSchema":{"additionalProperties":false,"properties":{"commandId":{"description":"Stable command identifier.","maxLength":200,"minLength":1,"type":"string"},"disposition":{"enum":["applied","duplicate"]},"entityRefs":{"description":"Entities affected by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"scheduledWakeIds":{"description":"Wake identifiers scheduled by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"stateRevision":{"minimum":0,"type":"integer"}},"required":["commandId","disposition","stateRevision","entityRefs","scheduledWakeIds"],"type":"object"}},{"annotations":{"exposure":"always","operationId":"register_deliverable","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Register attachment metadata and its artifact work product without credentials or bytes in the tool result.","inputSchema":{"additionalProperties":false,"properties":{"byteSize":{"maximum":100000000,"minimum":0,"type":"integer"},"contentRef":{"description":"Opaque package-local content reference.","maxLength":2000,"minLength":1,"type":"string"},"contentType":{"description":"Media type.","maxLength":200,"minLength":1,"type":"string"},"filename":{"description":"Display filename.","maxLength":500,"minLength":1,"type":"string"},"idempotencyKey":{"description":"Caller-stable retry key.","maxLength":240,"minLength":1,"type":"string"},"sha256":{"pattern":"^[a-fA-F0-9]{64}$","type":"string"},"title":{"description":"Work-product title.","maxLength":500,"minLength":1,"type":"string"}},"required":["idempotencyKey","filename","contentType","byteSize","sha256","contentRef","title"],"type":"object"},"name":"register_deliverable","outputSchema":{"additionalProperties":false,"properties":{"commandId":{"description":"Stable command identifier.","maxLength":200,"minLength":1,"type":"string"},"disposition":{"enum":["applied","duplicate"]},"entityRefs":{"description":"Entities affected by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"scheduledWakeIds":{"description":"Wake identifiers scheduled by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"stateRevision":{"minimum":0,"type":"integer"}},"required":["commandId","disposition","stateRevision","entityRefs","scheduledWakeIds"],"type":"object"}},{"annotations":{"exposure":"always","operationId":"finish_task","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Finish the active task with a durable summary.","inputSchema":{"additionalProperties":false,"properties":{"idempotencyKey":{"description":"Caller-stable retry key.","maxLength":240,"minLength":1,"type":"string"},"summary":{"description":"Completion summary.","maxLength":20000,"minLength":1,"type":"string"}},"required":["idempotencyKey","summary"],"type":"object"},"name":"finish_task","outputSchema":{"additionalProperties":false,"properties":{"commandId":{"description":"Stable command identifier.","maxLength":200,"minLength":1,"type":"string"},"disposition":{"enum":["applied","duplicate"]},"entityRefs":{"description":"Entities affected by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"scheduledWakeIds":{"description":"Wake identifiers scheduled by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"stateRevision":{"minimum":0,"type":"integer"}},"required":["commandId","disposition","stateRevision","entityRefs","scheduledWakeIds"],"type":"object"}},{"annotations":{"exposure":"always","operationId":"block_task","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Block the active task with a durable reason and optional first-class dependencies.","inputSchema":{"additionalProperties":false,"properties":{"blockedByTaskIds":{"description":"Internal task ids that block this task.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"idempotencyKey":{"description":"Caller-stable retry key.","maxLength":240,"minLength":1,"type":"string"},"reason":{"description":"Block reason.","maxLength":20000,"minLength":1,"type":"string"}},"required":["idempotencyKey","reason"],"type":"object"},"name":"block_task","outputSchema":{"additionalProperties":false,"properties":{"commandId":{"description":"Stable command identifier.","maxLength":200,"minLength":1,"type":"string"},"disposition":{"enum":["applied","duplicate"]},"entityRefs":{"description":"Entities affected by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"scheduledWakeIds":{"description":"Wake identifiers scheduled by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"stateRevision":{"minimum":0,"type":"integer"}},"required":["commandId","disposition","stateRevision","entityRefs","scheduledWakeIds"],"type":"object"}},{"annotations":{"exposure":"always","operationId":"request_review","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Move the active task to review with a durable summary.","inputSchema":{"additionalProperties":false,"properties":{"idempotencyKey":{"description":"Caller-stable retry key.","maxLength":240,"minLength":1,"type":"string"},"summary":{"description":"Review handoff summary.","maxLength":20000,"minLength":1,"type":"string"}},"required":["idempotencyKey","summary"],"type":"object"},"name":"request_review","outputSchema":{"additionalProperties":false,"properties":{"commandId":{"description":"Stable command identifier.","maxLength":200,"minLength":1,"type":"string"},"disposition":{"enum":["applied","duplicate"]},"entityRefs":{"description":"Entities affected by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"scheduledWakeIds":{"description":"Wake identifiers scheduled by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"stateRevision":{"minimum":0,"type":"integer"}},"required":["commandId","disposition","stateRevision","entityRefs","scheduledWakeIds"],"type":"object"}},{"annotations":{"exposure":"optional","operationId":"list_agents","requiredClaims":["discovery:agents:read"],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"List redacted actor profiles.","inputSchema":{"additionalProperties":false,"properties":{},"required":[],"type":"object"},"name":"list_agents","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"optional","operationId":"get_agent","requiredClaims":["discovery:agents:read"],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Read one redacted actor profile.","inputSchema":{"additionalProperties":false,"properties":{"actorId":{"description":"Actor id.","maxLength":200,"minLength":1,"type":"string"}},"required":["actorId"],"type":"object"},"name":"get_agent","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"optional","operationId":"hire_agent","requiredClaims":["delegation:agents:create"],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Create a persistent native Runner teammate with an identity and persona. The teammate reports to the caller and inherits the caller's native runtime; provider, adapter, environment, and credential settings are selected by Paperclip and are never caller-supplied here. Reuse a suitable teammate from list_agents when possible.","inputSchema":{"additionalProperties":false,"properties":{"capabilities":{"maxLength":2000,"type":["string","null"]},"instructions":{"maxLength":20000,"type":["string","null"]},"name":{"maxLength":200,"minLength":1,"type":"string"},"role":{"enum":["ceo","cto","cmo","cfo","security","engineer","designer","pm","qa","devops","researcher","general"],"type":"string"},"title":{"maxLength":300,"type":["string","null"]}},"required":["name"],"type":"object"},"name":"hire_agent","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"optional","operationId":"search_tasks","requiredClaims":["discovery:tasks:read"],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Search tasks by text and status within the run company.","inputSchema":{"additionalProperties":false,"properties":{"limit":{"default":50,"maximum":200,"minimum":1,"type":"integer"},"query":{"maxLength":500,"type":"string"},"statuses":{"items":{"enum":["backlog","todo","in_progress","in_review","done","blocked","cancelled"]},"maxItems":7,"type":"array","uniqueItems":true}},"required":[],"type":"object"},"name":"search_tasks","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"optional","operationId":"list_approvals","requiredClaims":["governance:approvals:read"],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"List approvals in the run company.","inputSchema":{"additionalProperties":false,"properties":{},"required":[],"type":"object"},"name":"list_approvals","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"optional","operationId":"get_approval","requiredClaims":["governance:approvals:read"],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Read one approval without protected data.","inputSchema":{"additionalProperties":false,"properties":{"approvalId":{"description":"Approval id.","maxLength":200,"minLength":1,"type":"string"}},"required":["approvalId"],"type":"object"},"name":"get_approval","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"optional","operationId":"get_approval_context","requiredClaims":["governance:approvals:read"],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Read one approval, its comments, and linked tasks.","inputSchema":{"additionalProperties":false,"properties":{"approvalId":{"description":"Approval id.","maxLength":200,"minLength":1,"type":"string"}},"required":["approvalId"],"type":"object"},"name":"get_approval_context","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"optional","operationId":"get_workspace_runtime","requiredClaims":["workspace:read"],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Read active-task workspace services.","inputSchema":{"additionalProperties":false,"properties":{},"required":[],"type":"object"},"name":"get_workspace_runtime","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"optional","operationId":"control_workspace_service","requiredClaims":["workspace:control"],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Start, stop, or fault one active-task workspace service.","inputSchema":{"additionalProperties":false,"properties":{"action":{"enum":["start","stop","fail"]},"idempotencyKey":{"description":"Caller-stable retry key.","maxLength":240,"minLength":1,"type":"string"},"serviceId":{"description":"Workspace service id.","maxLength":200,"minLength":1,"type":"string"},"url":{"description":"Optional service URL.","maxLength":20000,"type":["string","null"]}},"required":["idempotencyKey","serviceId","action"],"type":"object"},"name":"control_workspace_service","outputSchema":{"additionalProperties":false,"properties":{"commandId":{"description":"Stable command identifier.","maxLength":200,"minLength":1,"type":"string"},"disposition":{"enum":["applied","duplicate"]},"entityRefs":{"description":"Entities affected by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"scheduledWakeIds":{"description":"Wake identifiers scheduled by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"stateRevision":{"minimum":0,"type":"integer"}},"required":["commandId","disposition","stateRevision","entityRefs","scheduledWakeIds"],"type":"object"}},{"annotations":{"exposure":"optional","operationId":"set_dependencies","requiredClaims":["dependencies:write"],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Replace the active task's first-class blocker set.","inputSchema":{"additionalProperties":false,"properties":{"blockedByTaskIds":{"description":"Replacement blocker task ids.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"idempotencyKey":{"description":"Caller-stable retry key.","maxLength":240,"minLength":1,"type":"string"}},"required":["idempotencyKey","blockedByTaskIds"],"type":"object"},"name":"set_dependencies","outputSchema":{"additionalProperties":false,"properties":{"commandId":{"description":"Stable command identifier.","maxLength":200,"minLength":1,"type":"string"},"disposition":{"enum":["applied","duplicate"]},"entityRefs":{"description":"Entities affected by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"scheduledWakeIds":{"description":"Wake identifiers scheduled by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"stateRevision":{"minimum":0,"type":"integer"}},"required":["commandId","disposition","stateRevision","entityRefs","scheduledWakeIds"],"type":"object"}},{"annotations":{"exposure":"optional","operationId":"create_task","requiredClaims":["delegation:tasks:create"],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Create a project task from a conversation, or a child from an ordinary task. Persist initialPlan before execution. Set status to backlog when the user wants to save or plan work without starting it; backlog tasks never wake an agent. Omitted status means todo, subject to blockers.","inputSchema":{"additionalProperties":false,"properties":{"assigneeActorId":{"description":"Optional agent assignee. Omit to assign the current agent.","maxLength":20000,"type":["string","null"]},"blockedByTaskIds":{"description":"Initial blocker task ids.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"description":{"description":"Child task description.","maxLength":20000,"type":["string","null"]},"idempotencyKey":{"description":"Caller-stable retry key.","maxLength":240,"minLength":1,"type":"string"},"initialPlan":{"description":"Relevant markdown plan saved on the new task before execution starts.","maxLength":200000,"type":["string","null"]},"priority":{"enum":["critical","high","medium","low"]},"projectId":{"description":"Project ID for the task.","type":["string","null"]},"status":{"description":"Initial status. Use backlog to save work without executing it. Defaults to todo (blocked when dependencies are unresolved).","enum":["backlog","todo"]},"title":{"description":"Child task title.","maxLength":500,"minLength":1,"type":"string"}},"required":["idempotencyKey","title"],"type":"object"},"name":"create_task","outputSchema":{"additionalProperties":false,"properties":{"commandId":{"description":"Stable command identifier.","maxLength":200,"minLength":1,"type":"string"},"disposition":{"enum":["applied","duplicate"]},"entityRefs":{"description":"Entities affected by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"scheduledWakeIds":{"description":"Wake identifiers scheduled by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"stateRevision":{"minimum":0,"type":"integer"},"task":{"additionalProperties":false,"properties":{"assigneeActorId":{"type":["string","null"]},"id":{"minLength":1,"type":"string"},"identifier":{"type":["string","null"]},"parentId":{"minLength":1,"type":["string","null"]},"projectId":{"minLength":1,"type":["string","null"]},"status":{"minLength":1,"type":"string"}},"required":["id","identifier","parentId","status","assigneeActorId"],"type":"object"}},"required":["commandId","disposition","stateRevision","entityRefs","scheduledWakeIds","task"],"type":"object"}},{"annotations":{"exposure":"optional","operationId":"request_approval","requiredClaims":["governance:approvals:request"],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Create a governed approval and waiting posture.","inputSchema":{"additionalProperties":false,"properties":{"approvalType":{"description":"Stable approval type.","maxLength":200,"minLength":1,"type":"string"},"idempotencyKey":{"description":"Caller-stable retry key.","maxLength":240,"minLength":1,"type":"string"},"payload":{"additionalProperties":true,"type":"object"}},"required":["idempotencyKey","approvalType","payload"],"type":"object"},"name":"request_approval","outputSchema":{"additionalProperties":false,"properties":{"commandId":{"description":"Stable command identifier.","maxLength":200,"minLength":1,"type":"string"},"disposition":{"enum":["applied","duplicate"]},"entityRefs":{"description":"Entities affected by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"scheduledWakeIds":{"description":"Wake identifiers scheduled by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"stateRevision":{"minimum":0,"type":"integer"}},"required":["commandId","disposition","stateRevision","entityRefs","scheduledWakeIds"],"type":"object"}},{"annotations":{"exposure":"optional","operationId":"decide_approval","requiredClaims":["governance:approvals:decide"],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Decide an approval as an explicitly authorized approver.","inputSchema":{"additionalProperties":false,"properties":{"approvalId":{"description":"Approval id.","maxLength":200,"minLength":1,"type":"string"},"decision":{"enum":["approved","rejected","cancelled"]},"idempotencyKey":{"description":"Caller-stable retry key.","maxLength":240,"minLength":1,"type":"string"},"note":{"description":"Decision note.","maxLength":20000,"minLength":1,"type":"string"}},"required":["idempotencyKey","approvalId","decision","note"],"type":"object"},"name":"decide_approval","outputSchema":{"additionalProperties":false,"properties":{"commandId":{"description":"Stable command identifier.","maxLength":200,"minLength":1,"type":"string"},"disposition":{"enum":["applied","duplicate"]},"entityRefs":{"description":"Entities affected by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"scheduledWakeIds":{"description":"Wake identifiers scheduled by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"stateRevision":{"minimum":0,"type":"integer"}},"required":["commandId","disposition","stateRevision","entityRefs","scheduledWakeIds"],"type":"object"}},{"annotations":{"exposure":"optional","operationId":"comment_on_approval","requiredClaims":["governance:approvals:comment"],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Add a durable comment to an approval.","inputSchema":{"additionalProperties":false,"properties":{"approvalId":{"description":"Approval id.","maxLength":200,"minLength":1,"type":"string"},"body":{"description":"Approval comment.","maxLength":20000,"minLength":1,"type":"string"},"idempotencyKey":{"description":"Caller-stable retry key.","maxLength":240,"minLength":1,"type":"string"}},"required":["idempotencyKey","approvalId","body"],"type":"object"},"name":"comment_on_approval","outputSchema":{"additionalProperties":false,"properties":{"commandId":{"description":"Stable command identifier.","maxLength":200,"minLength":1,"type":"string"},"disposition":{"enum":["applied","duplicate"]},"entityRefs":{"description":"Entities affected by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"scheduledWakeIds":{"description":"Wake identifiers scheduled by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"stateRevision":{"minimum":0,"type":"integer"}},"required":["commandId","disposition","stateRevision","entityRefs","scheduledWakeIds"],"type":"object"}},{"annotations":{"exposure":"optional","operationId":"schedule_wake","requiredClaims":["control_plane:wakes"],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Schedule a deterministic continuation wake.","inputSchema":{"additionalProperties":false,"properties":{"delayTicks":{"maximum":10000,"minimum":1,"type":"integer"},"idempotencyKey":{"description":"Caller-stable retry key.","maxLength":240,"minLength":1,"type":"string"},"payload":{"additionalProperties":true,"type":"object"},"reason":{"enum":["manual","issue_commented","interaction_resolved","approval_resolved","blockers_resolved","scheduled_retry","resume"]}},"required":["idempotencyKey","reason","delayTicks"],"type":"object"},"name":"schedule_wake","outputSchema":{"additionalProperties":false,"properties":{"commandId":{"description":"Stable command identifier.","maxLength":200,"minLength":1,"type":"string"},"disposition":{"enum":["applied","duplicate"]},"entityRefs":{"description":"Entities affected by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"scheduledWakeIds":{"description":"Wake identifiers scheduled by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"stateRevision":{"minimum":0,"type":"integer"}},"required":["commandId","disposition","stateRevision","entityRefs","scheduledWakeIds"],"type":"object"}},{"annotations":{"exposure":"optional","operationId":"generic_api_request","requiredClaims":["test:generic_api_request"],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Test-only escape hatch. Disabled unless the scenario and explicit claim both enable it.","inputSchema":{"additionalProperties":false,"properties":{"body":{"additionalProperties":true,"type":"object"},"method":{"enum":["GET","POST","PATCH"]},"path":{"maxLength":500,"pattern":"^/mock/","type":"string"}},"required":["method","path"],"type":"object"},"name":"generic_api_request","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"read_agent_instructions","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Read the current canonical instruction entry and its revision, or inspect a historical revision by supplying both entryFile and revisionId. Read before updating; do not edit shared instruction caches.","inputSchema":{"additionalProperties":false,"properties":{"entryFile":{"description":"Configured relative entry filename returned by read_agent_instructions; retain it with the revision.","maxLength":4096,"minLength":1,"type":"string"},"revisionId":{"description":"Historical revision to inspect; also provide its entryFile.","pattern":"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$","type":"string"},"targetAgentId":{"description":"Same-company target agent. Omit to use the calling agent.","pattern":"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$","type":"string"}},"required":[],"type":"object"},"name":"read_agent_instructions","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"optional","operationId":"search_api","requiredClaims":["api:discover"],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Fallback only: discover Paperclip API operations when the available dedicated tools cannot express the task. Prefer dedicated tools for common operations; do not search before using them. For a persistent hire, first list_agents to reuse a suitable teammate. Search agent-hires for the hiring schema and agent-configurations for compatible adapter/runtime settings, then use call_api with the returned operationId. Supply role-specific instructionsBundle.files as a filename-to-content record. Use the returned agent.id for delegation and obey any pending approval. Provider helper threads are temporary workers, not Paperclip hires. If a hire response is uncertain, reconcile with list_agents before retrying.","inputSchema":{"additionalProperties":false,"properties":{"cursor":{"maxLength":200,"type":"string"},"limit":{"default":5,"maximum":10,"minimum":1,"type":"integer"},"query":{"maxLength":500,"minLength":1,"type":"string"}},"required":["query"],"type":"object"},"name":"search_api","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"update_agent_instructions","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Commit instruction content with the exact entryFile and baseRevisionId from a prior read. Requires the responsible user’s current target edit permission. On conflict, preserve your candidate and explicitly resolve it; never overwrite the newer head.","inputSchema":{"additionalProperties":false,"properties":{"baseRevisionId":{"description":"Revision read before editing; null only when no canonical entry exists. Never replace a stale base silently.","pattern":"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$","type":["string","null"]},"content":{"description":"Complete UTF-8 instruction content, at most 1 MiB; empty content is valid.","maxLength":1048576,"type":"string"},"entryFile":{"description":"Configured relative entry filename returned by read_agent_instructions; retain it with the revision.","maxLength":4096,"minLength":1,"type":"string"},"targetAgentId":{"description":"Same-company target agent. Omit to use the calling agent.","pattern":"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$","type":"string"}},"required":["entryFile","content","baseRevisionId"],"type":"object"},"name":"update_agent_instructions","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"optional","operationId":"call_api","requiredClaims":["api:call"],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Fallback only: call a discovered Paperclip API operation when dedicated tools lack the required operation or parameters. Uses your existing permissions. Prefer dedicated tools; never bypass a denial or runner lifecycle tool. For large text responses, read the returned artifact with GET /api/assets/{assetId}/content and responseText; follow nextOffsetBytes until null.","inputSchema":{"additionalProperties":false,"properties":{"body":{"anyOf":[{"additionalProperties":true,"type":"object"},{"items":{},"type":"array"},{"type":"string"},{"type":"number"},{"type":"boolean"},{"type":"null"}],"description":"Request value matching the discovered schema. For JSON object or array requests, pass the object or array directly, never a JSON-encoded string. Strings are for text bodies or endpoints whose schema explicitly accepts a string."},"contentType":{"maxLength":120,"type":"string"},"files":{"items":{"additionalProperties":false,"oneOf":[{"properties":{"artifactId":{}},"required":["artifactId"]},{"properties":{"path":{}},"required":["path"]}],"properties":{"artifactId":{"type":"string"},"field":{"type":"string"},"path":{"description":"File relative to the active issue workspace. Remote files must first be uploaded as an artifact.","type":"string"}},"type":"object"},"maxItems":10,"type":"array"},"operationId":{"description":"Exact operationId returned by search_api, for example GET /api/projects/{id}. Do not guess identifiers.","maxLength":500,"minLength":1,"type":"string"},"pathParams":{"additionalProperties":{"type":"string"},"type":"object"},"query":{"additionalProperties":true,"type":"object"},"responseText":{"additionalProperties":false,"description":"GET only: return a bounded UTF-8 text window inline, including JSON as text, without saving another artifact. Offsets and limits are bytes. Use the returned nextOffsetBytes to continue; null means complete. Prefer reading a saved artifact for a stable snapshot. New live responses have a 1 GiB capture limit and a 4 GiB per-run capture budget. Existing larger assets remain readable in bounded pages. If a live response returns an artifact, continue on its content operation for a stable snapshot.","properties":{"limitBytes":{"default":24576,"maximum":24576,"minimum":4,"type":"integer"},"offsetBytes":{"default":0,"maximum":9007199254740991,"minimum":0,"type":"integer"}},"type":"object"}},"required":["operationId"],"type":"object"},"name":"call_api","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"get_agent_instruction_history","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"List bounded canonical instruction revision metadata, including actor, source run, and restore origin. Use read_agent_instructions with entryFile and revisionId to inspect exact historical content.","inputSchema":{"additionalProperties":false,"properties":{"cursor":{"maxLength":2048,"minLength":1,"type":"string"},"entryFile":{"description":"Configured relative entry filename returned by read_agent_instructions; retain it with the revision.","maxLength":4096,"minLength":1,"type":"string"},"limit":{"maximum":100,"minimum":1,"type":"integer"},"targetAgentId":{"description":"Same-company target agent. Omit to use the calling agent.","pattern":"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$","type":"string"}},"required":["entryFile"],"type":"object"},"name":"get_agent_instruction_history","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"restore_agent_instructions","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Append a historical instruction revision as the current content using the current baseRevisionId. Requires the responsible user’s current target edit permission. History remains intact; conflicts require an explicit resolution.","inputSchema":{"additionalProperties":false,"properties":{"baseRevisionId":{"description":"Current revision read before restoring. Never replace a stale base silently.","pattern":"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$","type":"string"},"entryFile":{"description":"Configured relative entry filename returned by read_agent_instructions; retain it with the revision.","maxLength":4096,"minLength":1,"type":"string"},"revisionId":{"description":"Historical revision whose exact content should be restored.","pattern":"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$","type":"string"},"targetAgentId":{"description":"Same-company target agent. Omit to use the calling agent.","pattern":"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$","type":"string"}},"required":["entryFile","revisionId","baseRevisionId"],"type":"object"},"name":"restore_agent_instructions","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"optional","operationId":"create_project","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Create a project after considering existing projects and available repositories. repositoryIds and repositoryUrls accept multiple existing repositories. Use HTTPS GitHub repositoryUrls when an accessible repo is not in the catalog; this registers project repositories, not remote GitHub repositories. Non-code projects may omit repositories. Cannot combine repositoryIds/repositoryUrls with workspace. Reuse the idempotency key on retries.","inputSchema":{"additionalProperties":false,"properties":{"archivedAt":{"description":"Archive timestamp.","maxLength":20000,"type":["string","null"]},"color":{"description":"Project color.","maxLength":20000,"type":["string","null"]},"description":{"description":"Project outcome and context.","maxLength":20000,"type":["string","null"]},"env":{"additionalProperties":true,"type":"object"},"executionWorkspacePolicy":{"additionalProperties":true,"type":"object"},"goalId":{"description":"Goal ID.","maxLength":20000,"type":["string","null"]},"goalIds":{"description":"Goal IDs.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"icon":{"description":"Project icon.","enum":["folder","rocket","code","terminal","database","globe","package","boxes","box","layers","briefcase","compass","target","flame","zap","star","bug","wrench","hammer","lightbulb","sparkles","shield","lock","search","cog","brain","cpu","git-branch","file-code","puzzle","gem","atom","heart","mail","message-square","crown","radar","telescope","hexagon",null],"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":{"maxLength":2000,"pattern":"^https://github\\.com/(?!\\.{1,2}/)[A-Za-z0-9_.-]+/(?!\\.{1,2}/?$)[A-Za-z0-9_.-]+/?$","type":"string"},"maxItems":100,"type":"array"},"status":{"enum":["backlog","planned","in_progress","completed","cancelled"]},"targetDate":{"description":"Target date.","maxLength":20000,"type":["string","null"]},"workspace":{"additionalProperties":true,"type":"object"}},"required":["idempotencyKey","name"],"type":"object"},"name":"create_project","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"optional","operationId":"list_project_repositories","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"List authorized repositories with stable IDs and names. Consider appropriate repositories before creating a project; never invent IDs.","inputSchema":{"additionalProperties":false,"properties":{},"required":[],"type":"object"},"name":"list_project_repositories","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"optional","operationId":"list_projects","requiredClaims":["discovery:projects:read"],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Inspect available company projects before selecting a project for new work.","inputSchema":{"additionalProperties":false,"properties":{},"required":[],"type":"object"},"name":"list_projects","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"optional","operationId":"create_skill","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Create a reusable single-file skill in the company library. Supply a complete SKILL.md whose name and description match the inputs. This saves the skill and shows a card; it does not assign the skill to any agent. Reuse idempotencyKey on retries.","inputSchema":{"additionalProperties":false,"properties":{"description":{"maxLength":2000,"minLength":1,"type":"string"},"idempotencyKey":{"maxLength":240,"minLength":1,"type":"string"},"markdown":{"description":"Complete SKILL.md including name and description frontmatter and substantive instructions.","maxLength":200000,"minLength":1,"type":"string"},"name":{"description":"Lowercase skill name, matching SKILL.md frontmatter.","maxLength":120,"minLength":1,"pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$","type":"string"},"slug":{"description":"Optional; must equal name.","maxLength":120,"minLength":1,"pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$","type":"string"}},"required":["idempotencyKey","name","description","markdown"],"type":"object"},"name":"create_skill","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"optional","operationId":"reassign_task","requiredClaims":["delegation:tasks:assign"],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Reassign an existing task to another company agent. Read search_tasks first and supply its current assignee and statusVersion to prevent overwriting a concurrent change. Preserve the task, documents, dependencies, and blocked/backlog status. Active work is stopped before handoff. Use a stable idempotency key for retries. Cannot reassign this run’s own task, conversations, completed tasks, or pending reviews; use create_task for delegation from the current task.","inputSchema":{"additionalProperties":false,"properties":{"assigneeActorId":{"description":"New company agent ID from list_agents.","minLength":1,"type":"string"},"expectedAssigneeActorId":{"description":"Current assigneeAgentId from search_tasks; null means unassigned.","type":["string","null"]},"expectedStatusVersion":{"description":"Current statusVersion from search_tasks.","minimum":0,"type":"integer"},"idempotencyKey":{"description":"Caller-stable retry key.","maxLength":240,"minLength":1,"type":"string"},"reason":{"description":"Why the task should move and context for the new owner.","maxLength":20000,"minLength":1,"type":"string"},"taskId":{"description":"Existing task ID from search_tasks.","minLength":1,"type":"string"}},"required":["idempotencyKey","taskId","assigneeActorId","expectedAssigneeActorId","expectedStatusVersion","reason"],"type":"object"},"name":"reassign_task","outputSchema":{"additionalProperties":false,"properties":{"commandId":{"description":"Stable command identifier.","maxLength":200,"minLength":1,"type":"string"},"disposition":{"enum":["applied","duplicate"]},"entityRefs":{"description":"Entities affected by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"scheduledWakeIds":{"description":"Wake identifiers scheduled by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"stateRevision":{"minimum":0,"type":"integer"}},"required":["commandId","disposition","stateRevision","entityRefs","scheduledWakeIds"],"type":"object"}},{"annotations":{"exposure":"optional","operationId":"set_task_title","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Set a concise, descriptive title for the active task. When its titleNeedsGeneration is true, call this early with onlyIfProvisional: true to replace the initial prompt slice without overwriting a user's title. Use false only for an intentional rename. This changes no task status, ownership, or description.","inputSchema":{"additionalProperties":false,"properties":{"idempotencyKey":{"description":"Reuse this key on retries.","maxLength":240,"minLength":1,"type":"string"},"onlyIfProvisional":{"description":"True for automatic initial naming; preserves any title already chosen by a user or agent.","type":"boolean"},"title":{"description":"Short title describing the requested outcome.","maxLength":240,"minLength":1,"type":"string"}},"required":["idempotencyKey","title","onlyIfProvisional"],"type":"object"},"name":"set_task_title","outputSchema":{"additionalProperties":true,"type":"object"}}] +[{"annotations":{"exposure":"always","operationId":"get_task_context","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Read the active task and actor, including the exact approved Markdown revision when this issue has an accepted plan.","inputSchema":{"additionalProperties":false,"properties":{},"required":[],"type":"object"},"name":"get_task_context","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"get_task_history","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Read bounded comments on the active task.","inputSchema":{"additionalProperties":false,"properties":{"limit":{"default":50,"maximum":200,"minimum":1,"type":"integer"}},"required":[],"type":"object"},"name":"get_task_history","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"list_documents","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"List revisioned documents on the active task.","inputSchema":{"additionalProperties":false,"properties":{},"required":[],"type":"object"},"name":"list_documents","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"read_document","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Read the current revision of one active-task document.","inputSchema":{"additionalProperties":false,"properties":{"key":{"description":"Stable issue-document key.","maxLength":120,"minLength":1,"type":"string"}},"required":["key"],"type":"object"},"name":"read_document","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"list_document_revisions","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Read bounded revision history for one active-task document.","inputSchema":{"additionalProperties":false,"properties":{"key":{"description":"Stable issue-document key.","maxLength":120,"minLength":1,"type":"string"},"limit":{"default":50,"maximum":200,"minimum":1,"type":"integer"}},"required":["key"],"type":"object"},"name":"list_document_revisions","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"report_progress","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Append a durable progress comment to the active task.","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Multiline progress update.","maxLength":20000,"minLength":1,"type":"string"},"idempotencyKey":{"description":"Caller-stable retry key.","maxLength":240,"minLength":1,"type":"string"}},"required":["idempotencyKey","body"],"type":"object"},"name":"report_progress","outputSchema":{"additionalProperties":false,"properties":{"commandId":{"description":"Stable command identifier.","maxLength":200,"minLength":1,"type":"string"},"disposition":{"enum":["applied","duplicate"]},"entityRefs":{"description":"Entities affected by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"scheduledWakeIds":{"description":"Wake identifiers scheduled by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"stateRevision":{"minimum":0,"type":"integer"}},"required":["commandId","disposition","stateRevision","entityRefs","scheduledWakeIds"],"type":"object"}},{"annotations":{"exposure":"always","operationId":"answer_status_question","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Append the answer to a status-only wake without changing task disposition.","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Concise status answer.","maxLength":20000,"minLength":1,"type":"string"},"idempotencyKey":{"description":"Caller-stable retry key.","maxLength":240,"minLength":1,"type":"string"}},"required":["idempotencyKey","body"],"type":"object"},"name":"answer_status_question","outputSchema":{"additionalProperties":false,"properties":{"commandId":{"description":"Stable command identifier.","maxLength":200,"minLength":1,"type":"string"},"disposition":{"enum":["applied","duplicate"]},"entityRefs":{"description":"Entities affected by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"scheduledWakeIds":{"description":"Wake identifiers scheduled by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"stateRevision":{"minimum":0,"type":"integer"}},"required":["commandId","disposition","stateRevision","entityRefs","scheduledWakeIds"],"type":"object"}},{"annotations":{"exposure":"always","operationId":"write_document","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Create or update an active-task document with optimistic revision safety.","inputSchema":{"additionalProperties":false,"properties":{"baseRevisionId":{"description":"Current revision id, or null when creating.","maxLength":20000,"type":["string","null"]},"body":{"description":"Markdown document body.","maxLength":200000,"minLength":1,"type":"string"},"changeSummary":{"description":"Optional revision summary.","maxLength":20000,"type":["string","null"]},"idempotencyKey":{"description":"Caller-stable retry key.","maxLength":240,"minLength":1,"type":"string"},"key":{"description":"Stable issue-document key.","maxLength":120,"minLength":1,"type":"string"},"title":{"description":"Document title.","maxLength":300,"minLength":1,"type":"string"}},"required":["idempotencyKey","key","title","body","baseRevisionId"],"type":"object"},"name":"write_document","outputSchema":{"additionalProperties":false,"properties":{"commandId":{"description":"Stable command identifier.","maxLength":200,"minLength":1,"type":"string"},"disposition":{"enum":["applied","duplicate"]},"entityRefs":{"description":"Entities affected by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"scheduledWakeIds":{"description":"Wake identifiers scheduled by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"stateRevision":{"minimum":0,"type":"integer"}},"required":["commandId","disposition","stateRevision","entityRefs","scheduledWakeIds"],"type":"object"}},{"annotations":{"exposure":"always","operationId":"request_human_input","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Create a durable human question or approval card on the current Paperclip task bound to this run; Paperclip renders it and authenticates the response. Use questions with continuationPolicy='wake_assignee' when an answer is needed, including otherwise tool-free chat turns. Supply a stable idempotencyKey and reuse it on retries. For one question at a time, ask only the next unanswered question and wait for its real answer. Never fabricate answers or treat ambiguous clarification as approval. For an ordinary confirmation or checkbox card, record a clear user chat answer with call_api POST /api/issues/{id}/interactions/{interactionId}/resolve-from-comment, using the source commentId and decision (accept/reject), plus explicit selectedOptionIds for checkbox acceptance. Existing resolver permissions still apply; governed tool, secret, and connection approvals are excluded. Question forms retain their dedicated answer workflow. Preserve existing review gates. Call this tool before claiming a question was asked; if creation fails, report the failure. Do not fabricate answer links or Markdown buttons, post duplicate cards, or use call_api to create the card. Use one complete payload.questionSet for text and choice questions. Paperclip generates compatibility questions; see the payload schema for formats.","inputSchema":{"additionalProperties":false,"allOf":[{"if":{"properties":{"interactionKind":{"const":"questions"}},"required":["interactionKind"]},"then":{"properties":{"payload":{"anyOf":[{"required":["questionSet"]},{"required":["questions"]}],"required":["version"]}},"required":["payload"]}}],"properties":{"continuationPolicy":{"enum":["none","wake_assignee","wake_assignee_on_accept"]},"idempotencyKey":{"description":"Caller-stable retry key.","maxLength":240,"minLength":1,"type":"string"},"interactionKind":{"enum":["confirmation","checkbox","questions","suggest_tasks","item_verdicts"]},"payload":{"additionalProperties":true,"description":"Kind-specific interaction data. For questions, send version:1 and one complete questionSet containing every text and choice question. Paperclip generates compatibility questions. Each canonical question needs id, prompt, required, and answerMode: text, single_select, or multi_select. Text questions have no options or customAnswer. Choice questions need at least two meaningful options with id/label. Use customAnswer:{enabled:true} for an optional written answer to a choice question. Legacy questions remain supported; if both representations are supplied, they must describe the same complete form. Keep IDs stable across retries. For confirmation, payload may be {}.","properties":{"questionSet":{"additionalProperties":false,"properties":{"description":{"maxLength":100000,"type":"string"},"questions":{"items":{"additionalProperties":false,"allOf":[{"if":{"properties":{"answerMode":{"const":"text"}},"required":["answerMode"]},"then":{"not":{"required":["customAnswer"]},"properties":{"options":{"maxItems":0,"type":"array"}}}},{"if":{"properties":{"answerMode":{"enum":["single_select","multi_select"]}},"required":["answerMode"]},"then":{"properties":{"options":{"minItems":1,"type":"array"}},"required":["options"]}}],"properties":{"answerMode":{"enum":["single_select","multi_select","text"]},"customAnswer":{"additionalProperties":false,"properties":{"enabled":{"const":true},"label":{"maxLength":1000,"type":"string"},"placeholder":{"maxLength":1000,"type":"string"}},"required":["enabled"],"type":"object"},"header":{"maxLength":1000,"type":"string"},"helpText":{"maxLength":4000,"type":"string"},"id":{"maxLength":160,"minLength":1,"type":"string"},"options":{"items":{"additionalProperties":false,"properties":{"description":{"maxLength":4000,"type":"string"},"id":{"maxLength":160,"minLength":1,"type":"string"},"label":{"maxLength":1000,"minLength":1,"type":"string"},"recommended":{"type":"boolean"}},"required":["id","label"],"type":"object"},"maxItems":128,"type":"array"},"prompt":{"maxLength":4000,"minLength":1,"type":"string"},"required":{"type":"boolean"},"textValidation":{"additionalProperties":false,"properties":{"inputType":{"enum":["text","number","integer"]},"maxLength":{"maximum":100000,"minimum":0,"type":"integer"},"maximum":{"type":"number"},"minLength":{"maximum":100000,"minimum":0,"type":"integer"},"minimum":{"type":"number"},"pattern":{"maxLength":1000,"type":"string"}},"type":"object"}},"required":["id","prompt","required","answerMode"],"type":"object"},"maxItems":64,"minItems":1,"type":"array"},"schema":{"const":"paperclip.question_set.v1"},"submitLabel":{"maxLength":200,"type":"string"},"title":{"maxLength":1000,"type":"string"}},"required":["schema","questions"],"type":"object"},"questions":{"items":{"additionalProperties":true,"properties":{"id":{"maxLength":160,"minLength":1,"type":"string"},"options":{"items":{"additionalProperties":true,"properties":{"freeText":{"type":"boolean"},"id":{"maxLength":160,"minLength":1,"type":"string"},"label":{"maxLength":1000,"minLength":1,"type":"string"}},"required":["id","label"],"type":"object"},"maxItems":129,"minItems":1,"type":"array"},"prompt":{"maxLength":4000,"minLength":1,"type":"string"},"required":{"type":"boolean"},"selectionMode":{"enum":["single","multi"]}},"required":["id","prompt","selectionMode","options"],"type":"object"},"maxItems":64,"minItems":1,"type":"array"},"version":{"const":1}},"type":"object"},"prompt":{"description":"Question or decision prompt.","maxLength":10000,"minLength":1,"type":"string"},"targetRevisionId":{"description":"Optional bound document revision.","maxLength":20000,"type":["string","null"]},"title":{"description":"Interaction card title.","maxLength":300,"minLength":1,"type":"string"}},"required":["idempotencyKey","interactionKind","title","prompt","continuationPolicy"],"type":"object"},"name":"request_human_input","outputSchema":{"additionalProperties":false,"properties":{"commandId":{"description":"Stable command identifier.","maxLength":200,"minLength":1,"type":"string"},"disposition":{"enum":["applied","duplicate"]},"entityRefs":{"description":"Entities affected by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"scheduledWakeIds":{"description":"Wake identifiers scheduled by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"stateRevision":{"minimum":0,"type":"integer"}},"required":["commandId","disposition","stateRevision","entityRefs","scheduledWakeIds"],"type":"object"}},{"annotations":{"exposure":"always","operationId":"register_deliverable","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Register attachment metadata and its artifact work product without credentials or bytes in the tool result.","inputSchema":{"additionalProperties":false,"properties":{"byteSize":{"maximum":100000000,"minimum":0,"type":"integer"},"contentRef":{"description":"Opaque package-local content reference.","maxLength":2000,"minLength":1,"type":"string"},"contentType":{"description":"Media type.","maxLength":200,"minLength":1,"type":"string"},"filename":{"description":"Display filename.","maxLength":500,"minLength":1,"type":"string"},"idempotencyKey":{"description":"Caller-stable retry key.","maxLength":240,"minLength":1,"type":"string"},"sha256":{"pattern":"^[a-fA-F0-9]{64}$","type":"string"},"title":{"description":"Work-product title.","maxLength":500,"minLength":1,"type":"string"}},"required":["idempotencyKey","filename","contentType","byteSize","sha256","contentRef","title"],"type":"object"},"name":"register_deliverable","outputSchema":{"additionalProperties":false,"properties":{"commandId":{"description":"Stable command identifier.","maxLength":200,"minLength":1,"type":"string"},"disposition":{"enum":["applied","duplicate"]},"entityRefs":{"description":"Entities affected by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"scheduledWakeIds":{"description":"Wake identifiers scheduled by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"stateRevision":{"minimum":0,"type":"integer"}},"required":["commandId","disposition","stateRevision","entityRefs","scheduledWakeIds"],"type":"object"}},{"annotations":{"exposure":"always","operationId":"finish_task","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Finish the active task with a durable summary.","inputSchema":{"additionalProperties":false,"properties":{"idempotencyKey":{"description":"Caller-stable retry key.","maxLength":240,"minLength":1,"type":"string"},"summary":{"description":"Completion summary.","maxLength":20000,"minLength":1,"type":"string"}},"required":["idempotencyKey","summary"],"type":"object"},"name":"finish_task","outputSchema":{"additionalProperties":false,"properties":{"commandId":{"description":"Stable command identifier.","maxLength":200,"minLength":1,"type":"string"},"disposition":{"enum":["applied","duplicate"]},"entityRefs":{"description":"Entities affected by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"scheduledWakeIds":{"description":"Wake identifiers scheduled by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"stateRevision":{"minimum":0,"type":"integer"}},"required":["commandId","disposition","stateRevision","entityRefs","scheduledWakeIds"],"type":"object"}},{"annotations":{"exposure":"always","operationId":"block_task","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Block the active task with a durable reason and optional first-class dependencies.","inputSchema":{"additionalProperties":false,"properties":{"blockedByTaskIds":{"description":"Internal task ids that block this task.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"idempotencyKey":{"description":"Caller-stable retry key.","maxLength":240,"minLength":1,"type":"string"},"reason":{"description":"Block reason.","maxLength":20000,"minLength":1,"type":"string"}},"required":["idempotencyKey","reason"],"type":"object"},"name":"block_task","outputSchema":{"additionalProperties":false,"properties":{"commandId":{"description":"Stable command identifier.","maxLength":200,"minLength":1,"type":"string"},"disposition":{"enum":["applied","duplicate"]},"entityRefs":{"description":"Entities affected by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"scheduledWakeIds":{"description":"Wake identifiers scheduled by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"stateRevision":{"minimum":0,"type":"integer"}},"required":["commandId","disposition","stateRevision","entityRefs","scheduledWakeIds"],"type":"object"}},{"annotations":{"exposure":"always","operationId":"request_review","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Move the active task to review with a durable summary.","inputSchema":{"additionalProperties":false,"properties":{"idempotencyKey":{"description":"Caller-stable retry key.","maxLength":240,"minLength":1,"type":"string"},"summary":{"description":"Review handoff summary.","maxLength":20000,"minLength":1,"type":"string"}},"required":["idempotencyKey","summary"],"type":"object"},"name":"request_review","outputSchema":{"additionalProperties":false,"properties":{"commandId":{"description":"Stable command identifier.","maxLength":200,"minLength":1,"type":"string"},"disposition":{"enum":["applied","duplicate"]},"entityRefs":{"description":"Entities affected by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"scheduledWakeIds":{"description":"Wake identifiers scheduled by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"stateRevision":{"minimum":0,"type":"integer"}},"required":["commandId","disposition","stateRevision","entityRefs","scheduledWakeIds"],"type":"object"}},{"annotations":{"exposure":"optional","operationId":"list_agents","requiredClaims":["discovery:agents:read"],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"List redacted actor profiles.","inputSchema":{"additionalProperties":false,"properties":{},"required":[],"type":"object"},"name":"list_agents","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"optional","operationId":"get_agent","requiredClaims":["discovery:agents:read"],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Read one redacted actor profile.","inputSchema":{"additionalProperties":false,"properties":{"actorId":{"description":"Actor id.","maxLength":200,"minLength":1,"type":"string"}},"required":["actorId"],"type":"object"},"name":"get_agent","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"optional","operationId":"hire_agent","requiredClaims":["delegation:agents:create"],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Create a persistent native Runner teammate with an identity and persona. The teammate reports to the caller and inherits the caller's native runtime; provider, adapter, environment, and credential settings are selected by Paperclip and are never caller-supplied here. Reuse a suitable teammate from list_agents when possible.","inputSchema":{"additionalProperties":false,"properties":{"capabilities":{"maxLength":2000,"type":["string","null"]},"instructions":{"maxLength":20000,"type":["string","null"]},"name":{"maxLength":200,"minLength":1,"type":"string"},"role":{"enum":["ceo","cto","cmo","cfo","security","engineer","designer","pm","qa","devops","researcher","general"],"type":"string"},"title":{"maxLength":300,"type":["string","null"]}},"required":["name"],"type":"object"},"name":"hire_agent","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"optional","operationId":"search_tasks","requiredClaims":["discovery:tasks:read"],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Search tasks by text and status within the run company.","inputSchema":{"additionalProperties":false,"properties":{"limit":{"default":50,"maximum":200,"minimum":1,"type":"integer"},"query":{"maxLength":500,"type":"string"},"statuses":{"items":{"enum":["backlog","todo","in_progress","in_review","done","blocked","cancelled"]},"maxItems":7,"type":"array","uniqueItems":true}},"required":[],"type":"object"},"name":"search_tasks","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"optional","operationId":"list_approvals","requiredClaims":["governance:approvals:read"],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"List approvals in the run company.","inputSchema":{"additionalProperties":false,"properties":{},"required":[],"type":"object"},"name":"list_approvals","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"optional","operationId":"get_approval","requiredClaims":["governance:approvals:read"],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Read one approval without protected data.","inputSchema":{"additionalProperties":false,"properties":{"approvalId":{"description":"Approval id.","maxLength":200,"minLength":1,"type":"string"}},"required":["approvalId"],"type":"object"},"name":"get_approval","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"optional","operationId":"get_approval_context","requiredClaims":["governance:approvals:read"],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Read one approval, its comments, and linked tasks.","inputSchema":{"additionalProperties":false,"properties":{"approvalId":{"description":"Approval id.","maxLength":200,"minLength":1,"type":"string"}},"required":["approvalId"],"type":"object"},"name":"get_approval_context","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"optional","operationId":"get_workspace_runtime","requiredClaims":["workspace:read"],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Read active-task workspace services.","inputSchema":{"additionalProperties":false,"properties":{},"required":[],"type":"object"},"name":"get_workspace_runtime","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"optional","operationId":"control_workspace_service","requiredClaims":["workspace:control"],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Start, stop, or fault one active-task workspace service.","inputSchema":{"additionalProperties":false,"properties":{"action":{"enum":["start","stop","fail"]},"idempotencyKey":{"description":"Caller-stable retry key.","maxLength":240,"minLength":1,"type":"string"},"serviceId":{"description":"Workspace service id.","maxLength":200,"minLength":1,"type":"string"},"url":{"description":"Optional service URL.","maxLength":20000,"type":["string","null"]}},"required":["idempotencyKey","serviceId","action"],"type":"object"},"name":"control_workspace_service","outputSchema":{"additionalProperties":false,"properties":{"commandId":{"description":"Stable command identifier.","maxLength":200,"minLength":1,"type":"string"},"disposition":{"enum":["applied","duplicate"]},"entityRefs":{"description":"Entities affected by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"scheduledWakeIds":{"description":"Wake identifiers scheduled by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"stateRevision":{"minimum":0,"type":"integer"}},"required":["commandId","disposition","stateRevision","entityRefs","scheduledWakeIds"],"type":"object"}},{"annotations":{"exposure":"optional","operationId":"set_dependencies","requiredClaims":["dependencies:write"],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Replace the active task's first-class blocker set.","inputSchema":{"additionalProperties":false,"properties":{"blockedByTaskIds":{"description":"Replacement blocker task ids.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"idempotencyKey":{"description":"Caller-stable retry key.","maxLength":240,"minLength":1,"type":"string"}},"required":["idempotencyKey","blockedByTaskIds"],"type":"object"},"name":"set_dependencies","outputSchema":{"additionalProperties":false,"properties":{"commandId":{"description":"Stable command identifier.","maxLength":200,"minLength":1,"type":"string"},"disposition":{"enum":["applied","duplicate"]},"entityRefs":{"description":"Entities affected by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"scheduledWakeIds":{"description":"Wake identifiers scheduled by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"stateRevision":{"minimum":0,"type":"integer"}},"required":["commandId","disposition","stateRevision","entityRefs","scheduledWakeIds"],"type":"object"}},{"annotations":{"exposure":"optional","operationId":"create_task","requiredClaims":["delegation:tasks:create"],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Create a project task from a conversation, or a child from an ordinary task. Persist initialPlan before execution. Set status to backlog when the user wants to save or plan work without starting it; backlog tasks never wake an agent. Omitted status means todo, subject to blockers.","inputSchema":{"additionalProperties":false,"properties":{"assigneeActorId":{"description":"Optional agent assignee. Omit to assign the current agent.","maxLength":20000,"type":["string","null"]},"blockedByTaskIds":{"description":"Initial blocker task ids.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"description":{"description":"Child task description.","maxLength":20000,"type":["string","null"]},"idempotencyKey":{"description":"Caller-stable retry key.","maxLength":240,"minLength":1,"type":"string"},"initialPlan":{"description":"Relevant markdown plan saved on the new task before execution starts.","maxLength":200000,"type":["string","null"]},"priority":{"enum":["critical","high","medium","low"]},"projectId":{"description":"Project ID for the task.","type":["string","null"]},"status":{"description":"Initial status. Use backlog to save work without executing it. Defaults to todo (blocked when dependencies are unresolved).","enum":["backlog","todo"]},"title":{"description":"Child task title.","maxLength":500,"minLength":1,"type":"string"}},"required":["idempotencyKey","title"],"type":"object"},"name":"create_task","outputSchema":{"additionalProperties":false,"properties":{"commandId":{"description":"Stable command identifier.","maxLength":200,"minLength":1,"type":"string"},"disposition":{"enum":["applied","duplicate"]},"entityRefs":{"description":"Entities affected by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"scheduledWakeIds":{"description":"Wake identifiers scheduled by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"stateRevision":{"minimum":0,"type":"integer"},"task":{"additionalProperties":false,"properties":{"assigneeActorId":{"type":["string","null"]},"id":{"minLength":1,"type":"string"},"identifier":{"type":["string","null"]},"parentId":{"minLength":1,"type":["string","null"]},"projectId":{"minLength":1,"type":["string","null"]},"status":{"minLength":1,"type":"string"}},"required":["id","identifier","parentId","status","assigneeActorId"],"type":"object"}},"required":["commandId","disposition","stateRevision","entityRefs","scheduledWakeIds","task"],"type":"object"}},{"annotations":{"exposure":"optional","operationId":"request_approval","requiredClaims":["governance:approvals:request"],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Create a governed approval and waiting posture.","inputSchema":{"additionalProperties":false,"properties":{"approvalType":{"description":"Stable approval type.","maxLength":200,"minLength":1,"type":"string"},"idempotencyKey":{"description":"Caller-stable retry key.","maxLength":240,"minLength":1,"type":"string"},"payload":{"additionalProperties":true,"type":"object"}},"required":["idempotencyKey","approvalType","payload"],"type":"object"},"name":"request_approval","outputSchema":{"additionalProperties":false,"properties":{"commandId":{"description":"Stable command identifier.","maxLength":200,"minLength":1,"type":"string"},"disposition":{"enum":["applied","duplicate"]},"entityRefs":{"description":"Entities affected by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"scheduledWakeIds":{"description":"Wake identifiers scheduled by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"stateRevision":{"minimum":0,"type":"integer"}},"required":["commandId","disposition","stateRevision","entityRefs","scheduledWakeIds"],"type":"object"}},{"annotations":{"exposure":"optional","operationId":"decide_approval","requiredClaims":["governance:approvals:decide"],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Decide an approval as an explicitly authorized approver.","inputSchema":{"additionalProperties":false,"properties":{"approvalId":{"description":"Approval id.","maxLength":200,"minLength":1,"type":"string"},"decision":{"enum":["approved","rejected","cancelled"]},"idempotencyKey":{"description":"Caller-stable retry key.","maxLength":240,"minLength":1,"type":"string"},"note":{"description":"Decision note.","maxLength":20000,"minLength":1,"type":"string"}},"required":["idempotencyKey","approvalId","decision","note"],"type":"object"},"name":"decide_approval","outputSchema":{"additionalProperties":false,"properties":{"commandId":{"description":"Stable command identifier.","maxLength":200,"minLength":1,"type":"string"},"disposition":{"enum":["applied","duplicate"]},"entityRefs":{"description":"Entities affected by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"scheduledWakeIds":{"description":"Wake identifiers scheduled by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"stateRevision":{"minimum":0,"type":"integer"}},"required":["commandId","disposition","stateRevision","entityRefs","scheduledWakeIds"],"type":"object"}},{"annotations":{"exposure":"optional","operationId":"comment_on_approval","requiredClaims":["governance:approvals:comment"],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Add a durable comment to an approval.","inputSchema":{"additionalProperties":false,"properties":{"approvalId":{"description":"Approval id.","maxLength":200,"minLength":1,"type":"string"},"body":{"description":"Approval comment.","maxLength":20000,"minLength":1,"type":"string"},"idempotencyKey":{"description":"Caller-stable retry key.","maxLength":240,"minLength":1,"type":"string"}},"required":["idempotencyKey","approvalId","body"],"type":"object"},"name":"comment_on_approval","outputSchema":{"additionalProperties":false,"properties":{"commandId":{"description":"Stable command identifier.","maxLength":200,"minLength":1,"type":"string"},"disposition":{"enum":["applied","duplicate"]},"entityRefs":{"description":"Entities affected by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"scheduledWakeIds":{"description":"Wake identifiers scheduled by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"stateRevision":{"minimum":0,"type":"integer"}},"required":["commandId","disposition","stateRevision","entityRefs","scheduledWakeIds"],"type":"object"}},{"annotations":{"exposure":"optional","operationId":"schedule_wake","requiredClaims":["control_plane:wakes"],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Schedule a deterministic continuation wake.","inputSchema":{"additionalProperties":false,"properties":{"delayTicks":{"maximum":10000,"minimum":1,"type":"integer"},"idempotencyKey":{"description":"Caller-stable retry key.","maxLength":240,"minLength":1,"type":"string"},"payload":{"additionalProperties":true,"type":"object"},"reason":{"enum":["manual","issue_commented","interaction_resolved","approval_resolved","blockers_resolved","scheduled_retry","resume"]}},"required":["idempotencyKey","reason","delayTicks"],"type":"object"},"name":"schedule_wake","outputSchema":{"additionalProperties":false,"properties":{"commandId":{"description":"Stable command identifier.","maxLength":200,"minLength":1,"type":"string"},"disposition":{"enum":["applied","duplicate"]},"entityRefs":{"description":"Entities affected by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"scheduledWakeIds":{"description":"Wake identifiers scheduled by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"stateRevision":{"minimum":0,"type":"integer"}},"required":["commandId","disposition","stateRevision","entityRefs","scheduledWakeIds"],"type":"object"}},{"annotations":{"exposure":"optional","operationId":"generic_api_request","requiredClaims":["test:generic_api_request"],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Test-only escape hatch. Disabled unless the scenario and explicit claim both enable it.","inputSchema":{"additionalProperties":false,"properties":{"body":{"additionalProperties":true,"type":"object"},"method":{"enum":["GET","POST","PATCH"]},"path":{"maxLength":500,"pattern":"^/mock/","type":"string"}},"required":["method","path"],"type":"object"},"name":"generic_api_request","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"read_agent_instructions","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Read the current canonical instruction entry and its revision, or inspect a historical revision by supplying both entryFile and revisionId. Read before updating; do not edit shared instruction caches.","inputSchema":{"additionalProperties":false,"properties":{"entryFile":{"description":"Configured relative entry filename returned by read_agent_instructions; retain it with the revision.","maxLength":4096,"minLength":1,"type":"string"},"revisionId":{"description":"Historical revision to inspect; also provide its entryFile.","pattern":"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$","type":"string"},"targetAgentId":{"description":"Same-company target agent. Omit to use the calling agent.","pattern":"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$","type":"string"}},"required":[],"type":"object"},"name":"read_agent_instructions","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"optional","operationId":"search_api","requiredClaims":["api:discover"],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Fallback only: discover Paperclip API operations when the available dedicated tools cannot express the task. Prefer dedicated tools for common operations; do not search before using them. For a persistent hire, first list_agents to reuse a suitable teammate. Search agent-hires for the hiring schema and agent-configurations for compatible adapter/runtime settings, then use call_api with the returned operationId. Supply role-specific instructionsBundle.files as a filename-to-content record. Use the returned agent.id for delegation and obey any pending approval. Provider helper threads are temporary workers, not Paperclip hires. If a hire response is uncertain, reconcile with list_agents before retrying.","inputSchema":{"additionalProperties":false,"properties":{"cursor":{"maxLength":200,"type":"string"},"limit":{"default":5,"maximum":10,"minimum":1,"type":"integer"},"query":{"maxLength":500,"minLength":1,"type":"string"}},"required":["query"],"type":"object"},"name":"search_api","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"update_agent_instructions","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Commit instruction content with the exact entryFile and baseRevisionId from a prior read. Requires the responsible user’s current target edit permission. On conflict, preserve your candidate and explicitly resolve it; never overwrite the newer head.","inputSchema":{"additionalProperties":false,"properties":{"baseRevisionId":{"description":"Revision read before editing; null only when no canonical entry exists. Never replace a stale base silently.","pattern":"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$","type":["string","null"]},"content":{"description":"Complete UTF-8 instruction content, at most 1 MiB; empty content is valid.","maxLength":1048576,"type":"string"},"entryFile":{"description":"Configured relative entry filename returned by read_agent_instructions; retain it with the revision.","maxLength":4096,"minLength":1,"type":"string"},"targetAgentId":{"description":"Same-company target agent. Omit to use the calling agent.","pattern":"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$","type":"string"}},"required":["entryFile","content","baseRevisionId"],"type":"object"},"name":"update_agent_instructions","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"optional","operationId":"call_api","requiredClaims":["api:call"],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Fallback only: call a discovered Paperclip API operation when dedicated tools lack the required operation or parameters. Uses your existing permissions. Prefer dedicated tools; never bypass a denial or runner lifecycle tool. For large text responses, read the returned artifact with GET /api/assets/{assetId}/content and responseText; follow nextOffsetBytes until null.","inputSchema":{"additionalProperties":false,"properties":{"body":{"anyOf":[{"additionalProperties":true,"type":"object"},{"items":{},"type":"array"},{"type":"string"},{"type":"number"},{"type":"boolean"},{"type":"null"}],"description":"Request value matching the discovered schema. For JSON object or array requests, pass the object or array directly, never a JSON-encoded string. Strings are for text bodies or endpoints whose schema explicitly accepts a string."},"contentType":{"maxLength":120,"type":"string"},"files":{"items":{"additionalProperties":false,"oneOf":[{"properties":{"artifactId":{}},"required":["artifactId"]},{"properties":{"path":{}},"required":["path"]}],"properties":{"artifactId":{"type":"string"},"field":{"type":"string"},"path":{"description":"File relative to the active issue workspace. Remote files must first be uploaded as an artifact.","type":"string"}},"type":"object"},"maxItems":10,"type":"array"},"operationId":{"description":"Exact operationId returned by search_api, for example GET /api/projects/{id}. Do not guess identifiers.","maxLength":500,"minLength":1,"type":"string"},"pathParams":{"additionalProperties":{"type":"string"},"type":"object"},"query":{"additionalProperties":true,"type":"object"},"responseText":{"additionalProperties":false,"description":"GET only: return a bounded UTF-8 text window inline, including JSON as text, without saving another artifact. Offsets and limits are bytes. Use the returned nextOffsetBytes to continue; null means complete. Prefer reading a saved artifact for a stable snapshot. New live responses have a 1 GiB capture limit and a 4 GiB per-run capture budget. Existing larger assets remain readable in bounded pages. If a live response returns an artifact, continue on its content operation for a stable snapshot.","properties":{"limitBytes":{"default":24576,"maximum":24576,"minimum":4,"type":"integer"},"offsetBytes":{"default":0,"maximum":9007199254740991,"minimum":0,"type":"integer"}},"type":"object"}},"required":["operationId"],"type":"object"},"name":"call_api","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"get_agent_instruction_history","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"List bounded canonical instruction revision metadata, including actor, source run, and restore origin. Use read_agent_instructions with entryFile and revisionId to inspect exact historical content.","inputSchema":{"additionalProperties":false,"properties":{"cursor":{"maxLength":2048,"minLength":1,"type":"string"},"entryFile":{"description":"Configured relative entry filename returned by read_agent_instructions; retain it with the revision.","maxLength":4096,"minLength":1,"type":"string"},"limit":{"maximum":100,"minimum":1,"type":"integer"},"targetAgentId":{"description":"Same-company target agent. Omit to use the calling agent.","pattern":"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$","type":"string"}},"required":["entryFile"],"type":"object"},"name":"get_agent_instruction_history","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"restore_agent_instructions","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Append a historical instruction revision as the current content using the current baseRevisionId. Requires the responsible user’s current target edit permission. History remains intact; conflicts require an explicit resolution.","inputSchema":{"additionalProperties":false,"properties":{"baseRevisionId":{"description":"Current revision read before restoring. Never replace a stale base silently.","pattern":"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$","type":"string"},"entryFile":{"description":"Configured relative entry filename returned by read_agent_instructions; retain it with the revision.","maxLength":4096,"minLength":1,"type":"string"},"revisionId":{"description":"Historical revision whose exact content should be restored.","pattern":"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$","type":"string"},"targetAgentId":{"description":"Same-company target agent. Omit to use the calling agent.","pattern":"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$","type":"string"}},"required":["entryFile","revisionId","baseRevisionId"],"type":"object"},"name":"restore_agent_instructions","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"optional","operationId":"create_project","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Create a project after considering existing projects and available repositories. repositoryIds and repositoryUrls accept multiple existing repositories. Use HTTPS GitHub repositoryUrls when an accessible repo is not in the catalog; this registers project repositories, not remote GitHub repositories. Non-code projects may omit repositories. Cannot combine repositoryIds/repositoryUrls with workspace. Reuse the idempotency key on retries.","inputSchema":{"additionalProperties":false,"properties":{"archivedAt":{"description":"Archive timestamp.","maxLength":20000,"type":["string","null"]},"color":{"description":"Project color.","maxLength":20000,"type":["string","null"]},"description":{"description":"Project outcome and context.","maxLength":20000,"type":["string","null"]},"env":{"additionalProperties":true,"type":"object"},"executionWorkspacePolicy":{"additionalProperties":true,"type":"object"},"goalId":{"description":"Goal ID.","maxLength":20000,"type":["string","null"]},"goalIds":{"description":"Goal IDs.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"icon":{"description":"Project icon.","enum":["folder","rocket","code","terminal","database","globe","package","boxes","box","layers","briefcase","compass","target","flame","zap","star","bug","wrench","hammer","lightbulb","sparkles","shield","lock","search","cog","brain","cpu","git-branch","file-code","puzzle","gem","atom","heart","mail","message-square","crown","radar","telescope","hexagon",null],"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":{"maxLength":2000,"pattern":"^https://github\\.com/(?!\\.{1,2}/)[A-Za-z0-9_.-]+/(?!\\.{1,2}/?$)[A-Za-z0-9_.-]+/?$","type":"string"},"maxItems":100,"type":"array"},"status":{"enum":["backlog","planned","in_progress","completed","cancelled"]},"targetDate":{"description":"Target date.","maxLength":20000,"type":["string","null"]},"workspace":{"additionalProperties":true,"type":"object"}},"required":["idempotencyKey","name"],"type":"object"},"name":"create_project","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"optional","operationId":"list_project_repositories","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"List authorized repositories with stable IDs and names. Consider appropriate repositories before creating a project; never invent IDs.","inputSchema":{"additionalProperties":false,"properties":{},"required":[],"type":"object"},"name":"list_project_repositories","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"optional","operationId":"list_projects","requiredClaims":["discovery:projects:read"],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Inspect available company projects before selecting a project for new work.","inputSchema":{"additionalProperties":false,"properties":{},"required":[],"type":"object"},"name":"list_projects","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"optional","operationId":"create_skill","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Create a reusable single-file skill in the company library. Supply a complete SKILL.md whose name and description match the inputs. This saves the skill and shows a card; it does not assign the skill to any agent. Reuse idempotencyKey on retries.","inputSchema":{"additionalProperties":false,"properties":{"description":{"maxLength":2000,"minLength":1,"type":"string"},"idempotencyKey":{"maxLength":240,"minLength":1,"type":"string"},"markdown":{"description":"Complete SKILL.md including name and description frontmatter and substantive instructions.","maxLength":200000,"minLength":1,"type":"string"},"name":{"description":"Lowercase skill name, matching SKILL.md frontmatter.","maxLength":120,"minLength":1,"pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$","type":"string"},"slug":{"description":"Optional; must equal name.","maxLength":120,"minLength":1,"pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$","type":"string"}},"required":["idempotencyKey","name","description","markdown"],"type":"object"},"name":"create_skill","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"optional","operationId":"reassign_task","requiredClaims":["delegation:tasks:assign"],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Reassign an existing task to another company agent. Read search_tasks first and supply its current assignee and statusVersion to prevent overwriting a concurrent change. Preserve the task, documents, dependencies, and blocked/backlog status. Active work is stopped before handoff. Use a stable idempotency key for retries. Cannot reassign this run’s own task, conversations, completed tasks, or pending reviews; use create_task for delegation from the current task.","inputSchema":{"additionalProperties":false,"properties":{"assigneeActorId":{"description":"New company agent ID from list_agents.","minLength":1,"type":"string"},"expectedAssigneeActorId":{"description":"Current assigneeAgentId from search_tasks; null means unassigned.","type":["string","null"]},"expectedStatusVersion":{"description":"Current statusVersion from search_tasks.","minimum":0,"type":"integer"},"idempotencyKey":{"description":"Caller-stable retry key.","maxLength":240,"minLength":1,"type":"string"},"reason":{"description":"Why the task should move and context for the new owner.","maxLength":20000,"minLength":1,"type":"string"},"taskId":{"description":"Existing task ID from search_tasks.","minLength":1,"type":"string"}},"required":["idempotencyKey","taskId","assigneeActorId","expectedAssigneeActorId","expectedStatusVersion","reason"],"type":"object"},"name":"reassign_task","outputSchema":{"additionalProperties":false,"properties":{"commandId":{"description":"Stable command identifier.","maxLength":200,"minLength":1,"type":"string"},"disposition":{"enum":["applied","duplicate"]},"entityRefs":{"description":"Entities affected by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"scheduledWakeIds":{"description":"Wake identifiers scheduled by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"stateRevision":{"minimum":0,"type":"integer"}},"required":["commandId","disposition","stateRevision","entityRefs","scheduledWakeIds"],"type":"object"}},{"annotations":{"exposure":"optional","operationId":"set_task_title","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Set a concise, descriptive title for the active task. When its titleNeedsGeneration is true, call this early with onlyIfProvisional: true to replace the initial prompt slice without overwriting a user's title. Use false only for an intentional rename. This changes no task status, ownership, or description.","inputSchema":{"additionalProperties":false,"properties":{"idempotencyKey":{"description":"Reuse this key on retries.","maxLength":240,"minLength":1,"type":"string"},"onlyIfProvisional":{"description":"True for automatic initial naming; preserves any title already chosen by a user or agent.","type":"boolean"},"title":{"description":"Short title describing the requested outcome.","maxLength":240,"minLength":1,"type":"string"}},"required":["idempotencyKey","title","onlyIfProvisional"],"type":"object"},"name":"set_task_title","outputSchema":{"additionalProperties":true,"type":"object"}}] diff --git a/packages/paperclip-runner/protocol/fixtures/evals/native-execution-seeded.json b/packages/paperclip-runner/protocol/fixtures/evals/native-execution-seeded.json index 0c6a704d544..24624d64ea1 100644 --- a/packages/paperclip-runner/protocol/fixtures/evals/native-execution-seeded.json +++ b/packages/paperclip-runner/protocol/fixtures/evals/native-execution-seeded.json @@ -24,7 +24,7 @@ "prpVersion": 1, "nativeExecutionVersion": 1, "catalogVersion": 1, - "catalogSha256": "sha256:8a0dd1013634b17288a63f745782783d086c71f7c50881c9133534e73e372627", + "catalogSha256": "sha256:81ed59a7fa4ceb6e0f28dffabf7a7ae7bdfa9e23e6bb643a14612368262abfe3", "driverContractVersion": 1, "driverKind": "paperclip-deterministic", "driverVersion": "1.0.0" diff --git a/packages/paperclip-runner/protocol/manifest.json b/packages/paperclip-runner/protocol/manifest.json index d975a0ebafe..f2162c99cad 100644 --- a/packages/paperclip-runner/protocol/manifest.json +++ b/packages/paperclip-runner/protocol/manifest.json @@ -160,7 +160,7 @@ }, { "path": "fixtures/evals/native-execution-seeded.json", - "sha256": "a0f8a3804b062746cdd893d5713fb714ef137f6ef30e37102a4b1bcb7cf66ec5", + "sha256": "a5fde52c41fe77a8872ec1d1613ba9bb426b9231835e1464d943930ab799d933", "expectation": "accept", "compatibilityCase": "canonical" }, diff --git a/packages/paperclip-runner/spec/capability/capabilities.yaml b/packages/paperclip-runner/spec/capability/capabilities.yaml index 5ec6b6501d7..5e07d65358f 100644 --- a/packages/paperclip-runner/spec/capability/capabilities.yaml +++ b/packages/paperclip-runner/spec/capability/capabilities.yaml @@ -104,9 +104,9 @@ ] }, { - "id": "skill:skills/paperclip/SKILL.md:generated-artifacts-and-work-products:197", + "id": "skill:skills/paperclip/SKILL.md:generated-artifacts-and-work-products:196", "sourceKind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md:197", + "sourceAnchor": "skills/paperclip/SKILL.md:196", "title": "Generated Artifacts and Work Products", "expectedSemantics": "Skill guidance headed “Generated Artifacts and Work Products”.", "primaryDisposition": "always_agent_tool", @@ -115,13 +115,13 @@ "control_plane_invariant" ], "evidenceIds": [ - "skill:skills/paperclip/SKILL.md:197" + "skill:skills/paperclip/SKILL.md:196" ] }, { - "id": "skill:skills/paperclip/SKILL.md:status-quick-guide:245", + "id": "skill:skills/paperclip/SKILL.md:status-quick-guide:244", "sourceKind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md:245", + "sourceAnchor": "skills/paperclip/SKILL.md:244", "title": "Status Quick Guide", "expectedSemantics": "Skill guidance headed “Status Quick Guide”.", "primaryDisposition": "control_plane_owned", @@ -130,13 +130,13 @@ "control_plane_invariant" ], "evidenceIds": [ - "skill:skills/paperclip/SKILL.md:245" + "skill:skills/paperclip/SKILL.md:244" ] }, { - "id": "skill:skills/paperclip/SKILL.md:monitors-and-watchers-say-only-what-you-actually-scheduled:255", + "id": "skill:skills/paperclip/SKILL.md:monitors-and-watchers-say-only-what-you-actually-scheduled:254", "sourceKind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md:255", + "sourceAnchor": "skills/paperclip/SKILL.md:254", "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", @@ -145,13 +145,13 @@ "control_plane_invariant" ], "evidenceIds": [ - "skill:skills/paperclip/SKILL.md:255" + "skill:skills/paperclip/SKILL.md:254" ] }, { - "id": "skill:skills/paperclip/SKILL.md:delegating-review-tasks:268", + "id": "skill:skills/paperclip/SKILL.md:delegating-review-tasks:267", "sourceKind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md:268", + "sourceAnchor": "skills/paperclip/SKILL.md:267", "title": "Delegating review tasks", "expectedSemantics": "Skill guidance headed “Delegating review tasks”.", "primaryDisposition": "always_agent_tool", @@ -160,13 +160,13 @@ "control_plane_invariant" ], "evidenceIds": [ - "skill:skills/paperclip/SKILL.md:268" + "skill:skills/paperclip/SKILL.md:267" ] }, { - "id": "skill:skills/paperclip/SKILL.md:managing-a-user-s-inbox:279", + "id": "skill:skills/paperclip/SKILL.md:managing-a-user-s-inbox:278", "sourceKind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md:279", + "sourceAnchor": "skills/paperclip/SKILL.md:278", "title": "Managing A User's Inbox", "expectedSemantics": "Skill guidance headed “Managing A User's Inbox”.", "primaryDisposition": "control_plane_owned", @@ -175,13 +175,13 @@ "control_plane_invariant" ], "evidenceIds": [ - "skill:skills/paperclip/SKILL.md:279" + "skill:skills/paperclip/SKILL.md:278" ] }, { - "id": "skill:skills/paperclip/SKILL.md:issue-dependencies-blockers:287", + "id": "skill:skills/paperclip/SKILL.md:issue-dependencies-blockers:286", "sourceKind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md:287", + "sourceAnchor": "skills/paperclip/SKILL.md:286", "title": "Issue Dependencies (Blockers)", "expectedSemantics": "Skill guidance headed “Issue Dependencies (Blockers)”.", "primaryDisposition": "control_plane_owned", @@ -190,13 +190,13 @@ "control_plane_invariant" ], "evidenceIds": [ - "skill:skills/paperclip/SKILL.md:287" + "skill:skills/paperclip/SKILL.md:286" ] }, { - "id": "skill:skills/paperclip/SKILL.md:requesting-board-approval:312", + "id": "skill:skills/paperclip/SKILL.md:requesting-board-approval:311", "sourceKind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md:312", + "sourceAnchor": "skills/paperclip/SKILL.md:311", "title": "Requesting Board Approval", "expectedSemantics": "Skill guidance headed “Requesting Board Approval”.", "primaryDisposition": "optional_agent_tool", @@ -205,13 +205,13 @@ "control_plane_invariant" ], "evidenceIds": [ - "skill:skills/paperclip/SKILL.md:312" + "skill:skills/paperclip/SKILL.md:311" ] }, { - "id": "skill:skills/paperclip/SKILL.md:issue-thread-interactions:333", + "id": "skill:skills/paperclip/SKILL.md:issue-thread-interactions:332", "sourceKind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md:333", + "sourceAnchor": "skills/paperclip/SKILL.md:332", "title": "Issue-Thread Interactions", "expectedSemantics": "Skill guidance headed “Issue-Thread Interactions”.", "primaryDisposition": "optional_agent_tool", @@ -220,13 +220,13 @@ "control_plane_invariant" ], "evidenceIds": [ - "skill:skills/paperclip/SKILL.md:333" + "skill:skills/paperclip/SKILL.md:332" ] }, { - "id": "skill:skills/paperclip/SKILL.md:standalone-decisions:362", + "id": "skill:skills/paperclip/SKILL.md:standalone-decisions:361", "sourceKind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md:362", + "sourceAnchor": "skills/paperclip/SKILL.md:361", "title": "Standalone Decisions", "expectedSemantics": "Skill guidance headed “Standalone Decisions”.", "primaryDisposition": "optional_agent_tool", @@ -235,13 +235,13 @@ "control_plane_invariant" ], "evidenceIds": [ - "skill:skills/paperclip/SKILL.md:362" + "skill:skills/paperclip/SKILL.md:361" ] }, { - "id": "skill:skills/paperclip/SKILL.md:mcp-tool-approval-gates:466", + "id": "skill:skills/paperclip/SKILL.md:mcp-tool-approval-gates:465", "sourceKind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md:466", + "sourceAnchor": "skills/paperclip/SKILL.md:465", "title": "MCP Tool Approval Gates", "expectedSemantics": "Skill guidance headed “MCP Tool Approval Gates”.", "primaryDisposition": "optional_agent_tool", @@ -250,13 +250,13 @@ "control_plane_invariant" ], "evidenceIds": [ - "skill:skills/paperclip/SKILL.md:466" + "skill:skills/paperclip/SKILL.md:465" ] }, { - "id": "skill:skills/paperclip/SKILL.md:niche-workflow-pointers:508", + "id": "skill:skills/paperclip/SKILL.md:niche-workflow-pointers:507", "sourceKind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md:508", + "sourceAnchor": "skills/paperclip/SKILL.md:507", "title": "Niche Workflow Pointers", "expectedSemantics": "Skill guidance headed “Niche Workflow Pointers”.", "primaryDisposition": "optional_agent_tool", @@ -265,13 +265,13 @@ "control_plane_invariant" ], "evidenceIds": [ - "skill:skills/paperclip/SKILL.md:508" + "skill:skills/paperclip/SKILL.md:507" ] }, { - "id": "skill:skills/paperclip/SKILL.md:cases:518", + "id": "skill:skills/paperclip/SKILL.md:cases:517", "sourceKind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md:518", + "sourceAnchor": "skills/paperclip/SKILL.md:517", "title": "Cases", "expectedSemantics": "Skill guidance headed “Cases”.", "primaryDisposition": "optional_agent_tool", @@ -280,13 +280,13 @@ "control_plane_invariant" ], "evidenceIds": [ - "skill:skills/paperclip/SKILL.md:518" + "skill:skills/paperclip/SKILL.md:517" ] }, { - "id": "skill:skills/paperclip/SKILL.md:company-skills-workflow:523", + "id": "skill:skills/paperclip/SKILL.md:company-skills-workflow:522", "sourceKind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md:523", + "sourceAnchor": "skills/paperclip/SKILL.md:522", "title": "Company Skills Workflow", "expectedSemantics": "Skill guidance headed “Company Skills Workflow”.", "primaryDisposition": "optional_agent_tool", @@ -295,13 +295,13 @@ "control_plane_invariant" ], "evidenceIds": [ - "skill:skills/paperclip/SKILL.md:523" + "skill:skills/paperclip/SKILL.md:522" ] }, { - "id": "skill:skills/paperclip/SKILL.md:routines:534", + "id": "skill:skills/paperclip/SKILL.md:routines:533", "sourceKind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md:534", + "sourceAnchor": "skills/paperclip/SKILL.md:533", "title": "Routines", "expectedSemantics": "Skill guidance headed “Routines”.", "primaryDisposition": "optional_agent_tool", @@ -310,13 +310,13 @@ "control_plane_invariant" ], "evidenceIds": [ - "skill:skills/paperclip/SKILL.md:534" + "skill:skills/paperclip/SKILL.md:533" ] }, { - "id": "skill:skills/paperclip/SKILL.md:issue-workspace-runtime-controls:545", + "id": "skill:skills/paperclip/SKILL.md:issue-workspace-runtime-controls:544", "sourceKind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md:545", + "sourceAnchor": "skills/paperclip/SKILL.md:544", "title": "Issue Workspace Runtime Controls", "expectedSemantics": "Skill guidance headed “Issue Workspace Runtime Controls”.", "primaryDisposition": "optional_agent_tool", @@ -325,13 +325,13 @@ "control_plane_invariant" ], "evidenceIds": [ - "skill:skills/paperclip/SKILL.md:545" + "skill:skills/paperclip/SKILL.md:544" ] }, { - "id": "skill:skills/paperclip/SKILL.md:proposing-credentials-safely:552", + "id": "skill:skills/paperclip/SKILL.md:proposing-credentials-safely:551", "sourceKind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md:552", + "sourceAnchor": "skills/paperclip/SKILL.md:551", "title": "Proposing Credentials Safely", "expectedSemantics": "Skill guidance headed “Proposing Credentials Safely”.", "primaryDisposition": "optional_agent_tool", @@ -340,13 +340,13 @@ "control_plane_invariant" ], "evidenceIds": [ - "skill:skills/paperclip/SKILL.md:552" + "skill:skills/paperclip/SKILL.md:551" ] }, { - "id": "skill:skills/paperclip/SKILL.md:reading-granted-secrets:559", + "id": "skill:skills/paperclip/SKILL.md:reading-granted-secrets:558", "sourceKind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md:559", + "sourceAnchor": "skills/paperclip/SKILL.md:558", "title": "Reading Granted Secrets", "expectedSemantics": "Skill guidance headed “Reading Granted Secrets”.", "primaryDisposition": "optional_agent_tool", @@ -355,13 +355,13 @@ "control_plane_invariant" ], "evidenceIds": [ - "skill:skills/paperclip/SKILL.md:559" + "skill:skills/paperclip/SKILL.md:558" ] }, { - "id": "skill:skills/paperclip/SKILL.md:critical-rules:585", + "id": "skill:skills/paperclip/SKILL.md:critical-rules:584", "sourceKind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md:585", + "sourceAnchor": "skills/paperclip/SKILL.md:584", "title": "Critical Rules", "expectedSemantics": "Skill guidance headed “Critical Rules”.", "primaryDisposition": "optional_agent_tool", @@ -370,13 +370,13 @@ "control_plane_invariant" ], "evidenceIds": [ - "skill:skills/paperclip/SKILL.md:585" + "skill:skills/paperclip/SKILL.md:584" ] }, { - "id": "skill:skills/paperclip/SKILL.md:comment-style-required:606", + "id": "skill:skills/paperclip/SKILL.md:comment-style-required:605", "sourceKind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md:606", + "sourceAnchor": "skills/paperclip/SKILL.md:605", "title": "Comment Style (Required)", "expectedSemantics": "Skill guidance headed “Comment Style (Required)”.", "primaryDisposition": "always_agent_tool", @@ -385,13 +385,13 @@ "control_plane_invariant" ], "evidenceIds": [ - "skill:skills/paperclip/SKILL.md:606" + "skill:skills/paperclip/SKILL.md:605" ] }, { - "id": "skill:skills/paperclip/SKILL.md:update:638", + "id": "skill:skills/paperclip/SKILL.md:update:637", "sourceKind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md:638", + "sourceAnchor": "skills/paperclip/SKILL.md:637", "title": "Update", "expectedSemantics": "Skill guidance headed “Update”.", "primaryDisposition": "optional_agent_tool", @@ -400,13 +400,13 @@ "control_plane_invariant" ], "evidenceIds": [ - "skill:skills/paperclip/SKILL.md:638" + "skill:skills/paperclip/SKILL.md:637" ] }, { - "id": "skill:skills/paperclip/SKILL.md:planning-required-when-planning-requested:648", + "id": "skill:skills/paperclip/SKILL.md:planning-required-when-planning-requested:647", "sourceKind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md:648", + "sourceAnchor": "skills/paperclip/SKILL.md:647", "title": "Planning (Required when planning requested)", "expectedSemantics": "Skill guidance headed “Planning (Required when planning requested)”.", "primaryDisposition": "optional_agent_tool", @@ -415,13 +415,13 @@ "control_plane_invariant" ], "evidenceIds": [ - "skill:skills/paperclip/SKILL.md:648" + "skill:skills/paperclip/SKILL.md:647" ] }, { - "id": "skill:skills/paperclip/SKILL.md:key-endpoints-hot-routes:681", + "id": "skill:skills/paperclip/SKILL.md:key-endpoints-hot-routes:680", "sourceKind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md:681", + "sourceAnchor": "skills/paperclip/SKILL.md:680", "title": "Key Endpoints (Hot Routes)", "expectedSemantics": "Skill guidance headed “Key Endpoints (Hot Routes)”.", "primaryDisposition": "optional_agent_tool", @@ -430,13 +430,13 @@ "control_plane_invariant" ], "evidenceIds": [ - "skill:skills/paperclip/SKILL.md:681" + "skill:skills/paperclip/SKILL.md:680" ] }, { - "id": "skill:skills/paperclip/SKILL.md:searching-issues:710", + "id": "skill:skills/paperclip/SKILL.md:searching-issues:709", "sourceKind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md:710", + "sourceAnchor": "skills/paperclip/SKILL.md:709", "title": "Searching Issues", "expectedSemantics": "Skill guidance headed “Searching Issues”.", "primaryDisposition": "optional_agent_tool", @@ -445,13 +445,13 @@ "control_plane_invariant" ], "evidenceIds": [ - "skill:skills/paperclip/SKILL.md:710" + "skill:skills/paperclip/SKILL.md:709" ] }, { - "id": "skill:skills/paperclip/SKILL.md:full-reference:720", + "id": "skill:skills/paperclip/SKILL.md:full-reference:719", "sourceKind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md:720", + "sourceAnchor": "skills/paperclip/SKILL.md:719", "title": "Full Reference", "expectedSemantics": "Skill guidance headed “Full Reference”.", "primaryDisposition": "optional_agent_tool", @@ -460,13 +460,13 @@ "control_plane_invariant" ], "evidenceIds": [ - "skill:skills/paperclip/SKILL.md:720" + "skill:skills/paperclip/SKILL.md:719" ] }, { - "id": "skill:skills/paperclip/SKILL.md:conversational-confirmation-answers:724", + "id": "skill:skills/paperclip/SKILL.md:conversational-confirmation-answers:723", "sourceKind": "skill_heading", - "sourceAnchor": "skills/paperclip/SKILL.md:724", + "sourceAnchor": "skills/paperclip/SKILL.md:723", "title": "Conversational confirmation answers", "expectedSemantics": "Skill guidance headed “Conversational confirmation answers”.", "primaryDisposition": "always_agent_tool", @@ -475,7 +475,7 @@ "control_plane_invariant" ], "evidenceIds": [ - "skill:skills/paperclip/SKILL.md:724" + "skill:skills/paperclip/SKILL.md:723" ] }, { @@ -2099,9 +2099,9 @@ ] }, { - "id": "skill:skills/paperclip/references/api-reference.md:issue-thread-confirmations:1028", + "id": "skill:skills/paperclip/references/api-reference.md:issue-thread-confirmations:1025", "sourceKind": "skill_heading", - "sourceAnchor": "skills/paperclip/references/api-reference.md:1028", + "sourceAnchor": "skills/paperclip/references/api-reference.md:1025", "title": "Issue-thread confirmations", "expectedSemantics": "Skill guidance headed “Issue-thread confirmations”.", "primaryDisposition": "always_agent_tool", @@ -2110,13 +2110,13 @@ "control_plane_invariant" ], "evidenceIds": [ - "skill:skills/paperclip/references/api-reference.md:1028" + "skill:skills/paperclip/references/api-reference.md:1025" ] }, { - "id": "skill:skills/paperclip/references/api-reference.md:conversational-confirmation-answers:1086", + "id": "skill:skills/paperclip/references/api-reference.md:conversational-confirmation-answers:1083", "sourceKind": "skill_heading", - "sourceAnchor": "skills/paperclip/references/api-reference.md:1086", + "sourceAnchor": "skills/paperclip/references/api-reference.md:1083", "title": "Conversational confirmation answers", "expectedSemantics": "Skill guidance headed “Conversational confirmation answers”.", "primaryDisposition": "always_agent_tool", @@ -2125,13 +2125,13 @@ "control_plane_invariant" ], "evidenceIds": [ - "skill:skills/paperclip/references/api-reference.md:1086" + "skill:skills/paperclip/references/api-reference.md:1083" ] }, { - "id": "skill:skills/paperclip/references/api-reference.md:checkbox-confirmations:1092", + "id": "skill:skills/paperclip/references/api-reference.md:checkbox-confirmations:1089", "sourceKind": "skill_heading", - "sourceAnchor": "skills/paperclip/references/api-reference.md:1092", + "sourceAnchor": "skills/paperclip/references/api-reference.md:1089", "title": "Checkbox confirmations", "expectedSemantics": "Skill guidance headed “Checkbox confirmations”.", "primaryDisposition": "always_agent_tool", @@ -2140,13 +2140,13 @@ "control_plane_invariant" ], "evidenceIds": [ - "skill:skills/paperclip/references/api-reference.md:1092" + "skill:skills/paperclip/references/api-reference.md:1089" ] }, { - "id": "skill:skills/paperclip/references/api-reference.md:item-verdict-requests:1207", + "id": "skill:skills/paperclip/references/api-reference.md:item-verdict-requests:1204", "sourceKind": "skill_heading", - "sourceAnchor": "skills/paperclip/references/api-reference.md:1207", + "sourceAnchor": "skills/paperclip/references/api-reference.md:1204", "title": "Item verdict requests", "expectedSemantics": "Skill guidance headed “Item verdict requests”.", "primaryDisposition": "optional_agent_tool", @@ -2155,13 +2155,13 @@ "control_plane_invariant" ], "evidenceIds": [ - "skill:skills/paperclip/references/api-reference.md:1207" + "skill:skills/paperclip/references/api-reference.md:1204" ] }, { - "id": "skill:skills/paperclip/references/api-reference.md:checking-approval-status:1317", + "id": "skill:skills/paperclip/references/api-reference.md:checking-approval-status:1314", "sourceKind": "skill_heading", - "sourceAnchor": "skills/paperclip/references/api-reference.md:1317", + "sourceAnchor": "skills/paperclip/references/api-reference.md:1314", "title": "Checking approval status", "expectedSemantics": "Skill guidance headed “Checking approval status”.", "primaryDisposition": "optional_agent_tool", @@ -2170,13 +2170,13 @@ "control_plane_invariant" ], "evidenceIds": [ - "skill:skills/paperclip/references/api-reference.md:1317" + "skill:skills/paperclip/references/api-reference.md:1314" ] }, { - "id": "skill:skills/paperclip/references/api-reference.md:approval-follow-up-requesting-agent:1323", + "id": "skill:skills/paperclip/references/api-reference.md:approval-follow-up-requesting-agent:1320", "sourceKind": "skill_heading", - "sourceAnchor": "skills/paperclip/references/api-reference.md:1323", + "sourceAnchor": "skills/paperclip/references/api-reference.md:1320", "title": "Approval follow-up (requesting agent)", "expectedSemantics": "Skill guidance headed “Approval follow-up (requesting agent)”.", "primaryDisposition": "always_agent_tool", @@ -2185,13 +2185,13 @@ "control_plane_invariant" ], "evidenceIds": [ - "skill:skills/paperclip/references/api-reference.md:1323" + "skill:skills/paperclip/references/api-reference.md:1320" ] }, { - "id": "skill:skills/paperclip/references/api-reference.md:issue-lifecycle:1341", + "id": "skill:skills/paperclip/references/api-reference.md:issue-lifecycle:1338", "sourceKind": "skill_heading", - "sourceAnchor": "skills/paperclip/references/api-reference.md:1341", + "sourceAnchor": "skills/paperclip/references/api-reference.md:1338", "title": "Issue Lifecycle", "expectedSemantics": "Skill guidance headed “Issue Lifecycle”.", "primaryDisposition": "always_agent_tool", @@ -2200,13 +2200,13 @@ "control_plane_invariant" ], "evidenceIds": [ - "skill:skills/paperclip/references/api-reference.md:1341" + "skill:skills/paperclip/references/api-reference.md:1338" ] }, { - "id": "skill:skills/paperclip/references/api-reference.md:error-handling:1371", + "id": "skill:skills/paperclip/references/api-reference.md:error-handling:1368", "sourceKind": "skill_heading", - "sourceAnchor": "skills/paperclip/references/api-reference.md:1371", + "sourceAnchor": "skills/paperclip/references/api-reference.md:1368", "title": "Error Handling", "expectedSemantics": "Skill guidance headed “Error Handling”.", "primaryDisposition": "control_plane_owned", @@ -2215,13 +2215,13 @@ "control_plane_invariant" ], "evidenceIds": [ - "skill:skills/paperclip/references/api-reference.md:1371" + "skill:skills/paperclip/references/api-reference.md:1368" ] }, { - "id": "skill:skills/paperclip/references/api-reference.md:full-api-reference:1385", + "id": "skill:skills/paperclip/references/api-reference.md:full-api-reference:1382", "sourceKind": "skill_heading", - "sourceAnchor": "skills/paperclip/references/api-reference.md:1385", + "sourceAnchor": "skills/paperclip/references/api-reference.md:1382", "title": "Full API Reference", "expectedSemantics": "Skill guidance headed “Full API Reference”.", "primaryDisposition": "optional_agent_tool", @@ -2230,13 +2230,13 @@ "control_plane_invariant" ], "evidenceIds": [ - "skill:skills/paperclip/references/api-reference.md:1385" + "skill:skills/paperclip/references/api-reference.md:1382" ] }, { - "id": "skill:skills/paperclip/references/api-reference.md:agents:1387", + "id": "skill:skills/paperclip/references/api-reference.md:agents:1384", "sourceKind": "skill_heading", - "sourceAnchor": "skills/paperclip/references/api-reference.md:1387", + "sourceAnchor": "skills/paperclip/references/api-reference.md:1384", "title": "Agents", "expectedSemantics": "Skill guidance headed “Agents”.", "primaryDisposition": "optional_agent_tool", @@ -2245,13 +2245,13 @@ "control_plane_invariant" ], "evidenceIds": [ - "skill:skills/paperclip/references/api-reference.md:1387" + "skill:skills/paperclip/references/api-reference.md:1384" ] }, { - "id": "skill:skills/paperclip/references/api-reference.md:issues-tasks:1408", + "id": "skill:skills/paperclip/references/api-reference.md:issues-tasks:1405", "sourceKind": "skill_heading", - "sourceAnchor": "skills/paperclip/references/api-reference.md:1408", + "sourceAnchor": "skills/paperclip/references/api-reference.md:1405", "title": "Issues (Tasks)", "expectedSemantics": "Skill guidance headed “Issues (Tasks)”.", "primaryDisposition": "optional_agent_tool", @@ -2260,13 +2260,13 @@ "control_plane_invariant" ], "evidenceIds": [ - "skill:skills/paperclip/references/api-reference.md:1408" + "skill:skills/paperclip/references/api-reference.md:1405" ] }, { - "id": "skill:skills/paperclip/references/api-reference.md:companies-projects-goals:1449", + "id": "skill:skills/paperclip/references/api-reference.md:companies-projects-goals:1446", "sourceKind": "skill_heading", - "sourceAnchor": "skills/paperclip/references/api-reference.md:1449", + "sourceAnchor": "skills/paperclip/references/api-reference.md:1446", "title": "Companies, Projects, Goals", "expectedSemantics": "Skill guidance headed “Companies, Projects, Goals”.", "primaryDisposition": "optional_agent_tool", @@ -2275,13 +2275,13 @@ "control_plane_invariant" ], "evidenceIds": [ - "skill:skills/paperclip/references/api-reference.md:1449" + "skill:skills/paperclip/references/api-reference.md:1446" ] }, { - "id": "skill:skills/paperclip/references/api-reference.md:routines:1473", + "id": "skill:skills/paperclip/references/api-reference.md:routines:1470", "sourceKind": "skill_heading", - "sourceAnchor": "skills/paperclip/references/api-reference.md:1473", + "sourceAnchor": "skills/paperclip/references/api-reference.md:1470", "title": "Routines", "expectedSemantics": "Skill guidance headed “Routines”.", "primaryDisposition": "optional_agent_tool", @@ -2290,13 +2290,13 @@ "control_plane_invariant" ], "evidenceIds": [ - "skill:skills/paperclip/references/api-reference.md:1473" + "skill:skills/paperclip/references/api-reference.md:1470" ] }, { - "id": "skill:skills/paperclip/references/api-reference.md:approvals-costs-activity-dashboard:1489", + "id": "skill:skills/paperclip/references/api-reference.md:approvals-costs-activity-dashboard:1486", "sourceKind": "skill_heading", - "sourceAnchor": "skills/paperclip/references/api-reference.md:1489", + "sourceAnchor": "skills/paperclip/references/api-reference.md:1486", "title": "Approvals, Costs, Activity, Dashboard", "expectedSemantics": "Skill guidance headed “Approvals, Costs, Activity, Dashboard”.", "primaryDisposition": "optional_agent_tool", @@ -2305,13 +2305,13 @@ "control_plane_invariant" ], "evidenceIds": [ - "skill:skills/paperclip/references/api-reference.md:1489" + "skill:skills/paperclip/references/api-reference.md:1486" ] }, { - "id": "skill:skills/paperclip/references/api-reference.md:secrets:1511", + "id": "skill:skills/paperclip/references/api-reference.md:secrets:1508", "sourceKind": "skill_heading", - "sourceAnchor": "skills/paperclip/references/api-reference.md:1511", + "sourceAnchor": "skills/paperclip/references/api-reference.md:1508", "title": "Secrets", "expectedSemantics": "Skill guidance headed “Secrets”.", "primaryDisposition": "optional_agent_tool", @@ -2320,13 +2320,13 @@ "control_plane_invariant" ], "evidenceIds": [ - "skill:skills/paperclip/references/api-reference.md:1511" + "skill:skills/paperclip/references/api-reference.md:1508" ] }, { - "id": "skill:skills/paperclip/references/api-reference.md:agent-secret-proposals:1524", + "id": "skill:skills/paperclip/references/api-reference.md:agent-secret-proposals:1521", "sourceKind": "skill_heading", - "sourceAnchor": "skills/paperclip/references/api-reference.md:1524", + "sourceAnchor": "skills/paperclip/references/api-reference.md:1521", "title": "Agent secret proposals", "expectedSemantics": "Skill guidance headed “Agent secret proposals”.", "primaryDisposition": "optional_agent_tool", @@ -2335,13 +2335,13 @@ "control_plane_invariant" ], "evidenceIds": [ - "skill:skills/paperclip/references/api-reference.md:1524" + "skill:skills/paperclip/references/api-reference.md:1521" ] }, { - "id": "skill:skills/paperclip/references/api-reference.md:agent-secret-access:1624", + "id": "skill:skills/paperclip/references/api-reference.md:agent-secret-access:1621", "sourceKind": "skill_heading", - "sourceAnchor": "skills/paperclip/references/api-reference.md:1624", + "sourceAnchor": "skills/paperclip/references/api-reference.md:1621", "title": "Agent secret access", "expectedSemantics": "Skill guidance headed “Agent secret access”.", "primaryDisposition": "optional_agent_tool", @@ -2350,13 +2350,13 @@ "control_plane_invariant" ], "evidenceIds": [ - "skill:skills/paperclip/references/api-reference.md:1624" + "skill:skills/paperclip/references/api-reference.md:1621" ] }, { - "id": "skill:skills/paperclip/references/api-reference.md:common-mistakes:1664", + "id": "skill:skills/paperclip/references/api-reference.md:common-mistakes:1661", "sourceKind": "skill_heading", - "sourceAnchor": "skills/paperclip/references/api-reference.md:1664", + "sourceAnchor": "skills/paperclip/references/api-reference.md:1661", "title": "Common Mistakes", "expectedSemantics": "Skill guidance headed “Common Mistakes”.", "primaryDisposition": "optional_agent_tool", @@ -2365,7 +2365,7 @@ "control_plane_invariant" ], "evidenceIds": [ - "skill:skills/paperclip/references/api-reference.md:1664" + "skill:skills/paperclip/references/api-reference.md:1661" ] } ] diff --git a/packages/paperclip-runner/src/live/clean-room-server.test.ts b/packages/paperclip-runner/src/live/clean-room-server.test.ts index 67b94f9930e..051ee530763 100644 --- a/packages/paperclip-runner/src/live/clean-room-server.test.ts +++ b/packages/paperclip-runner/src/live/clean-room-server.test.ts @@ -234,7 +234,7 @@ class FakeCleanRoomTransport implements CodexAppServerTransport { interactionKind: "questions", title: "Which way should I take this?", prompt: "Pick a direction for the clean-room issue.", - payload: { questions: [{ id: "path", prompt: "Which path?", options: ["a", "b"] }] }, + payload: { version: 1, questionSet: { schema: "paperclip.question_set.v1", questions: [{ id: "path", prompt: "Which path?", required: true, answerMode: "single_select", options: [{ id: "a", label: "Path A" }, { id: "b", label: "Path B" }] }] } }, continuationPolicy: "wake_assignee", }); } diff --git a/packages/paperclip-runner/src/live/live-session.test.ts b/packages/paperclip-runner/src/live/live-session.test.ts index 938479a0da6..e99da299374 100644 --- a/packages/paperclip-runner/src/live/live-session.test.ts +++ b/packages/paperclip-runner/src/live/live-session.test.ts @@ -244,7 +244,7 @@ class FakeCapabilityCodexTransport implements CodexAppServerTransport { interactionKind: "questions", title: "Choose the mock path", prompt: "Which path should the mock agent take?", - payload: { fields: [{ id: "path", label: "Path" }] }, + payload: { version: 1, questionSet: { schema: "paperclip.question_set.v1", questions: [{ id: "path", prompt: "Which path?", required: true, answerMode: "single_select", options: [{ id: "safe", label: "Safe path" }, { id: "fast", label: "Fast path" }] }] } }, continuationPolicy: "wake_assignee", }, }, diff --git a/packages/paperclip-runner/src/protocol-actions/core-contracts.test.ts b/packages/paperclip-runner/src/protocol-actions/core-contracts.test.ts index 3d8a714a258..4eba22d1b53 100644 --- a/packages/paperclip-runner/src/protocol-actions/core-contracts.test.ts +++ b/packages/paperclip-runner/src/protocol-actions/core-contracts.test.ts @@ -6,6 +6,22 @@ import { PAPERCLIP_CORE_PROTOCOL_ACTIONS } from "./core.js"; describe("core Paperclip protocol action contracts", () => { const ajv = new Ajv2020({ allErrors: true, allowUnionTypes: true, strict: false }); + it("advertises a complete typed form for mixed text and choice questions", () => { + const action = PAPERCLIP_CORE_PROTOCOL_ACTIONS.find(action => action.id === "request_human_input")!; + const payload = { version: 1, questionSet: { schema: "paperclip.question_set.v1", questions: [ + { id: "repo", prompt: "Repository URL?", required: true, answerMode: "text" }, + { id: "scope", prompt: "Review scope?", required: true, answerMode: "single_select", options: [{ id: "all", label: "All changes" }, { id: "selected", label: "Selected changes" }] }, + ] } }; + for (const schema of [action.live!.descriptor.inputSchema, action.scenario!.descriptor.inputSchema]) { + const input = { ...action.examples.call.input, interactionKind: "questions", payload }; + if (schema === action.scenario!.descriptor.inputSchema) delete (input as { idempotencyKey?: string }).idempotencyKey; + expect(ajv.validate(schema, input), JSON.stringify(ajv.errors)).toBe(true); + expect(ajv.validate(schema, { ...input, payload: { version: 1 } })).toBe(false); + expect(ajv.validate(schema, { ...input, payload: { version: 1, questionSet: { ...payload.questionSet, questions: [{ ...payload.questionSet.questions[0], options: [{ id: "a", label: "A" }] }] } } })).toBe(false); + } + expect(action.documentation.description).not.toContain("questions for choices"); + }); + it("keeps the live input tool's guidance consistent with conversational confirmation recording", () => { const action = PAPERCLIP_CORE_PROTOCOL_ACTIONS.find(action => action.id === "request_human_input")!; expect(action.live!.descriptor.description).toBe(action.documentation.description); diff --git a/packages/paperclip-runner/src/protocol-actions/question-payload-schema.ts b/packages/paperclip-runner/src/protocol-actions/question-payload-schema.ts new file mode 100644 index 00000000000..55b54f6e6ee --- /dev/null +++ b/packages/paperclip-runner/src/protocol-actions/question-payload-schema.ts @@ -0,0 +1,66 @@ +import { questionSetSchema } from "../protocol/generated/schema-bundle.js"; + +// Inline the protocol's local references so each provider tool schema is +// self-contained, without a second definition of the canonical question form. +const canonicalQuestionSchema = { + ...questionSetSchema.$defs.question, + properties: { + ...questionSetSchema.$defs.question.properties, + options: { ...questionSetSchema.$defs.question.properties.options, items: questionSetSchema.$defs.option }, + customAnswer: questionSetSchema.$defs.customAnswer, + textValidation: questionSetSchema.$defs.textValidation, + }, +} as const; + +export const questionPayloadSchema = { + type: "object", + description: "Kind-specific interaction data. For questions, send version:1 and one complete questionSet containing every text and choice question. Paperclip generates compatibility questions. Each canonical question needs id, prompt, required, and answerMode: text, single_select, or multi_select. Text questions have no options or customAnswer. Choice questions need at least two meaningful options with id/label. Use customAnswer:{enabled:true} for an optional written answer to a choice question. Legacy questions remain supported; if both representations are supplied, they must describe the same complete form. Keep IDs stable across retries. For confirmation, payload may be {}.", + properties: { + version: { const: 1 }, + questionSet: { + type: "object", + required: questionSetSchema.required, + properties: { + ...questionSetSchema.properties, + questions: { ...questionSetSchema.properties.questions, items: canonicalQuestionSchema }, + }, + additionalProperties: false, + }, + questions: { + type: "array", minItems: 1, maxItems: 64, + items: { + type: "object", required: ["id", "prompt", "selectionMode", "options"], + properties: { + id: { type: "string", minLength: 1, maxLength: 160 }, + prompt: { type: "string", minLength: 1, maxLength: 4000 }, + selectionMode: { enum: ["single", "multi"] }, + required: { type: "boolean" }, + options: { + type: "array", minItems: 1, maxItems: 129, + items: { + type: "object", required: ["id", "label"], + properties: { + id: { type: "string", minLength: 1, maxLength: 160 }, + label: { type: "string", minLength: 1, maxLength: 1000 }, + freeText: { type: "boolean" }, + }, + additionalProperties: true, + }, + }, + }, + additionalProperties: true, + }, + }, + }, + additionalProperties: true, +} as const; + +export const questionsPayloadRequirement = { + if: { properties: { interactionKind: { const: "questions" } }, required: ["interactionKind"] }, + then: { + required: ["payload"], + properties: { + payload: { required: ["version"], anyOf: [{ required: ["questionSet"] }, { required: ["questions"] }] }, + }, + }, +} as const; diff --git a/packages/paperclip-runner/src/protocol-actions/request-human-input.ts b/packages/paperclip-runner/src/protocol-actions/request-human-input.ts index bcbea6e0370..625faf79e5e 100644 --- a/packages/paperclip-runner/src/protocol-actions/request-human-input.ts +++ b/packages/paperclip-runner/src/protocol-actions/request-human-input.ts @@ -1,3 +1,5 @@ +import { questionPayloadSchema, questionsPayloadRequirement } from "./question-payload-schema.js"; + /** Canonical definition and documentation for `request_human_input`. */ export const requestHumanInputAction = { "id": "request_human_input", @@ -27,7 +29,7 @@ export const requestHumanInputAction = { }, "documentation": { "title": "Request structured human input", - "description": "Create a durable human question or approval card on the current Paperclip task bound to this run; Paperclip renders it and authenticates the response. Use questions with continuationPolicy='wake_assignee' when an answer is needed, including otherwise tool-free chat turns. Supply a stable idempotencyKey and reuse it on retries. For one question at a time, ask only the next unanswered question and wait for its real answer. Never fabricate answers or treat ambiguous clarification as approval. For an ordinary confirmation or checkbox card, record a clear user chat answer with call_api POST /api/issues/{id}/interactions/{interactionId}/resolve-from-comment, using the source commentId and decision (accept/reject), plus explicit selectedOptionIds for checkbox acceptance. Existing resolver permissions still apply; governed tool, secret, and connection approvals are excluded. Question forms retain their dedicated answer workflow. Preserve existing review gates. Call this tool before claiming a question was asked; if creation fails, report the failure. Do not fabricate answer links or Markdown buttons, post duplicate cards, or use call_api to create the card. Use payload.questions for choices and payload.questionSet for text fields; see the payload schema for formats.", + "description": "Create a durable human question or approval card on the current Paperclip task bound to this run; Paperclip renders it and authenticates the response. Use questions with continuationPolicy='wake_assignee' when an answer is needed, including otherwise tool-free chat turns. Supply a stable idempotencyKey and reuse it on retries. For one question at a time, ask only the next unanswered question and wait for its real answer. Never fabricate answers or treat ambiguous clarification as approval. For an ordinary confirmation or checkbox card, record a clear user chat answer with call_api POST /api/issues/{id}/interactions/{interactionId}/resolve-from-comment, using the source commentId and decision (accept/reject), plus explicit selectedOptionIds for checkbox acceptance. Existing resolver permissions still apply; governed tool, secret, and connection approvals are excluded. Question forms retain their dedicated answer workflow. Preserve existing review gates. Call this tool before claiming a question was asked; if creation fails, report the failure. Do not fabricate answer links or Markdown buttons, post duplicate cards, or use call_api to create the card. Use one complete payload.questionSet for text and choice questions. Paperclip generates compatibility questions; see the payload schema for formats.", "note": null }, "examples": { @@ -74,7 +76,7 @@ export const requestHumanInputAction = { "operationId": "request_human_input", "version": 1, "title": "Request structured human input", - "description": "Create a durable human question or approval card on the current Paperclip task bound to this run; Paperclip renders it and authenticates the response. Use questions with continuationPolicy='wake_assignee' when an answer is needed, including otherwise tool-free chat turns. Supply a stable idempotencyKey and reuse it on retries. For one question at a time, ask only the next unanswered question and wait for its real answer. Never fabricate answers or treat ambiguous clarification as approval. For an ordinary confirmation or checkbox card, record a clear user chat answer with call_api POST /api/issues/{id}/interactions/{interactionId}/resolve-from-comment, using the source commentId and decision (accept/reject), plus explicit selectedOptionIds for checkbox acceptance. Existing resolver permissions still apply; governed tool, secret, and connection approvals are excluded. Question forms retain their dedicated answer workflow. Preserve existing review gates. Call this tool before claiming a question was asked; if creation fails, report the failure. Do not fabricate answer links or Markdown buttons, post duplicate cards, or use call_api to create the card. Use payload.questions for choices and payload.questionSet for text fields; see the payload schema for formats.", + "description": "Create a durable human question or approval card on the current Paperclip task bound to this run; Paperclip renders it and authenticates the response. Use questions with continuationPolicy='wake_assignee' when an answer is needed, including otherwise tool-free chat turns. Supply a stable idempotencyKey and reuse it on retries. For one question at a time, ask only the next unanswered question and wait for its real answer. Never fabricate answers or treat ambiguous clarification as approval. For an ordinary confirmation or checkbox card, record a clear user chat answer with call_api POST /api/issues/{id}/interactions/{interactionId}/resolve-from-comment, using the source commentId and decision (accept/reject), plus explicit selectedOptionIds for checkbox acceptance. Existing resolver permissions still apply; governed tool, secret, and connection approvals are excluded. Question forms retain their dedicated answer workflow. Preserve existing review gates. Call this tool before claiming a question was asked; if creation fails, report the failure. Do not fabricate answer links or Markdown buttons, post duplicate cards, or use call_api to create the card. Use one complete payload.questionSet for text and choice questions. Paperclip generates compatibility questions; see the payload schema for formats.", "exposure": "always", "requiredClaims": [], "allowedModes": [ @@ -84,6 +86,7 @@ export const requestHumanInputAction = { "skill_test" ], "inputSchema": { + "allOf": [questionsPayloadRequirement], "type": "object", "properties": { "idempotencyKey": { @@ -113,11 +116,7 @@ export const requestHumanInputAction = { "minLength": 1, "maxLength": 10000 }, - "payload": { - "type": "object", - "description": "Kind-specific interaction data. For interactionKind='questions', use version:1 and questions:[{id,prompt,selectionMode:'single'|'multi',required?,options:[{id,label,description?,freeText?}]}]. Choice questions need at least two distinct meaningful options. For an open-ended text answer, ALSO include questionSet:{schema:'paperclip.question_set.v1',questions:[{id,prompt,answerMode:'text',required?}]} with no options or customAnswer in its text questions. Keep matching IDs/prompts in both arrays; the required compatibility questions entry uses selectionMode:'single' and options:[{id:'describe',label:'Your answer',freeText:true}]. Without questionSet this incorrectly renders as a one-option choice. Never use a lone Other or describe option as the presentation. Option keys are id/label, not value. For confirmation, payload may be {}. Keep IDs stable across retries.", - "additionalProperties": true - }, + "payload": questionPayloadSchema, "targetRevisionId": { "type": [ "string", @@ -217,6 +216,7 @@ export const requestHumanInputAction = { "title": "Request human input", "description": "Create a typed confirmation, checkbox, question, task suggestion, or item-verdict request.", "inputSchema": { + "allOf": [questionsPayloadRequirement], "type": "object", "properties": { "interactionKind": { @@ -237,9 +237,7 @@ export const requestHumanInputAction = { "type": "string", "minLength": 1 }, - "payload": { - "description": "Kind-specific interaction data. For interactionKind='questions', use version:1 and questions:[{id,prompt,selectionMode:'single'|'multi',required?,options:[{id,label,description?,freeText?}]}]. Choice questions need at least two distinct meaningful options. For an open-ended text answer, ALSO include questionSet:{schema:'paperclip.question_set.v1',questions:[{id,prompt,answerMode:'text',required?}]} with no options or customAnswer in its text questions. Keep matching IDs/prompts in both arrays; the required compatibility questions entry uses selectionMode:'single' and options:[{id:'describe',label:'Your answer',freeText:true}]. Without questionSet this incorrectly renders as a one-option choice. Never use a lone Other or describe option as the presentation. Option keys are id/label, not value. For confirmation, payload may be {}. Keep IDs stable across retries." - }, + "payload": questionPayloadSchema, "targetRevisionId": { "oneOf": [ { diff --git a/packages/plugins/sdk/src/protocol.ts b/packages/plugins/sdk/src/protocol.ts index 4605866261f..5476c10becb 100644 --- a/packages/plugins/sdk/src/protocol.ts +++ b/packages/plugins/sdk/src/protocol.ts @@ -30,7 +30,7 @@ import type { IssueAssigneeAdapterOverrides, IssueAttachment, IssueThreadInteraction, - CreateIssueThreadInteraction, + CreateIssueThreadInteractionInput, Approval, PluginManagedAgentResolution, PluginManagedProjectResolution, @@ -2003,7 +2003,7 @@ export interface WorkerToHostMethods { params: { issueId: string; companyId: string; - interaction: CreateIssueThreadInteraction; + interaction: CreateIssueThreadInteractionInput; authorAgentId?: string | null; }, result: IssueThreadInteraction, diff --git a/packages/plugins/sdk/src/testing.ts b/packages/plugins/sdk/src/testing.ts index 07b4cc0d5e8..35d23c68072 100644 --- a/packages/plugins/sdk/src/testing.ts +++ b/packages/plugins/sdk/src/testing.ts @@ -1,5 +1,5 @@ import { randomUUID } from "node:crypto"; -import { pluginOperationIssueOriginKind } from "@paperclipai/shared"; +import { createIssueThreadInteractionSchema, pluginOperationIssueOriginKind } from "@paperclipai/shared"; import type { PaperclipPluginManifestV1, PluginCapability, @@ -16,7 +16,6 @@ import type { Issue, IssueComment, IssueThreadInteraction, - CreateIssueThreadInteraction, IssueAttachment, IssueDocument, Agent, @@ -1751,6 +1750,9 @@ export function createTestHarness(options: TestHarnessOptions): TestHarness { if (!isInCompany(parentIssue, companyId)) { throw new Error(`Issue not found: ${issueId}`); } + if (interaction.kind === "ask_user_questions") { + interaction = createIssueThreadInteractionSchema.parse(interaction); + } const now = new Date(); const current = issueInteractions.get(issueId) ?? []; if (interaction.idempotencyKey) { diff --git a/packages/plugins/sdk/src/types.ts b/packages/plugins/sdk/src/types.ts index 56a561a2984..55d0d0f850e 100644 --- a/packages/plugins/sdk/src/types.ts +++ b/packages/plugins/sdk/src/types.ts @@ -37,6 +37,7 @@ import type { RequestConfirmationInteraction, RequestCheckboxConfirmationInteraction, CreateIssueThreadInteraction, + CreateIssueThreadInteractionInput, PluginIssueOriginKind, IssueSurfaceVisibility, PluginManagedAgentResolution, @@ -143,6 +144,7 @@ export type { RequestConfirmationInteraction, RequestCheckboxConfirmationInteraction, CreateIssueThreadInteraction, + CreateIssueThreadInteractionInput, PluginIssueOriginKind, IssueSurfaceVisibility, Agent, @@ -1544,7 +1546,7 @@ export interface PluginIssuesClient { ): Promise; createInteraction( issueId: string, - interaction: CreateIssueThreadInteraction, + interaction: CreateIssueThreadInteractionInput, companyId: string, options?: { authorAgentId?: string }, ): Promise; @@ -1556,7 +1558,7 @@ export interface PluginIssuesClient { ): Promise; askUserQuestions( issueId: string, - interaction: Omit, "kind">, + interaction: Omit, "kind">, companyId: string, options?: { authorAgentId?: string }, ): Promise; diff --git a/packages/plugins/sdk/tests/testing-actions.test.ts b/packages/plugins/sdk/tests/testing-actions.test.ts index 5579890090e..ff5eeaf9cef 100644 --- a/packages/plugins/sdk/tests/testing-actions.test.ts +++ b/packages/plugins/sdk/tests/testing-actions.test.ts @@ -98,6 +98,18 @@ describe("createTestHarness managed routines", () => { }); describe("createTestHarness issue interactions", () => { + it("normalizes a canonical question form through the typed host helper", async () => { + const harness = createTestHarness({ manifest, capabilities: ["issues.create", "issue.interactions.create"] }); + const issue = await harness.ctx.issues.create({ companyId: "company-1", title: "Ask for a repository" }); + const input = { + idempotencyKey: "canonical:repo", + payload: { version: 1 as const, questionSet: { schema: "paperclip.question_set.v1" as const, questions: [{ id: "repo", prompt: "Repository URL?", required: true, answerMode: "text" as const }] } }, + }; + const created = await harness.ctx.issues.askUserQuestions(issue.id, input, "company-1"); + expect(created.payload.questionSet).toEqual(input.payload.questionSet); + expect(created.payload.questions).toMatchObject([{ id: "repo", options: [{ id: "paperclip_text_answer", freeText: true }] }]); + expect(await harness.ctx.issues.askUserQuestions(issue.id, input, "company-1")).toEqual(created); + }); it("creates request_checkbox_confirmation interactions through the typed host helper", async () => { const harness = createTestHarness({ manifest, diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 01507a44a5a..692abd65d17 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -199,6 +199,7 @@ export type { AttentionWorkspaceRef, } from "./types/attention.js"; export { ATTENTION_SOURCE_KINDS } from "./types/attention.js"; +export { questionSetToAskUserQuestionsPayload } from "./question-set.js"; export type { DecisionQueue, DecisionQueueItem, @@ -2080,6 +2081,7 @@ export { type CheckoutIssue, type AddIssueComment, type CreateIssueThreadInteraction, + type CreateIssueThreadInteractionInput, type AcceptIssueThreadInteraction, type RejectIssueThreadInteraction, type CancelIssueThreadInteraction, diff --git a/packages/shared/src/issue-thread-interactions.test.ts b/packages/shared/src/issue-thread-interactions.test.ts index 9610acc750d..6f516055744 100644 --- a/packages/shared/src/issue-thread-interactions.test.ts +++ b/packages/shared/src/issue-thread-interactions.test.ts @@ -18,6 +18,59 @@ import { } from "./validators/issue.js"; describe("issue thread interaction schemas", () => { + it("derives every legacy question from a complete mixed canonical form", () => { + const questionSet = paperclipQuestionSetPayloadSchema.parse({ + schema: "paperclip.question_set.v1", + questions: [ + { id: "repo", prompt: "Repository URL?", required: true, answerMode: "text", textValidation: { minLength: 1 } }, + { id: "scope", prompt: "Review scope?", required: true, answerMode: "single_select", options: [{ id: "all", label: "All changes", recommended: true }, { id: "selected", label: "Selected changes" }] }, + { id: "hosting", prompt: "Preview hosting?", required: false, answerMode: "multi_select", options: [{ id: "paperclip_custom_answer", label: "Existing host" }, { id: "new", label: "New host" }], customAnswer: { enabled: true } }, + ], + }); + const parsed = createIssueThreadInteractionSchema.parse({ kind: "ask_user_questions", payload: { version: 1, questionSet } }); + if (parsed.kind !== "ask_user_questions") throw new Error("expected questions"); + expect(parsed.payload.questionSet).toEqual(questionSet); + expect(parsed.payload.questions).toMatchObject([ + { id: "repo", required: true, selectionMode: "single", options: [{ id: "paperclip_text_answer", freeText: true }] }, + { id: "scope", required: true, selectionMode: "single", allowOther: false, options: [{ id: "all", label: "All changes" }, { id: "selected", label: "Selected changes" }] }, + { id: "hosting", required: false, selectionMode: "multi", allowOther: true, options: [{ id: "paperclip_custom_answer" }, { id: "new" }, { id: "paperclip_custom_answer_2", freeText: true }] }, + ]); + expect(askUserQuestionsPayloadSchema.parse(parsed.payload)).toEqual(parsed.payload); + }); + + it("rejects canonical-only forms with invalid modes or missing questions", () => { + for (const questionSet of [ + { schema: "paperclip.question_set.v1", questions: [] }, + { schema: "paperclip.question_set.v1", questions: [{ id: "repo", prompt: "Repository?", required: true, answerMode: "text", options: [{ id: "a", label: "A" }] }] }, + ]) expect(createIssueThreadInteractionSchema.safeParse({ kind: "ask_user_questions", payload: { version: 1, questionSet } }).success).toBe(false); + expect(createIssueThreadInteractionSchema.safeParse({ kind: "ask_user_questions", payload: { version: 1 } }).success).toBe(false); + }); + + it("preserves canonical display whitespace while deriving trimmed storage fields", () => { + const questionSet = { schema: "paperclip.question_set.v1", questions: [ + { id: "scope", prompt: " Review scope? \n", required: true, answerMode: "single_select", options: [{ id: "all", label: " All changes " }, { id: "selected", label: " Selected changes " }] }, + { id: "repo", prompt: " Repository URL? ", required: true, answerMode: "text" }, + ] }; + const parsed = createIssueThreadInteractionSchema.parse({ kind: "ask_user_questions", payload: { version: 1, questionSet } }); + if (parsed.kind !== "ask_user_questions") throw new Error("expected questions"); + expect(parsed.payload.questionSet).toEqual(questionSet); + expect(parsed.payload.questions[0]).toMatchObject({ prompt: "Review scope?", options: [{ label: "All changes" }, { label: "Selected changes" }] }); + expect(parsed.payload.questions[1].prompt).toBe("Repository URL?"); + }); + + it("rejects dual choice forms that disagree about written alternatives", () => { + const options = [{ id: "all", label: "All" }, { id: "selected", label: "Selected" }]; + for (const storage of [ + { options: [...options, { id: "other", label: "Other", freeText: true }] }, + { options, allowOther: true }, + ]) { + const parsed = createIssueThreadInteractionSchema.safeParse({ kind: "ask_user_questions", payload: { version: 1, + questions: [{ id: "scope", prompt: "Scope?", required: true, selectionMode: "single", ...storage }], + questionSet: { schema: "paperclip.question_set.v1", questions: [{ id: "scope", prompt: "Scope?", required: true, answerMode: "single_select", options }] }, + } }); + expect(parsed.success).toBe(false); + } + }); it("defines canonical resolver policies and normalizes compatibility aliases", () => { expect(ISSUE_THREAD_INTERACTION_CANONICAL_RESOLVER_POLICIES).toEqual([ "anyone", diff --git a/packages/shared/src/question-set.ts b/packages/shared/src/question-set.ts new file mode 100644 index 00000000000..4d46f35c480 --- /dev/null +++ b/packages/shared/src/question-set.ts @@ -0,0 +1,51 @@ +import type { AskUserQuestionsPayload, PaperclipQuestionSetPayload } from "./types/issue.js"; + +function syntheticOptionId(existing: readonly string[], preferred: string): string { + const ids = new Set(existing); + let candidate = preferred; + for (let suffix = 2; ids.has(candidate); suffix += 1) candidate = `${preferred}_${suffix}`; + return candidate; +} + +/** Project a complete canonical form into the legacy storage/answer contract. */ +export function questionSetToAskUserQuestionsPayload( + questionSet: PaperclipQuestionSetPayload, +): AskUserQuestionsPayload { + return { + version: 1, + ...(questionSet.title ? { title: questionSet.title.slice(0, 240) } : {}), + ...(questionSet.submitLabel ? { submitLabel: questionSet.submitLabel.slice(0, 120) } : {}), + questionSet, + questions: questionSet.questions.map((question) => { + const options = (question.options ?? []).map(({ id, label, description }) => ({ + id, label, ...(description !== undefined ? { description } : {}), + })); + const freeTextOption = question.answerMode === "text" + ? { + id: "paperclip_text_answer", + label: question.header || "Type an answer", + ...(question.textValidation?.inputType + ? { description: `Expected ${question.textValidation.inputType} input` } : {}), + freeText: true as const, + } + : question.customAnswer?.enabled + ? { + id: syntheticOptionId(options.map((option) => option.id), "paperclip_custom_answer"), + label: question.customAnswer.label || "Other", + ...(question.customAnswer.placeholder !== undefined + ? { description: question.customAnswer.placeholder } : {}), + freeText: true as const, + } + : null; + return { + id: question.id, + prompt: question.prompt, + ...((question.helpText || question.header) ? { helpText: question.helpText ?? question.header } : {}), + selectionMode: question.answerMode === "multi_select" ? "multi" : "single", + required: question.required, + allowOther: freeTextOption !== null, + options: freeTextOption ? [...options, freeTextOption] : options, + }; + }), + }; +} diff --git a/packages/shared/src/validators/index.ts b/packages/shared/src/validators/index.ts index b6891b92176..99dce757515 100644 --- a/packages/shared/src/validators/index.ts +++ b/packages/shared/src/validators/index.ts @@ -510,6 +510,7 @@ export { type CheckoutIssue, type AddIssueComment, type CreateIssueThreadInteraction, + type CreateIssueThreadInteractionInput, type AcceptIssueThreadInteraction, type RejectIssueThreadInteraction, type CancelIssueThreadInteraction, diff --git a/packages/shared/src/validators/issue.ts b/packages/shared/src/validators/issue.ts index 99f1c22f1eb..6545baa9f73 100644 --- a/packages/shared/src/validators/issue.ts +++ b/packages/shared/src/validators/issue.ts @@ -41,6 +41,7 @@ import { trustAuthorizationPolicySchema, } from "./trust-policy.js"; import { objectWithoutDefaults } from "./partial.js"; +import { questionSetToAskUserQuestionsPayload } from "../question-set.js"; export const issueBlockedInboxStateSchema = z.enum([ "needs_attention", @@ -1381,18 +1382,20 @@ export const paperclipQuestionSetPayloadSchema = z } }); +const askUserQuestionsPayloadFields = { + version: z.literal(1), + title: z.string().trim().max(240).nullable().optional(), + submitLabel: z.string().trim().max(120).nullable().optional(), + supersedeOnUserComment: z.boolean().optional(), + questions: z.array(askUserQuestionsQuestionSchema).min(1).max(64), + /** Exact canonical presentation retained for a recovered harness request. */ + questionSet: paperclipQuestionSetPayloadSchema.optional(), + /** Stable correlation for draft handoff from a live runtime request. */ + runtimeRequestId: z.string().trim().min(1).max(255).nullable().optional(), +}; + export const askUserQuestionsPayloadSchema = z - .object({ - version: z.literal(1), - title: z.string().trim().max(240).nullable().optional(), - submitLabel: z.string().trim().max(120).nullable().optional(), - supersedeOnUserComment: z.boolean().optional(), - questions: z.array(askUserQuestionsQuestionSchema).min(1).max(64), - /** Exact canonical presentation retained for a recovered harness request. */ - questionSet: paperclipQuestionSetPayloadSchema.optional(), - /** Stable correlation for draft handoff from a live runtime request. */ - runtimeRequestId: z.string().trim().min(1).max(255).nullable().optional(), - }) + .object(askUserQuestionsPayloadFields) .superRefine((value, ctx) => { const seenQuestionIds = new Set(); for (const [questionIndex, question] of value.questions.entries()) { @@ -1905,7 +1908,7 @@ const createIssueThreadInteractionCommon = { // Validate dual representations on creation, not when reading historical rows. // Otherwise a partial canonical form can hide required storage questions. -const createAskUserQuestionsPayloadSchema = askUserQuestionsPayloadSchema.superRefine((value, ctx) => { +const createLegacyAskUserQuestionsPayloadSchema = askUserQuestionsPayloadSchema.superRefine((value, ctx) => { if (!value.questionSet) return; const shown = new Set(value.questionSet.questions.map((question) => question.id)); const stored = new Set(value.questions.map((question) => question.id)); @@ -1925,7 +1928,7 @@ const createAskUserQuestionsPayloadSchema = askUserQuestionsPayloadSchema.superR path: ["questionSet", "questions", index, field], message: `questionSet ${field} must match the corresponding questions entry.`, }); - if (question.prompt !== storage.prompt) mismatch("prompt"); + if (question.prompt.trim() !== storage.prompt) mismatch("prompt"); // An omitted storage flag adds no constraint; an explicit flag must agree. if (storage.required !== undefined && question.required !== storage.required) mismatch("required"); const mode = question.answerMode === "multi_select" ? "multi" : "single"; @@ -1935,12 +1938,27 @@ const createAskUserQuestionsPayloadSchema = askUserQuestionsPayloadSchema.superR } else { // Text/custom-answer sentinels are storage compatibility, not visible choices. const choices = storage.options.filter((option) => !option.freeText); - const canonical = new Map((question.options ?? []).map((option) => [option.id, option.label])); + const canonical = new Map((question.options ?? []).map((option) => [option.id, option.label.trim()])); if (choices.length !== canonical.size || choices.some((option) => canonical.get(option.id) !== option.label)) mismatch("options"); + const allowsCustomAnswer = storage.allowOther === true || storage.options.some((option) => option.freeText); + if (Boolean(question.customAnswer?.enabled) !== allowsCustomAnswer) mismatch("customAnswer"); } } }); +const createAskUserQuestionsPayloadSchema = z.union([ + createLegacyAskUserQuestionsPayloadSchema, + z.object({ + ...askUserQuestionsPayloadFields, + // Do not discard an explicitly supplied conflicting compatibility form. + questions: z.never().optional(), + questionSet: paperclipQuestionSetPayloadSchema, + }).transform(({ questions: _questions, ...value }): z.input => ({ + ...questionSetToAskUserQuestionsPayload(value.questionSet), + ...value, + })).pipe(createLegacyAskUserQuestionsPayloadSchema), +]); + export const createIssueThreadInteractionSchema = z.discriminatedUnion("kind", [ z.object({ ...createIssueThreadInteractionCommon, @@ -2012,6 +2030,9 @@ export const createIssueThreadInteractionSchema = z.discriminatedUnion("kind", [ export type CreateIssueThreadInteraction = z.infer< typeof createIssueThreadInteractionSchema >; +export type CreateIssueThreadInteractionInput = z.input< + typeof createIssueThreadInteractionSchema +>; export const acceptIssueThreadInteractionSchema = z .object({ diff --git a/server/src/__tests__/hiring-operational-examples.test.ts b/server/src/__tests__/hiring-operational-examples.test.ts index 2da0eaf9b00..8b0510b3d92 100644 --- a/server/src/__tests__/hiring-operational-examples.test.ts +++ b/server/src/__tests__/hiring-operational-examples.test.ts @@ -33,11 +33,14 @@ describe("published hiring and human-input examples", () => { const skill = readFileSync(new URL("../../../skills/paperclip/SKILL.md", import.meta.url), "utf8"); const section = skill.split('')[1]!; const body = JSON.parse(section.match(/```json\n([\s\S]*?)\n```/)![1]); - expect(createIssueThreadInteractionSchema.safeParse(substituteIds(body))).toMatchObject({ success: true }); + const normalized = createIssueThreadInteractionSchema.parse(substituteIds(body)); expect(body).toMatchObject({ resolverPolicy: "human_only", continuationPolicy: "wake_assignee" }); expect(body).not.toHaveProperty("addresseeUserId"); expect(body.payload.questionSet.questions[0]).toMatchObject({ answerMode: "text" }); - expect(body.payload.questions[0].id).toBe(body.payload.questionSet.questions[0].id); + expect(body.payload).not.toHaveProperty("questions"); + expect(normalized.kind).toBe("ask_user_questions"); + if (normalized.kind !== "ask_user_questions") throw new Error("expected questions"); + expect(normalized.payload.questions[0].id).toBe(body.payload.questionSet.questions[0].id); }); it("keeps these examples in the generated runner reference without displacing confirmations", () => { diff --git a/server/src/__tests__/issue-thread-interaction-routes.test.ts b/server/src/__tests__/issue-thread-interaction-routes.test.ts index f0ee5a567d0..f44c061209d 100644 --- a/server/src/__tests__/issue-thread-interaction-routes.test.ts +++ b/server/src/__tests__/issue-thread-interaction-routes.test.ts @@ -612,6 +612,23 @@ describe.sequential("issue thread interaction routes", () => { ); }, 10_000); + it("normalizes a canonical-only mixed question form at the HTTP boundary", async () => { + const questionSet = { schema: "paperclip.question_set.v1", questions: [ + { id: "repo", prompt: "Repository URL?", required: true, answerMode: "text" }, + { id: "scope", prompt: "Review scope?", required: true, answerMode: "single_select", options: [{ id: "all", label: "All changes" }, { id: "selected", label: "Selected changes" }] }, + ] }; + const res = await request(await createApp()) + .post("/api/issues/aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa/interactions") + .send({ kind: "ask_user_questions", payload: { version: 1, questionSet } }); + expect(res.status).toBe(201); + expect(mockInteractionService.create).toHaveBeenCalledWith(expect.anything(), expect.objectContaining({ + payload: expect.objectContaining({ questionSet, questions: [ + expect.objectContaining({ id: "repo", options: [{ id: "paperclip_text_answer", label: "Type an answer", freeText: true }] }), + expect.objectContaining({ id: "scope", options: questionSet.questions[1].options }), + ] }), + }), expect.anything()); + }); + it("does not run historical-comment catch-up or queue recovery from the interaction read path", async () => { mockIssueService.getById.mockResolvedValue(createIssue({ status: "in_review", diff --git a/server/src/__tests__/issue-thread-interactions-service.test.ts b/server/src/__tests__/issue-thread-interactions-service.test.ts index 8af1e9b98f2..67ccf1213d2 100644 --- a/server/src/__tests__/issue-thread-interactions-service.test.ts +++ b/server/src/__tests__/issue-thread-interactions-service.test.ts @@ -213,6 +213,89 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => { }; } + it("persists and answers a canonical-only mixed question form", async () => { + const { companyId, issueId } = await seedSourceQuestionFixture({}); + const questionSet = { + schema: "paperclip.question_set.v1" as const, + questions: [ + { id: "repo", prompt: "Repository URL?", required: true, answerMode: "text" as const }, + { id: "scope", prompt: "Review scope?", required: true, answerMode: "single_select" as const, options: [{ id: "all", label: "All changes" }, { id: "selected", label: "Selected changes" }] }, + { id: "hosting", prompt: "Preview hosting?", required: true, answerMode: "multi_select" as const, options: [{ id: "existing", label: "Existing host" }, { id: "new", label: "New host" }] }, + ], + }; + const input = { kind: "ask_user_questions" as const, idempotencyKey: "canonical:mixed", payload: { version: 1 as const, questionSet } }; + const issue = { id: issueId, companyId }; + const created = await interactionsSvc.create(issue, input, { userId: "local-board" }); + if (created.kind !== "ask_user_questions") throw new Error("expected questions"); + expect(created.payload.questionSet).toEqual(questionSet); + expect(created.payload.questions.map((question) => question.id)).toEqual(["repo", "scope", "hosting"]); + expect(await interactionsSvc.create(issue, input, { userId: "local-board" })).toEqual(created); + await expect(interactionsSvc.answerQuestions(issue, created.id, { answers: [{ questionId: "repo", optionIds: [], otherText: "https://example.com/repo" }] }, { userId: "local-board" })).rejects.toThrow("requires an answer"); + const answered = await interactionsSvc.answerQuestions(issue, created.id, { answers: [ + { questionId: "repo", optionIds: [], otherText: "https://example.com/repo" }, + { questionId: "scope", optionIds: ["all"] }, + { questionId: "hosting", optionIds: ["existing", "new"] }, + ] }, { userId: "local-board" }); + expect(answered.status).toBe("answered"); + }); + + it("rejects canonical text and custom answers that violate constraints before resolution", async () => { + const { companyId, issueId } = await seedSourceQuestionFixture({}); + const issue = { id: issueId, companyId }; + const cases = [ + { textValidation: { minLength: 3, maxLength: 4 }, invalid: ["ab", "abcde"], valid: "abcd" }, + { textValidation: { pattern: "^https://" }, invalid: ["http://example.test"], valid: "https://example.test" }, + { textValidation: { inputType: "integer" as const, minimum: 2, maximum: 4 }, invalid: ["no", "3.5", "1", "5"], valid: "3" }, + { textValidation: { inputType: "number" as const, minimum: 2, maximum: 4 }, invalid: ["Infinity", "1.9", "4.1"], valid: "2.5" }, + ]; + for (const [index, entry] of cases.entries()) { + const created = await interactionsSvc.create(issue, { + kind: "ask_user_questions", payload: { version: 1, questionSet: { + schema: "paperclip.question_set.v1", questions: [{ id: "value", prompt: "Value?", required: true, answerMode: "text", textValidation: entry.textValidation }], + } }, + }, { userId: "local-board" }); + for (const otherText of entry.invalid) { + await expect(interactionsSvc.answerQuestions(issue, created.id, { answers: [{ questionId: "value", optionIds: [], otherText }] }, { userId: "local-board" })).rejects.toThrow(); + const [row] = await db.select().from(issueThreadInteractions).where(eq(issueThreadInteractions.id, created.id)); + expect(row?.status, `case ${index}: ${otherText}`).toBe("pending"); + } + expect((await interactionsSvc.answerQuestions(issue, created.id, { answers: [{ questionId: "value", optionIds: [], otherText: entry.valid }] }, { userId: "local-board" })).status).toBe("answered"); + } + const custom = await interactionsSvc.create(issue, { kind: "ask_user_questions", payload: { version: 1, questionSet: { + schema: "paperclip.question_set.v1", questions: [{ id: "scope", prompt: "Scope?", required: true, answerMode: "single_select", options: [{ id: "all", label: "All" }, { id: "selected", label: "Selected" }], customAnswer: { enabled: true }, textValidation: { minLength: 3 } }], + } } }, { userId: "local-board" }); + await expect(interactionsSvc.answerQuestions(issue, custom.id, { answers: [{ questionId: "scope", optionIds: [], otherText: "ab" }] }, { userId: "local-board" })).rejects.toThrow("at least 3"); + expect((await interactionsSvc.answerQuestions(issue, custom.id, { answers: [{ questionId: "scope", optionIds: [], otherText: "abc" }] }, { userId: "local-board" })).status).toBe("answered"); + }); + + it("accepts existing custom-answer IDs in compatible dual forms", async () => { + const { companyId, issueId } = await seedSourceQuestionFixture({}); + const issue = { id: issueId, companyId }; + const options = [{ id: "all", label: "All" }, { id: "selected", label: "Selected" }]; + const created = await interactionsSvc.create(issue, { kind: "ask_user_questions", payload: { + version: 1, + questions: [{ id: "scope", prompt: "Scope?", required: true, selectionMode: "single", options: [...options, { id: "existing-custom-id", label: "Other", freeText: true }] }], + questionSet: { schema: "paperclip.question_set.v1", questions: [{ id: "scope", prompt: "Scope?", required: true, answerMode: "single_select", options, customAnswer: { enabled: true }, textValidation: { minLength: 3 } }] }, + } }, { userId: "local-board" }); + const answered = await interactionsSvc.answerQuestions(issue, created.id, { answers: [{ questionId: "scope", optionIds: ["existing-custom-id"], otherText: "Specific files" }] }, { userId: "local-board" }); + expect(answered).toMatchObject({ status: "answered", result: { answers: [{ questionId: "scope", optionIds: ["existing-custom-id"], otherText: "Specific files" }] } }); + }); + + it("keeps historical pending written-answer paths usable with their text constraints", async () => { + const { companyId, issueId } = await seedSourceQuestionFixture({}); + const issue = { id: issueId, companyId }; + const created = await interactionsSvc.create(issue, { kind: "ask_user_questions", payload: { version: 1, questionSet: { + schema: "paperclip.question_set.v1", questions: [{ id: "scope", prompt: "Scope?", required: true, answerMode: "single_select", options: [{ id: "all", label: "All" }, { id: "selected", label: "Selected" }], customAnswer: { enabled: true }, textValidation: { minLength: 3 } }], + } } }, { userId: "local-board" }); + if (created.kind !== "ask_user_questions") throw new Error("expected questions"); + const historicalPayload = structuredClone(created.payload); + delete historicalPayload.questionSet!.questions[0].customAnswer; + await db.update(issueThreadInteractions).set({ payload: historicalPayload }).where(eq(issueThreadInteractions.id, created.id)); + await expect(interactionsSvc.answerQuestions(issue, created.id, { answers: [{ questionId: "scope", optionIds: ["paperclip_custom_answer"], otherText: "ab" }] }, { userId: "local-board" })).rejects.toThrow("at least 3"); + const answered = await interactionsSvc.answerQuestions(issue, created.id, { answers: [{ questionId: "scope", optionIds: ["paperclip_custom_answer"], otherText: "Specific files" }] }, { userId: "local-board" }); + expect(answered.status).toBe("answered"); + }); + async function seedQuestionUser(companyId: string, userId: string, role = "member", status = "active") { await db.insert(authUsers).values({ id: userId, name: "Question recipient", email: `${randomUUID()}@example.test`, createdAt: new Date(), updatedAt: new Date() }); await db.insert(companyMemberships).values({ companyId, principalType: "user", principalId: userId, membershipRole: role, status }); diff --git a/server/src/__tests__/question-response-delivery.test.ts b/server/src/__tests__/question-response-delivery.test.ts index d1088780c25..ad3b603ec75 100644 --- a/server/src/__tests__/question-response-delivery.test.ts +++ b/server/src/__tests__/question-response-delivery.test.ts @@ -214,6 +214,7 @@ describeEmbeddedPostgres("question response delivery", () => { id: "features", prompt: "Which features?", selectionMode: "multi", + allowOther: true, options: [ { id: "health", label: "Health check" }, { id: "logs", label: "Request logs" }, diff --git a/server/src/routes/issues.ts b/server/src/routes/issues.ts index 1363063c43c..828cfef4703 100644 --- a/server/src/routes/issues.ts +++ b/server/src/routes/issues.ts @@ -16477,7 +16477,7 @@ export function issueRoutes( const actor = getActorInfo(req); if (current.kind === "ask_user_questions") { - validateNativeQuestionResponseInput(current, req.body); + await validateNativeQuestionResponseInput(current, req.body); } const interaction = await interactionSvc.answerQuestions( issue, diff --git a/server/src/services/chat-channels.ts b/server/src/services/chat-channels.ts index a4c67d20b37..3dd8aeb3682 100644 --- a/server/src/services/chat-channels.ts +++ b/server/src/services/chat-channels.ts @@ -8667,7 +8667,7 @@ export function chatChannelService(db: Db, options: ChatChannelServiceOptions) { ); return true; } - validateNativeQuestionResponseInput(interaction, { answers }); + await validateNativeQuestionResponseInput(interaction, { answers }); // Canonical service rechecks required fields, options, audience, and // pending -> answered atomically with the response-delivery receipt. const issue = await db diff --git a/server/src/services/issue-thread-interactions.ts b/server/src/services/issue-thread-interactions.ts index 7ff415d5250..7c7cd643a3c 100644 --- a/server/src/services/issue-thread-interactions.ts +++ b/server/src/services/issue-thread-interactions.ts @@ -2,6 +2,7 @@ import { currentContinuationOrigins, deliveredContinuationCommentIds, } from "./execution-continuation.js"; +import { parseQuestionInteractionAnswers } from "./question-interaction-answers.js"; import { isUniqueViolation } from "../db-errors.js"; import { assertAgentRunWriteAllowed } from "../agent-run-cancellation.js"; import { connectionIntentDeliveries } from "@paperclipai/db"; @@ -47,6 +48,7 @@ import type { CancelIssueThreadInteraction, ConnectionIntentInteraction, CreateIssueThreadInteraction, + CreateIssueThreadInteractionInput, InteractionResolverGovernance, IssueReviewPolicy, IssueThreadInteraction, @@ -3309,7 +3311,7 @@ export function issueThreadInteractionService( create: async ( issue: { id: string; companyId: string }, - input: CreateIssueThreadInteraction, + input: CreateIssueThreadInteractionInput, actor: InteractionActor, options: CreateInteractionOptions = {}, ) => { @@ -4894,6 +4896,16 @@ export function issueThreadInteractionService( questions: interaction.payload.questions, answers: input.answers, }); + if (interaction.payload.questionSet) { + try { + await parseQuestionInteractionAnswers(interaction.payload.questionSet, normalizedAnswers, interaction.payload.questions); + } catch (error) { + throw unprocessable( + error instanceof Error ? error.message : "Invalid question response", + { code: "invalid_question_response" }, + ); + } + } const updated = await db.transaction(async (tx) => { await assertInteractionRunWriteAllowed(tx as unknown as Db, issue, actor); diff --git a/server/src/services/native-runtime/native-question-bridge.test.ts b/server/src/services/native-runtime/native-question-bridge.test.ts index 18b7fd5c3b1..baed10cd4b2 100644 --- a/server/src/services/native-runtime/native-question-bridge.test.ts +++ b/server/src/services/native-runtime/native-question-bridge.test.ts @@ -45,6 +45,7 @@ import { type IssuePostCommitAction, } from "../issues.js"; import { questionResponseDeliveryService } from "../question-response-delivery.js"; +import * as questionPatternValidation from "../question-pattern-validation.js"; import { heartbeatService } from "../heartbeat.js"; import { DurablePrpControlPlane } from "../../vendor/paperclip-runner/index.js"; import { PaperclipControlPlanePort } from "./paperclip-control-plane-port.js"; @@ -336,6 +337,33 @@ describeEmbeddedPostgres("native question bridge", () => { } finally { bridge.close(); } }); + it("delivers saved answers without entering pattern validation again", async () => { + await seed(); + const event = runtimeRequestEvent(); + (event.payload.request as any).input.questions = [{ id: "url", prompt: "URL?", required: true, answerMode: "text" }]; + const interaction = await projectNativeRuntimeRequest({ db, binding: binding(), event }); + const answered = await issueThreadInteractionService(db).answerQuestions( + { id: issueId, companyId, status: "in_progress" }, interaction!.id, + { answers: [{ questionId: "url", optionIds: [], otherText: "https://example.test" }] }, { userId: "operator-1" }, + ); + if (answered.kind !== "ask_user_questions") throw new Error("expected questions"); + const queueCommand = vi.fn(() => ({ commandId: "question", controllerSeq: 1 })); + const release = registerNativeQuestionCommandTarget({ binding: { companyId, issueId, runId, agentId }, queueCommand }); + const busy = vi.spyOn(questionPatternValidation, "validateQuestionPatterns").mockRejectedValue(new Error("Question format validation is busy; try again")); + try { + await flushNativeQuestionResponses(db, runId); + expect(queueCommand).toHaveBeenCalledWith("request.resolve", expect.objectContaining({ + response: { schema: "paperclip.question_response.v1", answers: { url: { text: "https://example.test" } } }, + }), `question_${interaction!.id}`); + expect(busy).not.toHaveBeenCalled(); + const [delivery] = await db.select().from(issueQuestionResponseDeliveries); + expect(delivery).toMatchObject({ status: "delivered" }); + } finally { + busy.mockRestore(); + release(); + } + }); + it.each(["codex", "claude"])("materializes, validates, and durably resumes a %s question response", async (provider) => { await seed(); const interaction = await projectNativeRuntimeRequest({ @@ -370,10 +398,10 @@ describeEmbeddedPostgres("native question bridge", () => { expect(await db.select().from(activityLog)).toHaveLength(1); const answer = { answers: [{ questionId: "color", optionIds: ["blue"] }] }; - validateNativeQuestionResponseInput(interaction!, answer); - expect(() => validateNativeQuestionResponseInput(interaction!, { + await validateNativeQuestionResponseInput(interaction!, answer); + await expect(validateNativeQuestionResponseInput(interaction!, { answers: [{ questionId: "color", optionIds: ["red"] }], - })).toThrow(/unknown option red/); + })).rejects.toThrow(/unknown option red/); const answered = await issueThreadInteractionService(db).answerQuestions( { id: issueId, companyId, status: "in_progress" }, @@ -648,7 +676,7 @@ describeEmbeddedPostgres("native question bridge", () => { otherText: "purple", }], }; - validateNativeQuestionResponseInput(interaction!, answer); + await validateNativeQuestionResponseInput(interaction!, answer); const answered = await issueThreadInteractionService(db).answerQuestions( { id: issueId, companyId, status: "in_progress" }, interaction!.id, diff --git a/server/src/services/native-runtime/native-question-bridge.ts b/server/src/services/native-runtime/native-question-bridge.ts index 171ad06cfe7..f31e341e0fa 100644 --- a/server/src/services/native-runtime/native-question-bridge.ts +++ b/server/src/services/native-runtime/native-question-bridge.ts @@ -3,18 +3,15 @@ import { and, eq, inArray, sql } from "drizzle-orm"; import type { Db } from "@paperclipai/db"; import { heartbeatRuns, issueThreadInteractions } from "@paperclipai/db"; import type { - AskUserQuestionsAnswer, AskUserQuestionsInteraction, - AskUserQuestionsQuestionOption, - PaperclipQuestionSetPayload, RespondIssueThreadInteraction, } from "@paperclipai/shared"; +import { questionSetToAskUserQuestionsPayload } from "@paperclipai/shared"; + import type { PrpEvent } from "../../vendor/paperclip-runner/index.js"; import { - parsePaperclipQuestionResponse, parsePaperclipQuestionSet, - type PaperclipQuestionResponse, type PaperclipQuestionSet, } from "../../vendor/paperclip-runner/index.js"; import { logger } from "../../middleware/logger.js"; @@ -24,10 +21,10 @@ import { issueThreadInteractionService } from "../issue-thread-interactions.js"; import { questionResponseDeliveryService } from "../question-response-delivery.js"; import type { NativeRunStoreBinding } from "./native-run-coordinator-store.js"; +import { parseQuestionInteractionAnswers, parseSavedQuestionInteractionAnswers } from "../question-interaction-answers.js"; + const QUESTION_KEY_PREFIX = "paperclip-runner-question:"; const REQUEST_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,159}$/; -const TEXT_ANSWER_OPTION_ID = "paperclip_text_answer"; -const CUSTOM_ANSWER_OPTION_ID = "paperclip_custom_answer"; export const NATIVE_QUESTION_CANCELLATION_CONTEXT_KEY = "nativeQuestionCancellation"; type QueueCommand = ( @@ -83,100 +80,15 @@ function requestIdForInteraction( : requestId; } -function uniqueSyntheticOptionId(existing: readonly string[], preferred: string): string { - const ids = new Set(existing); - if (!ids.has(preferred)) return preferred; - for (let suffix = 2; suffix < 10_000; suffix += 1) { - const candidate = `${preferred}_${suffix}`; - if (!ids.has(candidate)) return candidate; - } - throw new Error("native_question_synthetic_option_exhausted"); -} - function toInteractionPayload(questionSet: PaperclipQuestionSet, runtimeRequestId: string) { return { - version: 1 as const, - ...(questionSet.title ? { title: questionSet.title.slice(0, 240) } : {}), - ...(questionSet.submitLabel ? { submitLabel: questionSet.submitLabel.slice(0, 120) } : {}), - questions: questionSet.questions.map((question) => { - const canonicalOptions = question.options ?? []; - const options: AskUserQuestionsQuestionOption[] = canonicalOptions.map((option) => ({ - id: option.id, - label: option.label, - ...(option.description ? { description: option.description } : {}), - })); - if (question.answerMode === "text") { - options.push({ - id: uniqueSyntheticOptionId([], TEXT_ANSWER_OPTION_ID), - label: question.header ?? "Type an answer", - ...(question.textValidation?.inputType - ? { description: `Expected ${question.textValidation.inputType} input` } - : {}), - freeText: true, - }); - } else if (question.customAnswer?.enabled) { - options.push({ - id: uniqueSyntheticOptionId(canonicalOptions.map((option) => option.id), CUSTOM_ANSWER_OPTION_ID), - label: question.customAnswer.label ?? "Other", - ...(question.customAnswer.placeholder ? { description: question.customAnswer.placeholder } : {}), - freeText: true, - }); - } - return { - id: question.id, - prompt: question.prompt, - ...((question.helpText || question.header) - ? { helpText: question.helpText ?? question.header } - : {}), - selectionMode: question.answerMode === "multi_select" ? "multi" as const : "single" as const, - required: question.required, - allowOther: question.answerMode === "text" || question.customAnswer?.enabled === true, - options, - }; - }), - questionSet: questionSet as PaperclipQuestionSetPayload, + ...questionSetToAskUserQuestionsPayload(questionSet), runtimeRequestId, - // A generic task comment cannot satisfy this provider request. Keep the - // card actionable until a validated answer or an explicit terminal action. + // A generic task comment cannot satisfy this provider request. supersedeOnUserComment: false, }; } -function canonicalResponse( - questionSet: PaperclipQuestionSetPayload, - answers: readonly AskUserQuestionsAnswer[], -): PaperclipQuestionResponse { - const answerByQuestionId = new Map(answers.map((answer) => [answer.questionId, answer])); - const response: PaperclipQuestionResponse = { - schema: "paperclip.question_response.v1", - answers: {}, - }; - for (const question of questionSet.questions) { - const answer = answerByQuestionId.get(question.id); - if (!answer) continue; - if (question.answerMode === "text") { - response.answers[question.id] = { - ...(answer.otherText !== undefined && answer.otherText !== null - ? { text: answer.otherText } - : {}), - }; - } else { - const customOptionId = question.customAnswer?.enabled - ? uniqueSyntheticOptionId( - (question.options ?? []).map((option) => option.id), - CUSTOM_ANSWER_OPTION_ID, - ) - : null; - response.answers[question.id] = { - selectedOptionIds: answer.optionIds.filter((optionId) => optionId !== customOptionId), - ...(answer.otherText !== undefined && answer.otherText !== null - ? { customText: answer.otherText } - : {}), - }; - } - } - return parsePaperclipQuestionResponse(questionSet, response); -} async function authorizedNativeRun( db: Pick, @@ -312,13 +224,13 @@ export async function projectNativeRuntimeRequest(input: { } /** Validate untrusted board input before the existing interaction service persists it. */ -export function validateNativeQuestionResponseInput( +export async function validateNativeQuestionResponseInput( interaction: AskUserQuestionsInteraction, input: RespondIssueThreadInteraction, -): void { +): Promise { if (!requestIdForInteraction(interaction) || !interaction.payload.questionSet) return; try { - canonicalResponse(interaction.payload.questionSet, input.answers); + await parseQuestionInteractionAnswers(interaction.payload.questionSet, input.answers, interaction.payload.questions); } catch (error) { throw unprocessable( error instanceof Error ? error.message : "Invalid native question response", @@ -340,7 +252,7 @@ export async function deliverNativeQuestionResponse( // Fall through to durable fresh-wake delivery instead of waiting forever for // a command target that cannot return for this terminal run. if (!run || ["succeeded", "failed", "cancelled", "timed_out"].includes(run.status)) return "not_native"; - const response = canonicalResponse(interaction.payload.questionSet, interaction.result.answers); + const response = parseSavedQuestionInteractionAnswers(interaction.payload.questionSet, interaction.result.answers, interaction.payload.questions); const target = activeTargets.get(run.id); if ( !target diff --git a/server/src/services/native-runtime/paperclip-runner-tool-authority.test.ts b/server/src/services/native-runtime/paperclip-runner-tool-authority.test.ts index ce29a7f4e07..026ec993182 100644 --- a/server/src/services/native-runtime/paperclip-runner-tool-authority.test.ts +++ b/server/src/services/native-runtime/paperclip-runner-tool-authority.test.ts @@ -109,8 +109,10 @@ describe("PaperclipRunnerToolAuthority", () => { expect(questions.description).toContain("resolve-from-comment"); expect(questions.description).toContain("Existing resolver permissions still apply"); expect(questions.description).toContain("Do not fabricate answer links"); - expect(JSON.stringify(questions.inputSchema)).toContain("at least two distinct meaningful options"); - expect(JSON.stringify(questions.inputSchema)).toContain("answerMode:'text'"); + expect(JSON.stringify(questions.inputSchema)).toContain("at least two meaningful options"); + expect(questions.inputSchema).toMatchObject({ properties: { payload: { properties: { questionSet: { + properties: { questions: { items: { properties: { answerMode: { enum: ["single_select", "multi_select", "text"] } } } } }, + } } } } }); expect(authority.definitions().map((tool) => tool.name)).toEqual( expect.arrayContaining([ @@ -408,7 +410,7 @@ describe("PaperclipRunnerToolAuthority", () => { "current Paperclip task bound to this run", ); expect(advertised.description).toContain( - "payload.questions for choices", + "one complete payload.questionSet", ); expect(advertised.description).toContain( "Paperclip renders it and authenticates the response", @@ -427,7 +429,7 @@ describe("PaperclipRunnerToolAuthority", () => { }, ); - it.each(["choice", "text"] as const)("executes the advertised %s question once on the bound reviewed task", async (answerMode) => { + it.each(["choice", "text", "mixed"] as const)("executes the advertised %s question once on the bound reviewed task", async (answerMode) => { const binding = { companyId: randomUUID(), agentId: randomUUID(), @@ -437,7 +439,7 @@ describe("PaperclipRunnerToolAuthority", () => { await db.insert(companies).values({ id: binding.companyId, name: "Question invocation", - issuePrefix: answerMode === "choice" ? "RQA" : "RQT", + issuePrefix: answerMode === "choice" ? "RQA" : answerMode === "text" ? "RQT" : "RQM", }); await db.insert(agents).values({ id: binding.agentId, @@ -474,7 +476,7 @@ describe("PaperclipRunnerToolAuthority", () => { const advertised = authority .definitions() .find((tool) => tool.name === "request_human_input")!; - expect(advertised.description).toContain("payload.questions"); + expect(advertised.description).toContain("payload.questionSet"); const questions = [ { id: "color", @@ -490,28 +492,19 @@ describe("PaperclipRunnerToolAuthority", () => { const payloadDescription = (advertised.inputSchema as { properties: { payload: { description: string } }; }).properties.payload.description; - expect(payloadDescription).toContain("at least two distinct meaningful options"); + expect(payloadDescription).toContain("at least two meaningful options"); expect(payloadDescription).toContain("questionSet"); expect(payloadDescription).not.toContain("use exactly"); const payload = answerMode === "choice" ? { version: 1, questions } : { version: 1, - questions: [{ - id: "goal", - prompt: "What should we accomplish?", - selectionMode: "single", - required: true, - options: [{ id: "describe", label: "Your answer", freeText: true }], - }], questionSet: { schema: "paperclip.question_set.v1", - questions: [{ - id: "goal", - prompt: "What should we accomplish?", - answerMode: "text", - required: true, - }], + questions: [ + { id: "goal", prompt: "What should we accomplish?", answerMode: "text", required: true }, + ...(answerMode === "mixed" ? [{ id: "color", prompt: "Choose one color", answerMode: "single_select", required: true, options: questions[0].options }] : []), + ], }, }; const call = { @@ -547,6 +540,8 @@ describe("PaperclipRunnerToolAuthority", () => { .from(issueThreadInteractions) .where(eq(issueThreadInteractions.issueId, binding.issueId)); expect(rows).toHaveLength(1); + expect((rows[0].payload as { questions: { id: string }[] }).questions.map((question) => question.id)) + .toEqual(answerMode === "choice" ? ["color"] : answerMode === "text" ? ["goal"] : ["goal", "color"]); const [task] = await db .select() .from(issues) diff --git a/server/src/services/native-runtime/runner-api-reference.ts b/server/src/services/native-runtime/runner-api-reference.ts index 88e25bf5ea3..25f0b103cc5 100644 --- a/server/src/services/native-runtime/runner-api-reference.ts +++ b/server/src/services/native-runtime/runner-api-reference.ts @@ -198,21 +198,6 @@ export const runnerApiReference: Record { + const moduleUrl = new URL("./question-interaction-answers.ts", import.meta.url).href; + const script = ` + const { parseQuestionInteractionAnswers } = await import(${JSON.stringify(moduleUrl)}); + let ticks = 0; + const timer = setInterval(() => { ticks += 1; }, 10); + try { + await parseQuestionInteractionAnswers({ + schema: "paperclip.question_set.v1", + questions: [{ id: "value", prompt: "Value?", required: true, answerMode: "text", textValidation: { pattern: "^(a+)+$" } }], + }, [{ questionId: "value", optionIds: [], otherText: "a".repeat(99999) + "!" }], []); + process.exitCode = 1; + } catch (error) { + process.stdout.write(JSON.stringify({ message: error.message, ticks })); + } finally { clearInterval(timer); } + `; + // A future synchronous regression must fail this child deadline, not hang Vitest. + const child = spawnSync(process.execPath, ["--import", "tsx/esm", "--input-type=module", "-e", script], { + cwd: fileURLToPath(new URL("../..", import.meta.url)), encoding: "utf8", timeout: 10_000, + }); + expect(child.error).toBeUndefined(); + expect(child.status, child.stderr).toBe(0); + const result = JSON.parse(child.stdout); + expect(result.message).toContain("time limit"); + expect(result.ticks).toBeGreaterThan(1); +}, 15_000); diff --git a/server/src/services/question-interaction-answers.ts b/server/src/services/question-interaction-answers.ts new file mode 100644 index 00000000000..4c5c0d3f068 --- /dev/null +++ b/server/src/services/question-interaction-answers.ts @@ -0,0 +1,86 @@ +import type { AskUserQuestionsAnswer, AskUserQuestionsPayload, PaperclipQuestionSetPayload } from "@paperclipai/shared"; +import { parsePaperclipQuestionResponse, type PaperclipQuestionResponse } from "../vendor/paperclip-runner/index.js"; +import { validateQuestionPatterns } from "./question-pattern-validation.js"; + +function prepareQuestionInteractionAnswers( + questionSet: PaperclipQuestionSetPayload, + answers: readonly AskUserQuestionsAnswer[], + storageQuestions: AskUserQuestionsPayload["questions"], +) { + const answerByQuestionId = new Map(answers.map((answer) => [answer.questionId, answer])); + const response: PaperclipQuestionResponse = { + schema: "paperclip.question_response.v1", + answers: {}, + }; + for (const question of questionSet.questions) { + const answer = answerByQuestionId.get(question.id); + if (!answer) continue; + if (question.answerMode === "text") { + response.answers[question.id] = { + ...(answer.otherText !== undefined && answer.otherText !== null + ? { text: answer.otherText } + : {}), + }; + } else { + const customOptionId = storageQuestions.find((entry) => entry.id === question.id) + ?.options.find((option) => option.freeText)?.id ?? null; + response.answers[question.id] = { + selectedOptionIds: answer.optionIds.filter((optionId) => optionId !== customOptionId), + ...(answer.otherText !== undefined && answer.otherText !== null + ? { customText: answer.otherText } + : {}), + }; + } + } + // Historical dual forms may offer a written answer only in storage. Keep + // that pending answer path usable; new creation rejects this mismatch. + const answerableQuestionSet = { + ...questionSet, + questions: questionSet.questions.map((question) => { + const storage = storageQuestions.find((entry) => entry.id === question.id); + if (question.answerMode !== "text" && !question.customAnswer + && (storage?.allowOther === true || storage?.options.some((option) => option.freeText))) { + return { ...question, customAnswer: { enabled: true as const } }; + } + return question; + }), + }; + // The portable parser uses JavaScript regexes. Validate all other fields + // there, but run pattern matching only in a bounded, isolated worker. + const withoutPatterns = { + ...answerableQuestionSet, + questions: answerableQuestionSet.questions.map((question) => { + if (!question.textValidation) return question; + const { pattern: _pattern, ...textValidation } = question.textValidation; + return { ...question, textValidation }; + }), + }; + const parsed = parsePaperclipQuestionResponse(withoutPatterns, response); + const checks = answerableQuestionSet.questions.flatMap((question) => { + const pattern = question.textValidation?.pattern; + const answer = parsed.answers[question.id]; + const text = question.answerMode === "text" ? answer?.text : answer?.customText; + return pattern !== undefined && text !== undefined ? [{ questionId: question.id, pattern, text }] : []; + }); + return { parsed, checks }; +} + +/** Validate untrusted answers before persistence, including bounded patterns. */ +export async function parseQuestionInteractionAnswers( + questionSet: PaperclipQuestionSetPayload, + answers: readonly AskUserQuestionsAnswer[], + storageQuestions: AskUserQuestionsPayload["questions"], +): Promise { + const { parsed, checks } = prepareQuestionInteractionAnswers(questionSet, answers, storageQuestions); + await validateQuestionPatterns(checks); + return parsed; +} + +/** Saved answers already passed pattern validation; delivery must not depend on worker capacity. */ +export function parseSavedQuestionInteractionAnswers( + questionSet: PaperclipQuestionSetPayload, + answers: readonly AskUserQuestionsAnswer[], + storageQuestions: AskUserQuestionsPayload["questions"], +): PaperclipQuestionResponse { + return prepareQuestionInteractionAnswers(questionSet, answers, storageQuestions).parsed; +} diff --git a/server/src/services/question-pattern-validation.test.ts b/server/src/services/question-pattern-validation.test.ts new file mode 100644 index 00000000000..93dc00637e1 --- /dev/null +++ b/server/src/services/question-pattern-validation.test.ts @@ -0,0 +1,31 @@ +import { describe, expect, it } from "vitest"; +import { validateQuestionPatterns } from "./question-pattern-validation.js"; + +const pathological = { questionId: "value", pattern: "^(a+)+$", text: "a".repeat(99_999) + "!" }; + +describe("isolated question pattern validation", () => { + it("preserves matching and nonmatching regex semantics", async () => { + await validateQuestionPatterns([{ questionId: "url", pattern: "^https://", text: "https://example.test" }]); + await expect(validateQuestionPatterns([{ questionId: "url", pattern: "^https://", text: "http://example.test" }])).rejects.toThrow("does not match"); + }); + + it("terminates pathological matching while the event loop remains responsive", async () => { + let ticks = 0; + const timer = setInterval(() => { ticks += 1; }, 10); + try { + await expect(validateQuestionPatterns([pathological])).rejects.toThrow("time limit"); + expect(ticks).toBeGreaterThan(1); + await validateQuestionPatterns([{ questionId: "value", pattern: "^a$", text: "a" }]); + } finally { + clearInterval(timer); + } + }, 5_000); + + it("bounds concurrent workers and releases every slot after timeout", async () => { + const pending = Array.from({ length: 4 }, () => validateQuestionPatterns([pathological])); + const settled = Promise.allSettled(pending); + await expect(validateQuestionPatterns([pathological])).rejects.toThrow("busy"); + expect((await settled).every(result => result.status === "rejected")).toBe(true); + await validateQuestionPatterns([{ questionId: "value", pattern: "^a$", text: "a" }]); + }, 5_000); +}); diff --git a/server/src/services/question-pattern-validation.ts b/server/src/services/question-pattern-validation.ts new file mode 100644 index 00000000000..776bc6a7fd4 --- /dev/null +++ b/server/src/services/question-pattern-validation.ts @@ -0,0 +1,53 @@ +import { Worker } from "node:worker_threads"; + +interface PatternCheck { questionId: string; pattern: string; text: string } + +const MAX_WORKERS = 4; +const DEADLINE_MS = 1_000; +let activeWorkers = 0; + +const workerSource = ` +const { parentPort, workerData } = require("node:worker_threads"); +const invalid = workerData.find(check => !new RegExp(check.pattern).test(check.text)); +parentPort.postMessage(invalid ? invalid.questionId : null); +`; + +/** Keep caller-supplied regex execution outside the request-serving thread. */ +export async function validateQuestionPatterns(checks: readonly PatternCheck[]): Promise { + if (checks.length === 0) return; + if (activeWorkers >= MAX_WORKERS) throw new Error("Question format validation is busy; try again"); + activeWorkers += 1; + let worker: Worker; + try { + worker = new Worker(workerSource, { + eval: true, + workerData: checks, + execArgv: [], + resourceLimits: { maxOldGenerationSizeMb: 32, maxYoungGenerationSizeMb: 8, stackSizeMb: 2 }, + }); + } catch (error) { + activeWorkers -= 1; + throw error; + } + await new Promise((resolve, reject) => { + let settled = false; + const finish = (error?: Error) => { + if (settled) return; + settled = true; + clearTimeout(timer); + // Release the slot after the worker actually stops, including timeouts. + void worker.terminate().then(() => { + activeWorkers -= 1; + if (error) reject(error); else resolve(); + }, (terminationError: unknown) => { + activeWorkers -= 1; + reject(error ?? terminationError); + }); + }; + const timer = setTimeout(() => finish(new Error("Question format validation exceeded its time limit")), DEADLINE_MS); + worker.once("message", (questionId: string | null) => finish(questionId === null ? undefined + : new Error(`Question ${questionId} does not match the required format`))); + worker.once("error", finish); + worker.once("exit", () => finish(new Error("Question format validation stopped before completion"))); + }); +} diff --git a/skills/paperclip/SKILL.md b/skills/paperclip/SKILL.md index b1da87ce882..695f91abf43 100644 --- a/skills/paperclip/SKILL.md +++ b/skills/paperclip/SKILL.md @@ -163,7 +163,7 @@ If `currentParticipant` does not match you, do not try to advance the stage — **Human input questions.** -For an open answer, use a text field. Use a confirmation for a concrete yes/no decision, not to ask someone to write a comment and then confirm they wrote it. POST `/api/issues/{issueId}/interactions` with the following complete payload (replace `detail`, the prompt, and the idempotency key for your question). `questionSet` controls presentation; the matching `questions` entry is required storage compatibility and must not be sent alone. +For an open answer, use a text field. Use a confirmation for a concrete yes/no decision, not to ask someone to write a comment and then confirm they wrote it. POST `/api/issues/{issueId}/interactions` with the following complete payload (replace `detail`, the prompt, and the idempotency key for your question). Put every text and choice question in one complete `questionSet`. Paperclip generates the compatibility `questions` entries. Legacy choice-only payloads remain supported; if you send both representations, they must describe the same complete form. Omit `addresseeUserId` for ordinary questions. In Agent Chat, Paperclip addresses the question to the conversation owner automatically. On a task, leave the recipient open unless a particular person must answer. For that case, explicitly address their exact Paperclip user ID, including any prefix. The server rejects unknown or unauthorized recipients. Do not guess IDs or infer authority from a title. Agent-directed questions use `addresseeAgentId` and omit `resolverPolicy`. @@ -179,8 +179,7 @@ In Agent Chat, Paperclip addresses the question to the conversation owner automa "questionSet": { "schema": "paperclip.question_set.v1", "questions": [{ "id": "detail", "prompt": "What should I know?", "answerMode": "text", "required": true }] - }, - "questions": [{ "id": "detail", "prompt": "What should I know?", "selectionMode": "single", "required": true, "options": [{ "id": "text", "label": "Your answer", "freeText": true }] }] + } } } ``` diff --git a/skills/paperclip/references/api-reference.md b/skills/paperclip/references/api-reference.md index c886948217f..867c713ecf1 100644 --- a/skills/paperclip/references/api-reference.md +++ b/skills/paperclip/references/api-reference.md @@ -930,7 +930,7 @@ Agent-directed questions instead set `addresseeAgentId` and omit `resolverPolicy **Text answer (copy this complete payload)** -For an open-ended answer, render a text field using `payload.questionSet` with `answerMode: "text"`, no options, and no `customAnswer`. The REST API still requires matching `payload.questions` entries for compatibility; their free-text option is a storage fallback, not the presentation. Keep question IDs and prompts identical in both fields. Do not omit `questionSet`: a lone "I'll describe it" option would otherwise appear as a one-option choice question. +For an open-ended answer, render a text field using `payload.questionSet` with `answerMode: "text"`, no options, and no `customAnswer`. Send one complete `payload.questionSet` containing every text and choice question. The server generates matching `payload.questions` entries for storage and answer compatibility. Legacy choice-only payloads remain supported. If both fields are supplied, their IDs, prompts, required flags, modes, and visible options must agree. Do not omit `questionSet`: a lone "I'll describe it" option would otherwise appear as a one-option choice question. ```json POST /api/issues/{issueId}/interactions @@ -942,13 +942,6 @@ POST /api/issues/{issueId}/interactions "continuationPolicy": "wake_assignee", "payload": { "version": 1, - "questions": [{ - "id": "responsibility", - "prompt": "What should the new agent be responsible for?", - "selectionMode": "single", - "required": true, - "options": [{ "id": "describe", "label": "I'll describe it", "freeText": true }] - }], "questionSet": { "schema": "paperclip.question_set.v1", "questions": [{ @@ -964,7 +957,7 @@ POST /api/issues/{issueId}/interactions **Multiple choice** -Use `ask_user_questions` for a short question card. Each `payload.questions` entry requires `id`, `prompt`, `selectionMode`, and options with `id` and `label`. Choice questions must offer at least two distinct, meaningful choices; use the canonical text presentation above for open-ended questions. Do not send `question`/`type: "text"` or an empty options array in a `payload.questions` entry. Set `resolverPolicy: "human_only"` when the answer must come from the user. +Use `ask_user_questions` for a short question card. Use `payload.questionSet` with `answerMode: "single_select"` or `"multi_select"`, an explicit `required` flag, and options with `id` and `label`. Use `customAnswer: { "enabled": true }` to offer a written alternative. Choice questions must offer at least two distinct, meaningful choices; use the canonical text presentation above for open-ended questions. Do not send `question`/`type: "text"` or an empty options array in a `payload.questions` entry. Set `resolverPolicy: "human_only"` when the answer must come from the user. ```json POST /api/issues/{issueId}/interactions @@ -976,21 +969,25 @@ POST /api/issues/{issueId}/interactions "continuationPolicy": "wake_assignee", "payload": { "version": 1, - "questions": [{ - "id": "responsibility", - "prompt": "What should the new agent be responsible for?", - "selectionMode": "single", - "required": true, - "allowOther": true, - "options": [ - { "id": "research", "label": "Research", "description": "Find and summarize information." }, - { "id": "writing", "label": "Writing", "description": "Draft and edit content." } - ] - }] + "questionSet": { + "schema": "paperclip.question_set.v1", + "questions": [{ + "id": "responsibility", + "prompt": "What should the new agent be responsible for?", + "answerMode": "single_select", + "required": true, + "customAnswer": { "enabled": true }, + "options": [ + { "id": "research", "label": "Research", "description": "Find and summarize information." }, + { "id": "writing", "label": "Writing", "description": "Draft and edit content." } + ] + }] + } } } ``` + After verifying the interaction was saved and is pending, record the waiting state: ```json