@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
@@ -5,19 +5,28 @@ defines the mode vocabulary, local cache behavior, API shape, and runner
5
5
  contract. Hosted multi-tenant operation is implemented outside this public
6
6
  package.
7
7
 
8
+ Deployment modes describe **where the OpenLoops runtime stores and executes**
9
+ loops and workflows (`local` SQLite, `self_hosted` control plane, or `cloud`
10
+ contract). They are runtime placement concerns, not the automation product
11
+ surface — specs, queues, approvals, and audit for product automations remain in
12
+ `@hasna/automations` and `@hasna/actions`. See
13
+ [Runtime Boundary](./RUNTIME_BOUNDARY.md).
14
+
8
15
  ## Modes
9
16
 
10
17
  | Mode | Source of truth | Local storage role | Executor |
11
18
  | --- | --- | --- | --- |
12
19
  | `local` | SQLite in `LOOPS_DATA_DIR` | Authoritative | `loops-daemon` |
13
- | `self_hosted` | A user-operated `loops-api` control plane contract | Cache and offline spool | `loops-runner` foundation |
20
+ | `self_hosted` | Hasna-owned AWS/RDS control plane served by `loops-serve`/`loops-api` | Cache and offline spool | `loops-runner` foundation |
14
21
  | `cloud` | A configured hosted control plane contract | Cache and offline spool | `loops-runner` foundation |
15
22
 
16
23
  `local` remains the default. It must keep working without network access,
17
24
  tokens, Postgres, or hosted infrastructure.
18
25
 
19
- `self_hosted` is for users or teams running their own control plane. The public
20
- `@hasna/loops` package owns the API and runner contract for this mode.
26
+ `self_hosted` is the Hasna-owned AWS/RDS control-plane deployment. The public
27
+ `@hasna/loops` package owns `loops-serve`, the embeddable `loops-api` contract,
28
+ the Postgres storage adapter, migrations, HTTP SDK, and runner contract for this
29
+ mode.
21
30
 
22
31
  `cloud` is the hosted control-plane contract. The public package exposes the
23
32
  client and runner contract, but tenant auth, account administration, and hosted
@@ -58,6 +67,8 @@ loops self-hosted runner-register --runner-id <id> --machine-id <machine>
58
67
  loops self-hosted runner-register --runner-id <id> --machine-id <machine> --apply
59
68
  loops cloud status
60
69
  loops-api status
70
+ loops-serve version
71
+ HASNA_LOOPS_DATABASE_URL=... loops-serve migrate --dry-run
61
72
  loops-runner status
62
73
  loops export --file ./loops-export.json --dry-run
63
74
  loops export --file ./loops-export.json
@@ -88,11 +99,12 @@ JSON uses these field names:
88
99
  - `schedulerState.localStore`: always names the local SQLite store and local
89
100
  run artifact files. In `local` mode this store is authoritative; in
90
101
  non-local modes it is a cache, offline spool, and audit copy.
91
- - `schedulerState.remoteStore`: names the remote scheduler contract:
102
+ - `schedulerState.remoteStore`: names the non-local scheduler contract:
92
103
  `api_control_plane_contract`, `postgres_contract`,
93
104
  `hosted_control_plane_contract`, `unconfigured`, or `none`. Remote apply is
94
- `false` in this public package until a control-plane host wires a full
95
- storage adapter.
105
+ `false` in the standalone CLI until the control-plane API exposes
106
+ id-preserving import endpoints. `loops-serve` itself wires the Postgres
107
+ storage adapter for normal control-plane CRUD and runner protocol routes.
96
108
  - `schedulerState.remoteStore.objectArtifacts`: `object_store_contract` means
97
109
  remote artifact/object storage is a control-plane contract. The public package
98
110
  does not create or mutate S3 buckets, AWS resources, or hosted credentials.
@@ -102,13 +114,18 @@ JSON uses these field names:
102
114
  Live active counts use admitted/running work items; dry-runs do not open or
103
115
  migrate the live store to compute counts.
104
116
 
