Merge upstream/master into dev (13 commits — includes #5922, #5938, blocked inbox, recovery actions)

This commit is contained in:
2026-05-13 22:35:18 -04:00
180 changed files with 31626 additions and 545 deletions
+34
View File
@@ -215,6 +215,40 @@ export type IssueOriginKind = BuiltInIssueOriginKind | PluginIssueOriginKind;
export const ISSUE_SURFACE_VISIBILITIES = ["default", "plugin_operation"] as const;
export type IssueSurfaceVisibility = (typeof ISSUE_SURFACE_VISIBILITIES)[number];
export const ISSUE_RECOVERY_ACTION_KINDS = [
"missing_disposition",
"stranded_assigned_issue",
"active_run_watchdog",
"issue_graph_liveness",
] as const;
export type IssueRecoveryActionKind = (typeof ISSUE_RECOVERY_ACTION_KINDS)[number];
export const ISSUE_RECOVERY_ACTION_STATUSES = [
"active",
"escalated",
"resolved",
"cancelled",
] as const;
export type IssueRecoveryActionStatus = (typeof ISSUE_RECOVERY_ACTION_STATUSES)[number];
export const ISSUE_RECOVERY_ACTION_OWNER_TYPES = [
"agent",
"user",
"board",
"system",
] as const;
export type IssueRecoveryActionOwnerType = (typeof ISSUE_RECOVERY_ACTION_OWNER_TYPES)[number];
export const ISSUE_RECOVERY_ACTION_OUTCOMES = [
"restored",
"delegated",
"false_positive",
"blocked",
"escalated",
"cancelled",
] as const;
export type IssueRecoveryActionOutcome = (typeof ISSUE_RECOVERY_ACTION_OUTCOMES)[number];
export function pluginOperationIssueOriginKind(pluginKey: string): PluginIssueOriginKind {
return `plugin:${pluginKey}:operation`;
}
+25
View File
@@ -31,6 +31,10 @@ export {
ISSUE_THREAD_INTERACTION_CONTINUATION_POLICIES,
ISSUE_ORIGIN_KINDS,
ISSUE_SURFACE_VISIBILITIES,
ISSUE_RECOVERY_ACTION_KINDS,
ISSUE_RECOVERY_ACTION_STATUSES,
ISSUE_RECOVERY_ACTION_OWNER_TYPES,
ISSUE_RECOVERY_ACTION_OUTCOMES,
pluginOperationIssueOriginKind,
isPluginOperationIssueOriginKind,
ISSUE_RELATION_TYPES,
@@ -149,6 +153,10 @@ export {
type PluginIssueOriginKind,
type IssueOriginKind,
type IssueSurfaceVisibility,
type IssueRecoveryActionKind,
type IssueRecoveryActionStatus,
type IssueRecoveryActionOwnerType,
type IssueRecoveryActionOutcome,
type IssueRelationType,
type IssueTreeControlMode,
type IssueTreeHoldReleasePolicyStrategy,
@@ -371,8 +379,18 @@ export type {
IssueBlockerAttention,
IssueBlockerAttentionReason,
IssueBlockerAttentionState,
IssueInboxAttentionKind,
IssueBlockedInboxAction,
IssueBlockedInboxAttention,
IssueBlockedInboxIssueRef,
IssueBlockedInboxOwner,
IssueBlockedInboxOwnerType,
IssueBlockedInboxReason,
IssueBlockedInboxSeverity,
IssueBlockedInboxState,
IssueProductivityReview,
IssueProductivityReviewTrigger,
IssueRecoveryAction,
SuccessfulRunHandoffState,
SuccessfulRunHandoffStateKind,
IssueScheduledRetry,
@@ -753,9 +771,15 @@ export {
createChildIssueSchema,
resolveCreateIssueStatusDefault,
createIssueLabelSchema,
issueBlockedInboxAttentionSchema,
issueBlockedInboxIssueRefSchema,
issueBlockedInboxReasonSchema,
issueBlockedInboxSeveritySchema,
issueBlockedInboxStateSchema,
updateIssueSchema,
issueExecutionPolicySchema,
issueExecutionStateSchema,
resolveIssueRecoveryActionSchema,
issueReviewRequestSchema,
issueExecutionWorkspaceSettingsSchema,
checkoutIssueSchema,
@@ -815,6 +839,7 @@ export {
type CreateChildIssue,
type CreateIssueLabel,
type UpdateIssue,
type ResolveIssueRecoveryAction,
type CheckoutIssue,
type AddIssueComment,
type CreateIssueThreadInteraction,
+10
View File
@@ -149,8 +149,18 @@ export type {
IssueBlockerAttention,
IssueBlockerAttentionReason,
IssueBlockerAttentionState,
IssueInboxAttentionKind,
IssueBlockedInboxAction,
IssueBlockedInboxAttention,
IssueBlockedInboxIssueRef,
IssueBlockedInboxOwner,
IssueBlockedInboxOwnerType,
IssueBlockedInboxReason,
IssueBlockedInboxSeverity,
IssueBlockedInboxState,
IssueProductivityReview,
IssueProductivityReviewTrigger,
IssueRecoveryAction,
SuccessfulRunHandoffState,
SuccessfulRunHandoffStateKind,
IssueScheduledRetry,
+109
View File
@@ -15,6 +15,10 @@ import type {
IssueExecutionStateStatus,
IssueOriginKind,
IssuePriority,
IssueRecoveryActionKind,
IssueRecoveryActionOutcome,
IssueRecoveryActionOwnerType,
IssueRecoveryActionStatus,
IssueWorkMode,
ModelProfileKey,
IssueThreadInteractionContinuationPolicy,
@@ -131,6 +135,7 @@ export interface IssueRelationIssueSummary {
assigneeAgentId: string | null;
assigneeUserId: string | null;
terminalBlockers?: IssueRelationIssueSummary[];
activeRecoveryAction?: IssueRecoveryAction | null;
}
export type IssueBlockerAttentionState = "none" | "covered" | "stalled" | "needs_attention";
@@ -153,6 +158,75 @@ export interface IssueBlockerAttention {
sampleStalledBlockerIdentifier: string | null;
}
export type IssueInboxAttentionKind = "blocked";
export type IssueBlockedInboxState =
| "needs_attention"
| "awaiting_decision"
| "external_wait"
| "recovery_open"
| "missing_disposition";
export type IssueBlockedInboxSeverity = "critical" | "high" | "medium" | "low";
export type IssueBlockedInboxReason =
| "blocked_by_unassigned_issue"
| "blocked_by_assigned_backlog_issue"
| "blocked_by_uninvokable_assignee"
| "blocked_by_cancelled_issue"
| "blocked_chain_stalled"
| "invalid_review_participant"
| "in_review_without_action_path"
| "missing_successful_run_disposition"
| "pending_board_decision"
| "pending_user_decision"
| "external_owner_action"
| "open_recovery_issue";
export type IssueBlockedInboxOwnerType = "agent" | "user" | "board" | "external" | "unknown";
export interface IssueBlockedInboxIssueRef {
id: string;
identifier: string | null;
title: string;
status: IssueStatus;
priority: IssuePriority;
assigneeAgentId: string | null;
assigneeUserId: string | null;
}
export interface IssueBlockedInboxOwner {
type: IssueBlockedInboxOwnerType;
agentId: string | null;
userId: string | null;
label: string | null;
}
export interface IssueBlockedInboxAction {
label: string;
detail: string | null;
}
export interface IssueBlockedInboxAttention {
kind: IssueInboxAttentionKind;
state: IssueBlockedInboxState;
reason: IssueBlockedInboxReason;
severity: IssueBlockedInboxSeverity;
stoppedSinceAt: string | null;
owner: IssueBlockedInboxOwner;
action: IssueBlockedInboxAction;
sourceIssue: IssueBlockedInboxIssueRef | null;
leafIssue: IssueBlockedInboxIssueRef | null;
recoveryIssue: IssueBlockedInboxIssueRef | null;
approvalId: string | null;
interactionId: string | null;
sampleIssueIdentifier: string | null;
redaction: {
externalDetailsRedacted: boolean;
secretFieldsOmitted: true;
};
}
export type IssueProductivityReviewTrigger =
| "no_comment_streak"
| "long_active_duration"
@@ -169,6 +243,35 @@ export interface IssueProductivityReview {
updatedAt: Date;
}
export interface IssueRecoveryAction {
id: string;
companyId: string;
sourceIssueId: string;
recoveryIssueId: string | null;
kind: IssueRecoveryActionKind;
status: IssueRecoveryActionStatus;
ownerType: IssueRecoveryActionOwnerType;
ownerAgentId: string | null;
ownerUserId: string | null;
previousOwnerAgentId: string | null;
returnOwnerAgentId: string | null;
cause: string;
fingerprint: string;
evidence: Record<string, unknown>;
nextAction: string;
wakePolicy: Record<string, unknown> | null;
monitorPolicy: Record<string, unknown> | null;
attemptCount: number;
maxAttempts: number | null;
timeoutAt: Date | string | null;
lastAttemptAt: Date | string | null;
outcome: IssueRecoveryActionOutcome | null;
resolutionNote: string | null;
resolvedAt: Date | string | null;
createdAt: Date | string;
updatedAt: Date | string;
}
export type SuccessfulRunHandoffStateKind = "required" | "resolved" | "escalated";
export interface SuccessfulRunHandoffState {
@@ -371,7 +474,9 @@ export interface Issue {
blockedBy?: IssueRelationIssueSummary[];
blocks?: IssueRelationIssueSummary[];
blockerAttention?: IssueBlockerAttention;
blockedInboxAttention?: IssueBlockedInboxAttention | null;
productivityReview?: IssueProductivityReview | null;
activeRecoveryAction?: IssueRecoveryAction | null;
successfulRunHandoff?: SuccessfulRunHandoffState | null;
scheduledRetry?: IssueScheduledRetry | null;
relatedWork?: IssueRelatedWorkSummary;
@@ -399,6 +504,10 @@ export interface IssueComment {
authorType: IssueCommentAuthorType;
authorAgentId: string | null;
authorUserId: string | null;
createdByRunId?: string | null;
derivedAuthorAgentId?: string | null;
derivedCreatedByRunId?: string | null;
derivedAuthorSource?: "run_log_comment_post" | null;
body: string;
presentation: IssueCommentPresentation | null;
metadata: IssueCommentMetadata | null;
+9
View File
@@ -155,9 +155,16 @@ export {
createChildIssueSchema,
resolveCreateIssueStatusDefault,
createIssueLabelSchema,
issueBlockedInboxAttentionSchema,
issueBlockedInboxIssueRefSchema,
issueBlockedInboxReasonSchema,
issueBlockedInboxSeveritySchema,
issueBlockedInboxStateSchema,
updateIssueSchema,
issueExecutionPolicySchema,
issueExecutionStateSchema,
issueRecoveryActionReadModelSchema,
resolveIssueRecoveryActionSchema,
issueReviewRequestSchema,
issueExecutionWorkspaceSettingsSchema,
checkoutIssueSchema,
@@ -200,6 +207,8 @@ export {
type CreateIssueLabel,
type UpdateIssue,
type IssueExecutionWorkspaceSettings,
type IssueRecoveryActionReadModel,
type ResolveIssueRecoveryAction,
type CheckoutIssue,
type AddIssueComment,
type CreateIssueThreadInteraction,
@@ -3,6 +3,8 @@ import { MAX_ISSUE_REQUEST_DEPTH } from "../index.js";
import {
addIssueCommentSchema,
createIssueSchema,
issueBlockedInboxAttentionSchema,
resolveIssueRecoveryActionSchema,
respondIssueThreadInteractionSchema,
suggestedTaskDraftSchema,
updateIssueSchema,
@@ -46,6 +48,70 @@ describe("issue validators", () => {
expect(parsed.comment).toBe("Done\n\n- Verified the route");
});
it("allows false-positive recovery resolutions to atomically restore the source issue status", () => {
expect(
resolveIssueRecoveryActionSchema.parse({
outcome: "false_positive",
sourceIssueStatus: "in_review",
}),
).toMatchObject({
outcome: "false_positive",
sourceIssueStatus: "in_review",
});
expect(
resolveIssueRecoveryActionSchema.safeParse({
outcome: "false_positive",
sourceIssueStatus: "blocked",
}).success,
).toBe(false);
expect(
resolveIssueRecoveryActionSchema.safeParse({
outcome: "false_positive",
}).success,
).toBe(false);
});
it("allows cancelled recovery resolutions to atomically restore the source issue status", () => {
expect(
resolveIssueRecoveryActionSchema.parse({
outcome: "cancelled",
sourceIssueStatus: "in_review",
}),
).toMatchObject({
outcome: "cancelled",
sourceIssueStatus: "in_review",
});
expect(
resolveIssueRecoveryActionSchema.safeParse({
outcome: "cancelled",
sourceIssueStatus: "blocked",
}).success,
).toBe(false);
expect(
resolveIssueRecoveryActionSchema.safeParse({
outcome: "cancelled",
}).success,
).toBe(false);
});
it("rejects recovery outcomes that are not supported by the source-scoped resolution endpoint", () => {
expect(
resolveIssueRecoveryActionSchema.safeParse({
outcome: "delegated",
}).success,
).toBe(false);
expect(
resolveIssueRecoveryActionSchema.safeParse({
outcome: "escalated",
}).success,
).toBe(false);
});
it("normalizes escaped line breaks in issue comment bodies", () => {
const parsed = addIssueCommentSchema.parse({
body: "Progress update\\r\\n\\r\\nNext action.",
@@ -153,6 +219,50 @@ describe("issue validators", () => {
}).workMode).toBe("planning");
});
it("validates blocked inbox attention payloads and requires redacted secret fields", () => {
const parsed = issueBlockedInboxAttentionSchema.parse({
kind: "blocked",
state: "needs_attention",
reason: "blocked_by_unassigned_issue",
severity: "critical",
stoppedSinceAt: "2026-05-09T12:00:00.000Z",
owner: { type: "unknown", agentId: null, userId: null, label: null },
action: { label: "Assign blocker", detail: "Assign the leaf blocker." },
sourceIssue: {
id: "11111111-1111-4111-8111-111111111111",
identifier: "PAP-1",
title: "Blocked source",
status: "blocked",
priority: "high",
assigneeAgentId: null,
assigneeUserId: null,
},
leafIssue: {
id: "22222222-2222-4222-8222-222222222222",
identifier: "PAP-2",
title: "Unassigned leaf",
status: "todo",
priority: "medium",
assigneeAgentId: null,
assigneeUserId: null,
},
recoveryIssue: null,
approvalId: null,
interactionId: null,
sampleIssueIdentifier: "PAP-2",
redaction: {
externalDetailsRedacted: false,
secretFieldsOmitted: true,
},
});
expect(parsed.redaction.secretFieldsOmitted).toBe(true);
expect(issueBlockedInboxAttentionSchema.safeParse({
...parsed,
redaction: { externalDetailsRedacted: false, secretFieldsOmitted: false },
}).success).toBe(false);
});
it("rejects unknown issue work modes", () => {
expect(createIssueSchema.safeParse({ title: "Plan first", workMode: "normal" }).success).toBe(false);
expect(suggestedTaskDraftSchema.safeParse({
+150
View File
@@ -14,6 +14,10 @@ import {
ISSUE_COMMENT_PRESENTATION_TONES,
ISSUE_MONITOR_SCHEDULED_BY,
ISSUE_PRIORITIES,
ISSUE_RECOVERY_ACTION_KINDS,
ISSUE_RECOVERY_ACTION_OUTCOMES,
ISSUE_RECOVERY_ACTION_OWNER_TYPES,
ISSUE_RECOVERY_ACTION_STATUSES,
ISSUE_WORK_MODES,
clampIssueRequestDepth,
ISSUE_STATUSES,
@@ -24,6 +28,69 @@ import {
} from "../constants.js";
import { multilineTextSchema } from "./text.js";
export const issueBlockedInboxStateSchema = z.enum([
"needs_attention",
"awaiting_decision",
"external_wait",
"recovery_open",
"missing_disposition",
]);
export const issueBlockedInboxSeveritySchema = z.enum(["critical", "high", "medium", "low"]);
export const issueBlockedInboxReasonSchema = z.enum([
"blocked_by_unassigned_issue",
"blocked_by_assigned_backlog_issue",
"blocked_by_uninvokable_assignee",
"blocked_by_cancelled_issue",
"blocked_chain_stalled",
"invalid_review_participant",
"in_review_without_action_path",
"missing_successful_run_disposition",
"pending_board_decision",
"pending_user_decision",
"external_owner_action",
"open_recovery_issue",
]);
export const issueBlockedInboxIssueRefSchema = z.object({
id: z.string().uuid(),
identifier: z.string().nullable(),
title: z.string(),
status: z.enum(ISSUE_STATUSES),
priority: z.enum(ISSUE_PRIORITIES),
assigneeAgentId: z.string().uuid().nullable(),
assigneeUserId: z.string().nullable(),
}).strict();
export const issueBlockedInboxAttentionSchema = z.object({
kind: z.literal("blocked"),
state: issueBlockedInboxStateSchema,
reason: issueBlockedInboxReasonSchema,
severity: issueBlockedInboxSeveritySchema,
stoppedSinceAt: z.string().datetime().nullable(),
owner: z.object({
type: z.enum(["agent", "user", "board", "external", "unknown"]),
agentId: z.string().uuid().nullable(),
userId: z.string().nullable(),
label: z.string().nullable(),
}).strict(),
action: z.object({
label: z.string().trim().min(1),
detail: z.string().nullable(),
}).strict(),
sourceIssue: issueBlockedInboxIssueRefSchema.nullable(),
leafIssue: issueBlockedInboxIssueRefSchema.nullable(),
recoveryIssue: issueBlockedInboxIssueRefSchema.nullable(),
approvalId: z.string().uuid().nullable(),
interactionId: z.string().uuid().nullable(),
sampleIssueIdentifier: z.string().nullable(),
redaction: z.object({
externalDetailsRedacted: z.boolean(),
secretFieldsOmitted: z.literal(true),
}).strict(),
}).strict();
export const ISSUE_EXECUTION_WORKSPACE_PREFERENCES = [
"inherit",
"shared_workspace",
@@ -167,6 +234,89 @@ export const issueExecutionStateSchema = z.object({
monitor: issueExecutionMonitorStateSchema.optional().nullable(),
});
export const issueRecoveryActionReadModelSchema = z.object({
id: z.string().uuid(),
companyId: z.string().uuid(),
sourceIssueId: z.string().uuid(),
recoveryIssueId: z.string().uuid().nullable(),
kind: z.enum(ISSUE_RECOVERY_ACTION_KINDS),
status: z.enum(ISSUE_RECOVERY_ACTION_STATUSES),
ownerType: z.enum(ISSUE_RECOVERY_ACTION_OWNER_TYPES),
ownerAgentId: z.string().uuid().nullable(),
ownerUserId: z.string().nullable(),
previousOwnerAgentId: z.string().uuid().nullable(),
returnOwnerAgentId: z.string().uuid().nullable(),
cause: z.string().min(1),
fingerprint: z.string().min(1),
evidence: z.record(z.unknown()),
nextAction: z.string().min(1),
wakePolicy: z.record(z.unknown()).nullable(),
monitorPolicy: z.record(z.unknown()).nullable(),
attemptCount: z.number().int().nonnegative(),
maxAttempts: z.number().int().positive().nullable(),
timeoutAt: z.union([z.date(), z.string().datetime()]).nullable(),
lastAttemptAt: z.union([z.date(), z.string().datetime()]).nullable(),
outcome: z.enum(ISSUE_RECOVERY_ACTION_OUTCOMES).nullable(),
resolutionNote: z.string().nullable(),
resolvedAt: z.union([z.date(), z.string().datetime()]).nullable(),
createdAt: z.union([z.date(), z.string().datetime()]),
updatedAt: z.union([z.date(), z.string().datetime()]),
});
export type IssueRecoveryActionReadModel = z.infer<typeof issueRecoveryActionReadModelSchema>;
const RESOLVE_ISSUE_RECOVERY_ACTION_OUTCOMES = [
"restored",
"false_positive",
"blocked",
"cancelled",
] as const;
export const resolveIssueRecoveryActionSchema = z.object({
actionId: z.string().uuid().optional(),
outcome: z.enum(RESOLVE_ISSUE_RECOVERY_ACTION_OUTCOMES),
sourceIssueStatus: z.enum(["done", "in_review", "blocked"]),
resolutionNote: multilineTextSchema.optional().nullable(),
}).strict().superRefine((value, ctx) => {
if (value.outcome === "restored") {
if (value.sourceIssueStatus !== "done" && value.sourceIssueStatus !== "in_review") {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Restored recovery actions must move the source issue to done or in_review",
path: ["sourceIssueStatus"],
});
}
return;
}
if (value.outcome === "blocked") {
if (value.sourceIssueStatus !== "blocked") {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Blocked recovery actions must move the source issue to blocked",
path: ["sourceIssueStatus"],
});
}
return;
}
if (value.outcome === "false_positive" || value.outcome === "cancelled") {
if (
value.sourceIssueStatus !== "done" &&
value.sourceIssueStatus !== "in_review"
) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "This recovery outcome requires sourceIssueStatus to be done or in_review",
path: ["sourceIssueStatus"],
});
}
return;
}
});
export type ResolveIssueRecoveryAction = z.infer<typeof resolveIssueRecoveryActionSchema>;
const issueRequestDepthInputSchema = z
.number()
.int()