[codex] Add checkbox confirmation issue interactions (#7649)
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - Agent work is coordinated through issues, comments, interactions, and approval-style handoffs. > - Existing issue-thread interactions could ask questions, suggest tasks, and request confirmation, but they did not support a structured checkbox confirmation payload for choosing one or more options. > - That gap made board/user confirmations harder to validate consistently across API callers, plugin helpers, CLI tooling, and the UI. > - This pull request adds the shared checkbox confirmation contract, server handling, client helpers, and issue-thread UI needed to render and submit structured selections. > - The benefit is that agents can request bounded multi-select confirmations in the same audited issue-thread flow as other Paperclip interactions. ## Linked Issues or Issue Description - No public GitHub issue found for this exact branch. Internal Paperclip issue: PAP-10415 / PAP-10441 requested creating this PR for the checkbox confirmation issue-thread UI component work. - GitHub duplicate search performed for checkbox confirmation / issue-thread interaction PRs; no matching open PR was found. - Related issue search result `#7497` was unrelated company file cleanup work, so it is not linked as a related issue. ## What Changed - Added shared types, validators, constants, and tests for `request_checkbox_confirmation` interactions. - Extended server issue-thread interaction service and routes for checkbox confirmation creation, validation, expiration, and response handling. - Added CLI, MCP, and plugin SDK helper coverage so external callers can create the new interaction shape consistently. - Updated the issue-thread interaction UI to render checkbox confirmations with min/max bounds, selection summaries, stale-target states, and accept/decline flows. - Documented the checkbox confirmation interaction contract in the Paperclip skill/API reference. ## Verification - Rebased cleanly on `paperclipai/paperclip` `master` fetched into `public-gh/master` at `a4fa0eaf5`. - Confirmed the PR diff does not include `pnpm-lock.yaml` or `.github/workflows` changes. - Ran focused tests with `NODE_ENV=test`: ```sh NODE_ENV=test pnpm run preflight:workspace-links NODE_ENV=test pnpm exec vitest run packages/shared/src/issue-thread-interactions.test.ts server/src/__tests__/issue-thread-interaction-routes.test.ts server/src/__tests__/issue-thread-interactions-service.test.ts ui/src/components/IssueThreadInteractionCard.test.tsx ui/src/lib/issue-thread-interactions.test.ts cli/src/__tests__/issue-subresources.test.ts cli/src/__tests__/project-goal.test.ts packages/mcp-server/src/tools.test.ts packages/plugins/sdk/tests/testing-actions.test.ts ``` Result: 8 test files passed, 78 tests passed. - CI on latest head `63b9e55` is green. - Greptile Review passed on latest head; GraphQL review-thread check shows all Greptile threads resolved. ## Risks - Medium surface area because the interaction contract touches shared validators, server routes/services, UI rendering, CLI, MCP, plugin SDK helpers, and docs. - No database migrations are included. - `pnpm-lock.yaml` is intentionally excluded per repository lockfile policy. - UI screenshots are not attached because the task explicitly requested not to add design screenshots or images unless they were part of the work; component tests cover the new rendering and interaction states. > For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and discuss it in `#dev` before opening the PR. Feature PRs that overlap with planned core work may need to be redirected — check the roadmap first. See `CONTRIBUTING.md`. ## Model Used - OpenAI Codex coding agent based on GPT-5, with repository file access, shell command execution, git/GitHub CLI tooling, and Paperclip control-plane API access. Exact hosted model ID/context-window metadata is not exposed inside this runtime. ## 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 run tests locally and they pass - [x] I have added or updated tests where applicable - [ ] If this change affects the UI, I have included before/after screenshots - [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 <noreply@paperclip.ing>
This commit is contained in:
@@ -190,6 +190,67 @@ POST /api/companies/{companyId}/approvals
|
||||
|
||||
`issueIds` links the approval into the issue thread. When approved, Paperclip wakes the requester with `PAPERCLIP_APPROVAL_ID`/`PAPERCLIP_APPROVAL_STATUS`. Keep the payload concise and decision-ready.
|
||||
|
||||
## Issue-Thread Interactions
|
||||
|
||||
Issue-thread interactions are first-class cards that render in the issue thread and capture a typed board/user response. Use them instead of asking the board to type yes/no or a checklist in markdown — interactions create audit trails, drive idempotency, and wake the assignee through a structured continuation path.
|
||||
|
||||
Four kinds are supported. Pick the smallest kind that fits the decision shape:
|
||||
|
||||
| Kind | When to use | When **not** to use |
|
||||
| ------------------------------- | -------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
|
||||
| `request_confirmation` | Single yes/no decision bound to a target (e.g. accept a plan revision, approve a launch). | Multi-select choices, free-form answers, or proposing tasks the board can pick from. |
|
||||
| `request_checkbox_confirmation` | Board must select any subset of a known list (up to 200 options) and then confirm or reject. | Yes/no decisions (use `request_confirmation`), or proposing new tasks (use `suggest_tasks`). |
|
||||
| `ask_user_questions` | Short structured form: a handful of typed questions, each with answers/options/text. | Selecting many items from a long list, or single accept/reject decisions. |
|
||||
| `suggest_tasks` | Proposing concrete tasks for the board to accept; accepted tasks become real subtasks. | Asking the board to confirm a plan or arbitrary selection. Tasks are the unit; not arbitrary ids. |
|
||||
|
||||
Key shared semantics:
|
||||
|
||||
- **Continuation policy.** `request_checkbox_confirmation` defaults to `wake_assignee`, which wakes you after the board resolves the selection. `request_confirmation` defaults to `none`, so set `wake_assignee` or `wake_assignee_on_accept` when you need to resume after a yes/no decision. `none` never wakes you — only use it when you truly do not need to resume.
|
||||
- **Target binding and staleness.** `request_confirmation` and `request_checkbox_confirmation` both accept a `target` (typically `{ type: "issue_document", key, revisionId, … }`). When a newer revision lands, Paperclip expires the pending interaction with `outcome: "stale_target"`. Rebuild against the latest revision and create a fresh interaction.
|
||||
- **Supersede on user comment.** Both confirmation kinds default `supersedeOnUserComment: true`, so a later board/user comment cancels the pending request with `outcome: "superseded_by_comment"`. On the wake, address the comment and create a new interaction if approval is still required.
|
||||
- **Idempotency.** Use a deterministic `idempotencyKey` such as `confirmation:${issueId}:plan:${revisionId}` or `checkbox:${issueId}:${decisionKey}:${revisionId}` so retries do not stack duplicate cards.
|
||||
- **Source issue posture.** After creating a pending interaction, move the source issue to `in_review` with a comment that names what the board must decide. The pending interaction is the explicit waiting path.
|
||||
|
||||
Create a `request_checkbox_confirmation` (board selects any subset, then confirms):
|
||||
|
||||
```json
|
||||
POST /api/issues/{issueId}/interactions
|
||||
{
|
||||
"kind": "request_checkbox_confirmation",
|
||||
"idempotencyKey": "checkbox:{issueId}:cleanup-files:{planRevisionId}",
|
||||
"title": "Confirm files to delete",
|
||||
"summary": "Pick the files you want removed before I run the cleanup.",
|
||||
"continuationPolicy": "wake_assignee",
|
||||
"payload": {
|
||||
"version": 1,
|
||||
"prompt": "Check the files you want deleted.",
|
||||
"detailsMarkdown": "I will run the deletion against everything you check, then report back here.",
|
||||
"options": [
|
||||
{ "id": "draft-report-march", "label": "Old draft report", "description": "QA test pass, March." },
|
||||
{ "id": "tmp-export-2025", "label": "tmp/export-2025.csv" }
|
||||
],
|
||||
"defaultSelectedOptionIds": ["draft-report-march"],
|
||||
"minSelected": 0,
|
||||
"maxSelected": null,
|
||||
"acceptLabel": "Delete selected",
|
||||
"rejectLabel": "Request changes",
|
||||
"rejectRequiresReason": true,
|
||||
"rejectReasonLabel": "What should change?",
|
||||
"supersedeOnUserComment": true,
|
||||
"target": {
|
||||
"type": "issue_document",
|
||||
"issueId": "{issueId}",
|
||||
"key": "plan",
|
||||
"revisionId": "{latestPlanRevisionId}"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
When the board accepts, your wake delivers `result.selectedOptionIds` — the option ids they picked (which may be empty if `minSelected: 0`). Rejection delivers `result.reason` and a `commentId`.
|
||||
|
||||
For full payload schemas, validation limits (option count, label lengths, min/max rules), accept/reject route bodies, and result fields, see `references/api-reference.md` -> **Checkbox confirmations**.
|
||||
|
||||
## Niche Workflow Pointers
|
||||
|
||||
Load `references/workflows.md` when the task matches one of these:
|
||||
|
||||
@@ -686,6 +686,118 @@ Rules:
|
||||
- A pending interaction is an explicit waiting path. Before ending the heartbeat, update the source issue into a visible waiting posture, normally `in_review`, and leave a comment that names what the board/user must decide.
|
||||
- For plan approval, update the `plan` issue document first, create the confirmation against the latest plan revision, set the source issue to `in_review`, and wait for acceptance before creating implementation subtasks.
|
||||
|
||||
### Checkbox confirmations
|
||||
|
||||
Use `request_checkbox_confirmation` when the board needs to **select any subset of a known list** (up to 200 options) and then confirm or reject. It is a confirmation, not a question — the board accepts/rejects the whole interaction; the selected ids ride along on the accept call.
|
||||
|
||||
When to choose this kind over the others:
|
||||
|
||||
- Choose `request_checkbox_confirmation` over `ask_user_questions` when the decision is a single multi-select (especially with more than a handful of options or near the ~100-option range). `ask_user_questions` is for short structured forms, not long lists.
|
||||
- Choose `request_checkbox_confirmation` over `request_confirmation` when the board's decision is "yes, but only these items," not a pure yes/no.
|
||||
- Choose `request_checkbox_confirmation` over `suggest_tasks` when the items are not concrete tasks to be created. `suggest_tasks` is the right answer when accepted items must become subtasks; checkbox confirmation is the right answer when the agent will act on the selected set itself.
|
||||
|
||||
Create a checkbox confirmation:
|
||||
|
||||
```json
|
||||
POST /api/issues/{issueId}/interactions
|
||||
{
|
||||
"kind": "request_checkbox_confirmation",
|
||||
"idempotencyKey": "checkbox:{issueId}:cleanup-files:{planRevisionId}",
|
||||
"title": "Confirm files to delete",
|
||||
"summary": "Pick the files you want removed before I run the cleanup.",
|
||||
"continuationPolicy": "wake_assignee",
|
||||
"payload": {
|
||||
"version": 1,
|
||||
"prompt": "Check the files you want deleted.",
|
||||
"detailsMarkdown": "I will run the deletion against everything you check, then report back here.",
|
||||
"options": [
|
||||
{ "id": "draft-report-march", "label": "Old draft report", "description": "QA test pass, March." },
|
||||
{ "id": "tmp-export-2025", "label": "tmp/export-2025.csv" }
|
||||
],
|
||||
"defaultSelectedOptionIds": ["draft-report-march"],
|
||||
"minSelected": 0,
|
||||
"maxSelected": null,
|
||||
"acceptLabel": "Delete selected",
|
||||
"rejectLabel": "Request changes",
|
||||
"rejectRequiresReason": true,
|
||||
"rejectReasonLabel": "What should change?",
|
||||
"allowDeclineReason": true,
|
||||
"declineReasonPlaceholder": "Tell me what to revise.",
|
||||
"supersedeOnUserComment": true,
|
||||
"target": {
|
||||
"type": "issue_document",
|
||||
"issueId": "{issueId}",
|
||||
"key": "plan",
|
||||
"revisionId": "{latestPlanRevisionId}"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Payload field reference (`RequestCheckboxConfirmationPayload`):
|
||||
|
||||
| Field | Type | Default | Notes |
|
||||
| --------------------------- | ------------------------------------------ | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `version` | `1` | required | Versioned for forward compatibility. |
|
||||
| `prompt` | string (1–1000 chars) | required | Headline rendered above the checkbox list. |
|
||||
| `detailsMarkdown` | string (≤ 20000 chars) \| `null` | `null` | Optional markdown context above the list. |
|
||||
| `options` | `[{ id, label, description? }]` | required, 1–200 entries | Option `id` and `label` are 1–120 chars; `description` ≤ 500 chars. Option ids must be unique within the payload. |
|
||||
| `defaultSelectedOptionIds` | string array | `[]` | Pre-checks these option ids in the UI. Each id must reference an option in `options`. Length must not exceed `maxSelected` when set. |
|
||||
| `minSelected` | integer ≥ 0 | `0` | Server rejects acceptances below this floor. Cannot exceed `options.length`. |
|
||||
| `maxSelected` | integer ≥ 0 \| `null` | `null` (unbounded) | Must satisfy `maxSelected ≥ minSelected` and `maxSelected ≤ options.length` when set. |
|
||||
| `acceptLabel` | string (1–80) \| `null` | `null` (UI default) | Button label for accept. |
|
||||
| `rejectLabel` | string (1–80) \| `null` | `null` (UI default) | Button label for reject/request-changes. |
|
||||
| `rejectRequiresReason` | boolean | `false` | When `true`, the board must supply a non-empty `reason` on reject; the server returns 422 otherwise. |
|
||||
| `rejectReasonLabel` | string (1–160) \| `null` | `null` | Field label for the reject reason. |
|
||||
| `allowDeclineReason` | boolean | `true` | Whether to render the reason input at all. |
|
||||
| `declineReasonPlaceholder` | string (1–240) \| `null` | `null` | Placeholder text in the reason input. |
|
||||
| `supersedeOnUserComment` | boolean | `true` (set server-side) | When `true`, a board/user comment after the interaction supersedes it with `outcome: "superseded_by_comment"`. |
|
||||
| `target` | `RequestConfirmationTarget` \| `null` | `null` | Reuses the `request_confirmation` target schema. Stale-target expiration is identical: when the targeted document revision is no longer current, the interaction expires with `outcome: "stale_target"`. |
|
||||
|
||||
Envelope defaults that differ from other kinds:
|
||||
|
||||
- `continuationPolicy` defaults to `"wake_assignee"` for `request_checkbox_confirmation` (same as `suggest_tasks` and `ask_user_questions`). Use `"wake_assignee_on_accept"` to skip rejection wakes; use `"none"` only when you truly do not need to resume.
|
||||
|
||||
Accept (board action, requires board/user role; agents creating the interaction cannot accept):
|
||||
|
||||
```json
|
||||
POST /api/issues/{issueId}/interactions/{interactionId}/accept
|
||||
{ "selectedOptionIds": ["draft-report-march", "tmp-export-2025"] }
|
||||
```
|
||||
|
||||
If `selectedOptionIds` is omitted on accept, the server falls back to the payload's `defaultSelectedOptionIds`. The server validates that every id references a known option, deduplicates, and enforces `minSelected`/`maxSelected`. Unknown ids return 422.
|
||||
|
||||
Reject:
|
||||
|
||||
```json
|
||||
POST /api/issues/{issueId}/interactions/{interactionId}/reject
|
||||
{ "reason": "Keep the March draft; only delete tmp/export-2025.csv." }
|
||||
```
|
||||
|
||||
`reason` is required when `rejectRequiresReason: true`, otherwise optional.
|
||||
|
||||
Resolved result (`RequestCheckboxConfirmationResult`):
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 1,
|
||||
"outcome": "accepted",
|
||||
"selectedOptionIds": ["draft-report-march", "tmp-export-2025"]
|
||||
}
|
||||
```
|
||||
|
||||
Other outcomes match `request_confirmation`:
|
||||
|
||||
- `rejected` — `{ outcome: "rejected", reason, commentId }`. `selectedOptionIds` is absent.
|
||||
- `superseded_by_comment` — `{ outcome: "superseded_by_comment", commentId }`. The next board/user comment after a pending interaction with `supersedeOnUserComment: true` triggers this.
|
||||
- `stale_target` — `{ outcome: "stale_target", staleTarget }`. Emitted when the targeted issue document revision is no longer current.
|
||||
|
||||
Best practice:
|
||||
|
||||
- Use a deterministic idempotency key like `checkbox:${issueId}:${decisionKey}:${revisionId}` so retries (e.g. after a transient error) reuse the same card instead of stacking duplicates.
|
||||
- After creating a pending checkbox confirmation, move the source issue to `in_review` with a comment that names exactly what the board must decide. Pending interactions are an explicit waiting path, not a synonym for `done`.
|
||||
- When a `superseded_by_comment` or `stale_target` wake fires, address the new comment or rebuild the target, then create a fresh checkbox confirmation with an idempotency key that includes the new revision id.
|
||||
|
||||
### Checking approval status
|
||||
|
||||
```
|
||||
@@ -792,8 +904,8 @@ Terminal states: `done`, `cancelled`
|
||||
| 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` | Create issue-thread interaction (`suggest_tasks`, `ask_user_questions`, `request_confirmation`, `request_checkbox_confirmation`) |
|
||||
| POST | `/api/issues/:issueId/interactions/:interactionId/accept` | Accept suggested tasks or confirmation (body: `selectedClientKeys` for `suggest_tasks`; `selectedOptionIds` for `request_checkbox_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 |
|
||||
|
||||
Reference in New Issue
Block a user