@hasna/loops 0.4.13 → 0.4.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/CHANGELOG.md +99 -3
  2. package/README.md +171 -39
  3. package/dist/api/index.d.ts +36 -0
  4. package/dist/api/index.js +1518 -15
  5. package/dist/cli/index.js +7280 -3213
  6. package/dist/cli/ui.d.ts +44 -0
  7. package/dist/daemon/index.js +1433 -303
  8. package/dist/generated/storage-kit/health.d.ts +19 -0
  9. package/dist/generated/storage-kit/index.d.ts +7 -0
  10. package/dist/generated/storage-kit/migrations.d.ts +47 -0
  11. package/dist/generated/storage-kit/mode.d.ts +47 -0
  12. package/dist/generated/storage-kit/pool.d.ts +33 -0
  13. package/dist/generated/storage-kit/query.d.ts +35 -0
  14. package/dist/generated/storage-kit/tls.d.ts +25 -0
  15. package/dist/index.d.ts +2 -2
  16. package/dist/index.js +8689 -4837
  17. package/dist/lib/cloud/mode.d.ts +17 -0
  18. package/dist/lib/cloud/resolve.d.ts +16 -0
  19. package/dist/lib/cloud/storage.d.ts +82 -0
  20. package/dist/lib/cloud/transport.d.ts +148 -0
  21. package/dist/lib/format.d.ts +2 -1
  22. package/dist/lib/health.d.ts +83 -3
  23. package/dist/lib/migration.d.ts +40 -0
  24. package/dist/lib/mode.js +15 -3
  25. package/dist/lib/route/index.d.ts +2 -0
  26. package/dist/lib/route/policies.d.ts +51 -0
  27. package/dist/lib/route/provider-admission.d.ts +37 -0
  28. package/dist/lib/route/throttle.d.ts +4 -0
  29. package/dist/lib/route/types.d.ts +8 -0
  30. package/dist/lib/run-receipts.d.ts +14 -0
  31. package/dist/lib/scheduler.d.ts +5 -2
  32. package/dist/lib/storage/contract.d.ts +7 -1
  33. package/dist/lib/storage/index.d.ts +1 -0
  34. package/dist/lib/storage/index.js +2028 -231
  35. package/dist/lib/storage/pg-executor.d.ts +27 -0
  36. package/dist/lib/storage/pg-runner-claim.d.ts +40 -0
  37. package/dist/lib/storage/postgres-loop-storage.d.ts +120 -0
  38. package/dist/lib/storage/postgres-schema.js +29 -0
  39. package/dist/lib/storage/postgres.js +29 -0
  40. package/dist/lib/storage/sqlite.d.ts +6 -0
  41. package/dist/lib/storage/sqlite.js +748 -26
  42. package/dist/lib/store/index.d.ts +268 -0
  43. package/dist/lib/store.d.ts +282 -1
  44. package/dist/lib/store.js +746 -26
  45. package/dist/lib/template-kit.d.ts +25 -0
  46. package/dist/lib/templates.d.ts +16 -1
  47. package/dist/mcp/http.d.ts +16 -0
  48. package/dist/mcp/index.js +2885 -634
  49. package/dist/runner/index.js +198 -32
  50. package/dist/sdk/http.d.ts +182 -0
  51. package/dist/sdk/http.js +166 -0
  52. package/dist/sdk/index.d.ts +55 -18
  53. package/dist/sdk/index.js +2734 -617
  54. package/dist/serve/index.d.ts +4 -0
  55. package/dist/serve/index.js +9095 -0
  56. package/dist/types.d.ts +52 -0
  57. package/docs/AUTOMATION_RUNTIME_DESIGN.md +442 -1
  58. package/docs/CUTOVER-RUNBOOK.md +78 -0
  59. package/docs/DEPLOYMENT_MODES.md +35 -18
  60. package/docs/RUNTIME_BOUNDARY.md +203 -0
  61. package/docs/USAGE.md +195 -38
  62. package/package.json +15 -3
