feat: add attention queue and Decisions surface (#9380)

## Thinking Path

> - Paperclip is the control plane for autonomous AI companies, where
operators need a reliable way to find and act on work awaiting their
input.
> - The attention and issue-thread interaction subsystems expose those
decision points across server APIs and the board UI.
> - The previous navigation and interaction presentation left these
actions fragmented and did not offer a controlled rollout for the
Decisions surface.
> - This branch adds the attention feed, richer interaction cards,
grouping, dismiss/snooze behavior, and a gated Decisions sidebar entry.
> - It also keeps experimental settings and API contracts synchronized,
with an idempotent migration for the new dismissal state.
> - This pull request delivers the complete, tested attention/Decisions
experience as one reviewable unit.

## Linked Issues or Issue Description

- Adds an operator-focused attention queue and Decisions experience:
grouped decision cards, semantic interaction actions, dismiss/snooze
handling, resilient interaction states, and an experimental flag to
control the Decisions navigation entry.


## Feature Context

### Problem or Motivation

Operators currently have to hunt across approvals, interactions, failed
runs, and budget alerts to find decisions that need their action.

### Proposed Solution

Provide a gated Decisions attention queue that groups actionable items,
supports direct resolution, and preserves operator control through
dismiss and snooze actions.

### Alternatives Considered

Keep separate, source-specific views only; this leaves cross-cutting
operator decisions fragmented and harder to prioritize.

### Roadmap Alignment

This improves the V1 control-plane operator workflow by making pending
governed actions discoverable in one company-scoped surface.

## What Changed

- Added server attention-feed services, routes, interaction handling,
dismiss/snooze support, and an idempotent `0145` inbox-dismissal
migration.
- Added shared attention, inbox-dismissal, and experimental-settings
contracts.
- Added Decisions/attention UI, interaction-card states, sidebar
badge/navigation integration, grouping, keyboard support, and Storybook
coverage.
- Added tests for attention behavior, thread interactions, settings
normalization, dismissals, and API behavior.
- Removed generated screenshots from the final PR diff and rebased the
branch onto current `master`.

## Verification

- `pnpm check:token-gates` — passed.
- `pnpm exec vitest run
packages/shared/src/issue-thread-interactions.test.ts
server/src/__tests__/attention-service.test.ts
server/src/__tests__/inbox-dismissals.test.ts
server/src/__tests__/issue-thread-interactions-service.test.ts
server/src/__tests__/issue-thread-interaction-routes.test.ts
ui/src/lib/attention.test.ts
ui/src/components/AttentionQueueRow.test.tsx
ui/src/components/IssueThreadInteractionCard.test.tsx
ui/src/pages/InstanceExperimentalSettings.test.tsx` — passed: 158 tests
across 9 focused files.
- GitHub Actions for `ad636f560`: build and typecheck/release-registry
have passed; remaining general-server and Greptile checks are in
progress.

## Risks

- Moderate: this is a cross-layer attention/interaction feature with a
new migration and navigation behavior.
- The `enableDecisions` experimental setting defaults to off, limiting
rollout impact.
- Existing dismissal data is backfilled to `dismiss`; the migration is
idempotent and uses guarded constraint creation.

> ROADMAP.md was checked; no duplicate planned core feature was
identified. Related open pull requests were searched before opening this
PR.

## Model Used

- OpenAI GPT-5.5 via Codex CLI, with tool use and local code execution.
Context-window size unavailable in this environment.

## 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 public PR branch name describes the change and contains no
internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally; focused tests pass and the remaining
unrelated AWS test failure is documented above
- [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
- [ ] All Paperclip CI gates are green
- [ ] 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>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
authored and GitHub committed 2026-07-10 17:09:57 -05:00
1 parent ac66fd65cb
commit 36ec79c196
70 files changed
+9067 -90

No files matched your search

+2
View File
@@ -248,10 +248,12 @@ export const ISSUE_THREAD_INTERACTION_KINDS = [
"ask_user_questions",
"request_confirmation",
"request_checkbox_confirmation",
"request_item_verdicts",
] as const;
export type IssueThreadInteractionKind = (typeof ISSUE_THREAD_INTERACTION_KINDS)[number];
export const REQUEST_CHECKBOX_CONFIRMATION_OPTION_LIMIT = 200;
export const REQUEST_ITEM_VERDICTS_ITEM_LIMIT = REQUEST_CHECKBOX_CONFIRMATION_OPTION_LIMIT;
export const ISSUE_THREAD_INTERACTION_STATUSES = [
"pending",
+30
View File
@@ -49,6 +49,21 @@ export {
type ResponsibleUserDenialCopy,
type ResponsibleUserDenialTone,
} from "./responsible-user-denial.js";
export type {
AttentionDecisionVerb,
AttentionDetailImage,
AttentionFeed,
AttentionItem,
AttentionItemDetail,
AttentionItemDismissal,
AttentionProjectRef,
AttentionSeverity,
AttentionSourceKind,
AttentionSubject,
AttentionSubjectKind,
AttentionWorkspaceRef,
} from "./types/attention.js";
export type {
PipelineAutomationRetryBlocker,
PipelineAutomationRetryCleanupOptions,
@@ -296,6 +311,7 @@ export {
type IssueThreadInteractionStatus,
type IssueThreadInteractionContinuationPolicy,
REQUEST_CHECKBOX_CONFIRMATION_OPTION_LIMIT,
REQUEST_ITEM_VERDICTS_ITEM_LIMIT,
type BuiltInIssueOriginKind,
type PluginIssueOriginKind,
type IssueOriginKind,
@@ -770,6 +786,11 @@ export type {
RequestCheckboxConfirmationOption,
RequestCheckboxConfirmationPayload,
RequestCheckboxConfirmationResult,
RequestItemVerdictValue,
RequestItemVerdictsItem,
RequestItemVerdictsPayload,
RequestItemVerdictsResultItem,
RequestItemVerdictsResult,
AcceptedPlanDecompositionStatus,
AcceptedPlanDecompositionChild,
AcceptedPlanDecomposition,
@@ -781,6 +802,7 @@ export type {
AskUserQuestionsInteraction,
RequestConfirmationInteraction,
RequestCheckboxConfirmationInteraction,
RequestItemVerdictsInteraction,
IssueThreadInteraction,
IssueThreadInteractionPayload,
IssueThreadInteractionResult,
@@ -857,6 +879,7 @@ export type {
SidebarBadges,
SidebarOrderPreference,
InboxDismissal,
InboxDismissalKind,
AccessUserProfile,
CompanyMemberRecord,
CompanyMembersResponse,
@@ -1284,11 +1307,17 @@ export {
requestCheckboxConfirmationOptionSchema,
requestCheckboxConfirmationPayloadSchema,
requestCheckboxConfirmationResultSchema,
requestItemVerdictValueSchema,
requestItemVerdictsItemSchema,
requestItemVerdictsPayloadSchema,
requestItemVerdictsResultItemSchema,
requestItemVerdictsResultSchema,
createIssueThreadInteractionSchema,
acceptIssueThreadInteractionSchema,
rejectIssueThreadInteractionSchema,
cancelIssueThreadInteractionSchema,
respondIssueThreadInteractionSchema,
submitIssueThreadInteractionVerdictsSchema,
linkIssueApprovalSchema,
createIssueAttachmentMetadataSchema,
createIssueWorkProductSchema,
@@ -1350,6 +1379,7 @@ export {
type RejectIssueThreadInteraction,
type CancelIssueThreadInteraction,
type RespondIssueThreadInteraction,
type SubmitIssueThreadInteractionVerdicts,
type LinkIssueApproval,
type CreateIssueAttachmentMetadata,
type CreateIssueWorkProduct,
@@ -3,6 +3,7 @@ import {
acceptIssueThreadInteractionSchema,
askUserQuestionsResultSchema,
createIssueThreadInteractionSchema,
submitIssueThreadInteractionVerdictsSchema,
} from "./validators/issue.js";
describe("issue thread interaction schemas", () => {
@@ -306,4 +307,111 @@ describe("issue thread interaction schemas", () => {
selectedOptionIds: ["item-1", "item-1"],
})).toThrow("selectedOptionIds must be unique");
});
it("parses request_item_verdicts payloads with defaults", () => {
const parsed = createIssueThreadInteractionSchema.parse({
kind: "request_item_verdicts",
payload: {
version: 1,
prompt: "Review these generated items.",
items: [
{ id: "api", label: "API route", description: "Server submit endpoint" },
{ id: "docs", label: "Docs", previewMarkdown: "Document the route." },
],
},
});
expect(parsed).toMatchObject({
kind: "request_item_verdicts",
continuationPolicy: "wake_assignee",
payload: {
verdicts: ["approve", "reject"],
requireReasonOn: ["reject"],
allowBulkApprove: true,
},
});
});
it("accepts request_item_verdicts defer when enabled explicitly", () => {
const parsed = createIssueThreadInteractionSchema.parse({
kind: "request_item_verdicts",
payload: {
version: 1,
prompt: "Review these generated items.",
items: [{ id: "api", label: "API route" }],
verdicts: ["approve", "reject", "defer"],
requireReasonOn: ["reject", "defer"],
},
});
expect(parsed).toMatchObject({
kind: "request_item_verdicts",
payload: {
verdicts: ["approve", "reject", "defer"],
requireReasonOn: ["reject", "defer"],
},
});
});
it("rejects invalid request_item_verdicts item and reason references", () => {
const base = {
kind: "request_item_verdicts",
payload: {
version: 1,
prompt: "Review these generated items.",
items: [
{ id: "api", label: "API route" },
{ id: "docs", label: "Docs" },
],
},
} as const;
expect(() => createIssueThreadInteractionSchema.parse({
...base,
payload: {
...base.payload,
items: [],
},
})).toThrow();
expect(() => createIssueThreadInteractionSchema.parse({
...base,
payload: {
...base.payload,
items: [
{ id: "api", label: "API route" },
{ id: "api", label: "Duplicate" },
],
},
})).toThrow("Item ids must be unique within one item verdict request");
expect(() => createIssueThreadInteractionSchema.parse({
...base,
payload: {
...base.payload,
items: Array.from({ length: 201 }, (_value, index) => ({
id: `item-${index}`,
label: `Item ${index}`,
})),
},
})).toThrow();
expect(() => createIssueThreadInteractionSchema.parse({
...base,
payload: {
...base.payload,
verdicts: ["approve", "reject"],
requireReasonOn: ["defer"],
},
})).toThrow("requireReasonOn must reference enabled verdicts");
});
it("rejects duplicate request_item_verdicts submit ids", () => {
expect(() => submitIssueThreadInteractionVerdictsSchema.parse({
verdicts: [
{ id: "api", verdict: "approve" },
{ id: "api", verdict: "reject", reason: "Needs revision" },
],
})).toThrow("verdict item ids must be unique");
});
});
+177
View File
@@ -0,0 +1,177 @@
import type { InboxDismissalKind } from "./inbox-dismissal.js";
export type AttentionSourceKind =
| "approval"
| "issue_thread_interaction"
| "join_request"
| "recovery_action"
| "productivity_review"
| "blocker_attention"
| "review"
| "failed_run"
| "budget_alert"
| "agent_error_alert";
export type AttentionSubjectKind =
| "approval"
| "issue"
| "interaction"
| "join_request"
| "recovery_action"
| "run"
| "budget_incident"
| "agent";
export type AttentionSeverity = "critical" | "high" | "medium" | "low";
export interface AttentionSubject {
kind: AttentionSubjectKind;
id: string;
companyId: string;
title: string | null;
identifier: string | null;
status: string | null;
href: string | null;
metadata?: Record<string, unknown>;
}
export interface AttentionDecisionVerb {
id: string;
label: string;
description: string | null;
}
export interface AttentionProjectRef {
id: string;
name: string;
urlKey: string;
color: string | null;
icon: string | null;
}
export interface AttentionWorkspaceRef {
id: string;
name: string;
}
export interface AttentionDetailImage {
assetId: string;
alt?: string | null;
}
export interface AttentionItemDismissal {
kind: InboxDismissalKind;
dismissedAt: string;
snoozedUntil: string | null;
isActive: boolean;
}
export type AttentionItemDetail =
| {
kind: "approval";
approvalType: string;
summaryExcerpt: string | null;
images: AttentionDetailImage[];
}
| {
kind: "plan_approval";
issueTitle: string | null;
planTitle: string | null;
summaryExcerpt: string | null;
images: AttentionDetailImage[];
}
| {
kind: "confirmation";
promptExcerpt: string | null;
isPlanTarget: false;
images: AttentionDetailImage[];
}
| {
kind: "questions";
questionCount: number;
firstQuestionText: string | null;
images: AttentionDetailImage[];
}
| {
kind: "suggested_tasks";
taskCount: number;
firstTaskTitle: string | null;
images: AttentionDetailImage[];
}
| {
kind: "checkbox_confirmation";
optionCount: number;
promptExcerpt: string | null;
images: AttentionDetailImage[];
}
| {
kind: "item_verdicts";
itemCount: number;
promptExcerpt: string | null;
images: AttentionDetailImage[];
}
| {
kind: "failed_run";
agentName: string | null;
failureReasonExcerpt: string | null;
images: AttentionDetailImage[];
}
| {
kind: "blocker";
blockingIssue: {
id: string | null;
identifier: string | null;
title: string | null;
} | null;
images: AttentionDetailImage[];
}
| {
kind: "budget";
observedPercent: number;
amountObserved: number;
amountLimit: number;
images: AttentionDetailImage[];
}
| {
kind: "agent_error";
agentName: string | null;
failureReasonExcerpt: string | null;
images: AttentionDetailImage[];
}
| {
kind: "generic";
summaryExcerpt: string | null;
images: AttentionDetailImage[];
};
export interface AttentionItem {
id: string;
companyId: string;
sourceKind: AttentionSourceKind;
subject: AttentionSubject;
whyNow: string;
decisionVerbs: AttentionDecisionVerb[];
inlineResolvable: boolean;
entryRule: string;
exitRule: string;
dedupKey: string;
dismissalKey: string;
dismissal: AttentionItemDismissal | null;
severity: AttentionSeverity;
rank: number;
activityAt: string;
createdAt: string;
updatedAt: string;
relatedIssue: AttentionSubject | null;
project: AttentionProjectRef | null;
workspace: AttentionWorkspaceRef | null;
detail: AttentionItemDetail | null;
}
export interface AttentionFeed {
companyId: string;
generatedAt: string;
totalCount: number;
countsBySourceKind: Record<AttentionSourceKind, number>;
items: AttentionItem[];
}
@@ -1,9 +1,13 @@
export type InboxDismissalKind = "dismiss" | "snooze";
export interface InboxDismissal {
id: string;
companyId: string;
userId: string;
itemKey: string;
kind: InboxDismissalKind;
dismissedAt: Date;
snoozedUntil: Date | null;
createdAt: Date;
updatedAt: Date;
}
+21 -1
View File
@@ -1,4 +1,18 @@
export type { Company } from "./company.js";
export type {
AttentionDecisionVerb,
AttentionDetailImage,
AttentionFeed,
AttentionItem,
AttentionItemDetail,
AttentionItemDismissal,
AttentionProjectRef,
AttentionSeverity,
AttentionSourceKind,
AttentionSubject,
AttentionSubjectKind,
AttentionWorkspaceRef,
} from "./attention.js";
export type {
Environment,
EnvironmentDeleteBlastRadius,
@@ -400,6 +414,11 @@ export type {
RequestCheckboxConfirmationOption,
RequestCheckboxConfirmationPayload,
RequestCheckboxConfirmationResult,
RequestItemVerdictValue,
RequestItemVerdictsItem,
RequestItemVerdictsPayload,
RequestItemVerdictsResultItem,
RequestItemVerdictsResult,
AcceptedPlanDecompositionStatus,
AcceptedPlanDecompositionChild,
AcceptedPlanDecomposition,
@@ -411,6 +430,7 @@ export type {
AskUserQuestionsInteraction,
RequestConfirmationInteraction,
RequestCheckboxConfirmationInteraction,
RequestItemVerdictsInteraction,
IssueThreadInteraction,
IssueThreadInteractionPayload,
IssueThreadInteractionResult,
@@ -558,7 +578,7 @@ export type {
UpdateResourceMembership,
} from "./resource-memberships.js";
export { RESOURCE_MEMBERSHIP_STATES } from "./resource-memberships.js";
export type { InboxDismissal } from "./inbox-dismissal.js";
export type { InboxDismissal, InboxDismissalKind } from "./inbox-dismissal.js";
export type {
AccessUserProfile,
CompanyMemberRecord,
+1
View File
@@ -57,6 +57,7 @@ export interface InstanceExperimentalSettings {
enableCloudSync: boolean;
enableExternalObjects: boolean;
enableBuiltInAgents: boolean;
enableDecisions: boolean;
enableGoalsSidebarLink: boolean;
enableServerInfoDebugView: boolean;
autoRestartDevServerWhenIdle: boolean;
+54 -3
View File
@@ -1082,6 +1082,30 @@ export interface RequestCheckboxConfirmationPayload {
target?: RequestConfirmationTarget | null;
}
export type RequestItemVerdictValue = "approve" | "reject" | "defer";
export interface RequestItemVerdictsItem {
id: string;
label: string;
description?: string | null;
previewMarkdown?: string | null;
href?: string | null;
attachmentId?: string | null;
}
export interface RequestItemVerdictsPayload {
version: 1;
prompt: string;
detailsMarkdown?: string | null;
items: RequestItemVerdictsItem[];
verdicts?: RequestItemVerdictValue[];
requireReasonOn?: RequestItemVerdictValue[];
reasonLabel?: string | null;
allowBulkApprove?: boolean;
supersedeOnUserComment?: boolean;
target?: RequestConfirmationTarget | null;
}
export interface RequestConfirmationResult {
version: 1;
outcome: "accepted" | "rejected" | "superseded_by_comment" | "stale_target";
@@ -1104,6 +1128,24 @@ export interface RequestCheckboxConfirmationResult extends RequestConfirmationRe
selectedOptionIds?: string[];
}
export interface RequestItemVerdictsResultItem {
id: string;
verdict: RequestItemVerdictValue;
reason?: string | null;
resolvedByUserId: string;
resolvedAt: Date | string;
commentId?: string | null;
}
export interface RequestItemVerdictsResult {
version: 1;
outcome: "resolved" | "superseded_by_comment" | "stale_target" | "cancelled";
complete: boolean;
items: RequestItemVerdictsResultItem[];
commentId?: string | null;
staleTarget?: RequestConfirmationTarget | null;
}
export interface IssueThreadInteractionBase extends IssueThreadInteractionActorFields {
id: string;
companyId: string;
@@ -1145,23 +1187,32 @@ export interface RequestCheckboxConfirmationInteraction extends IssueThreadInter
result?: RequestCheckboxConfirmationResult | null;
}
export interface RequestItemVerdictsInteraction extends IssueThreadInteractionBase {
kind: "request_item_verdicts";
payload: RequestItemVerdictsPayload;
result?: RequestItemVerdictsResult | null;
}
export type IssueThreadInteraction =
| SuggestTasksInteraction
| AskUserQuestionsInteraction
| RequestConfirmationInteraction
| RequestCheckboxConfirmationInteraction;
| RequestCheckboxConfirmationInteraction
| RequestItemVerdictsInteraction;
export type IssueThreadInteractionPayload =
| SuggestTasksPayload
| AskUserQuestionsPayload
| RequestConfirmationPayload
| RequestCheckboxConfirmationPayload;
| RequestCheckboxConfirmationPayload
| RequestItemVerdictsPayload;
export type IssueThreadInteractionResult =
| SuggestTasksResult
| AskUserQuestionsResult
| RequestConfirmationResult
| RequestCheckboxConfirmationResult;
| RequestCheckboxConfirmationResult
| RequestItemVerdictsResult;
export interface IssueAttachment {
id: string;
+7
View File
@@ -364,11 +364,17 @@ export {
requestCheckboxConfirmationOptionSchema,
requestCheckboxConfirmationPayloadSchema,
requestCheckboxConfirmationResultSchema,
requestItemVerdictValueSchema,
requestItemVerdictsItemSchema,
requestItemVerdictsPayloadSchema,
requestItemVerdictsResultItemSchema,
requestItemVerdictsResultSchema,
createIssueThreadInteractionSchema,
acceptIssueThreadInteractionSchema,
rejectIssueThreadInteractionSchema,
cancelIssueThreadInteractionSchema,
respondIssueThreadInteractionSchema,
submitIssueThreadInteractionVerdictsSchema,
linkIssueApprovalSchema,
createIssueAttachmentMetadataSchema,
issueDocumentFormatSchema,
@@ -391,6 +397,7 @@ export {
type RejectIssueThreadInteraction,
type CancelIssueThreadInteraction,
type RespondIssueThreadInteraction,
type SubmitIssueThreadInteractionVerdicts,
type LinkIssueApproval,
type CreateIssueAttachmentMetadata,
type IssueDocumentFormat,
@@ -60,6 +60,22 @@ describe("instance experimental settings validators", () => {
});
});
it("defaults the decisions sidebar link off", () => {
const settings = instanceExperimentalSettingsSchema.parse({});
expect(settings.enableDecisions).toBe(false);
});
it("accepts decisions patches", () => {
expect(
patchInstanceExperimentalSettingsSchema.parse({
enableDecisions: true,
}),
).toEqual({
enableDecisions: true,
});
});
it("accepts server info debug view patches", () => {
expect(
patchInstanceExperimentalSettingsSchema.parse({
@@ -51,6 +51,7 @@ export const instanceExperimentalSettingsSchema = z.object({
enableCloudSync: z.boolean().default(false),
enableExternalObjects: z.boolean().default(false),
enableBuiltInAgents: z.boolean().default(false),
enableDecisions: z.boolean().default(false),
enableGoalsSidebarLink: z.boolean().default(false),
enableServerInfoDebugView: z.boolean().default(false),
autoRestartDevServerWhenIdle: z.boolean().default(false),
+149
View File
@@ -28,6 +28,7 @@ import {
ISSUE_WATCHDOG_DISCOVERY_KINDS,
MODEL_PROFILE_KEYS,
REQUEST_CHECKBOX_CONFIRMATION_OPTION_LIMIT,
REQUEST_ITEM_VERDICTS_ITEM_LIMIT,
} from "../constants.js";
import { multilineTextSchema } from "./text.js";
import { lowTrustReviewPresetPolicySchema, trustAuthorizationPolicySchema } from "./trust-policy.js";
@@ -894,6 +895,121 @@ export const requestCheckboxConfirmationResultSchema = requestConfirmationResult
}
});
export const requestItemVerdictValueSchema = z.enum(["approve", "reject", "defer"]);
export const requestItemVerdictsItemSchema = z.object({
id: z.string().trim().min(1).max(120),
label: z.string().trim().min(1).max(120),
description: z.string().trim().max(500).nullable().optional(),
previewMarkdown: z.string().max(20000).nullable().optional(),
href: requestConfirmationHrefSchema.nullable().optional(),
attachmentId: z.string().uuid().nullable().optional(),
});
export const requestItemVerdictsPayloadSchema = z.object({
version: z.literal(1),
prompt: z.string().trim().min(1).max(1000),
detailsMarkdown: z.string().max(20000).nullable().optional(),
items: z.array(requestItemVerdictsItemSchema)
.min(1)
.max(REQUEST_ITEM_VERDICTS_ITEM_LIMIT),
verdicts: z.array(requestItemVerdictValueSchema)
.min(2)
.max(3)
.optional()
.default(["approve", "reject"]),
requireReasonOn: z.array(requestItemVerdictValueSchema)
.max(3)
.optional()
.default(["reject"]),
reasonLabel: z.string().trim().min(1).max(160).nullable().optional(),
allowBulkApprove: z.boolean().optional().default(true),
supersedeOnUserComment: z.boolean().optional(),
target: requestConfirmationTargetSchema.nullable().optional(),
}).superRefine((value, ctx) => {
const itemIds = new Set<string>();
for (const [index, item] of value.items.entries()) {
if (itemIds.has(item.id)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Item ids must be unique within one item verdict request",
path: ["items", index, "id"],
});
}
itemIds.add(item.id);
}
const verdicts = new Set<string>();
for (const [index, verdict] of value.verdicts.entries()) {
if (verdicts.has(verdict)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "verdicts must be unique",
path: ["verdicts", index],
});
}
verdicts.add(verdict);
}
if (!verdicts.has("approve") || !verdicts.has("reject")) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "verdicts must include approve and reject; defer is optional",
path: ["verdicts"],
});
}
const reasonVerdicts = new Set<string>();
for (const [index, verdict] of value.requireReasonOn.entries()) {
if (reasonVerdicts.has(verdict)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "requireReasonOn must be unique",
path: ["requireReasonOn", index],
});
continue;
}
reasonVerdicts.add(verdict);
if (!verdicts.has(verdict)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "requireReasonOn must reference enabled verdicts",
path: ["requireReasonOn", index],
});
}
}
});
export const requestItemVerdictsResultItemSchema = z.object({
id: z.string().trim().min(1).max(120),
verdict: requestItemVerdictValueSchema,
reason: z.string().trim().max(4000).nullable().optional(),
resolvedByUserId: z.string().trim().min(1).max(255),
resolvedAt: z.union([z.string().datetime(), z.date()]),
commentId: z.string().uuid().nullable().optional(),
});
export const requestItemVerdictsResultSchema = z.object({
version: z.literal(1),
outcome: z.enum(["resolved", "superseded_by_comment", "stale_target", "cancelled"]),
complete: z.boolean(),
items: z.array(requestItemVerdictsResultItemSchema)
.max(REQUEST_ITEM_VERDICTS_ITEM_LIMIT),
commentId: z.string().uuid().nullable().optional(),
staleTarget: requestConfirmationTargetSchema.nullable().optional(),
}).superRefine((value, ctx) => {
const itemIds = new Set<string>();
for (const [index, item] of value.items.entries()) {
if (itemIds.has(item.id)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "result item ids must be unique",
path: ["items", index, "id"],
});
}
itemIds.add(item.id);
}
});
export const createIssueThreadInteractionSchema = z.discriminatedUnion("kind", [
z.object({
kind: z.literal("suggest_tasks"),
@@ -935,6 +1051,16 @@ export const createIssueThreadInteractionSchema = z.discriminatedUnion("kind", [
continuationPolicy: issueThreadInteractionContinuationPolicySchema.optional().default("wake_assignee"),
payload: requestCheckboxConfirmationPayloadSchema,
}),
z.object({
kind: z.literal("request_item_verdicts"),
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: requestItemVerdictsPayloadSchema,
}),
]);
export type CreateIssueThreadInteraction = z.infer<typeof createIssueThreadInteractionSchema>;
@@ -989,6 +1115,29 @@ export const respondIssueThreadInteractionSchema = z.object({
});
export type RespondIssueThreadInteraction = z.infer<typeof respondIssueThreadInteractionSchema>;
export const submitIssueThreadInteractionVerdictsSchema = z.object({
verdicts: z.array(z.object({
id: z.string().trim().min(1).max(120),
verdict: requestItemVerdictValueSchema,
reason: z.string().trim().max(4000).nullable().optional(),
}))
.min(1)
.max(REQUEST_ITEM_VERDICTS_ITEM_LIMIT),
}).superRefine((value, ctx) => {
const itemIds = new Set<string>();
for (const [index, verdict] of value.verdicts.entries()) {
if (itemIds.has(verdict.id)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "verdict item ids must be unique",
path: ["verdicts", index, "id"],
});
}
itemIds.add(verdict.id);
}
});
export type SubmitIssueThreadInteractionVerdicts = z.infer<typeof submitIssueThreadInteractionVerdictsSchema>;
export const linkIssueApprovalSchema = z.object({
approvalId: z.string().uuid(),
});