Add routines automation workflows
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -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(", ")}`,
|
||||
|
||||
97
packages/db/src/migrations/0038_fat_magneto.sql
Normal file
97
packages/db/src/migrations/0038_fat_magneto.sql
Normal 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");
|
||||
5
packages/db/src/migrations/0039_eager_shotgun.sql
Normal file
5
packages/db/src/migrations/0039_eager_shotgun.sql
Normal 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");
|
||||
11312
packages/db/src/migrations/meta/0038_snapshot.json
Normal file
11312
packages/db/src/migrations/meta/0038_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
11355
packages/db/src/migrations/meta/0039_snapshot.json
Normal file
11355
packages/db/src/migrations/meta/0039_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
@@ -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')`,
|
||||
),
|
||||
}),
|
||||
);
|
||||
|
||||
110
packages/db/src/schema/routines.ts
Normal file
110
packages/db/src/schema/routines.ts
Normal 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),
|
||||
}),
|
||||
);
|
||||
@@ -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];
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
123
packages/shared/src/types/routine.ts
Normal file
123
packages/shared/src/types/routine.ts
Normal 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;
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
72
packages/shared/src/validators/routine.ts
Normal file
72
packages/shared/src/validators/routine.ts
Normal 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>;
|
||||
Reference in New Issue
Block a user