105
- `loops-api` is a separate process in the same public package. It is not a
106
- separate package at this stage because self-hosted users and the hosted service
107
- must share the same public contract. The API server can expose storage-backed
108
- `/v1` loop CRUD and run listing when an embedding host injects the public
109
- storage contract. The standalone `loops-api serve` binary still fails closed
110
- for those routes until a self-hosted storage adapter is wired by the operator or
111
- platform host.
117
+ `loops-serve` is the self-hosted HTTP control-plane binary in this public
118
+ package. It reads and writes Postgres directly, serves open foundation probes
119
+ (`GET /health`, `/ready`, `/version`, `/openapi.json`), gates `/v1` loop/run and
120
+ runner-protocol routes with API-key auth on non-local binds, and applies the
121
+ Postgres migrations plus the shared `api_keys` table with `loops-serve migrate`.
122
+
123
+ `loops-api` is the embeddable API contract and local/dev foundation server in
124
+ the same public package. It is not a separate package because self-hosted users
125
+ and the hosted service must share the same public contract. The standalone
126
+ `loops-api serve` path still fails closed for storage-backed routes unless an
127
+ embedding host injects a storage adapter; `loops-serve` is the shipped
128
+ Postgres-backed self-hosted host.
112
129
 
113
130
  `loops-runner` is the process that connects a machine to a non-local control
114
131
  plane. The current public package supports a bounded one-shot protocol:
@@ -166,9 +183,10 @@ would generate new ids, so it is not a no-loss migration. Local SQLite remains
166
183
  authoritative until a safe import is applied; in non-local modes it may remain
167
184
  a cache, offline spool, and audit copy.
168
185
 
169
- `LOOPS_DATABASE_URL` selects the self-hosted Postgres scheduler-state contract,
170
- but it does not make the standalone CLI mutate a remote database by itself.
171
- Remote execution still flows through a configured control-plane API and runner
186
+ `LOOPS_DATABASE_URL` or `HASNA_LOOPS_DATABASE_URL` selects the self-hosted
187
+ Postgres scheduler-state contract and is required by `loops-serve`. It does not
188
+ make the standalone `loops` CLI mutate a remote database by itself. Remote
189
+ execution still flows through a configured control-plane API and runner
172
190
  protocol. `loops-runner` needs `LOOPS_API_URL` or `HASNA_LOOPS_API_URL` to claim
173
191
  work; a database URL alone is migration/readiness configuration.
174
192
 
@@ -203,9 +221,8 @@ mode.
203
221
 
204
222
  ## Follow-Up Work
205
223
 
206
- Non-local execution needs these follow-up releases before it is live:
224
+ Non-local execution needs these follow-up releases before it is complete:
207
225
 
208
- - A full Postgres control-plane adapter behind the public storage contract.
209
226
  - Long-running runner daemon mode with backoff, fleet observability, and
210
227
  durable machine registration records.
211
228
  - Workflow target execution over the remote protocol.