@@ -0,0 +1,17 @@
1
+ export type StorageMode = "local" | "cloud";
2
+ /** Deprecated mode aliases that all normalize to `cloud`. */
3
+ export declare const DEPRECATED_STORAGE_MODE_ALIASES: readonly ["remote", "hybrid", "self_hosted"];
4
+ export type Env = Record<string, string | undefined>;
5
+ export interface StorageModeNormalization {
6
+ mode: StorageMode;
7
+ /** The deprecated alias that was normalized to `cloud`, if any. */
8
+ deprecatedAlias: string | null;
9
+ }
10
+ /**
11
+ * Normalize a raw storage-mode string to the `local | cloud` runtime enum.
12
+ * Accepts deprecated aliases (`remote`, `hybrid`, `self_hosted`) and maps them
13
+ * to `cloud`. Throws on any other value.
14
+ */
15
+ export declare function normalizeStorageMode(value: string): StorageModeNormalization;
16
+ /** Upper-snake env token for an app name, e.g. `todos` -> `TODOS`. */
17
+ export declare function envToken(name: string): string;
@@ -0,0 +1,16 @@
1
+ import { type Env } from "./mode.js";
2
+ import { type HasnaStorageClient } from "./storage.js";
3
+ export type CloudStorageResolution = {
4
+ transport: "local";
5
+ client: null;
6
+ } | {
7
+ transport: "cloud-http";
8
+ client: HasnaStorageClient;
9
+ baseUrl: string;
10
+ };
11
+ /**
12
+ * Resolve whether `name`'s data lives in the cloud (hosted `/v1` API) or the
13
+ * local store for the current environment. Never returns partially-built cloud
14
+ * state and never exposes the API key.
15
+ */
16
+ export declare function resolveCloudStorage(name: string, env?: Env): CloudStorageResolution;
@@ -0,0 +1,82 @@
1
+ import type { Env } from "./mode.js";
2
+ import { createClientTransport, type HasnaHttpTransport, type HasnaRequestOptions, type QueryParams } from "./transport.js";
3
+ /** Options for a list() call: filters/pagination as query params. */
4
+ export interface StorageListOptions extends Pick<HasnaRequestOptions, "timeoutMs" | "headers" | "retry" | "signal"> {
5
+ /** Query params (limit, offset, cursor, filters, ...). */
6
+ query?: QueryParams;
7
+ }
8
+ /** Options for a get() call. */
9
+ export type StorageGetOptions = Pick<HasnaRequestOptions, "timeoutMs" | "headers" | "retry" | "signal" | "query">;
10
+ /** Options for a create() call. */
11
+ export interface StorageCreateOptions extends Pick<HasnaRequestOptions, "timeoutMs" | "headers" | "retry" | "signal" | "query"> {
12
+ /**
13
+ * Idempotency key for the create. Defaults to a fresh UUID so a transparently
14
+ * retried POST is deduped by the server instead of creating a duplicate. Pass
15
+ * a stable value to make an app-level operation idempotent across calls.
16
+ */
17
+ idempotencyKey?: string;
18
+ }
19
+ /** Options for an update() call. */
20
+ export interface StorageUpdateOptions extends Pick<HasnaRequestOptions, "timeoutMs" | "headers" | "retry" | "signal" | "query"> {
21
+ /** HTTP verb for the update. Default `PATCH` (partial); use `PUT` for replace. */
22
+ method?: "PATCH" | "PUT";
23
+ /** Idempotency key. PUT is idempotent by definition; set this to make PATCH retry-safe too. */
24
+ idempotencyKey?: string;
25
+ }
26
+ /** Options for a delete() call. */
27
+ export type StorageDeleteOptions = Pick<HasnaRequestOptions, "timeoutMs" | "headers" | "retry" | "signal" | "query">;
28
+ /** Result of a list() call. `items` is the extracted array; `raw` is the full envelope. */
29
+ export interface StorageListResult<T> {
30
+ items: T[];
31
+ /** Total count when the server reports one (`total`/`count`), else null. */
32
+ total: number | null;
33
+ /** Opaque pagination cursor when the server reports one, else null. */
34
+ cursor: string | null;
35
+ /** The full parsed response body (envelope preserved). */
36
+ raw: unknown;
37
+ }
38
+ /**
39
+ * The app storage interface, HTTP edition. This is deliberately the same small
40
+ * CRUD surface a local store exposes, so an app's resolver can return either a
41
+ * local implementation or this one behind one interface.
42
+ */
43
+ export interface HasnaStorageClient {
44
+ /** App slug this client targets. */
45
+ readonly name: string;
46
+ /** `<origin>/v1` base URL. */
47
+ readonly baseUrl: string;
48
+ /** The underlying HTTP transport (escape hatch for non-CRUD routes). */
49
+ readonly transport: HasnaHttpTransport;
50
+ /** List a collection. Returns extracted `items` plus the raw envelope. */
51
+ list<T = unknown>(resource: string, options?: StorageListOptions): Promise<StorageListResult<T>>;
52
+ /** Fetch one entity by id. Returns `null` on 404. */
53
+ get<T = unknown>(resource: string, id: string, options?: StorageGetOptions): Promise<T | null>;
54
+ /** Create one entity. Retry-safe via an auto `Idempotency-Key`. */
55
+ create<T = unknown>(resource: string, body: unknown, options?: StorageCreateOptions): Promise<T>;
56
+ /** Update one entity by id (PATCH by default). */
57
+ update<T = unknown>(resource: string, id: string, patch: unknown, options?: StorageUpdateOptions): Promise<T>;
58
+ /** Delete one entity by id. Resolves for 2xx and 404 (already gone). */
59
+ delete(resource: string, id: string, options?: StorageDeleteOptions): Promise<void>;
60
+ }
61
+ /**
62
+ * Wrap an HTTP transport with the resource CRUD storage interface. Use this when
63
+ * you already have a transport (e.g. from `createClientTransport`).
64
+ */
65
+ export declare function createHasnaStorageClient(name: string, transport: HasnaHttpTransport): HasnaStorageClient;
66
+ /** Result of {@link resolveStorageClient}. */
67
+ export type ResolveStorageClientResult = {
68
+ transport: "local";
69
+ client: null;
70
+ } | {
71
+ transport: "cloud-http";
72
+ client: HasnaStorageClient;
73
+ };
74
+ /**
75
+ * The one call an app's storage resolver makes. Reads the client-flip env for
76
+ * `name`; when it resolves to `cloud-http` (mode=cloud/self_hosted + API_URL +
77
+ * API_KEY), returns a ready {@link HasnaStorageClient}. Otherwise returns
78
+ * `{ transport: 'local', client: null }` so the app uses its local store.
79
+ * Throws if cloud was requested but is misconfigured (so callers never silently
80
+ * read the wrong dataset).
81
+ */
82
+ export declare function resolveStorageClient(name: string, env?: Env, overrides?: Parameters<typeof createClientTransport>[2]): ResolveStorageClientResult;
@@ -0,0 +1,148 @@
1
+ import { type Env } from "./mode.js";
2
+ import type { StorageMode } from "./mode.js";
3
+ /** Default cloud host template. `<app>` is the app slug. */
4
+ export declare function defaultCloudBaseUrl(name: string): string;
5
+ export interface ClientTransportEnvKeys {
6
+ /** Mode keys, in precedence order. */
7
+ modeKeys: string[];
8
+ /** API base-URL keys, in precedence order. */
9
+ apiUrlKeys: string[];
10
+ /** API-key keys, in precedence order. */
11
+ apiKeyKeys: string[];
12
+ }
13
+ /** Resolve the canonical client-flip env-key spec for an app. */
14
+ export declare function clientTransportEnvKeys(name: string): ClientTransportEnvKeys;
15
+ /** Normalize a base URL to `<origin>/v1` (dropping any trailing slash or existing /v1). */
16
+ export declare function toV1BaseUrl(apiUrl: string): string;
17
+ export type ClientTransportKind = "local" | "cloud-http";
18
+ export interface ClientTransportResolution {
19
+ /** Where the client should read/write from. */
20
+ transport: ClientTransportKind;
21
+ /** Resolved storage mode (`local` | `cloud`). */
22
+ mode: StorageMode;
23
+ /** Deprecated mode alias that was normalized (e.g. `self_hosted`), if any. */
24
+ deprecatedAlias: string | null;
25
+ /** Env key the mode was read from, or `"default"`. */
26
+ modeSource: string;
27
+ /** `<origin>/v1` base for the cloud API when transport is cloud-http, else null. */
28
+ baseUrl: string | null;
29
+ /** Env key the API URL came from, `"default"` (host template), or null. */
30
+ apiUrlSource: string | null;
31
+ /** Whether an API key is present (value never exposed). */
32
+ apiKeyPresent: boolean;
33
+ /** Env key the API key came from, or null. */
34
+ apiKeySource: string | null;
35
+ /**
36
+ * True when the operator asked for cloud but the config is incomplete (no API
37
+ * key), so we fell back to local. Callers SHOULD treat this as an error rather
38
+ * than silently reading local data.
39
+ */
40
+ misconfigured: boolean;
41
+ /** Human-readable warning, or null. Never contains secret values. */
42
+ warning: string | null;
43
+ }
44
+ /**
45
+ * Resolve how a client should reach an app's data given the environment.
46
+ *
47
+ * Precedence for the mode: the first present of `HASNA_<NAME>_STORAGE_MODE`,
48
+ * `HASNA_<NAME>_MODE`, `<NAME>_STORAGE_MODE`, `<NAME>_MODE`, else `local`.
49
+ */
50
+ export declare function resolveClientTransport(name: string, env?: Env): ClientTransportResolution;
51
+ /** Thrown when a cloud HTTP request returns a non-2xx status. */
52
+ export declare class HasnaHttpError extends Error {
53
+ readonly status: number;
54
+ readonly method: string;
55
+ readonly path: string;
56
+ readonly body: unknown;
57
+ constructor(method: string, path: string, status: number, body: unknown);
58
+ }
59
+ type FetchLike = (input: string, init?: RequestInit) => Promise<Response>;
60
+ /** Query params for a request. Nullish values are dropped; arrays repeat the key. */
61
+ export type QueryParams = URLSearchParams | Record<string, string | number | boolean | null | undefined | ReadonlyArray<string | number | boolean>>;
62
+ /** Retry policy for transient failures (network errors, timeouts, 5xx, 429). */
63
+ export interface HasnaRetryOptions {
64
+ /** Max RETRY attempts after the first try. Default 2 (=> up to 3 total tries). */
65
+ retries?: number;
66
+ /** Base backoff in ms for exponential backoff. Default 200. */
67
+ baseDelayMs?: number;
68
+ /** Backoff ceiling in ms. Default 2000. */
69
+ maxDelayMs?: number;
70
+ /** HTTP statuses that trigger a retry. Default 408, 425, 429, 500, 502, 503, 504. */
71
+ retryStatuses?: number[];
72
+ }
73
+ /** Per-call request options: query, idempotency, timeout, retry, extra headers. */
74
+ export interface HasnaRequestOptions {
75
+ /** Query string params appended to the URL. */
76
+ query?: QueryParams;
77
+ /**
78
+ * Idempotency key sent as `Idempotency-Key`. When set, unsafe methods (POST)
79
+ * become safe to retry: the server dedupes replays. Auto-generated for
80
+ * `create()` in the storage client.
81
+ */
82
+ idempotencyKey?: string;
83
+ /** Override the transport timeout for this call (ms). */
84
+ timeoutMs?: number;
85
+ /** Extra headers merged into this call (override transport headers). */
86
+ headers?: Record<string, string>;
87
+ /** Override or disable retry for this call. `false` disables retries. */
88
+ retry?: HasnaRetryOptions | false;
89
+ /** Caller abort signal, combined with the internal timeout. */
90
+ signal?: AbortSignal;
91
+ }
92
+ export interface HasnaHttpTransportOptions {
93
+ /** App slug (for error context / default host). */
94
+ name: string;
95
+ /** `<origin>/v1` base. Usually from `resolveClientTransport().baseUrl`. */
96
+ baseUrl: string;
97
+ /** The API key (secret). Sent as both `x-api-key` and `Authorization: Bearer`. */
98
+ apiKey: string;
99
+ /** Override fetch (tests). Defaults to global fetch. */
100
+ fetchImpl?: FetchLike;
101
+ /** Extra headers merged into every request. */
102
+ headers?: Record<string, string>;
103
+ /** Per-request timeout in ms. Default 30000. */
104
+ timeoutMs?: number;
105
+ /** Default retry policy for all requests. Pass `false` to disable. */
106
+ retry?: HasnaRetryOptions | false;
107
+ /** Injectable sleep (tests). Defaults to a real timer. */
108
+ sleepImpl?: (ms: number) => Promise<void>;
109
+ }
110
+ export interface HasnaHttpTransport {
111
+ readonly baseUrl: string;
112
+ request<T = unknown>(method: string, path: string, body?: unknown, opts?: HasnaRequestOptions): Promise<T>;
113
+ get<T = unknown>(path: string, opts?: HasnaRequestOptions): Promise<T>;
114
+ post<T = unknown>(path: string, body?: unknown, opts?: HasnaRequestOptions): Promise<T>;
115
+ put<T = unknown>(path: string, body?: unknown, opts?: HasnaRequestOptions): Promise<T>;
116
+ patch<T = unknown>(path: string, body?: unknown, opts?: HasnaRequestOptions): Promise<T>;
117
+ del<T = unknown>(path: string, body?: unknown, opts?: HasnaRequestOptions): Promise<T>;
118
+ }
119
+ /** Append query params to a `/v1`-relative path (no-op when empty). */
120
+ export declare function appendQuery(path: string, query?: QueryParams): string;
121
+ /**
122
+ * Build an authenticated HTTP transport for an app's cloud `/v1` API. Sends the
123
+ * API key on every request as BOTH `x-api-key` and `Authorization: Bearer`
124
+ * (serve apps accept either), returns parsed JSON, times out, and retries
125
+ * transient failures with exponential backoff + jitter. Never logs the key.
126
+ *
127
+ * Retry safety: idempotent methods (GET/HEAD/PUT/DELETE/OPTIONS) are always
128
+ * retried on transient failure; POST/PATCH are retried ONLY when an
129
+ * `Idempotency-Key` is supplied, so replays can't create duplicates.
130
+ */
131
+ export declare function createHasnaHttpTransport(options: HasnaHttpTransportOptions): HasnaHttpTransport;
132
+ /**
133
+ * Convenience: resolve transport from env and, when cloud-http, build the HTTP
134
+ * client in one call. Returns `{ transport: 'local', resolution }` for local, or
135
+ * `{ transport: 'cloud-http', client, resolution }` for cloud. Throws if the
136
+ * config is `misconfigured` (cloud requested but unusable) so callers can't drift
137
+ * onto local data by accident.
138
+ */
139
+ export declare function createClientTransport(name: string, env?: Env, overrides?: Partial<Pick<HasnaHttpTransportOptions, "fetchImpl" | "headers" | "timeoutMs" | "retry" | "sleepImpl">>): {
140
+ transport: "local";
141
+ client: null;
142
+ resolution: ClientTransportResolution;
143
+ } | {
144
+ transport: "cloud-http";
145
+ client: HasnaHttpTransport;
146
+ resolution: ClientTransportResolution;
147
+ };
148
+ export {};
@@ -1,4 +1,4 @@
1
- import type { ExecutorResult, Goal, GoalRun, Loop, LoopRun, WorkflowEvent, WorkflowInvocation, WorkflowRun, WorkflowSpec, WorkflowStepRun, WorkflowWorkItem } from "../types.js";
1
+ import type { ExecutorResult, Goal, GoalRun, Loop, LoopRun, RunReceipt, WorkflowEvent, WorkflowInvocation, WorkflowRun, WorkflowSpec, WorkflowStepRun, WorkflowWorkItem } from "../types.js";
2
2
  export declare function redact(value: string | undefined, visible?: number): string | undefined;
