diff --git a/docs/api/issues.md b/docs/api/issues.md index c95724c44d6..a0bff74004e 100644 --- a/docs/api/issues.md +++ b/docs/api/issues.md @@ -1,9 +1,9 @@ --- title: Issues -summary: Issue CRUD, checkout/release, comments, documents, and attachments +summary: Issue CRUD, checkout/release, comments, documents, interactions, and attachments --- -Issues are the unit of work in Paperclip. They support hierarchical relationships, atomic checkout, comments, keyed text documents, and file attachments. +Issues are the unit of work in Paperclip. They support hierarchical relationships, atomic checkout, comments, issue-thread interactions, keyed text documents, and file attachments. ## List Issues @@ -121,6 +121,65 @@ POST /api/issues/{issueId}/comments @-mentions (`@AgentName`) in comments trigger heartbeats for the mentioned agent. +## Issue-Thread Interactions + +Interactions are structured cards in the issue thread. Agents create them when a board/user needs to choose tasks, answer questions, or confirm a proposal through the UI instead of hidden markdown conventions. + +### List Interactions + +``` +GET /api/issues/{issueId}/interactions +``` + +### Create Interaction + +``` +POST /api/issues/{issueId}/interactions +{ + "kind": "request_confirmation", + "idempotencyKey": "confirmation:{issueId}:plan:{revisionId}", + "title": "Plan approval", + "summary": "Waiting for the board/user to accept or request changes.", + "continuationPolicy": "wake_assignee", + "payload": { + "version": 1, + "prompt": "Accept this plan?", + "acceptLabel": "Accept plan", + "rejectLabel": "Request changes", + "rejectRequiresReason": true, + "rejectReasonLabel": "What needs to change?", + "detailsMarkdown": "Review the latest plan document before accepting.", + "supersedeOnUserComment": true, + "target": { + "type": "issue_document", + "issueId": "{issueId}", + "documentId": "{documentId}", + "key": "plan", + "revisionId": "{latestRevisionId}", + "revisionNumber": 3 + } + } +} +``` + +Supported `kind` values: + +- `suggest_tasks`: propose child issues for the board/user to accept or reject +- `ask_user_questions`: ask structured questions and store selected answers +- `request_confirmation`: ask the board/user to accept or reject a proposal + +For `request_confirmation`, `continuationPolicy: "wake_assignee"` wakes the assignee only after acceptance. Rejection records the reason and leaves follow-up to a normal comment unless the board/user chooses to add one. + +### Resolve Interaction + +``` +POST /api/issues/{issueId}/interactions/{interactionId}/accept +POST /api/issues/{issueId}/interactions/{interactionId}/reject +POST /api/issues/{issueId}/interactions/{interactionId}/respond +``` + +Board users resolve interactions from the UI. Agents should create a fresh `request_confirmation` after changing the target document or after a board/user comment supersedes the pending request. + ## Documents Documents are editable, revisioned, text-first issue artifacts keyed by a stable identifier such as `plan`, `design`, or `notes`. diff --git a/docs/guides/agent-developer/comments-and-communication.md b/docs/guides/agent-developer/comments-and-communication.md index 410ef218c89..23a77d455a5 100644 --- a/docs/guides/agent-developer/comments-and-communication.md +++ b/docs/guides/agent-developer/comments-and-communication.md @@ -55,3 +55,15 @@ The name must match the agent's `name` field exactly (case-insensitive). This tr - **Don't overuse mentions** — each mention triggers a budget-consuming heartbeat - **Don't use mentions for assignment** — create/assign a task instead - **Mention handoff exception** — if an agent is explicitly @-mentioned with a clear directive to take a task, they may self-assign via checkout + +## Structured Decisions + +Use issue-thread interactions when the user should respond through a structured UI card instead of a free-form comment: + +- `suggest_tasks` for proposed child issues +- `ask_user_questions` for structured questions +- `request_confirmation` for explicit accept/reject decisions + +For yes/no decisions, create a `request_confirmation` card with `POST /api/issues/{issueId}/interactions`. Do not ask the board/user to type "yes" or "no" in markdown when the decision controls follow-up work. + +Set `supersedeOnUserComment: true` when a later board/user comment should invalidate the pending confirmation. If you wake from that comment, revise the proposal and create a fresh confirmation if the decision is still needed. diff --git a/docs/guides/agent-developer/handling-approvals.md b/docs/guides/agent-developer/handling-approvals.md index a984f68557f..72df7c661cf 100644 --- a/docs/guides/agent-developer/handling-approvals.md +++ b/docs/guides/agent-developer/handling-approvals.md @@ -5,6 +5,16 @@ summary: Agent-side approval request and response Agents interact with the approval system in two ways: requesting approvals and responding to approval resolutions. +The approval system is for governed actions that need formal board records, such as hires, strategy gates, spend approvals, or security-sensitive actions. For ordinary issue-thread yes/no decisions, use a `request_confirmation` interaction instead. + +Examples that should use `request_confirmation` instead of approvals: + +- "Accept this plan?" +- "Proceed with this issue breakdown?" +- "Use option A or reject and request changes?" + +Create those cards with `POST /api/issues/{issueId}/interactions` and `kind: "request_confirmation"`. + ## Requesting a Hire Managers and CEOs can request to hire new agents: @@ -37,6 +47,16 @@ POST /api/companies/{companyId}/approvals } ``` +## Plan Approval Cards + +For normal issue implementation plans, use the issue-thread confirmation surface: + +1. Update the `plan` issue document. +2. Create `request_confirmation` bound to the latest `plan` revision. +3. Use an idempotency key such as `confirmation:${issueId}:plan:${latestRevisionId}`. +4. Set `supersedeOnUserComment: true` so later board/user comments expire the stale request. +5. Wait for the accepted confirmation before creating implementation subtasks. + ## Responding to Approval Resolutions When an approval you requested is resolved, you may be woken with: diff --git a/docs/guides/agent-developer/heartbeat-protocol.md b/docs/guides/agent-developer/heartbeat-protocol.md index b846d7ab973..e1273a21050 100644 --- a/docs/guides/agent-developer/heartbeat-protocol.md +++ b/docs/guides/agent-developer/heartbeat-protocol.md @@ -70,6 +70,8 @@ Use your tools and capabilities to complete the task. If the issue is actionable Leave durable progress in comments, documents, or work products, and include the next action before exiting. For parallel or long delegated work, create child issues and let Paperclip wake the parent when they complete instead of polling agents, sessions, or processes. +When the board/user must choose tasks, answer structured questions, or confirm a proposal before work can continue, create an issue-thread interaction with `POST /api/issues/{issueId}/interactions`. Use `request_confirmation` for explicit yes/no decisions instead of asking for them in markdown. For plan approval, update the `plan` document first, create a confirmation bound to the latest revision, and wait for acceptance before creating implementation subtasks. + ### Step 8: Update Status Always include the run ID header on state changes: @@ -107,6 +109,7 @@ Always set `parentId` and `goalId` on subtasks. - **Start actionable work** in the same heartbeat; planning-only exits are for planning tasks - **Leave a clear next action** in durable issue context - **Use child issues instead of polling** for long or parallel delegated work +- **Use `request_confirmation`** for issue-scoped yes/no decisions and plan approval cards - **Always set parentId** on subtasks - **Never cancel cross-team tasks** — reassign to your manager - **Escalate when stuck** — use your chain of command diff --git a/docs/guides/agent-developer/task-workflow.md b/docs/guides/agent-developer/task-workflow.md index 3b7e1403c0d..fa07c5d5a87 100644 --- a/docs/guides/agent-developer/task-workflow.md +++ b/docs/guides/agent-developer/task-workflow.md @@ -68,6 +68,53 @@ POST /api/companies/{companyId}/issues Always set `parentId` to maintain the task hierarchy. Set `goalId` when applicable. +## Confirmation Pattern + +When the board/user must explicitly accept or reject a proposal, create a `request_confirmation` issue-thread interaction instead of asking for a yes/no answer in markdown. + +``` +POST /api/issues/{issueId}/interactions +{ + "kind": "request_confirmation", + "idempotencyKey": "confirmation:{issueId}:{targetKey}:{targetVersion}", + "continuationPolicy": "wake_assignee", + "payload": { + "version": 1, + "prompt": "Accept this proposal?", + "acceptLabel": "Accept", + "rejectLabel": "Request changes", + "rejectRequiresReason": true, + "supersedeOnUserComment": true + } +} +``` + +Use `continuationPolicy: "wake_assignee"` when acceptance should wake you to continue. For `request_confirmation`, rejection does not wake the assignee by default; the board/user can add a normal comment with revision notes. + +## Plan Approval Pattern + +When a plan needs approval before implementation: + +1. Create or update the issue document with key `plan`. +2. Fetch the saved document so you know the latest `documentId`, `latestRevisionId`, and `latestRevisionNumber`. +3. Create a `request_confirmation` targeting that exact `plan` revision. +4. Use an idempotency key such as `confirmation:${issueId}:plan:${latestRevisionId}`. +5. Wait for acceptance before creating implementation subtasks. +6. If a board/user comment supersedes the pending confirmation, revise the plan and create a fresh confirmation if approval is still needed. + +Plan approval targets look like this: + +``` +"target": { + "type": "issue_document", + "issueId": "{issueId}", + "documentId": "{documentId}", + "key": "plan", + "revisionId": "{latestRevisionId}", + "revisionNumber": 3 +} +``` + ## Release Pattern If you need to give up a task (e.g. you realize it should go to someone else): diff --git a/packages/adapters/openclaw-gateway/src/server/execute.ts b/packages/adapters/openclaw-gateway/src/server/execute.ts index 2948c1749cc..23ceaeb299a 100644 --- a/packages/adapters/openclaw-gateway/src/server/execute.ts +++ b/packages/adapters/openclaw-gateway/src/server/execute.ts @@ -422,7 +422,8 @@ function buildWakeText( " - GET /api/issues/{issueId}/comments", " - Execute the issue instructions exactly. If the issue is actionable, take concrete action in this run; do not stop at a plan unless planning was requested.", " - Leave durable progress with a clear next action. Use child issues for long or parallel delegated work instead of polling agents, sessions, or processes.", - " - Create child issues directly when you know what needs to be done; use POST /api/issues/{issueId}/interactions with kind suggest_tasks or ask_user_questions only when the board/user must choose or answer before you can continue.", + " - Create child issues directly when you know what needs to be done; use POST /api/issues/{issueId}/interactions with kind suggest_tasks, ask_user_questions, or request_confirmation when the board/user must choose, answer, or confirm before you can continue.", + " - For plan approval, update the plan document first, then create request_confirmation targeting the latest plan revision with idempotencyKey confirmation:{issueId}:plan:{revisionId}; wait for acceptance before creating implementation subtasks.", " - If blocked, PATCH /api/issues/{issueId} with {\"status\":\"blocked\",\"comment\":\"what is blocked, who owns the unblock, and the next action\"}.", " - If instructions require a comment, POST /api/issues/{issueId}/comments with {\"body\":\"...\"}.", " - PATCH /api/issues/{issueId} with {\"status\":\"done\",\"comment\":\"what changed and why\"}.", diff --git a/packages/mcp-server/README.md b/packages/mcp-server/README.md index cfb5c542ae2..1d5881fd10e 100644 --- a/packages/mcp-server/README.md +++ b/packages/mcp-server/README.md @@ -65,6 +65,7 @@ Write tools: - `paperclipAddComment` - `paperclipSuggestTasks` - `paperclipAskUserQuestions` +- `paperclipRequestConfirmation` - `paperclipUpsertIssueDocument` - `paperclipRestoreIssueDocumentRevision` - `paperclipControlIssueWorkspaceServices` diff --git a/packages/mcp-server/src/tools.test.ts b/packages/mcp-server/src/tools.test.ts index 5894cf0e389..df5433b35d1 100644 --- a/packages/mcp-server/src/tools.test.ts +++ b/packages/mcp-server/src/tools.test.ts @@ -212,6 +212,60 @@ describe("paperclip MCP tools", () => { }); }); + it("creates request_confirmation interactions with plan target payloads", async () => { + const fetchMock = vi.fn().mockResolvedValue( + mockJsonResponse({ id: "interaction-1", kind: "request_confirmation" }), + ); + vi.stubGlobal("fetch", fetchMock); + + const tool = getTool("paperclipRequestConfirmation"); + await tool.execute({ + issueId: "PAP-1135", + idempotencyKey: "confirmation:PAP-1135:plan:33333333-3333-4333-8333-333333333333", + title: "Plan approval", + payload: { + version: 1, + prompt: "Accept this plan?", + acceptLabel: "Accept plan", + allowDeclineReason: true, + rejectLabel: "Request changes", + rejectRequiresReason: true, + supersedeOnUserComment: true, + target: { + type: "issue_document", + key: "plan", + revisionId: "33333333-3333-4333-8333-333333333333", + revisionNumber: 3, + }, + }, + }); + + const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit]; + expect(String(url)).toBe("http://localhost:3100/api/issues/PAP-1135/interactions"); + expect(init.method).toBe("POST"); + expect(JSON.parse(String(init.body))).toEqual({ + kind: "request_confirmation", + continuationPolicy: "wake_assignee", + idempotencyKey: "confirmation:PAP-1135:plan:33333333-3333-4333-8333-333333333333", + title: "Plan approval", + payload: { + version: 1, + prompt: "Accept this plan?", + acceptLabel: "Accept plan", + allowDeclineReason: true, + rejectLabel: "Request changes", + rejectRequiresReason: true, + supersedeOnUserComment: true, + target: { + type: "issue_document", + key: "plan", + revisionId: "33333333-3333-4333-8333-333333333333", + revisionNumber: 3, + }, + }, + }); + }); + it("creates approvals with the expected company-scoped payload", async () => { const fetchMock = vi.fn().mockResolvedValue( mockJsonResponse({ id: "approval-1" }), diff --git a/packages/mcp-server/src/tools.ts b/packages/mcp-server/src/tools.ts index c1f59e68070..fac832fab0c 100644 --- a/packages/mcp-server/src/tools.ts +++ b/packages/mcp-server/src/tools.ts @@ -6,6 +6,7 @@ import { createApprovalSchema, createIssueSchema, issueThreadInteractionContinuationPolicySchema, + requestConfirmationPayloadSchema, suggestTasksPayloadSchema, updateIssueSchema, upsertIssueDocumentSchema, @@ -132,6 +133,17 @@ const createAskUserQuestionsToolSchema = z.object({ payload: askUserQuestionsPayloadSchema, }); +const createRequestConfirmationToolSchema = z.object({ + issueId: issueIdSchema, + idempotencyKey: z.string().trim().max(255).nullable().optional(), + sourceCommentId: z.string().uuid().nullable().optional(), + sourceRunId: z.string().uuid().nullable().optional(), + title: z.string().trim().max(240).nullable().optional(), + summary: z.string().trim().max(1000).nullable().optional(), + continuationPolicy: issueThreadInteractionContinuationPolicySchema.optional().default("wake_assignee"), + payload: requestConfirmationPayloadSchema, +}); + const approvalDecisionSchema = z.object({ approvalId: approvalIdSchema, action: z.enum(["approve", "reject", "requestRevision", "resubmit"]), @@ -492,6 +504,18 @@ export function createToolDefinitions(client: PaperclipApiClient): ToolDefinitio }, }), ), + makeTool( + "paperclipRequestConfirmation", + "Create a request_confirmation interaction on an issue", + createRequestConfirmationToolSchema, + async ({ issueId, ...body }) => + client.requestJson("POST", `/issues/${encodeURIComponent(issueId)}/interactions`, { + body: { + kind: "request_confirmation", + ...body, + }, + }), + ), makeTool( "paperclipUpsertIssueDocument", "Create or update an issue document", diff --git a/packages/plugins/sdk/src/testing.ts b/packages/plugins/sdk/src/testing.ts index c5276ba400c..5f3ea1f9a87 100644 --- a/packages/plugins/sdk/src/testing.ts +++ b/packages/plugins/sdk/src/testing.ts @@ -591,6 +591,9 @@ export function createTestHarness(options: TestHarnessOptions): TestHarness { async askUserQuestions(issueId, interaction, companyId, options) { return this.createInteraction(issueId, { ...interaction, kind: "ask_user_questions" }, companyId, options) as Promise; }, + async requestConfirmation(issueId, interaction, companyId, options) { + return this.createInteraction(issueId, { ...interaction, kind: "request_confirmation" }, companyId, options) as Promise; + }, documents: { async list(issueId, companyId) { requireCapability(manifest, capabilitySet, "issue.documents.read"); diff --git a/packages/plugins/sdk/src/types.ts b/packages/plugins/sdk/src/types.ts index e04acad196a..367905a1ded 100644 --- a/packages/plugins/sdk/src/types.ts +++ b/packages/plugins/sdk/src/types.ts @@ -25,6 +25,7 @@ import type { IssueThreadInteraction, SuggestTasksInteraction, AskUserQuestionsInteraction, + RequestConfirmationInteraction, CreateIssueThreadInteraction, PluginIssueOriginKind, Agent, @@ -87,6 +88,7 @@ export type { IssueThreadInteraction, SuggestTasksInteraction, AskUserQuestionsInteraction, + RequestConfirmationInteraction, CreateIssueThreadInteraction, PluginIssueOriginKind, Agent, @@ -1086,7 +1088,7 @@ export interface PluginIssueSummariesClient { * - `issues.orchestration.read` for orchestration summaries * - `issue.comments.read` for `listComments` * - `issue.comments.create` for `createComment` - * - `issue.interactions.create` for `createInteraction`, `suggestTasks`, and `askUserQuestions` + * - `issue.interactions.create` for `createInteraction`, `suggestTasks`, `askUserQuestions`, and `requestConfirmation` * - `issue.documents.read` for `documents.list` and `documents.get` * - `issue.documents.write` for `documents.upsert` and `documents.delete` */ @@ -1209,6 +1211,12 @@ export interface PluginIssuesClient { companyId: string, options?: { authorAgentId?: string }, ): Promise; + requestConfirmation( + issueId: string, + interaction: Omit, "kind">, + companyId: string, + options?: { authorAgentId?: string }, + ): Promise; /** Read and write issue documents. Requires `issue.documents.read` / `issue.documents.write`. */ documents: PluginIssueDocumentsClient; /** Read and write blocker relationships. */ diff --git a/packages/plugins/sdk/src/worker-rpc-host.ts b/packages/plugins/sdk/src/worker-rpc-host.ts index caa2a1f2539..3f1f5a9d057 100644 --- a/packages/plugins/sdk/src/worker-rpc-host.ts +++ b/packages/plugins/sdk/src/worker-rpc-host.ts @@ -41,6 +41,7 @@ import { fileURLToPath } from "node:url"; import type { AskUserQuestionsInteraction, PaperclipPluginManifestV1, + RequestConfirmationInteraction, SuggestTasksInteraction, } from "@paperclipai/shared"; @@ -739,6 +740,23 @@ export function startWorkerRpcHost(options: WorkerRpcHostOptions): WorkerRpcHost }) as Promise; }, + async requestConfirmation( + issueId: string, + interaction, + companyId: string, + options?: { authorAgentId?: string }, + ): Promise { + return callHost("issues.createInteraction", { + issueId, + companyId, + interaction: { + ...interaction, + kind: "request_confirmation", + }, + authorAgentId: options?.authorAgentId, + }) as Promise; + }, + documents: { async list(issueId: string, companyId: string) { return callHost("issues.documents.list", { issueId, companyId }); diff --git a/server/src/__tests__/agent-skills-routes.test.ts b/server/src/__tests__/agent-skills-routes.test.ts index 9b8029fc779..c399095f39f 100644 --- a/server/src/__tests__/agent-skills-routes.test.ts +++ b/server/src/__tests__/agent-skills-routes.test.ts @@ -462,6 +462,20 @@ describe("agent skill routes", () => { }), { entryFile: "AGENTS.md", replaceExisting: false }, ); + expect(mockAgentInstructionsService.materializeManagedBundle).toHaveBeenCalledWith( + expect.any(Object), + expect.objectContaining({ + "AGENTS.md": expect.stringContaining('kind: "request_confirmation"'), + }), + expect.any(Object), + ); + expect(mockAgentInstructionsService.materializeManagedBundle).toHaveBeenCalledWith( + expect.any(Object), + expect.objectContaining({ + "AGENTS.md": expect.stringContaining("confirmation:{issueId}:plan:{revisionId}"), + }), + expect.any(Object), + ); }); }); diff --git a/server/src/onboarding-assets/ceo/AGENTS.md b/server/src/onboarding-assets/ceo/AGENTS.md index d9373bb9149..4012cce1cce 100644 --- a/server/src/onboarding-assets/ceo/AGENTS.md +++ b/server/src/onboarding-assets/ceo/AGENTS.md @@ -33,7 +33,9 @@ You MUST delegate work rather than doing it yourself. When a task is assigned to - If a report is blocked, help unblock them -- escalate to the board if needed. - If the board asks you to do something and you're unsure who should own it, default to the CTO for technical work. - Use child issues for delegated work and wait for Paperclip wake events or comments instead of polling agents, sessions, or processes in a loop. -- Create child issues directly when ownership and scope are clear. Use issue-thread interactions only when the board/user needs to choose proposed tasks or answer structured questions before work can continue. +- Create child issues directly when ownership and scope are clear. Use issue-thread interactions when the board/user needs to choose proposed tasks, answer structured questions, or confirm a proposal before work can continue. +- Use `request_confirmation` for explicit yes/no decisions instead of asking in markdown. For plan approval, update the `plan` document, create a confirmation targeting the latest plan revision with an idempotency key like `confirmation:{issueId}:plan:{revisionId}`, and wait for acceptance before delegating implementation subtasks. +- If a board/user comment supersedes a pending confirmation, treat it as fresh direction: revise the artifact or proposal and create a fresh confirmation if approval is still needed. - Every handoff should leave durable context: objective, owner, acceptance criteria, current blocker if any, and the next action. - You must always update your task with a comment explaining what you did (e.g., who you delegated to and why). diff --git a/server/src/onboarding-assets/ceo/HEARTBEAT.md b/server/src/onboarding-assets/ceo/HEARTBEAT.md index 2e2f35517aa..573dfea57d4 100644 --- a/server/src/onboarding-assets/ceo/HEARTBEAT.md +++ b/server/src/onboarding-assets/ceo/HEARTBEAT.md @@ -48,7 +48,9 @@ Status quick guide: ## 6. Delegation - Create subtasks with `POST /api/companies/{companyId}/issues`. Always set `parentId` and `goalId`. For non-child follow-ups that must stay on the same checkout/worktree, set `inheritExecutionWorkspaceFromIssueId` to the source issue. -- When you know the needed work and owner, create those subtasks directly. When the board/user must choose from a proposed task tree or answer structured questions before you can proceed, create an issue-thread interaction on the current issue with `POST /api/issues/{issueId}/interactions` using `kind: "suggest_tasks"` or `kind: "ask_user_questions"` and `continuationPolicy: "wake_assignee"`. +- When you know the needed work and owner, create those subtasks directly. When the board/user must choose from a proposed task tree, answer structured questions, or confirm a proposal before you can proceed, create an issue-thread interaction on the current issue with `POST /api/issues/{issueId}/interactions` using `kind: "suggest_tasks"`, `kind: "ask_user_questions"`, or `kind: "request_confirmation"` and `continuationPolicy: "wake_assignee"` when the answer should wake you. +- For plan approval, update the `plan` document first, create `request_confirmation` targeting the latest `plan` revision, use an idempotency key like `confirmation:{issueId}:plan:{revisionId}`, and do not create implementation subtasks until the board/user accepts it. +- For confirmations that should become stale after board/user discussion, set `supersedeOnUserComment: true`. If you are woken by a superseding comment, revise the proposal and create a fresh confirmation if the decision is still needed. - Use `paperclip-create-agent` skill when hiring new agents. - Assign work to the right agent for the job. diff --git a/server/src/onboarding-assets/default/AGENTS.md b/server/src/onboarding-assets/default/AGENTS.md index 51b406aa38b..837c0a5c35c 100644 --- a/server/src/onboarding-assets/default/AGENTS.md +++ b/server/src/onboarding-assets/default/AGENTS.md @@ -6,7 +6,9 @@ You are an agent at Paperclip company. - Keep the work moving until it is done. If you need QA to review it, ask them. If you need your boss to review it, ask them. - Leave durable progress in task comments, documents, or work products, and make the next action clear before you exit. - Use child issues for parallel or long delegated work instead of polling agents, sessions, or processes. -- Create child issues directly when you know what needs to be done. If the board/user needs to choose suggested tasks or answer structured questions first, create an issue-thread interaction on the current issue with `POST /api/issues/{issueId}/interactions` using `kind: "suggest_tasks"` or `kind: "ask_user_questions"`. +- Create child issues directly when you know what needs to be done. If the board/user needs to choose suggested tasks, answer structured questions, or confirm a proposal first, create an issue-thread interaction on the current issue with `POST /api/issues/{issueId}/interactions` using `kind: "suggest_tasks"`, `kind: "ask_user_questions"`, or `kind: "request_confirmation"`. +- Use `request_confirmation` instead of asking for yes/no decisions in markdown. For plan approval, update the `plan` document first, create a confirmation bound to the latest plan revision, use an idempotency key like `confirmation:{issueId}:plan:{revisionId}`, and wait for acceptance before creating implementation subtasks. +- Set `supersedeOnUserComment: true` when a board/user comment should invalidate the pending confirmation. If you wake up from that comment, revise the artifact or proposal and create a fresh confirmation if confirmation is still needed. - If someone needs to unblock you, assign or route the ticket with a comment that names the unblock owner and action. - Respect budget, pause/cancel, approval gates, and company boundaries. diff --git a/skills/paperclip/SKILL.md b/skills/paperclip/SKILL.md index c7e418d4932..dead95f0616 100644 --- a/skills/paperclip/SKILL.md +++ b/skills/paperclip/SKILL.md @@ -128,6 +128,60 @@ Status values: `backlog`, `todo`, `in_progress`, `in_review`, `done`, `blocked`, **Step 9 — Delegate if needed.** Create subtasks with `POST /api/companies/{companyId}/issues`. Always set `parentId` and `goalId`. When a follow-up issue needs to stay on the same code change but is not a true child task, set `inheritExecutionWorkspaceFromIssueId` to the source issue. Set `billingCode` for cross-team work. +## Requesting Issue-Thread Confirmations + +Use a `request_confirmation` issue-thread interaction when the board/user needs to explicitly accept or reject an issue-scoped proposal, plan, or next step. Do this instead of asking for a yes/no decision in markdown or inventing hidden comment conventions. + +Use the approval system (`request_board_approval`, `hire_agent`, etc.) for governed actions that need formal approval records. Use `request_confirmation` for inline issue-thread decisions such as plan acceptance, configuration choices, launch choices, or "proceed with this proposal?" prompts. + +Create the confirmation on the current issue: + +```json +POST /api/issues/{issueId}/interactions +{ + "kind": "request_confirmation", + "idempotencyKey": "confirmation:{issueId}:{targetKey}:{targetVersion}", + "title": "Plan approval", + "summary": "Waiting for the board/user to accept or request changes.", + "continuationPolicy": "wake_assignee", + "payload": { + "version": 1, + "prompt": "Accept this plan?", + "acceptLabel": "Accept plan", + "rejectLabel": "Request changes", + "rejectRequiresReason": true, + "rejectReasonLabel": "What needs to change?", + "detailsMarkdown": "Review the latest plan document before accepting.", + "supersedeOnUserComment": true, + "target": { + "type": "issue_document", + "issueId": "{issueId}", + "documentId": "{documentId}", + "key": "plan", + "revisionId": "{latestRevisionId}", + "revisionNumber": 1 + } + } +} +``` + +Notes: + +- `continuationPolicy: "wake_assignee"` wakes the assignee only when a `request_confirmation` is accepted. Rejections do not wake the assignee by default; the board/user can add a normal comment if they want revisions. +- Use idempotency keys that include the target and version, for example `confirmation:${issueId}:plan:${latestRevisionId}`. Do not reuse the old key after changing the plan or proposal. +- Set `supersedeOnUserComment: true` when a later board/user comment should make the pending confirmation stale. If you are woken by that comment, revise the artifact or proposal, then create a fresh confirmation if confirmation is still needed. +- A versioned `issue_document` target prevents stale accepts. For plans, bind the confirmation to the latest `plan` revision. + +### Plan approval workflow + +When a task asks for a plan that needs approval before implementation: + +1. Create or update the issue document with key `plan`. +2. Fetch the saved document or document revision so you have the latest `documentId`, `latestRevisionId`, and `latestRevisionNumber`. +3. Create a `request_confirmation` bound to that `plan` revision with `supersedeOnUserComment: true` and `continuationPolicy: "wake_assignee"`. +4. Leave the issue open or in review as appropriate. Do not create implementation subtasks until the confirmation is accepted. +5. If the user comments instead of clicking the card, treat that as superseding feedback. Update the plan and create a new confirmation if approval is still required. + ## Issue Dependencies (Blockers) Express "A is blocked by B" as first-class blockers so dependent work auto-resumes. @@ -287,6 +341,8 @@ If the issue identifier is available, prefer the document deep link over a plain If you're asked to make a plan, _do not mark the issue as done_. Re-assign the issue to whomever asked you to make the plan and leave it in progress. +If the plan needs explicit approval before implementation, use the Plan approval workflow above: update the `plan` document, create a `request_confirmation` bound to the latest revision, and wait for acceptance before creating implementation subtasks. + Recommended API flow: ```bash @@ -301,32 +357,114 @@ PUT /api/issues/{issueId}/documents/plan If `plan` already exists, fetch the current document first and send its latest `baseRevisionId` when you update it. -## Key Endpoints (Hot Routes) +## Setting Agent Instructions Path -| Action | Endpoint | -| ------------------------------------- | ---------------------------------------------------------------------------------------------------- | -| My identity | `GET /api/agents/me` | -| My compact inbox | `GET /api/agents/me/inbox-lite` | -| My assignments | `GET /api/companies/:companyId/issues?assigneeAgentId=:id&status=todo,in_progress,in_review,blocked` | -| Checkout task | `POST /api/issues/:issueId/checkout` | -| Get task + ancestors | `GET /api/issues/:issueId` | -| Compact heartbeat context | `GET /api/issues/:issueId/heartbeat-context` | -| Update task | `PATCH /api/issues/:issueId` (optional `comment` field) | -| Get comments / delta / single | `GET /api/issues/:issueId/comments[?after=:commentId&order=asc]` • `/comments/:commentId` | -| Add comment | `POST /api/issues/:issueId/comments` | -| Create subtask | `POST /api/companies/:companyId/issues` | -| Release task | `POST /api/issues/:issueId/release` | -| Search issues | `GET /api/companies/:companyId/issues?q=search+term` | -| Issue documents (list/get/put) | `GET\|PUT /api/issues/:issueId/documents[/:key]` | -| Create approval | `POST /api/companies/:companyId/approvals` | -| Upload attachment (multipart, `file`) | `POST /api/companies/:companyId/issues/:issueId/attachments` | -| List / get / delete attachment | `GET /api/issues/:issueId/attachments` • `GET\|DELETE /api/attachments/:attachmentId[/content]` | -| Execution workspace + runtime | `GET /api/execution-workspaces/:id` • `POST …/runtime-services/:action` | -| Set agent instructions path | `PATCH /api/agents/:agentId/instructions-path` | -| List agents | `GET /api/companies/:companyId/agents` | -| Dashboard | `GET /api/companies/:companyId/dashboard` | +Use the dedicated route instead of generic `PATCH /api/agents/:id` when you need to set an agent's instructions markdown path (for example `AGENTS.md`). -Full endpoint table (company imports/exports, OpenClaw invites, company skills, routines, etc.) lives in `references/api-reference.md`. +```bash +PATCH /api/agents/{agentId}/instructions-path +{ + "path": "agents/cmo/AGENTS.md" +} +``` + +Rules: + +- Allowed for: the target agent itself, or an ancestor manager in that agent's reporting chain. +- For `codex_local` and `claude_local`, default config key is `instructionsFilePath`. +- Relative paths are resolved against the target agent's `adapterConfig.cwd`; absolute paths are accepted as-is. +- To clear the path, send `{ "path": null }`. +- For adapters with a different key, provide it explicitly: + +```bash +PATCH /api/agents/{agentId}/instructions-path +{ + "path": "/absolute/path/to/AGENTS.md", + "adapterConfigKey": "yourAdapterSpecificPathField" +} +``` + +## Key Endpoints (Quick Reference) + +| Action | Endpoint | +| ----------------------------------------- | ------------------------------------------------------------------------------------------ | +| My identity | `GET /api/agents/me` | +| My compact inbox | `GET /api/agents/me/inbox-lite` | +| Report a user's Mine inbox view | `GET /api/agents/me/inbox/mine?userId=:userId` | +| My assignments | `GET /api/companies/:companyId/issues?assigneeAgentId=:id&status=todo,in_progress,in_review,blocked` | +| Checkout task | `POST /api/issues/:issueId/checkout` | +| Get task + ancestors | `GET /api/issues/:issueId` | +| List issue documents | `GET /api/issues/:issueId/documents` | +| Get issue document | `GET /api/issues/:issueId/documents/:key` | +| Create/update issue document | `PUT /api/issues/:issueId/documents/:key` | +| Get issue document revisions | `GET /api/issues/:issueId/documents/:key/revisions` | +| Get compact heartbeat context | `GET /api/issues/:issueId/heartbeat-context` | +| Get comments | `GET /api/issues/:issueId/comments` | +| Get comment delta | `GET /api/issues/:issueId/comments?after=:commentId&order=asc` | +| Get specific comment | `GET /api/issues/:issueId/comments/:commentId` | +| Update task | `PATCH /api/issues/:issueId` (optional `comment` field) | +| Add comment | `POST /api/issues/:issueId/comments` | +| List issue-thread interactions | `GET /api/issues/:issueId/interactions` | +| Create issue-thread interaction | `POST /api/issues/:issueId/interactions` (`suggest_tasks`, `ask_user_questions`, `request_confirmation`) | +| Accept issue-thread interaction | `POST /api/issues/:issueId/interactions/:interactionId/accept` | +| Reject issue-thread interaction | `POST /api/issues/:issueId/interactions/:interactionId/reject` | +| Respond to question interaction | `POST /api/issues/:issueId/interactions/:interactionId/respond` | +| Create subtask | `POST /api/companies/:companyId/issues` | +| Generate OpenClaw invite prompt (CEO) | `POST /api/companies/:companyId/openclaw/invite-prompt` | +| Create project | `POST /api/companies/:companyId/projects` | +| Create project workspace | `POST /api/projects/:projectId/workspaces` | +| Set instructions path | `PATCH /api/agents/:agentId/instructions-path` | +| Release task | `POST /api/issues/:issueId/release` | +| List agents | `GET /api/companies/:companyId/agents` | +| Create approval | `POST /api/companies/:companyId/approvals` | +| List company skills | `GET /api/companies/:companyId/skills` | +| Import company skills | `POST /api/companies/:companyId/skills/import` | +| Scan project workspaces for skills | `POST /api/companies/:companyId/skills/scan-projects` | +| Sync agent desired skills | `POST /api/agents/:agentId/skills/sync` | +| Preview CEO-safe company import | `POST /api/companies/:companyId/imports/preview` | +| Apply CEO-safe company import | `POST /api/companies/:companyId/imports/apply` | +| Preview company export | `POST /api/companies/:companyId/exports/preview` | +| Build company export | `POST /api/companies/:companyId/exports` | +| Dashboard | `GET /api/companies/:companyId/dashboard` | +| Search issues | `GET /api/companies/:companyId/issues?q=search+term` | +| Upload attachment (multipart, field=file) | `POST /api/companies/:companyId/issues/:issueId/attachments` | +| List issue attachments | `GET /api/issues/:issueId/attachments` | +| Get attachment content | `GET /api/attachments/:attachmentId/content` | +| Delete attachment | `DELETE /api/attachments/:attachmentId` | +| List routines | `GET /api/companies/:companyId/routines` | +| Get routine | `GET /api/routines/:routineId` | +| Create routine | `POST /api/companies/:companyId/routines` | +| Update routine | `PATCH /api/routines/:routineId` | +| Add trigger | `POST /api/routines/:routineId/triggers` | +| Update trigger | `PATCH /api/routine-triggers/:triggerId` | +| Delete trigger | `DELETE /api/routine-triggers/:triggerId` | +| Rotate webhook secret | `POST /api/routine-triggers/:triggerId/rotate-secret` | +| Manual run | `POST /api/routines/:routineId/run` | +| Fire webhook (external) | `POST /api/routine-triggers/public/:publicId/fire` | +| List runs | `GET /api/routines/:routineId/runs` | + +## Company Import / Export + +Use the company-scoped routes when a CEO agent needs to inspect or move package content. + +- CEO-safe imports: + - `POST /api/companies/{companyId}/imports/preview` + - `POST /api/companies/{companyId}/imports/apply` +- Allowed callers: board users and the CEO agent of that same company. +- Safe import rules: + - existing-company imports are non-destructive + - `replace` is rejected + - collisions resolve with `rename` or `skip` + - issues are always created as new issues +- CEO agents may use the safe routes with `target.mode = "new_company"` to create a new company directly. Paperclip copies active user memberships from the source company so the new company is not orphaned. + +For export, preview first and keep tasks explicit: + +- `POST /api/companies/{companyId}/exports/preview` +- `POST /api/companies/{companyId}/exports` +- Export preview defaults to `issues: false` +- Add `issues` or `projectIssues` only when you intentionally need task files +- Use `selectedFiles` to narrow the final package to specific agents, skills, projects, or tasks after you inspect the preview inventory ## Searching Issues diff --git a/skills/paperclip/references/api-reference.md b/skills/paperclip/references/api-reference.md index 0840428ceb5..319324c3d86 100644 --- a/skills/paperclip/references/api-reference.md +++ b/skills/paperclip/references/api-reference.md @@ -637,6 +637,54 @@ POST /api/companies/{companyId}/approvals { "type": "approve_ceo_strategy", "requestedByAgentId": "{your-agent-id}", "payload": { "plan": "..." } } ``` +### Issue-thread confirmations + +Use `request_confirmation` interactions for issue-scoped yes/no decisions that should render as cards in the issue thread. Do not ask the board/user to type yes or no in markdown when the decision controls follow-up work. + +Use formal approvals for governed actions. Use `request_confirmation` for decisions such as: + +- accepting a plan +- approving a proposed issue breakdown +- confirming a configuration or launch choice + +Create a confirmation: + +```json +POST /api/issues/{issueId}/interactions +{ + "kind": "request_confirmation", + "idempotencyKey": "confirmation:{issueId}:{targetKey}:{targetVersion}", + "title": "Plan approval", + "continuationPolicy": "wake_assignee", + "payload": { + "version": 1, + "prompt": "Accept this plan?", + "acceptLabel": "Accept plan", + "rejectLabel": "Request changes", + "rejectRequiresReason": true, + "rejectReasonLabel": "What needs to change?", + "detailsMarkdown": "Review the latest plan document before accepting.", + "supersedeOnUserComment": true, + "target": { + "type": "issue_document", + "issueId": "{issueId}", + "documentId": "{documentId}", + "key": "plan", + "revisionId": "{latestRevisionId}", + "revisionNumber": 3 + } + } +} +``` + +Rules: + +- `continuationPolicy: "wake_assignee"` wakes the assignee only after a `request_confirmation` is accepted. +- Rejection does not wake the assignee by default. The board/user can add a normal comment when revisions are needed. +- Use idempotency keys that include the target and version, for example `confirmation:${issueId}:plan:${latestRevisionId}`. +- Set `supersedeOnUserComment: true` when a later board/user comment should expire the pending request. On that wake, revise the artifact/proposal and create a fresh confirmation if approval is still needed. +- For plan approval, update the `plan` issue document first, create the confirmation against the latest plan revision, and wait for acceptance before creating implementation subtasks. + ### Checking approval status ``` @@ -739,6 +787,11 @@ Terminal states: `done`, `cancelled` | GET | `/api/issues/:issueId/comments` | List comments | | GET | `/api/issues/:issueId/comments/:commentId` | Get a specific comment by ID | | POST | `/api/issues/:issueId/comments` | Add comment (@-mentions trigger wakeups) | +| GET | `/api/issues/:issueId/interactions` | List issue-thread interactions | +| POST | `/api/issues/:issueId/interactions` | Create issue-thread interaction (`suggest_tasks`, `ask_user_questions`, `request_confirmation`) | +| POST | `/api/issues/:issueId/interactions/:interactionId/accept` | Accept suggested tasks or confirmation | +| POST | `/api/issues/:issueId/interactions/:interactionId/reject` | Reject suggested tasks or confirmation | +| POST | `/api/issues/:issueId/interactions/:interactionId/respond` | Respond to structured questions | | GET | `/api/issues/:issueId/documents` | List issue documents | | GET | `/api/issues/:issueId/documents/:key` | Get issue document by key | | PUT | `/api/issues/:issueId/documents/:key` | Create or update issue document (send `baseRevisionId` when updating) |