@@ -0,0 +1,203 @@
1
+ # OpenLoops Runtime Boundary
2
+
3
+ OpenLoops is the **runtime, scheduler, and workflow engine** for automations.
4
+ It executes work that external systems have already materialized or explicitly
5
+ handed off. It is **not** the automation domain model: specs, triggers, queue
6
+ ownership, approvals, DLQ/replay, idempotency, and audit evidence live in
7
+ `@hasna/automations`, `@hasna/actions`, and related product packages.
8
+
9
+ Use this document when you need a single operator-facing boundary reference.
10
+ For the planned upsert/DLQ/strict-mode contract, see
11
+ [`AUTOMATION_RUNTIME_DESIGN.md`](./AUTOMATION_RUNTIME_DESIGN.md). For
12
+ deployment-mode vocabulary (local vs self-hosted vs cloud), see
13
+ [`DEPLOYMENT_MODES.md`](./DEPLOYMENT_MODES.md).
14
+
15
+ ## Ownership Split
16
+
17
+ | Concern | Owner | OpenLoops role |
18
+ | --- | --- | --- |
19
+ | Automation specs and trigger materialization | `@hasna/automations` | None — do not store specs in OpenLoops |
20
+ | Product automation action queues, leases, DLQ, replay, idempotency, approvals, audit | `@hasna/automations` | Execute claimed actions; complete/fail by action id + runner id |
21
+ | Action compilation and action-target rendering | `@hasna/actions` | Accept planned upsert handoff; do not write SQLite rows directly |
22
+ | Scheduler, daemon, loop/workflow storage | OpenLoops | Authoritative in `local` mode |
23
+ | Workflow invocation, admission, execution, run manifests | OpenLoops | After explicit handoff or todos-task route admission |
24
+ | Provider routing, worktrees, run artifacts | OpenLoops | Bounded by route and template policy |
25
+ | Todos-task route drains (`auto:route`, `route_enabled`, `automation.allowed`) | OpenLoops-native | Opt-in task/event routing — **not** OpenAutomations queue replacement |
26
+
27
+ The SDK exposes the canonical boundary strings via `openAutomationsRuntimeBinding()`:
28
+
29
+ ```ts
30
+ import { openAutomationsRuntimeBinding } from "@hasna/loops";
31
+
32
+ const binding = openAutomationsRuntimeBinding();
33
+ // binding.handoff === "claim-queue"
34
+ // binding.queueOwner === "open-automations"
35
+ // binding.runtimeOwner === "open-loops"
36
+ console.log(binding.guarantees);
37
+ console.log(binding.nonGoals);
38
+ ```
39
+
40
+ **Guarantees (summary):**
41
+
42
+ - OpenAutomations owns automation specs, run materialization, queue state, DLQ,
43
+ replay, idempotency, and approvals.
44
+ - OpenLoops may execute claimed actions through explicit command or SDK handoff
45
+ only.
46
+ - OpenLoops may consume exported event envelopes only through explicit
47
+ `loops routes create` commands.
48
+ - Workers must complete or fail actions by action id and runner id so
49
+ OpenAutomations can enforce queue leases.
50
+
51
+ **Non-goals (summary):**
52
+
53
+ - OpenLoops must not become the OpenAutomations product surface.
54
+ - OpenLoops must not store automation specs or replace the OpenAutomations queue.
55
+ - OpenLoops must not infer automation trigger semantics from event transport
56
+ alone.
57
+
58
+ ## External Compiler Handoff Paths
59
+
60
+ External compilers (`@hasna/automations`, `@hasna/actions`, event routers)
61
+ materialize automation intent in their own stores. OpenLoops receives **rendered
62
+ work** through one of the paths below. Do not bypass these boundaries by writing
63
+ OpenLoops SQLite rows from another package.
64
+
65
+ ### 1. OpenAutomations claim-queue (implemented)
66
+
67
+ OpenAutomations owns the action queue. An OpenLoops worker claims work, runs the
68
+ rendered target, and reports completion back to OpenAutomations.
69
+
70
+ ```bash
71
+ # Worker claims from the OpenAutomations queue
72
+ automations queue claim --runner open-loops:<worker-id>
73
+
74
+ # After OpenLoops executes the handed-off command/workflow:
75
+ automations queue complete <action-id> --runner open-loops:<worker-id>
76
+ # or on failure:
77
+ automations queue fail <action-id> --runner open-loops:<worker-id> \
78
+ --code <code> --message <message>
79
+ ```
80
+
81
+ Keep `HASNA_AUTOMATIONS_DIR` pointing at the owning OpenAutomations data root.
82
+ Preserve the runner id in every complete/fail call so OpenAutomations can enforce
83
+ action leases.
84
+
85
+ SDK reference:
86
+
87
+ ```ts
88
+ const binding = openAutomationsRuntimeBinding();
89
+ console.log(binding.claimCommand); // "automations queue claim"
90
+ console.log(binding.completeCommand); // "automations queue complete"
91
+ console.log(binding.failCommand); // "automations queue fail"
92
+ ```
93
+
94
+ ### 2. `@hasna/actions` compiler → planned upsert-one-shot (design)
95
+
96
+ `@hasna/actions` compiles action targets into a fully rendered one-shot workflow
97
+ loop request. The stable handoff is an **idempotent upsert** — not direct
98
+ SQLite access.
99
+
100
+ **Status:** `loops workflows upsert-one-shot` and the matching SDK method are
101
+ **planned** (see [`AUTOMATION_RUNTIME_DESIGN.md`](./AUTOMATION_RUNTIME_DESIGN.md)).
102
+ The shapes below are the contract target; they are not shipped in this release.
103
+
104
+ Example request the compiler would send (abbreviated):
105
+
106
+ ```ts
107
+ type WorkflowUpsertRequest = {
108
+ idempotencyKey: "actions:<action-id>:<version>",
109
+ source: { kind: "action", id: "<action-id>" },
110
+ subject: { kind: "repo", path: "/path/to/repo" },
111
+ workflow: { name: "action-<action-id>", steps: [/* rendered steps */] },
112
+ loop: { name: "action-<action-id>", schedule: { type: "once", at: "2026-07-01T12:00:00Z" } },
113
+ mode: "preflight", // dry-run | preflight | commit
114
+ dispatch: "schedule", // schedule | run-now | none
115
+ };
116
+ ```
117
+
118
+ Planned CLI mirror:
119
+
120
+ ```bash
121
+ loops workflows upsert-one-shot ./workflow.json \
122
+ --idempotency-key actions:<action-id>:<version> \
123
+ --source-kind action \
124
+ --source-id <action-id> \
125
+ --subject-kind repo \
126
+ --subject-path /path/to/repo \
127
+ --loop-name action-<action-id> \
128
+ --at 2026-07-01T12:00:00Z \
129
+ --mode preflight \
130
+ --dispatch schedule \
131
+ --json
132
+ ```
133
+
134
+ OpenLoops returns durable refs (`workflowId`, `loopId`, `runId`, `manifestPath`)
135
+ so `@hasna/actions` can inspect, cancel, or replay without querying SQLite.
136
+ Full semantics (idempotency on `idempotencyKey` + `specHash`, redaction,
137
+ strict-mode policy) are specified in the design doc — not duplicated here.
138
+
139
+ ### 3. Event envelope handoff (implemented, explicit only)
140
+
141
+ For workflows that should run from a normalized event envelope — **without**
142
+ OpenAutomations materializing automation specs inside OpenLoops — pipe an
143
+ exported envelope into the generic route:
144
+
145
+ ```bash
146
+ automations --json webhooks event <route> --body-json '<json>' \
147
+ | loops --json routes create generic
148
+ ```
149
+
150
+ This is **not** automation materialization in OpenLoops. OpenAutomations still
151
+ owns deterministic automation specs, webhook normalization, queue state,
152
+ approvals, DLQ, and replay. OpenLoops owns agent workflow invocation after the
153
+ operator routes the envelope to `loops routes create generic`.
154
+
155
+ ```ts
156
+ const { eventHandoff } = openAutomationsRuntimeBinding();
157
+ console.log(eventHandoff.handlerCommand); // "loops routes create generic"
158
+ console.log(eventHandoff.boundary);
159
+ ```
160
+
161
+ ## OpenLoops-Native Path: Todos-Task Routes
162
+
163
+ Todos-task routing is an **OpenLoops-native** admission path. It is separate from
164
+ the OpenAutomations product queue:
165
+
166
+ ```bash
167
+ cat task-created-event.json | loops routes create todos-task \
168
+ --template task-lifecycle \
169
+ --worktree-mode required
170
+ ```
171
+
172
+ Tasks must opt in (`auto:route`, `route_enabled=true`, or
173
+ `automation.allowed=true`). OpenLoops admits deduped one-shot workflow loops,
174
+ drains them on a schedule, and writes run manifests under
175
+ `.hasna/loops/runs/<project-slug>/<subject-key>/<run-id>/`.
176
+
177
+ This path does **not** make OpenAutomations the queue owner for todos task/PR/review
178
+ agent workflows. It also does **not** replace OpenAutomations action queues for
179
+ product automations. Choose the handoff path that matches who owns the queue and
180
+ audit trail for your automation.
181
+
182
+ See [`USAGE.md`](./USAGE.md) § Templates And Task Events for templates,
183
+ provider rules, and drain examples.
184
+
185
+ ## Anti-Patterns
186
+
187
+ Do **not**:
188
+
189
+ - Store automation specs in OpenLoops loop/workflow rows.
190
+ - Infer automation triggers from event transport alone.
191
+ - Replace the OpenAutomations queue with loop/workflow rows.
192
+ - Write OpenLoops SQLite directly from `@hasna/actions` or `@hasna/automations`.
193
+ - Dispatch or paste task prompts into tmux panes from route drains (use headless
194
+ workflow templates instead).
195
+
196
+ ## Related Docs
197
+
198
+ - [`AUTOMATION_RUNTIME_DESIGN.md`](./AUTOMATION_RUNTIME_DESIGN.md) — planned
199
+ upsert SDK, DLQ/replay, strict automation execution mode.
200
+ - [`USAGE.md`](./USAGE.md) — templates, todos-task routes, provider pools.
201
+ - [`DEPLOYMENT_MODES.md`](./DEPLOYMENT_MODES.md) — local/self-hosted/cloud;
202
+ deployment concerns are runtime placement, not automation product surface.
203
+ - README § OpenAutomations Runtime Binding — short summary and quick references.