/** Job model shared by every host (server plugin, companion, CLI). Pure data, JSON-serializable. */ export const JOB_ID_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/; export const RUN_ID_PATTERN = /^[0-9]{8}T[0-9]{6}Z-[a-z0-9]{6}$/; export type Schedule = | { kind: "at"; at: string } | { kind: "cron"; expr: string; tz?: string } | { kind: "every"; everyMs: number; anchor?: string }; export type MisfirePolicy = "skip" | "run-once" | "ask"; export type ConcurrencyPolicy = "forbid" | "allow"; export interface RetryPolicy { /** Total attempts including the first one. */ maxAttempts: number; backoffMs: number; factor?: number; /** ISO instant after which no retry is scheduled. */ until?: string; } interface StepBase { id: string; /** Run this step only if the referenced condition step evaluated to true. */ when?: string; timeoutMs?: number; /** Continue the pipeline even if this step fails. */ continueOnError?: boolean; } export interface CommandStep extends StepBase { type: "command"; argv: string[]; cwd?: string; env?: Record; /** Parse stdout as JSON and expose it as steps..json. */ parseJson?: boolean; /** Exit codes considered success (default [0]). */ okExitCodes?: number[]; } export interface HttpStep extends StepBase { type: "http"; url: string; method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE"; headers?: Record; body?: string; parseJson?: boolean; okStatus?: number[]; } export type Aggregate = "first" | "last" | "max" | "min" | "avg" | "sum" | "count"; export type CompareOp = ">" | ">=" | "<" | "<=" | "==" | "!=" | "contains" | "exists"; export interface Predicate { /** Dotted path in the run context, `[*]` flattens arrays (e.g. steps.m.json.value[*].v). */ path: string; aggregate?: Aggregate; op: CompareOp; value?: string | number | boolean | null; } export interface ConditionStep extends StepBase { type: "condition"; any?: Predicate[]; all?: Predicate[]; /** What to do when the condition is false. "stop" ends the run successfully, "retry" schedules a retry. */ onFalse?: "continue" | "stop" | "retry"; } export interface AgentStep extends StepBase { type: "agent"; prompt: string; /** Tool allowlist; glob suffix `*` supported. Empty/undefined = pi defaults. */ tools?: string[]; mode?: "auto" | "hosted" | "headless"; model?: string; } export interface NotifyStep extends StepBase { type: "notify"; severity?: "info" | "warning" | "error"; message: string; } export type Step = CommandStep | HttpStep | ConditionStep | AgentStep | NotifyStep; export type RunStatus = "running" | "succeeded" | "failed" | "unmet" | "skipped" | "cancelled" | "interrupted"; export type RunTrigger = "schedule" | "manual" | "retry" | "misfire"; export interface JobState { nextRunAt: string | null; lastRunAt?: string; lastRunId?: string; lastStatus?: RunStatus; /** Pending retry (overrides nextRunAt while earlier). */ retry?: { attempt: number; at: string; scheduledFor: string }; /** Missed run waiting for user decision (misfire = ask). */ pendingMisfire?: { dueAt: string; detectedAt: string }; /** Out-of-process "run now" request (companion tool, CLI): picked up at next tick. */ runRequestedAt?: string; } export interface Job { version: 1; id: string; name: string; description?: string; enabled: boolean; /** Working directory for commands and agent sessions. */ cwd: string; schedule: Schedule; misfire: MisfirePolicy; misfireGraceMs?: number; concurrency?: ConcurrencyPolicy; timeoutMs?: number; retry?: RetryPolicy; steps: Step[]; tags?: string[]; createdAt: string; updatedAt: string; createdBy?: string; state: JobState; } export interface StepResult { id: string; type: Step["type"]; status: "succeeded" | "failed" | "skipped"; startedAt: string; endedAt: string; exitCode?: number | null; httpStatus?: number; stdout?: string; stderr?: string; json?: unknown; value?: boolean; text?: string; sessionId?: string; error?: string; } export interface Run { version: 1; id: string; jobId: string; trigger: RunTrigger; attempt: number; scheduledFor: string; startedAt: string; endedAt?: string; status: RunStatus; host: string; steps: StepResult[]; error?: string; } export const DEFAULT_TZ = "Europe/Rome"; export const DEFAULT_MISFIRE_GRACE_MS = 5 * 60_000; export const DEFAULT_JOB_TIMEOUT_MS = 30 * 60_000; export const MAX_OUTPUT_CHARS = 64_000;