Add routines automation workflows

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
dotta
2026-03-19 08:39:24 -05:00
parent 7a652b8998
commit 8f5196f7d6
35 changed files with 25977 additions and 5 deletions

View File

@@ -670,7 +670,18 @@ export async function applyPendingMigrations(url: string): Promise<void> {
await sql.end();
}
const bootstrappedState = await inspectMigrations(url);
let bootstrappedState = await inspectMigrations(url);
if (bootstrappedState.status === "upToDate") return;
if (bootstrappedState.reason === "pending-migrations") {
const repair = await reconcilePendingMigrationHistory(url);
if (repair.repairedMigrations.length > 0) {
bootstrappedState = await inspectMigrations(url);
}
if (bootstrappedState.status === "needsMigrations" && bootstrappedState.reason === "pending-migrations") {
await applyPendingMigrationsManually(url, bootstrappedState.pendingMigrations);
bootstrappedState = await inspectMigrations(url);
}
}
if (bootstrappedState.status === "upToDate") return;
throw new Error(
`Failed to bootstrap migrations: ${bootstrappedState.pendingMigrations.join(", ")}`,

View File

@@ -0,0 +1,97 @@
CREATE TABLE "routine_runs" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"company_id" uuid NOT NULL,
"routine_id" uuid NOT NULL,
"trigger_id" uuid,
"source" text NOT NULL,
"status" text DEFAULT 'received' NOT NULL,
"triggered_at" timestamp with time zone DEFAULT now() NOT NULL,
"idempotency_key" text,
"trigger_payload" jsonb,
"linked_issue_id" uuid,
"coalesced_into_run_id" uuid,
"failure_reason" text,
"completed_at" timestamp with time zone,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "routine_triggers" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"company_id" uuid NOT NULL,
"routine_id" uuid NOT NULL,
"kind" text NOT NULL,
"label" text,
"enabled" boolean DEFAULT true NOT NULL,
"cron_expression" text,
"timezone" text,
"next_run_at" timestamp with time zone,
"last_fired_at" timestamp with time zone,
"public_id" text,
"secret_id" uuid,
"signing_mode" text,
"replay_window_sec" integer,
"last_rotated_at" timestamp with time zone,
"last_result" text,
"created_by_agent_id" uuid,
"created_by_user_id" text,
"updated_by_agent_id" uuid,
"updated_by_user_id" text,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "routines" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"company_id" uuid NOT NULL,
"project_id" uuid NOT NULL,
"goal_id" uuid,
"parent_issue_id" uuid,
"title" text NOT NULL,
"description" text,
"assignee_agent_id" uuid NOT NULL,
"priority" text DEFAULT 'medium' NOT NULL,
"status" text DEFAULT 'active' NOT NULL,
"concurrency_policy" text DEFAULT 'coalesce_if_active' NOT NULL,
"catch_up_policy" text DEFAULT 'skip_missed' NOT NULL,
"created_by_agent_id" uuid,
"created_by_user_id" text,
"updated_by_agent_id" uuid,
"updated_by_user_id" text,
"last_triggered_at" timestamp with time zone,
"last_enqueued_at" timestamp with time zone,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
ALTER TABLE "issues" ADD COLUMN "origin_kind" text DEFAULT 'manual' NOT NULL;--> statement-breakpoint
ALTER TABLE "issues" ADD COLUMN "origin_id" text;--> statement-breakpoint
ALTER TABLE "issues" ADD COLUMN "origin_run_id" text;--> statement-breakpoint
ALTER TABLE "routine_runs" ADD CONSTRAINT "routine_runs_company_id_companies_id_fk" FOREIGN KEY ("company_id") REFERENCES "public"."companies"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "routine_runs" ADD CONSTRAINT "routine_runs_routine_id_routines_id_fk" FOREIGN KEY ("routine_id") REFERENCES "public"."routines"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "routine_runs" ADD CONSTRAINT "routine_runs_trigger_id_routine_triggers_id_fk" FOREIGN KEY ("trigger_id") REFERENCES "public"."routine_triggers"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "routine_runs" ADD CONSTRAINT "routine_runs_linked_issue_id_issues_id_fk" FOREIGN KEY ("linked_issue_id") REFERENCES "public"."issues"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "routine_triggers" ADD CONSTRAINT "routine_triggers_company_id_companies_id_fk" FOREIGN KEY ("company_id") REFERENCES "public"."companies"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "routine_triggers" ADD CONSTRAINT "routine_triggers_routine_id_routines_id_fk" FOREIGN KEY ("routine_id") REFERENCES "public"."routines"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "routine_triggers" ADD CONSTRAINT "routine_triggers_secret_id_company_secrets_id_fk" FOREIGN KEY ("secret_id") REFERENCES "public"."company_secrets"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "routine_triggers" ADD CONSTRAINT "routine_triggers_created_by_agent_id_agents_id_fk" FOREIGN KEY ("created_by_agent_id") REFERENCES "public"."agents"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "routine_triggers" ADD CONSTRAINT "routine_triggers_updated_by_agent_id_agents_id_fk" FOREIGN KEY ("updated_by_agent_id") REFERENCES "public"."agents"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "routines" ADD CONSTRAINT "routines_company_id_companies_id_fk" FOREIGN KEY ("company_id") REFERENCES "public"."companies"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "routines" ADD CONSTRAINT "routines_project_id_projects_id_fk" FOREIGN KEY ("project_id") REFERENCES "public"."projects"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "routines" ADD CONSTRAINT "routines_goal_id_goals_id_fk" FOREIGN KEY ("goal_id") REFERENCES "public"."goals"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "routines" ADD CONSTRAINT "routines_parent_issue_id_issues_id_fk" FOREIGN KEY ("parent_issue_id") REFERENCES "public"."issues"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "routines" ADD CONSTRAINT "routines_assignee_agent_id_agents_id_fk" FOREIGN KEY ("assignee_agent_id") REFERENCES "public"."agents"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "routines" ADD CONSTRAINT "routines_created_by_agent_id_agents_id_fk" FOREIGN KEY ("created_by_agent_id") REFERENCES "public"."agents"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "routines" ADD CONSTRAINT "routines_updated_by_agent_id_agents_id_fk" FOREIGN KEY ("updated_by_agent_id") REFERENCES "public"."agents"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
CREATE INDEX "routine_runs_company_routine_idx" ON "routine_runs" USING btree ("company_id","routine_id","created_at");--> statement-breakpoint
CREATE INDEX "routine_runs_trigger_idx" ON "routine_runs" USING btree ("trigger_id","created_at");--> statement-breakpoint
CREATE INDEX "routine_runs_linked_issue_idx" ON "routine_runs" USING btree ("linked_issue_id");--> statement-breakpoint
CREATE INDEX "routine_runs_trigger_idempotency_idx" ON "routine_runs" USING btree ("trigger_id","idempotency_key");--> statement-breakpoint
CREATE INDEX "routine_triggers_company_routine_idx" ON "routine_triggers" USING btree ("company_id","routine_id");--> statement-breakpoint
CREATE INDEX "routine_triggers_company_kind_idx" ON "routine_triggers" USING btree ("company_id","kind");--> statement-breakpoint
CREATE INDEX "routine_triggers_next_run_idx" ON "routine_triggers" USING btree ("next_run_at");--> statement-breakpoint
CREATE INDEX "routine_triggers_public_id_idx" ON "routine_triggers" USING btree ("public_id");--> statement-breakpoint
CREATE INDEX "routines_company_status_idx" ON "routines" USING btree ("company_id","status");--> statement-breakpoint
CREATE INDEX "routines_company_assignee_idx" ON "routines" USING btree ("company_id","assignee_agent_id");--> statement-breakpoint
CREATE INDEX "routines_company_project_idx" ON "routines" USING btree ("company_id","project_id");--> statement-breakpoint
CREATE INDEX "issues_company_origin_idx" ON "issues" USING btree ("company_id","origin_kind","origin_id");

View File

@@ -0,0 +1,5 @@
CREATE UNIQUE INDEX "issues_open_routine_execution_uq" ON "issues" USING btree ("company_id","origin_kind","origin_id") WHERE "issues"."origin_kind" = 'routine_execution'
and "issues"."origin_id" is not null
and "issues"."hidden_at" is null
and "issues"."status" in ('backlog', 'todo', 'in_progress', 'in_review', 'blocked');--> statement-breakpoint
CREATE UNIQUE INDEX "routine_triggers_public_id_uq" ON "routine_triggers" USING btree ("public_id");

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -267,6 +267,20 @@
"when": 1773756922363,
"tag": "0037_friendly_eddie_brock",
"breakpoints": true
},
{
"idx": 38,
"version": "7",
"when": 1773926116580,
"tag": "0038_fat_magneto",
"breakpoints": true
},
{
"idx": 39,
"version": "7",
"when": 1773927102783,
"tag": "0039_eager_shotgun",
"breakpoints": true
}
]
}

