| import { z } from "zod"; |
| import { |
| ISSUE_EXECUTION_DECISION_OUTCOMES, |
| ISSUE_EXECUTION_POLICY_MODES, |
| ISSUE_EXECUTION_STAGE_TYPES, |
| ISSUE_EXECUTION_STATE_STATUSES, |
| ISSUE_PRIORITIES, |
| ISSUE_STATUSES, |
| ISSUE_THREAD_INTERACTION_CONTINUATION_POLICIES, |
| ISSUE_THREAD_INTERACTION_KINDS, |
| ISSUE_THREAD_INTERACTION_STATUSES, |
| } from "../constants.js"; |
| import { multilineTextSchema } from "./text.js"; |
|
|
| export const ISSUE_EXECUTION_WORKSPACE_PREFERENCES = [ |
| "inherit", |
| "shared_workspace", |
| "isolated_workspace", |
| "operator_branch", |
| "reuse_existing", |
| "agent_default", |
| ] as const; |
|
|
| const executionWorkspaceStrategySchema = z |
| .object({ |
| type: z.enum(["project_primary", "git_worktree", "adapter_managed", "cloud_sandbox"]).optional(), |
| baseRef: z.string().optional().nullable(), |
| branchTemplate: z.string().optional().nullable(), |
| worktreeParentDir: z.string().optional().nullable(), |
| provisionCommand: z.string().optional().nullable(), |
| teardownCommand: z.string().optional().nullable(), |
| }) |
| .strict(); |
|
|
| export const issueExecutionWorkspaceSettingsSchema = z |
| .object({ |
| mode: z.enum(ISSUE_EXECUTION_WORKSPACE_PREFERENCES).optional(), |
| environmentId: z.string().uuid().optional().nullable(), |
| workspaceStrategy: executionWorkspaceStrategySchema.optional().nullable(), |
| workspaceRuntime: z.record(z.unknown()).optional().nullable(), |
| }) |
| .strict(); |
|
|
| export const issueAssigneeAdapterOverridesSchema = z |
| .object({ |
| adapterConfig: z.record(z.unknown()).optional(), |
| useProjectWorkspace: z.boolean().optional(), |
| }) |
| .strict(); |
|
|
| const issueExecutionStagePrincipalBaseSchema = z.object({ |
| type: z.enum(["agent", "user"]), |
| agentId: z.string().uuid().optional().nullable(), |
| userId: z.string().optional().nullable(), |
| }); |
|
|
| export const issueExecutionStagePrincipalSchema = issueExecutionStagePrincipalBaseSchema |
| .superRefine((value, ctx) => { |
| if (value.type === "agent") { |
| if (!value.agentId) { |
| ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Agent participants require agentId", path: ["agentId"] }); |
| } |
| if (value.userId) { |
| ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Agent participants cannot set userId", path: ["userId"] }); |
| } |
| return; |
| } |
| if (!value.userId) { |
| ctx.addIssue({ code: z.ZodIssueCode.custom, message: "User participants require userId", path: ["userId"] }); |
| } |
| if (value.agentId) { |
| ctx.addIssue({ code: z.ZodIssueCode.custom, message: "User participants cannot set agentId", path: ["agentId"] }); |
| } |
| }); |
|
|
| export const issueExecutionStageParticipantSchema = issueExecutionStagePrincipalBaseSchema.extend({ |
| id: z.string().uuid().optional(), |
| }).superRefine((value, ctx) => { |
| if (value.type === "agent") { |
| if (!value.agentId) { |
| ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Agent participants require agentId", path: ["agentId"] }); |
| } |
| if (value.userId) { |
| ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Agent participants cannot set userId", path: ["userId"] }); |
| } |
| return; |
| } |
| if (!value.userId) { |
| ctx.addIssue({ code: z.ZodIssueCode.custom, message: "User participants require userId", path: ["userId"] }); |
| } |
| if (value.agentId) { |
| ctx.addIssue({ code: z.ZodIssueCode.custom, message: "User participants cannot set agentId", path: ["agentId"] }); |
| } |
| }); |
|
|
| export const issueExecutionStageSchema = z.object({ |
| id: z.string().uuid().optional(), |
| type: z.enum(ISSUE_EXECUTION_STAGE_TYPES), |
| approvalsNeeded: z.literal(1).optional().default(1), |
| participants: z.array(issueExecutionStageParticipantSchema).default([]), |
| }); |
|
|
| export const issueExecutionPolicySchema = z.object({ |
| mode: z.enum(ISSUE_EXECUTION_POLICY_MODES).optional().default("normal"), |
| commentRequired: z.boolean().optional().default(true), |
| stages: z.array(issueExecutionStageSchema).default([]), |
| }); |
|
|
| export const issueReviewRequestSchema = z.object({ |
| instructions: z.string().trim().min(1).max(20000), |
| }).strict(); |
|
|
| export const issueExecutionStateSchema = z.object({ |
| status: z.enum(ISSUE_EXECUTION_STATE_STATUSES), |
| currentStageId: z.string().uuid().nullable(), |
| currentStageIndex: z.number().int().nonnegative().nullable(), |
| currentStageType: z.enum(ISSUE_EXECUTION_STAGE_TYPES).nullable(), |
| currentParticipant: issueExecutionStagePrincipalSchema.nullable(), |
| returnAssignee: issueExecutionStagePrincipalSchema.nullable(), |
| reviewRequest: issueReviewRequestSchema.nullable().optional().default(null), |
| completedStageIds: z.array(z.string().uuid()).default([]), |
| lastDecisionId: z.string().uuid().nullable(), |
| lastDecisionOutcome: z.enum(ISSUE_EXECUTION_DECISION_OUTCOMES).nullable(), |
| }); |
|
|
| export const createIssueSchema = z.object({ |
| projectId: z.string().uuid().optional().nullable(), |
| projectWorkspaceId: z.string().uuid().optional().nullable(), |
| goalId: z.string().uuid().optional().nullable(), |
| parentId: z.string().uuid().optional().nullable(), |
| blockedByIssueIds: z.array(z.string().uuid()).optional(), |
| inheritExecutionWorkspaceFromIssueId: z.string().uuid().optional().nullable(), |
| title: z.string().min(1), |
| description: multilineTextSchema.optional().nullable(), |
| status: z.enum(ISSUE_STATUSES).optional().default("backlog"), |
| priority: z.enum(ISSUE_PRIORITIES).optional().default("medium"), |
| assigneeAgentId: z.string().uuid().optional().nullable(), |
| assigneeUserId: z.string().optional().nullable(), |
| requestDepth: z.number().int().nonnegative().optional().default(0), |
| billingCode: z.string().optional().nullable(), |
| assigneeAdapterOverrides: issueAssigneeAdapterOverridesSchema.optional().nullable(), |
| executionPolicy: issueExecutionPolicySchema.optional().nullable(), |
| executionWorkspaceId: z.string().uuid().optional().nullable(), |
| executionWorkspacePreference: z.enum(ISSUE_EXECUTION_WORKSPACE_PREFERENCES).optional().nullable(), |
| executionWorkspaceSettings: issueExecutionWorkspaceSettingsSchema.optional().nullable(), |
| labelIds: z.array(z.string().uuid()).optional(), |
| }); |
|
|
| export type CreateIssue = z.infer<typeof createIssueSchema>; |
|
|
| export const createChildIssueSchema = createIssueSchema |
| .omit({ |
| parentId: true, |
| inheritExecutionWorkspaceFromIssueId: true, |
| }) |
| .extend({ |
| acceptanceCriteria: z.array(z.string().trim().min(1).max(500)).max(20).optional(), |
| blockParentUntilDone: z.boolean().optional().default(false), |
| }); |
|
|
| export type CreateChildIssue = z.infer<typeof createChildIssueSchema>; |
|
|
| export const createIssueLabelSchema = z.object({ |
| name: z.string().trim().min(1).max(48), |
| color: z.string().regex(/^#(?:[0-9a-fA-F]{6})$/, "Color must be a 6-digit hex value"), |
| }); |
|
|
| export type CreateIssueLabel = z.infer<typeof createIssueLabelSchema>; |
|
|
| export const updateIssueSchema = createIssueSchema.partial().extend({ |
| assigneeAgentId: z.string().trim().min(1).optional().nullable(), |
| comment: multilineTextSchema.pipe(z.string().min(1)).optional(), |
| reviewRequest: issueReviewRequestSchema.optional().nullable(), |
| reopen: z.boolean().optional(), |
| resume: z.boolean().optional(), |
| interrupt: z.boolean().optional(), |
| hiddenAt: z.string().datetime().nullable().optional(), |
| }); |
|
|
| export type UpdateIssue = z.infer<typeof updateIssueSchema>; |
| export type IssueExecutionWorkspaceSettings = z.infer<typeof issueExecutionWorkspaceSettingsSchema>; |
|
|
| export const checkoutIssueSchema = z.object({ |
| agentId: z.string().uuid(), |
| expectedStatuses: z.array(z.enum(ISSUE_STATUSES)).nonempty(), |
| }); |
|
|
| export type CheckoutIssue = z.infer<typeof checkoutIssueSchema>; |
|
|
| export const addIssueCommentSchema = z.object({ |
| body: multilineTextSchema.pipe(z.string().min(1)), |
| reopen: z.boolean().optional(), |
| resume: z.boolean().optional(), |
| interrupt: z.boolean().optional(), |
| }); |
|
|
| export type AddIssueComment = z.infer<typeof addIssueCommentSchema>; |
|
|
| export const issueThreadInteractionStatusSchema = z.enum(ISSUE_THREAD_INTERACTION_STATUSES); |
| export const issueThreadInteractionKindSchema = z.enum(ISSUE_THREAD_INTERACTION_KINDS); |
| export const issueThreadInteractionContinuationPolicySchema = z.enum( |
| ISSUE_THREAD_INTERACTION_CONTINUATION_POLICIES, |
| ); |
|
|
| export const issueDocumentKeySchema = z |
| .string() |
| .trim() |
| .min(1) |
| .max(64) |
| .regex(/^[a-z0-9][a-z0-9_-]*$/, "Document key must be lowercase letters, numbers, _ or -"); |
|
|
| export const suggestedTaskDraftSchema = z.object({ |
| clientKey: z.string().trim().min(1).max(120), |
| parentClientKey: z.string().trim().min(1).max(120).nullable().optional(), |
| parentId: z.string().uuid().nullable().optional(), |
| title: z.string().trim().min(1).max(240), |
| description: multilineTextSchema.pipe(z.string().trim().max(20000)).nullable().optional(), |
| priority: z.enum(ISSUE_PRIORITIES).nullable().optional(), |
| assigneeAgentId: z.string().uuid().nullable().optional(), |
| assigneeUserId: z.string().trim().min(1).nullable().optional(), |
| projectId: z.string().uuid().nullable().optional(), |
| goalId: z.string().uuid().nullable().optional(), |
| billingCode: z.string().trim().max(120).nullable().optional(), |
| labels: z.array(z.string().trim().min(1).max(48)).max(20).optional(), |
| hiddenInPreview: z.boolean().optional(), |
| }).superRefine((value, ctx) => { |
| if (value.assigneeAgentId && value.assigneeUserId) { |
| ctx.addIssue({ |
| code: z.ZodIssueCode.custom, |
| message: "Suggested tasks can only target one assignee", |
| path: ["assigneeAgentId"], |
| }); |
| } |
| }); |
|
|
| export const suggestTasksPayloadSchema = z.object({ |
| version: z.literal(1), |
| defaultParentId: z.string().uuid().nullable().optional(), |
| tasks: z.array(suggestedTaskDraftSchema).min(1).max(50), |
| }).superRefine((value, ctx) => { |
| const seenClientKeys = new Set<string>(); |
| for (const [index, task] of value.tasks.entries()) { |
| if (seenClientKeys.has(task.clientKey)) { |
| ctx.addIssue({ |
| code: z.ZodIssueCode.custom, |
| message: "clientKey must be unique within one interaction", |
| path: ["tasks", index, "clientKey"], |
| }); |
| continue; |
| } |
| seenClientKeys.add(task.clientKey); |
| } |
| }); |
|
|
| export const suggestTasksResultCreatedTaskSchema = z.object({ |
| clientKey: z.string().trim().min(1).max(120), |
| issueId: z.string().uuid(), |
| identifier: z.string().trim().min(1).nullable().optional(), |
| title: z.string().trim().min(1).nullable().optional(), |
| parentIssueId: z.string().uuid().nullable().optional(), |
| parentIdentifier: z.string().trim().min(1).nullable().optional(), |
| }); |
|
|
| export const suggestTasksResultSchema = z.object({ |
| version: z.literal(1), |
| createdTasks: z.array(suggestTasksResultCreatedTaskSchema).max(50).optional(), |
| skippedClientKeys: z.array(z.string().trim().min(1).max(120)).max(50).optional(), |
| rejectionReason: z.string().trim().max(4000).nullable().optional(), |
| }); |
|
|
| export const askUserQuestionsQuestionOptionSchema = 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(), |
| }); |
|
|
| export const askUserQuestionsQuestionSchema = z.object({ |
| id: z.string().trim().min(1).max(120), |
| prompt: z.string().trim().min(1).max(500), |
| helpText: z.string().trim().max(1000).nullable().optional(), |
| selectionMode: z.enum(["single", "multi"]), |
| required: z.boolean().optional(), |
| options: z.array(askUserQuestionsQuestionOptionSchema).min(1).max(10), |
| }); |
|
|
| 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(), |
| questions: z.array(askUserQuestionsQuestionSchema).min(1).max(10), |
| }).superRefine((value, ctx) => { |
| const seenQuestionIds = new Set<string>(); |
| for (const [questionIndex, question] of value.questions.entries()) { |
| if (seenQuestionIds.has(question.id)) { |
| ctx.addIssue({ |
| code: z.ZodIssueCode.custom, |
| message: "Question ids must be unique within one interaction", |
| path: ["questions", questionIndex, "id"], |
| }); |
| } |
| seenQuestionIds.add(question.id); |
|
|
| const seenOptionIds = new Set<string>(); |
| for (const [optionIndex, option] of question.options.entries()) { |
| if (seenOptionIds.has(option.id)) { |
| ctx.addIssue({ |
| code: z.ZodIssueCode.custom, |
| message: "Option ids must be unique within one question", |
| path: ["questions", questionIndex, "options", optionIndex, "id"], |
| }); |
| } |
| seenOptionIds.add(option.id); |
| } |
| } |
| }); |
|
|
| export const askUserQuestionsAnswerSchema = z.object({ |
| questionId: z.string().trim().min(1).max(120), |
| optionIds: z.array(z.string().trim().min(1).max(120)).max(20), |
| }); |
|
|
| export const askUserQuestionsResultSchema = z.object({ |
| version: z.literal(1), |
| answers: z.array(askUserQuestionsAnswerSchema).max(20), |
| summaryMarkdown: z.string().max(20000).nullable().optional(), |
| }); |
|
|
| const requestConfirmationHrefSchema = z.string().trim().min(1).max(2000).refine((value) => { |
| const lower = value.toLowerCase(); |
| return !lower.startsWith("javascript:") |
| && !lower.startsWith("data:") |
| && !value.startsWith("//"); |
| }, "href must not use javascript:, data:, or protocol-relative URLs"); |
|
|
| const requestConfirmationTargetBaseSchema = z.object({ |
| label: z.string().trim().min(1).max(120).nullable().optional(), |
| href: requestConfirmationHrefSchema.nullable().optional(), |
| }); |
|
|
| export const requestConfirmationIssueDocumentTargetSchema = requestConfirmationTargetBaseSchema.extend({ |
| type: z.literal("issue_document"), |
| issueId: z.string().uuid().nullable().optional(), |
| documentId: z.string().uuid().nullable().optional(), |
| key: issueDocumentKeySchema, |
| revisionId: z.string().uuid(), |
| revisionNumber: z.number().int().positive().nullable().optional(), |
| }); |
|
|
| export const requestConfirmationCustomTargetSchema = requestConfirmationTargetBaseSchema.extend({ |
| type: z.literal("custom"), |
| key: z.string().trim().min(1).max(120), |
| revisionId: z.string().trim().min(1).max(255).nullable().optional(), |
| revisionNumber: z.number().int().positive().nullable().optional(), |
| }); |
|
|
| export const requestConfirmationTargetSchema = z.discriminatedUnion("type", [ |
| requestConfirmationIssueDocumentTargetSchema, |
| requestConfirmationCustomTargetSchema, |
| ]); |
|
|
| export const requestConfirmationPayloadSchema = z.object({ |
| version: z.literal(1), |
| prompt: z.string().trim().min(1).max(1000), |
| acceptLabel: z.string().trim().min(1).max(80).nullable().optional(), |
| rejectLabel: z.string().trim().min(1).max(80).nullable().optional(), |
| rejectRequiresReason: z.boolean().optional(), |
| rejectReasonLabel: z.string().trim().min(1).max(160).nullable().optional(), |
| allowDeclineReason: z.boolean().optional().default(true), |
| declineReasonPlaceholder: z.string().trim().min(1).max(240).nullable().optional(), |
| detailsMarkdown: z.string().max(20000).nullable().optional(), |
| supersedeOnUserComment: z.boolean().optional(), |
| target: requestConfirmationTargetSchema.nullable().optional(), |
| }); |
|
|
| export const requestConfirmationResultSchema = z.object({ |
| version: z.literal(1), |
| outcome: z.enum(["accepted", "rejected", "superseded_by_comment", "stale_target"]), |
| reason: z.string().trim().max(4000).nullable().optional(), |
| commentId: z.string().uuid().nullable().optional(), |
| staleTarget: requestConfirmationTargetSchema.nullable().optional(), |
| }); |
|
|
| export const createIssueThreadInteractionSchema = z.discriminatedUnion("kind", [ |
| z.object({ |
| kind: z.literal("suggest_tasks"), |
| 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: suggestTasksPayloadSchema, |
| }), |
| z.object({ |
| kind: z.literal("ask_user_questions"), |
| 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: askUserQuestionsPayloadSchema, |
| }), |
| z.object({ |
| kind: z.literal("request_confirmation"), |
| 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("none"), |
| payload: requestConfirmationPayloadSchema, |
| }), |
| ]); |
|
|
| export type CreateIssueThreadInteraction = z.infer<typeof createIssueThreadInteractionSchema>; |
|
|
| export const acceptIssueThreadInteractionSchema = z.object({ |
| selectedClientKeys: z.array(z.string().trim().min(1).max(120)).min(1).max(50).optional(), |
| }).superRefine((value, ctx) => { |
| const seenClientKeys = new Set<string>(); |
| for (const [index, clientKey] of (value.selectedClientKeys ?? []).entries()) { |
| if (seenClientKeys.has(clientKey)) { |
| ctx.addIssue({ |
| code: z.ZodIssueCode.custom, |
| message: "selectedClientKeys must be unique", |
| path: ["selectedClientKeys", index], |
| }); |
| continue; |
| } |
| seenClientKeys.add(clientKey); |
| } |
| }); |
| export type AcceptIssueThreadInteraction = z.infer<typeof acceptIssueThreadInteractionSchema>; |
|
|
| export const rejectIssueThreadInteractionSchema = z.object({ |
| reason: z.string().trim().max(4000).optional(), |
| }); |
| export type RejectIssueThreadInteraction = z.infer<typeof rejectIssueThreadInteractionSchema>; |
|
|
| export const respondIssueThreadInteractionSchema = z.object({ |
| answers: z.array(askUserQuestionsAnswerSchema).max(20), |
| summaryMarkdown: multilineTextSchema.pipe(z.string().max(20000)).nullable().optional(), |
| }); |
| export type RespondIssueThreadInteraction = z.infer<typeof respondIssueThreadInteractionSchema>; |
|
|
| export const linkIssueApprovalSchema = z.object({ |
| approvalId: z.string().uuid(), |
| }); |
|
|
| export type LinkIssueApproval = z.infer<typeof linkIssueApprovalSchema>; |
|
|
| export const createIssueAttachmentMetadataSchema = z.object({ |
| issueCommentId: z.string().uuid().optional().nullable(), |
| }); |
|
|
| export type CreateIssueAttachmentMetadata = z.infer<typeof createIssueAttachmentMetadataSchema>; |
|
|
| export const ISSUE_DOCUMENT_FORMATS = ["markdown"] as const; |
|
|
| export const issueDocumentFormatSchema = z.enum(ISSUE_DOCUMENT_FORMATS); |
|
|
| export const upsertIssueDocumentSchema = z.object({ |
| title: z.string().trim().max(200).nullable().optional(), |
| format: issueDocumentFormatSchema, |
| body: multilineTextSchema.pipe(z.string().max(524288)), |
| changeSummary: z.string().trim().max(500).nullable().optional(), |
| baseRevisionId: z.string().uuid().nullable().optional(), |
| }); |
|
|
| export const restoreIssueDocumentRevisionSchema = z.object({}); |
|
|
| export type IssueDocumentFormat = z.infer<typeof issueDocumentFormatSchema>; |
| export type UpsertIssueDocument = z.infer<typeof upsertIssueDocumentSchema>; |
| export type RestoreIssueDocumentRevision = z.infer<typeof restoreIssueDocumentRevisionSchema>; |
|
|