@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.
- package/CHANGELOG.md +99 -3
- package/README.md +171 -39
- package/dist/api/index.d.ts +36 -0
- package/dist/api/index.js +1518 -15
- package/dist/cli/index.js +7280 -3213
- package/dist/cli/ui.d.ts +44 -0
- package/dist/daemon/index.js +1433 -303
- package/dist/generated/storage-kit/health.d.ts +19 -0
- package/dist/generated/storage-kit/index.d.ts +7 -0
- package/dist/generated/storage-kit/migrations.d.ts +47 -0
- package/dist/generated/storage-kit/mode.d.ts +47 -0
- package/dist/generated/storage-kit/pool.d.ts +33 -0
- package/dist/generated/storage-kit/query.d.ts +35 -0
- package/dist/generated/storage-kit/tls.d.ts +25 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +8689 -4837
- package/dist/lib/cloud/mode.d.ts +17 -0
- package/dist/lib/cloud/resolve.d.ts +16 -0
- package/dist/lib/cloud/storage.d.ts +82 -0
- package/dist/lib/cloud/transport.d.ts +148 -0
- package/dist/lib/format.d.ts +2 -1
- package/dist/lib/health.d.ts +83 -3
- package/dist/lib/migration.d.ts +40 -0
- package/dist/lib/mode.js +15 -3
- package/dist/lib/route/index.d.ts +2 -0
- package/dist/lib/route/policies.d.ts +51 -0
- package/dist/lib/route/provider-admission.d.ts +37 -0
- package/dist/lib/route/throttle.d.ts +4 -0
- package/dist/lib/route/types.d.ts +8 -0
- package/dist/lib/run-receipts.d.ts +14 -0
- package/dist/lib/scheduler.d.ts +5 -2
- package/dist/lib/storage/contract.d.ts +7 -1
- package/dist/lib/storage/index.d.ts +1 -0
- package/dist/lib/storage/index.js +2028 -231
- package/dist/lib/storage/pg-executor.d.ts +27 -0
- package/dist/lib/storage/pg-runner-claim.d.ts +40 -0
- package/dist/lib/storage/postgres-loop-storage.d.ts +120 -0
- package/dist/lib/storage/postgres-schema.js +29 -0
- package/dist/lib/storage/postgres.js +29 -0
- package/dist/lib/storage/sqlite.d.ts +6 -0
- package/dist/lib/storage/sqlite.js +748 -26
- package/dist/lib/store/index.d.ts +268 -0
- package/dist/lib/store.d.ts +282 -1
- package/dist/lib/store.js +746 -26
- package/dist/lib/template-kit.d.ts +25 -0
- package/dist/lib/templates.d.ts +16 -1
- package/dist/mcp/http.d.ts +16 -0
- package/dist/mcp/index.js +2885 -634
- package/dist/runner/index.js +198 -32
- package/dist/sdk/http.d.ts +182 -0
- package/dist/sdk/http.js +166 -0
- package/dist/sdk/index.d.ts +55 -18
- package/dist/sdk/index.js +2734 -617
- package/dist/serve/index.d.ts +4 -0
- package/dist/serve/index.js +9095 -0
- package/dist/types.d.ts +52 -0
- package/docs/AUTOMATION_RUNTIME_DESIGN.md +442 -1
- package/docs/CUTOVER-RUNBOOK.md +78 -0
- package/docs/DEPLOYMENT_MODES.md +35 -18
- package/docs/RUNTIME_BOUNDARY.md +203 -0
- package/docs/USAGE.md +195 -38
- package/package.json +15 -3
package/docs/DEPLOYMENT_MODES.md
CHANGED
|
@@ -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` |
|
|
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
|
|
20
|
-
`@hasna/loops` package owns the
|
|
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
|
|
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
|
|
95
|
-
|
|
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-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
|
170
|
-
|
|
171
|
-
|
|
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
|
|
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.
|