View File

@@ -23,6 +23,7 @@ export { workspaceRuntimeServices } from "./workspace_runtime_services.js";
export { projectGoals } from "./project_goals.js";
export { goals } from "./goals.js";
export { issues } from "./issues.js";
export { routines, routineTriggers, routineRuns } from "./routines.js";
export { issueWorkProducts } from "./issue_work_products.js";
export { labels } from "./labels.js";
export { issueLabels } from "./issue_labels.js";

View File

@@ -1,3 +1,4 @@
import { sql } from "drizzle-orm";
import {
type AnyPgColumn,
pgTable,
@@ -40,6 +41,9 @@ export const issues = pgTable(
createdByUserId: text("created_by_user_id"),
issueNumber: integer("issue_number"),
identifier: text("identifier"),
originKind: text("origin_kind").notNull().default("manual"),
originId: text("origin_id"),
originRunId: text("origin_run_id"),
requestDepth: integer("request_depth").notNull().default(0),
billingCode: text("billing_code"),
assigneeAdapterOverrides: jsonb("assignee_adapter_overrides").$type<Record<string, unknown>>(),
@@ -68,8 +72,17 @@ export const issues = pgTable(
),
parentIdx: index("issues_company_parent_idx").on(table.companyId, table.parentId),
projectIdx: index("issues_company_project_idx").on(table.companyId, table.projectId),
originIdx: index("issues_company_origin_idx").on(table.companyId, table.originKind, table.originId),
projectWorkspaceIdx: index("issues_company_project_workspace_idx").on(table.companyId, table.projectWorkspaceId),
executionWorkspaceIdx: index("issues_company_execution_workspace_idx").on(table.companyId, table.executionWorkspaceId),
identifierIdx: uniqueIndex("issues_identifier_idx").on(table.identifier),
openRoutineExecutionIdx: uniqueIndex("issues_open_routine_execution_uq")
.on(table.companyId, table.originKind, table.originId)
.where(
sql`${table.originKind} = 'routine_execution'
and ${table.originId} is not null
and ${table.hiddenAt} is null
and ${table.status} in ('backlog', 'todo', 'in_progress', 'in_review', 'blocked')`,
),
}),
);