3
3
  export declare function textOutputBlocks(value: Pick<LoopRun | WorkflowStepRun, "stdout" | "stderr">, opts?: {
4
4
  indent?: string;
@@ -7,6 +7,7 @@ export declare function publicLoop(loop: Loop): Record<string, unknown>;
7
7
  export declare function publicRun(run: LoopRun, showOutput?: boolean, opts?: {
8
8
  redactError?: boolean;
9
9
  }): Record<string, unknown>;
10
+ export declare function publicRunReceipt(receipt: RunReceipt): Record<string, unknown>;
10
11
  export declare function publicExecutorResult(result: ExecutorResult, showOutput?: boolean): Record<string, unknown>;
11
12
  export declare function publicWorkflow(workflow: WorkflowSpec): Record<string, unknown>;
12
13
  export declare function publicWorkflowRun(run: WorkflowRun): Record<string, unknown>;
@@ -1,10 +1,13 @@
1
- import type { Loop, LoopRun } from "../types.js";
1
+ import type { Loop, LoopRun, LoopStatus } from "../types.js";
2
+ import type { DaemonStatus } from "../daemon/control.js";
3
+ import type { DoctorCheck, DoctorReport } from "./doctor.js";
2
4
  import type { Store } from "./store.js";
3
- export type RunFailureClassification = "rate_limit" | "auth" | "model_not_found" | "context_length" | "schema_response_format" | "node_init" | "preflight" | "route_functional" | "timeout" | "sigsegv" | "skipped_previous_active" | "circuit_breaker" | "unknown";
5
+ export type RunFailureClassification = "rate_limit" | "auth" | "provider_unavailable" | "model_not_found" | "context_length" | "schema_response_format" | "node_init" | "preflight" | "route_functional" | "timeout" | "sigsegv" | "restart_interrupted" | "skipped_previous_active" | "circuit_breaker" | "unknown";
4
6
  export interface RunFailureSignal {
5
7
  classification: RunFailureClassification;
6
8
  fingerprint: string;
7
9
  evidence: {
10
+ summary?: string;
8
11
  error?: string;
9
12
  stdout?: string;
10
13
  stderr?: string;
@@ -31,7 +34,7 @@ export interface RecommendedTaskUpsert {
31
34
  };
32
35
  }
33
36
  export interface LoopExpectationResult {
34
- loop: Pick<Loop, "id" | "name" | "status" | "nextRunAt">;
37
+ loop: Pick<Loop, "id" | "name" | "status" | "nextRunAt" | "retryScheduledFor">;
35
38
  ok: boolean;
36
39
  check: {
37
40
  id: "latest-run-succeeded" | "route-functional-health";
@@ -62,6 +65,81 @@ export interface LoopsHealthReport {
62
65
  classifications: Record<RunFailureClassification, number>;
63
66
  expectations: LoopExpectationResult[];
64
67
  }
68
+ export type HealthScanStatus = "ok" | "degraded" | "critical";
69
+ export type HealthScanFindingKind = "daemon" | "doctor" | "preflight" | "latest-run" | "stale-running";
70
+ export type HealthScanFindingSeverity = "critical" | "high" | "medium" | "low";
71
+ export interface HealthScanSelfHealAction {
72
+ kind: "daemon-start";
73
+ attempted: boolean;
74
+ ok?: boolean;
75
+ reason: string;
76
+ result?: Record<string, unknown>;
77
+ }
78
+ export interface HealthScanFinding {
79
+ kind: HealthScanFindingKind;
80
+ severity: HealthScanFindingSeverity;
81
+ fingerprint: string;
82
+ title: string;
83
+ message: string;
84
+ loop?: Pick<Loop, "id" | "name" | "status" | "nextRunAt"> & {
85
+ leaseMs?: number;
86
+ };
87
+ run?: LoopRun;
88
+ route?: LoopExpectationResult["route"];
89
+ ageMs?: number;
90
+ staleThresholdMs?: number;
91
+ classification?: RunFailureClassification;
92
+ doctorCheck?: DoctorCheck;
93
+ recommendedTask?: RecommendedTaskUpsert;
94
+ }
95
+ export interface LoopsHealthScan {
96
+ ok: boolean;
97
+ status: HealthScanStatus;
98
+ generatedAt: string;
99
+ includedStatuses: LoopStatus[];
100
+ counts: {
101
+ loops: number;
102
+ active: number;
103
+ paused: number;
104
+ stopped: number;
105
+ expired: number;
106
+ latestRunFindings: number;
107
+ staleRunning: number;
108
+ daemonFindings: number;
109
+ doctorFindings: number;
110
+ preflightFindings: number;
111
+ findings: number;
112
+ reportedFindings: number;
113
+ truncatedFindings: number;
114
+ };
115
+ daemon?: Pick<DaemonStatus, "running" | "stale" | "pid" | "host" | "loops" | "runs" | "logPath">;
116
+ doctor?: DoctorReport;
117
+ health: LoopsHealthReport;
118
+ selfHeals: HealthScanSelfHealAction[];
119
+ findings: HealthScanFinding[];
120
+ reports?: {
121
+ dir: string;
122
+ json: string;
123
+ markdown: string;
124
+ };
125
+ todos?: Record<string, unknown>;
126
+ }
127
+ export interface BuildHealthScanOptions {
128
+ includeStatuses?: LoopStatus[];
129
+ includeArchived?: boolean;
130
+ limit?: number;
131
+ latestRun?: boolean;
132
+ doctor?: DoctorReport;
133
+ daemon?: DaemonStatus;
134
+ selfHeals?: HealthScanSelfHealAction[];
135
+ maxFindings?: number;
136
+ staleRunningMs?: number;
137
+ now?: Date;
138
+ }
139
+ export interface WriteHealthScanReportsOptions {
140
+ reportDir?: string;
141
+ }
142
+ export declare const RESTART_INTERRUPTED_RUN_PREFIX = "daemon restart interrupted active run";
65
143
  export declare function classifyRunFailure(run: LoopRun): RunFailureSignal | undefined;
66
144
  export declare function expectationForLoop(store: Store, loop: Loop): LoopExpectationResult;
67
145
  export declare function buildHealthReport(store: Store, opts?: {
@@ -69,3 +147,5 @@ export declare function buildHealthReport(store: Store, opts?: {
69
147
  includeInactive?: boolean;
70
148
  limit?: number;
71
149
  }): LoopsHealthReport;
150
+ export declare function buildHealthScan(store: Store, opts?: BuildHealthScanOptions): LoopsHealthScan;
151
+ export declare function writeHealthScanReports(scan: LoopsHealthScan, opts?: WriteHealthScanReportsOptions): LoopsHealthScan;
@@ -104,6 +104,46 @@ export declare function validateLoopsMigrationBundle(value: unknown): LoopsMigra
104
104
  export declare function buildImportMigrationPlan(store: Store, bundle: LoopsMigrationBundle, opts?: ImportLoopsMigrationOptions): LoopsMigrationPlan;
105
105
  export declare function applyImportMigrationBundle(store: Store, bundle: LoopsMigrationBundle, opts?: ImportLoopsMigrationOptions): ApplyLoopsMigrationResult;
106
106
  export declare function buildSelfHostedMigrationPlan(store: Store, opts: SelfHostedPlanOptions): Promise<LoopsMigrationPlan>;
107
+ export interface SelfHostedPushOptions {
108
+ apiUrl?: string;
109
+ apiToken?: string;
110
+ includeRuns?: boolean;
111
+ replace?: boolean;
112
+ fetchImpl?: typeof fetch;
113
+ env?: NodeJS.ProcessEnv;
114
+ /** Max rows per workflow/loop batch (default 200). */
115
+ batchRows?: number;
116
+ /** Approx max JSON bytes per run batch (default 4 MiB). */
117
+ runBatchBytes?: number;
118
+ onProgress?: (event: {
119
+ phase: "workflows" | "loops" | "runs";
120
+ sent: number;
121
+ requests: number;
122
+ }) => void;
123
+ }
124
+ export interface SelfHostedPushResult {
125
+ ok: boolean;
126
+ apiUrl: string;
127
+ applied: {
128
+ workflows: number;
129
+ loops: number;
130
+ runs: number;
131
+ };
132
+ skipped: {
133
+ runningRuns: number;
134
+ orphanRuns: number;
135
+ };
136
+ requests: number;
137
+ }
138
+ /**
139
+ * Apply a local->self-hosted backfill through the control plane's id-preserving
140
+ * `/v1/import` endpoint. Rows are pushed FK-safe (workflows, loops, then runs).
141
+ * Runs are streamed in bounded pages so a busy host's multi-hundred-MB run
142
+ * history never loads into memory at once; volatile `running` runs and orphan
143
+ * runs (whose parent loop is absent) are dropped and counted. Idempotent: the
144
+ * endpoint upserts by id, so re-running never duplicates rows.
145
+ */
146
+ export declare function applySelfHostedPush(store: Store, opts: SelfHostedPushOptions): Promise<SelfHostedPushResult>;
107
147
  export declare function registerSelfHostedRunner(opts: RunnerRegistrationOptions): Promise<RunnerRegistrationResult>;
108
148
  export declare function publicMigrationBundle(bundle: LoopsMigrationBundle): Record<string, unknown>;
109
149
  export declare function selfHostedControlPlaneSummary(env?: NodeJS.ProcessEnv): Record<string, unknown>;
package/dist/lib/mode.js CHANGED
@@ -2,7 +2,7 @@
2
2
  // package.json
3
3
  var package_default = {
4
4
  name: "@hasna/loops",
5
- version: "0.4.13",
5
+ version: "0.4.22",
6
6
  description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
7
7
  type: "module",
8
8
  main: "dist/index.js",
@@ -11,6 +11,7 @@ var package_default = {
11
11
  loops: "dist/cli/index.js",
12
12
  "loops-daemon": "dist/daemon/index.js",
13
13
  "loops-api": "dist/api/index.js",
14
+ "loops-serve": "dist/serve/index.js",
14
15
  "loops-runner": "dist/runner/index.js",
15
16
  "loops-mcp": "dist/mcp/index.js"
16
17
  },
@@ -23,6 +24,14 @@ var package_default = {
23
24
  types: "./dist/sdk/index.d.ts",
24
25
  import: "./dist/sdk/index.js"
25
26
  },
27
+ "./sdk/http": {
28
+ types: "./dist/sdk/http.d.ts",
29
+ import: "./dist/sdk/http.js"
30
+ },
31
+ "./serve": {
32
+ types: "./dist/serve/index.d.ts",
33
+ import: "./dist/serve/index.js"
34
+ },
26
35
  "./mcp": {
27
36
  types: "./dist/mcp/index.d.ts",
28
37
  import: "./dist/mcp/index.js"
@@ -68,7 +77,7 @@ var package_default = {
68
77
  "LICENSE"
69
78
  ],
70
79
  scripts: {
71
- build: "rm -rf dist && bun build src/cli/index.ts src/daemon/index.ts src/api/index.ts src/runner/index.ts src/mcp/index.ts src/index.ts src/sdk/index.ts src/lib/store.ts src/lib/mode.ts src/lib/storage/index.ts src/lib/storage/contract.ts src/lib/storage/sqlite.ts src/lib/storage/postgres.ts src/lib/storage/postgres-schema.ts --root src --outdir dist --target bun --packages external && chmod +x dist/cli/index.js dist/daemon/index.js dist/api/index.js dist/runner/index.js dist/mcp/index.js && tsc -p tsconfig.build.json --emitDeclarationOnly --outDir dist",
80
+ build: "rm -rf dist && bun build src/cli/index.ts src/daemon/index.ts src/api/index.ts src/serve/index.ts src/runner/index.ts src/mcp/index.ts src/index.ts src/sdk/index.ts src/sdk/http.ts src/lib/store.ts src/lib/mode.ts src/lib/storage/index.ts src/lib/storage/contract.ts src/lib/storage/sqlite.ts src/lib/storage/postgres.ts src/lib/storage/postgres-schema.ts --root src --outdir dist --target bun --packages external && chmod +x dist/cli/index.js dist/daemon/index.js dist/api/index.js dist/serve/index.js dist/runner/index.js dist/mcp/index.js && tsc -p tsconfig.build.json --emitDeclarationOnly --outDir dist",
72
81
  "build:bin": "bun build src/cli/index.ts --compile --outfile dist/loops",
73
82
  typecheck: "tsc --noEmit",
74
83
  test: "bun test",
@@ -108,13 +117,16 @@ var package_default = {
108
117
  "@openrouter/ai-sdk-provider": "2.9.1",
109
118
  ai: "6.0.204",
110
119
  commander: "^13.1.0",
120
+ pg: "^8.13.1",
111
121
  zod: "4.4.3"
112
122
  },
113
123
  optionalDependencies: {
114
- "@hasna/machines": "0.0.49"
124
+ "@hasna/machines": "0.0.49",
125
+ "@hasna/contracts": "^0.4.2"
115
126
  },
116
127
  devDependencies: {
117
128
  "@types/bun": "latest",
129
+ "@types/pg": "^8.11.10",
118
130
  typescript: "^5.7.3"
119
131
  },
120
132
  publishConfig: {
@@ -9,6 +9,7 @@ export { eventData, eventMetadata, objectField, slugSegment, stableHash, stableS
9
9
  export { routeCursorKey, selectRouteItems, writeRouteCursor, writeRouteEvidence, type RouteSelection } from "./cursors.js";
10
10
  export { GateError, normalizeLoopTargetForStorage, normalizeWorkflowForStorage, preflightLoopTarget, preflightStoredWorkflow, workflowBodyFromFile, workflowSpecForPreflight, } from "./gates.js";
11
11
  export { accountPoolFromOpts, normalizeAgentProvider, permissionModeFromOpts, providerAuthProfileFromOpts, providerRoutingPublic, resolveProviderRouting, roleAccountFromOpts, sandboxFromOpts, SUPPORTED_AGENT_PROVIDERS, type ProviderRoutingDecision, } from "./provider.js";
12
+ export { checkProviderAdmission, parseCodewithAdmissionDiagnostics, providerActiveCapFromOpts, providerAdmissionPlanFromOpts, type CodewithAdmissionDiagnostics, type ProviderAdmissionDecision, type ProviderAdmissionPlan, } from "./provider-admission.js";
12
13
  export { prReviewRoutingDecision, type PrReviewRoutingDecision } from "./pr-review.js";
13
14
  export { hasThrottleLimits, normalizeRoutePath, routeThrottleDecision, routeThrottleLimitsFromOpts, type RouteThrottleDecision, type RouteThrottleLimits, } from "./throttle.js";
14
15
  export { generatedRouteSandboxPreflight, readEventEnvelopeInput, routeEventByKind, routeGenericEvent, routeTodosTaskEvent, todosTaskRouteTemplateId, } from "./route-event.js";
@@ -16,3 +17,4 @@ export { drainTodosTaskRoutes, type DrainResult } from "./drain.js";
16
17
  export { buildHygieneRouteTasks, parseHygieneChecks, taskAutoRoute, upsertRouteTasks, type HygieneCheckKind, type HygieneRouteTask, type RouteTaskSpec, type UpsertRouteTasksOptions, } from "./route-tasks.js";
17
18
  export { defaultLoopsProject, ensureTodosTaskList, runLocalCommand, runLocalCommandWithStdoutFile } from "./todos-cli.js";
18
19
  export { addAgentRoutingOptions, addRouteEventOptions, addTodosDrainOptions, routeDrainArgs, type AgentRoutingOptionConfig } from "./options.js";
20
+ export { applyRoutePolicyToDrainOptions, applyRoutePolicyToScheduleOptions, getRoutePolicy, listRoutePolicies, renderRoutePolicy, routePolicyEvidenceFromOptions, validateRoutePolicy, type RoutePolicyDefinition, type RoutePolicyId, type RoutePolicyRender, } from "./policies.js";
@@ -0,0 +1,51 @@
1
+ import type { TodosDrainOptions } from "./types.js";
2
+ /** Named route-drain policies keep recurring task routes auditable and replayable. */
3
+ export type RoutePolicyId = "repoops-pr-queue" | "oss" | "pilot" | "machine-sync";
4
+ export type RoutePolicySafety = "unattended" | "manual-break-glass";
5
+ export interface RoutePolicySchedule {
6
+ every?: string;
7
+ cron?: string;
8
+ at?: string;
9
+ dynamic?: boolean;
10
+ catchUp?: string;
11
+ catchUpLimit?: string;
12
+ overlap?: string;
13
+ attempts?: string;
14
+ retryDelay?: string;
15
+ lease?: string;
16
+ }
17
+ export interface RoutePolicyGuard {
18
+ kind: string;
19
+ description: string;
20
+ [key: string]: unknown;
21
+ }
22
+ export interface RoutePolicyDefinition {
23
+ id: RoutePolicyId;
24
+ title: string;
25
+ description: string;
26
+ routeKind: "todos-task";
27
+ safety: RoutePolicySafety;
28
+ source: string;
29
+ aliases?: string[];
30
+ drain: Partial<TodosDrainOptions>;
31
+ schedule: RoutePolicySchedule;
32
+ guards?: RoutePolicyGuard[];
33
+ notes?: string[];
34
+ requiresExplicitOptions?: Array<keyof TodosDrainOptions>;
35
+ }
36
+ export interface RoutePolicyRender {
37
+ policy: RoutePolicyDefinition;
38
+ drain: Partial<TodosDrainOptions>;
39
+ schedule: RoutePolicySchedule;
40
+ args: string[];
41
+ command: string;
42
+ }
43
+ export declare function listRoutePolicies(): RoutePolicyDefinition[];
44
+ export declare function getRoutePolicy(idOrAlias: string | undefined): RoutePolicyDefinition | undefined;
45
+ export declare function applyRoutePolicyToDrainOptions<T extends TodosDrainOptions>(opts: T, applyOpts?: {
46
+ requireExplicitSafety?: boolean;
47
+ }): T;
48
+ export declare function applyRoutePolicyToScheduleOptions<T extends TodosDrainOptions & Record<string, unknown>>(opts: T): T;
49
+ export declare function routePolicyEvidenceFromOptions(opts: TodosDrainOptions): Record<string, unknown> | undefined;
50
+ export declare function renderRoutePolicy(idOrAlias: string): RoutePolicyRender;
51
+ export declare function validateRoutePolicy(idOrAlias: string): RoutePolicyRender;
@@ -0,0 +1,37 @@
1
+ import type { AgentProvider } from "../../types.js";
2
+ import type { TodosTaskRouteOptions } from "./types.js";
3
+ /** Provider-native admission checks for routes that create background agents. */
4
+ export interface CodewithAdmissionDiagnostics {
5
+ activeRunCount?: number;
6
+ maxActiveRunsPerUser?: number;
7
+ availableActiveRunSlots?: number;
8
+ }
9
+ export interface ProviderAdmissionPlan {
10
+ provider: AgentProvider;
11
+ authProfile?: string;
12
+ authProfiles?: Array<string | undefined>;
13
+ activeCap?: number;
14
+ admissionCheck: boolean;
15
+ }
16
+ export interface ProviderAdmissionDecision {
17
+ allowed: boolean;
18
+ provider: AgentProvider;
19
+ checked: boolean;
20
+ check: "codewith-agent-diagnostics" | "unsupported-provider" | "dry-run";
21
+ reason?: string;
22
+ authProfile?: string;
23
+ authProfiles?: string[];
24
+ activeCap?: number;
25
+ fatal?: boolean;
26
+ diagnostics?: CodewithAdmissionDiagnostics | Record<string, unknown>;
27
+ }
28
+ export declare function parseCodewithAdmissionDiagnostics(stdout: string): CodewithAdmissionDiagnostics;
29
+ export declare function providerActiveCapFromOpts(opts: Pick<TodosTaskRouteOptions, "providerActiveCap" | "codewithActiveCap">): number | undefined;
30
+ export declare function providerAdmissionPlanFromOpts(opts: TodosTaskRouteOptions, args: {
31
+ provider: AgentProvider;
32
+ authProfile?: string;
33
+ authProfiles?: Array<string | undefined>;
34
+ }): ProviderAdmissionPlan | undefined;
35
+ export declare function providerAdmissionPlanWithAuthProfiles(plan: ProviderAdmissionPlan | undefined, authProfiles: Array<string | undefined>): ProviderAdmissionPlan | undefined;
36
+ export declare function providerAdmissionDryRunPreview(plan: ProviderAdmissionPlan | undefined): ProviderAdmissionDecision | undefined;
37
+ export declare function checkProviderAdmission(plan: ProviderAdmissionPlan | undefined): ProviderAdmissionDecision | undefined;
@@ -2,8 +2,10 @@ import type { Store } from "../store.js";
2
2
  /** Active-workflow admission throttles and canonical project-path handling. */
3
3
  export interface RouteThrottleLimits {
4
4
  maxActive?: number;
5
+ maxActiveScope?: string;
5
6
  maxActivePerProject?: number;
6
7
  maxActivePerProjectGroup?: number;
8
+ maxPerProfile?: number;
7
9
  }
8
10
  export interface RouteThrottleDecision {
9
11
  allowed: boolean;
@@ -19,8 +21,10 @@ export interface RouteThrottleDecision {
19
21
  }
20
22
  export declare function routeThrottleLimitsFromOpts(opts: {
21
23
  maxActive?: string;
24
+ maxActiveScope?: string;
22
25
  maxActivePerProject?: string;
23
26
  maxActivePerProjectGroup?: string;
27
+ maxPerProfile?: string;
24
28
  }): RouteThrottleLimits;
25
29
  export declare function hasThrottleLimits(limits: RouteThrottleLimits): boolean;
26
30
  export declare function normalizeRoutePath(value: string | undefined): string | undefined;
@@ -2,6 +2,9 @@
2
2
  export interface TodosTaskRouteOptions {
3
3
  eventFile?: string;
4
4
  eventJson?: string;
5
+ policy?: string;
6
+ preset?: string;
7
+ routePolicyEvidence?: string;
5
8
  template?: string;
6
9
  provider?: string;
7
10
  providerRule?: string[];
@@ -33,6 +36,9 @@ export interface TodosTaskRouteOptions {
33
36
  maxActivePerProject?: string;
34
37
  maxActivePerProjectGroup?: string;
35
38
  maxActiveScope?: string;
39
+ providerActiveCap?: string;
40
+ codewithActiveCap?: string;
41
+ providerAdmissionCheck?: boolean;
36
42
  maxPerProfile?: string;
37
43
  worktreeMode?: string;
38
44
  worktreeRoot?: string;
@@ -86,6 +92,8 @@ export interface TodosDrainOptions extends TodosTaskRouteOptions {
86
92
  limit?: string;
87
93
  scanLimit?: string;
88
94
  maxDispatch?: string;
95
+ launchGate?: string;
96
+ launchGateBlocker?: string[];
89
97
  evidenceDir?: string;
90
98
  compact?: boolean;
91
99
  }