View File

@@ -0,0 +1,110 @@
import {
boolean,
index,
integer,
jsonb,
pgTable,
text,
timestamp,
uniqueIndex,
uuid,
} from "drizzle-orm/pg-core";
import { agents } from "./agents.js";
import { companies } from "./companies.js";
import { companySecrets } from "./company_secrets.js";
import { issues } from "./issues.js";
import { projects } from "./projects.js";
import { goals } from "./goals.js";
export const routines = pgTable(
"routines",
{
id: uuid("id").primaryKey().defaultRandom(),
companyId: uuid("company_id").notNull().references(() => companies.id, { onDelete: "cascade" }),
projectId: uuid("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
goalId: uuid("goal_id").references(() => goals.id, { onDelete: "set null" }),
parentIssueId: uuid("parent_issue_id").references(() => issues.id, { onDelete: "set null" }),
title: text("title").notNull(),
description: text("description"),
assigneeAgentId: uuid("assignee_agent_id").notNull().references(() => agents.id),
priority: text("priority").notNull().default("medium"),
status: text("status").notNull().default("active"),
concurrencyPolicy: text("concurrency_policy").notNull().default("coalesce_if_active"),
catchUpPolicy: text("catch_up_policy").notNull().default("skip_missed"),
createdByAgentId: uuid("created_by_agent_id").references(() => agents.id, { onDelete: "set null" }),
createdByUserId: text("created_by_user_id"),
updatedByAgentId: uuid("updated_by_agent_id").references(() => agents.id, { onDelete: "set null" }),
updatedByUserId: text("updated_by_user_id"),
lastTriggeredAt: timestamp("last_triggered_at", { withTimezone: true }),
lastEnqueuedAt: timestamp("last_enqueued_at", { withTimezone: true }),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
},
(table) => ({
companyStatusIdx: index("routines_company_status_idx").on(table.companyId, table.status),
companyAssigneeIdx: index("routines_company_assignee_idx").on(table.companyId, table.assigneeAgentId),
companyProjectIdx: index("routines_company_project_idx").on(table.companyId, table.projectId),
}),
);
export const routineTriggers = pgTable(
"routine_triggers",
{
id: uuid("id").primaryKey().defaultRandom(),
companyId: uuid("company_id").notNull().references(() => companies.id, { onDelete: "cascade" }),
routineId: uuid("routine_id").notNull().references(() => routines.id, { onDelete: "cascade" }),
kind: text("kind").notNull(),
label: text("label"),
enabled: boolean("enabled").notNull().default(true),
cronExpression: text("cron_expression"),
timezone: text("timezone"),
nextRunAt: timestamp("next_run_at", { withTimezone: true }),
lastFiredAt: timestamp("last_fired_at", { withTimezone: true }),
publicId: text("public_id"),
secretId: uuid("secret_id").references(() => companySecrets.id, { onDelete: "set null" }),
signingMode: text("signing_mode"),
replayWindowSec: integer("replay_window_sec"),
lastRotatedAt: timestamp("last_rotated_at", { withTimezone: true }),
lastResult: text("last_result"),
createdByAgentId: uuid("created_by_agent_id").references(() => agents.id, { onDelete: "set null" }),
createdByUserId: text("created_by_user_id"),
updatedByAgentId: uuid("updated_by_agent_id").references(() => agents.id, { onDelete: "set null" }),
updatedByUserId: text("updated_by_user_id"),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
},
(table) => ({
companyRoutineIdx: index("routine_triggers_company_routine_idx").on(table.companyId, table.routineId),
companyKindIdx: index("routine_triggers_company_kind_idx").on(table.companyId, table.kind),
nextRunIdx: index("routine_triggers_next_run_idx").on(table.nextRunAt),
publicIdIdx: index("routine_triggers_public_id_idx").on(table.publicId),
publicIdUq: uniqueIndex("routine_triggers_public_id_uq").on(table.publicId),
}),
);
export const routineRuns = pgTable(
"routine_runs",
{
id: uuid("id").primaryKey().defaultRandom(),
companyId: uuid("company_id").notNull().references(() => companies.id, { onDelete: "cascade" }),
routineId: uuid("routine_id").notNull().references(() => routines.id, { onDelete: "cascade" }),
triggerId: uuid("trigger_id").references(() => routineTriggers.id, { onDelete: "set null" }),
source: text("source").notNull(),
status: text("status").notNull().default("received"),
triggeredAt: timestamp("triggered_at", { withTimezone: true }).notNull().defaultNow(),
idempotencyKey: text("idempotency_key"),
triggerPayload: jsonb("trigger_payload").$type<Record<string, unknown>>(),
linkedIssueId: uuid("linked_issue_id").references(() => issues.id, { onDelete: "set null" }),
coalescedIntoRunId: uuid("coalesced_into_run_id"),
failureReason: text("failure_reason"),
completedAt: timestamp("completed_at", { withTimezone: true }),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
},
(table) => ({
companyRoutineIdx: index("routine_runs_company_routine_idx").on(table.companyId, table.routineId, table.createdAt),
triggerIdx: index("routine_runs_trigger_idx").on(table.triggerId, table.createdAt),
linkedIssueIdx: index("routine_runs_linked_issue_idx").on(table.linkedIssueId),
idempotencyIdx: index("routine_runs_trigger_idempotency_idx").on(table.triggerId, table.idempotencyKey),
}),
);

View File

@@ -122,6 +122,9 @@ export type IssueStatus = (typeof ISSUE_STATUSES)[number];
export const ISSUE_PRIORITIES = ["critical", "high", "medium", "low"] as const;
export type IssuePriority = (typeof ISSUE_PRIORITIES)[number];
export const ISSUE_ORIGIN_KINDS = ["manual", "routine_execution"] as const;
export type IssueOriginKind = (typeof ISSUE_ORIGIN_KINDS)[number];
export const GOAL_LEVELS = ["company", "team", "agent", "task"] as const;
export type GoalLevel = (typeof GOAL_LEVELS)[number];
@@ -137,6 +140,34 @@ export const PROJECT_STATUSES = [
] as const;
export type ProjectStatus = (typeof PROJECT_STATUSES)[number];
export const ROUTINE_STATUSES = ["active", "paused", "archived"] as const;
export type RoutineStatus = (typeof ROUTINE_STATUSES)[number];
export const ROUTINE_CONCURRENCY_POLICIES = ["coalesce_if_active", "always_enqueue", "skip_if_active"] as const;
export type RoutineConcurrencyPolicy = (typeof ROUTINE_CONCURRENCY_POLICIES)[number];
export const ROUTINE_CATCH_UP_POLICIES = ["skip_missed", "enqueue_missed_with_cap"] as const;
export type RoutineCatchUpPolicy = (typeof ROUTINE_CATCH_UP_POLICIES)[number];
export const ROUTINE_TRIGGER_KINDS = ["schedule", "webhook", "api"] as const;
export type RoutineTriggerKind = (typeof ROUTINE_TRIGGER_KINDS)[number];
export const ROUTINE_TRIGGER_SIGNING_MODES = ["bearer", "hmac_sha256"] as const;
export type RoutineTriggerSigningMode = (typeof ROUTINE_TRIGGER_SIGNING_MODES)[number];
export const ROUTINE_RUN_STATUSES = [
"received",
"coalesced",
"skipped",
"issue_created",
"completed",
"failed",
] as const;
export type RoutineRunStatus = (typeof ROUTINE_RUN_STATUSES)[number];
export const ROUTINE_RUN_SOURCES = ["schedule", "manual", "api", "webhook"] as const;
export type RoutineRunSource = (typeof ROUTINE_RUN_SOURCES)[number];
export const PAUSE_REASONS = ["manual", "budget", "system"] as const;
export type PauseReason = (typeof PAUSE_REASONS)[number];

View File

@@ -10,9 +10,17 @@ export {
AGENT_ICON_NAMES,
ISSUE_STATUSES,
ISSUE_PRIORITIES,
ISSUE_ORIGIN_KINDS,
GOAL_LEVELS,
GOAL_STATUSES,
PROJECT_STATUSES,
ROUTINE_STATUSES,
ROUTINE_CONCURRENCY_POLICIES,
ROUTINE_CATCH_UP_POLICIES,
ROUTINE_TRIGGER_KINDS,
ROUTINE_TRIGGER_SIGNING_MODES,
ROUTINE_RUN_STATUSES,
ROUTINE_RUN_SOURCES,
PAUSE_REASONS,
PROJECT_COLORS,
APPROVAL_TYPES,
@@ -69,9 +77,17 @@ export {
type AgentIconName,
type IssueStatus,
type IssuePriority,
type IssueOriginKind,
type GoalLevel,
type GoalStatus,
type ProjectStatus,
type RoutineStatus,
type RoutineConcurrencyPolicy,
type RoutineCatchUpPolicy,
type RoutineTriggerKind,
type RoutineTriggerSigningMode,
type RoutineRunStatus,
type RoutineRunSource,
type PauseReason,
type ApprovalType,
type ApprovalStatus,
@@ -258,6 +274,14 @@ export type {
AgentEnvConfig,
CompanySecret,
SecretProviderDescriptor,
Routine,
RoutineTrigger,
RoutineRun,
RoutineTriggerSecretMaterial,
RoutineDetail,
RoutineRunSummary,
RoutineExecutionIssueOrigin,
RoutineListItem,
JsonSchema,
PluginJobDeclaration,
PluginWebhookDeclaration,
@@ -389,9 +413,21 @@ export {
createSecretSchema,
rotateSecretSchema,
updateSecretSchema,
createRoutineSchema,
updateRoutineSchema,
createRoutineTriggerSchema,
updateRoutineTriggerSchema,
runRoutineSchema,
rotateRoutineTriggerSecretSchema,
type CreateSecret,
type RotateSecret,
type UpdateSecret,
type CreateRoutine,
type UpdateRoutine,
type CreateRoutineTrigger,
type UpdateRoutineTrigger,
type RunRoutine,
type RotateRoutineTriggerSecret,
createCostEventSchema,
createFinanceEventSchema,
updateBudgetSchema,

View File

@@ -104,6 +104,16 @@ export type {
CompanySecret,
SecretProviderDescriptor,
} from "./secrets.js";
export type {
Routine,
RoutineTrigger,
RoutineRun,
RoutineTriggerSecretMaterial,
RoutineDetail,
RoutineRunSummary,
RoutineExecutionIssueOrigin,
RoutineListItem,
} from "./routine.js";
export type { CostEvent, CostSummary, CostByAgent, CostByProviderModel, CostByBiller, CostByAgentModel, CostWindowSpendRow, CostByProject } from "./cost.js";
export type { FinanceEvent, FinanceSummary, FinanceByBiller, FinanceByKind } from "./finance.js";
export type {

View File

@@ -1,4 +1,4 @@
import type { IssuePriority, IssueStatus } from "../constants.js";
import type { IssueOriginKind, IssuePriority, IssueStatus } from "../constants.js";
import type { Goal } from "./goal.js";
import type { Project, ProjectWorkspace } from "./project.js";
import type { ExecutionWorkspace, IssueExecutionWorkspaceSettings } from "./workspace-runtime.js";
@@ -116,6 +116,9 @@ export interface Issue {
createdByUserId: string | null;
issueNumber: number | null;
identifier: string | null;
originKind?: IssueOriginKind;
originId?: string | null;
originRunId?: string | null;
requestDepth: number;
billingCode: string | null;
assigneeAdapterOverrides: IssueAssigneeAdapterOverrides | null;

View File

@@ -0,0 +1,123 @@
import type { IssueOriginKind } from "../constants.js";
export interface RoutineProjectSummary {
id: string;
name: string;
description: string | null;
status: string;
goalId?: string | null;
}
export interface RoutineAgentSummary {
id: string;
name: string;
role: string;
title: string | null;
urlKey?: string | null;
}
export interface RoutineIssueSummary {
id: string;
identifier: string | null;
title: string;
status: string;
priority: string;
updatedAt: Date;
}
export interface Routine {
id: string;
companyId: string;
projectId: string;
goalId: string | null;
parentIssueId: string | null;
title: string;
description: string | null;
assigneeAgentId: string;
priority: string;
status: string;
concurrencyPolicy: string;
catchUpPolicy: string;
createdByAgentId: string | null;
createdByUserId: string | null;
updatedByAgentId: string | null;
updatedByUserId: string | null;
lastTriggeredAt: Date | null;
lastEnqueuedAt: Date | null;
createdAt: Date;
updatedAt: Date;
}
export interface RoutineTrigger {
id: string;
companyId: string;
routineId: string;
kind: string;
label: string | null;
enabled: boolean;
cronExpression: string | null;
timezone: string | null;
nextRunAt: Date | null;
lastFiredAt: Date | null;
publicId: string | null;
secretId: string | null;
signingMode: string | null;
replayWindowSec: number | null;
lastRotatedAt: Date | null;
lastResult: string | null;
createdByAgentId: string | null;
createdByUserId: string | null;
updatedByAgentId: string | null;
updatedByUserId: string | null;
createdAt: Date;
updatedAt: Date;
}
export interface RoutineRun {
id: string;
companyId: string;
routineId: string;
triggerId: string | null;
source: string;
status: string;
triggeredAt: Date;
idempotencyKey: string | null;
triggerPayload: Record<string, unknown> | null;
linkedIssueId: string | null;
coalescedIntoRunId: string | null;
failureReason: string | null;
completedAt: Date | null;
createdAt: Date;
updatedAt: Date;
}
export interface RoutineTriggerSecretMaterial {
webhookUrl: string;
webhookSecret: string;
}
export interface RoutineDetail extends Routine {
project: RoutineProjectSummary | null;
assignee: RoutineAgentSummary | null;
parentIssue: RoutineIssueSummary | null;
triggers: RoutineTrigger[];
recentRuns: RoutineRunSummary[];
activeIssue: RoutineIssueSummary | null;
}
export interface RoutineRunSummary extends RoutineRun {
linkedIssue: RoutineIssueSummary | null;
trigger: Pick<RoutineTrigger, "id" | "kind" | "label"> | null;
}
export interface RoutineExecutionIssueOrigin {
kind: Extract<IssueOriginKind, "routine_execution">;
routineId: string;
runId: string | null;
}
export interface RoutineListItem extends Routine {
triggers: Pick<RoutineTrigger, "id" | "kind" | "label" | "enabled" | "nextRunAt" | "lastFiredAt" | "lastResult">[];
lastRun: RoutineRunSummary | null;
activeIssue: RoutineIssueSummary | null;
}

View File

@@ -184,6 +184,21 @@ export {
type UpdateSecret,
} from "./secret.js";
export {
createRoutineSchema,
updateRoutineSchema,
createRoutineTriggerSchema,
updateRoutineTriggerSchema,
runRoutineSchema,
rotateRoutineTriggerSecretSchema,
type CreateRoutine,
type UpdateRoutine,
type CreateRoutineTrigger,
type UpdateRoutineTrigger,
type RunRoutine,
type RotateRoutineTriggerSecret,
} from "./routine.js";
export {
createCostEventSchema,
updateBudgetSchema,

View File

@@ -0,0 +1,72 @@
import { z } from "zod";
import {
ISSUE_PRIORITIES,
ROUTINE_CATCH_UP_POLICIES,
ROUTINE_CONCURRENCY_POLICIES,
ROUTINE_STATUSES,
ROUTINE_TRIGGER_SIGNING_MODES,
} from "../constants.js";
export const createRoutineSchema = z.object({
projectId: z.string().uuid(),
goalId: z.string().uuid().optional().nullable(),
parentIssueId: z.string().uuid().optional().nullable(),
title: z.string().trim().min(1).max(200),
description: z.string().optional().nullable(),
assigneeAgentId: z.string().uuid(),
priority: z.enum(ISSUE_PRIORITIES).optional().default("medium"),
status: z.enum(ROUTINE_STATUSES).optional().default("active"),
concurrencyPolicy: z.enum(ROUTINE_CONCURRENCY_POLICIES).optional().default("coalesce_if_active"),
catchUpPolicy: z.enum(ROUTINE_CATCH_UP_POLICIES).optional().default("skip_missed"),
});
export type CreateRoutine = z.infer<typeof createRoutineSchema>;
export const updateRoutineSchema = createRoutineSchema.partial();
export type UpdateRoutine = z.infer<typeof updateRoutineSchema>;
const baseTriggerSchema = z.object({
label: z.string().trim().max(120).optional().nullable(),
enabled: z.boolean().optional().default(true),
});
export const createRoutineTriggerSchema = z.discriminatedUnion("kind", [
baseTriggerSchema.extend({
kind: z.literal("schedule"),
cronExpression: z.string().trim().min(1),
timezone: z.string().trim().min(1).default("UTC"),
}),
baseTriggerSchema.extend({
kind: z.literal("webhook"),
signingMode: z.enum(ROUTINE_TRIGGER_SIGNING_MODES).optional().default("bearer"),
replayWindowSec: z.number().int().min(30).max(86_400).optional().default(300),
}),
baseTriggerSchema.extend({
kind: z.literal("api"),
}),
]);
export type CreateRoutineTrigger = z.infer<typeof createRoutineTriggerSchema>;
export const updateRoutineTriggerSchema = z.object({
label: z.string().trim().max(120).optional().nullable(),
enabled: z.boolean().optional(),
cronExpression: z.string().trim().min(1).optional().nullable(),
timezone: z.string().trim().min(1).optional().nullable(),
signingMode: z.enum(ROUTINE_TRIGGER_SIGNING_MODES).optional().nullable(),
replayWindowSec: z.number().int().min(30).max(86_400).optional().nullable(),
});
export type UpdateRoutineTrigger = z.infer<typeof updateRoutineTriggerSchema>;
export const runRoutineSchema = z.object({
triggerId: z.string().uuid().optional().nullable(),
payload: z.record(z.unknown()).optional().nullable(),
idempotencyKey: z.string().trim().max(255).optional().nullable(),
source: z.enum(["manual", "api"]).optional().default("manual"),
});
export type RunRoutine = z.infer<typeof runRoutineSchema>;
export const rotateRoutineTriggerSecretSchema = z.object({});
export type RotateRoutineTriggerSecret = z.infer<typeof rotateRoutineTriggerSecretSchema>;