@hasna/loops 0.4.14 → 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 +25 -0
- package/README.md +119 -29
- package/dist/api/index.d.ts +1 -0
- package/dist/api/index.js +825 -7
- package/dist/cli/index.js +5419 -2619
- package/dist/daemon/index.js +478 -84
- package/dist/generated/storage-kit/index.d.ts +1 -1
- package/dist/index.js +2521 -226
- 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/migration.d.ts +40 -0
- package/dist/lib/mode.js +3 -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/storage/contract.d.ts +7 -1
- package/dist/lib/storage/index.js +710 -31
- package/dist/lib/storage/postgres-loop-storage.d.ts +6 -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 +412 -18
- package/dist/lib/store/index.d.ts +268 -0
- package/dist/lib/store.d.ts +48 -1
- package/dist/lib/store.js +396 -18
- 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 +1658 -168
- package/dist/runner/index.js +76 -9
- package/dist/sdk/http.d.ts +67 -0
- package/dist/sdk/http.js +21 -0
- package/dist/sdk/index.d.ts +50 -17
- package/dist/sdk/index.js +1509 -132
- package/dist/serve/index.js +1598 -122
- package/dist/types.d.ts +49 -0
- package/docs/AUTOMATION_RUNTIME_DESIGN.md +442 -1
- package/docs/CUTOVER-RUNBOOK.md +73 -56
- package/docs/DEPLOYMENT_MODES.md +35 -18
- package/docs/RUNTIME_BOUNDARY.md +203 -0
- package/docs/USAGE.md +145 -27
- package/package.json +3 -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.
|
package/docs/USAGE.md
CHANGED
|
@@ -15,12 +15,13 @@ It supports deterministic command loops, JSON-defined workflows, and guarded CLI
|
|
|
15
15
|
|
|
16
16
|
OpenLoops defaults to `local`, where SQLite in `LOOPS_DATA_DIR` is
|
|
17
17
|
authoritative and `loops-daemon` executes scheduled work. The package also
|
|
18
|
-
defines `self_hosted` and `cloud` contracts for
|
|
19
|
-
planes:
|
|
18
|
+
defines `self_hosted` and `cloud` contracts for non-local control planes:
|
|
20
19
|
|
|
21
|
-
- `self_hosted`:
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
- `self_hosted`: the Hasna-owned AWS/RDS control-plane deployment, served by
|
|
21
|
+
`loops-serve` and backed by Postgres, with the embeddable `loops-api` contract
|
|
22
|
+
shared by serve, SDK, and tests. This release exposes storage-backed `/v1`
|
|
23
|
+
loop CRUD and run listing, runner registration/claim/heartbeat/finalize
|
|
24
|
+
foundations, and local migration previews.
|
|
24
25
|
- `cloud`: hosted control-plane contract; this release exposes client/runner
|
|
25
26
|
status only, and requires `LOOPS_CLOUD_API_URL` plus `LOOPS_CLOUD_TOKEN` or
|
|
26
27
|
`HASNA_LOOPS_CLOUD_TOKEN` before status can report ready.
|
|
@@ -29,13 +30,16 @@ Scheduler state is explicit in status JSON. `schedulerState.localStore` is
|
|
|
29
30
|
SQLite plus local run artifact files: authoritative in `local`, cache/spool in
|
|
30
31
|
non-local modes. `schedulerState.remoteStore` names the non-local contract
|
|
31
32
|
(`api_control_plane_contract`, `postgres_contract`, or
|
|
32
|
-
`hosted_control_plane_contract`)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
`hosted_control_plane_contract`). The standalone `loops` CLI reports
|
|
34
|
+
`applySupported=false` for non-local apply because it does not perform
|
|
35
|
+
id-preserving remote migration, S3/object storage mutation, AWS resource
|
|
36
|
+
mutation, or hosted credential mutation. `loops-serve` mutates self-hosted
|
|
37
|
+
Postgres for normal control-plane CRUD and runner protocol routes when it is
|
|
38
|
+
explicitly configured. Route admission remains bounded by `max_dispatch`,
|
|
39
|
+
`max_active`, `max_active_per_project`, `max_active_per_project_group`,
|
|
40
|
+
`max_active_scope`, and `max_per_profile`.
|
|
37
41
|
|
|
38
|
-
Useful status commands:
|
|
42
|
+
Useful status and setup commands:
|
|
39
43
|
|
|
40
44
|
```bash
|
|
41
45
|
loops mode
|
|
@@ -48,6 +52,8 @@ loops self-hosted runner-register --runner-id <id> --machine-id <machine>
|
|
|
48
52
|
loops self-hosted runner-register --runner-id <id> --machine-id <machine> --apply
|
|
49
53
|
loops cloud status
|
|
50
54
|
loops-api status
|
|
55
|
+
loops-serve version
|
|
56
|
+
HASNA_LOOPS_DATABASE_URL=... loops-serve migrate --dry-run
|
|
51
57
|
loops-runner status
|
|
52
58
|
```
|
|
53
59
|
|
|
@@ -93,14 +99,16 @@ The preview may inspect `LOOPS_API_URL`/`HASNA_LOOPS_API_URL`, but it refuses
|
|
|
93
99
|
remote apply because normal loop CRUD would generate new ids. Use
|
|
94
100
|
`loops self-hosted runner-register` to verify runner registration against an
|
|
95
101
|
API, then use `loops-runner run-once` for the current bounded non-workflow
|
|
96
|
-
claim/execute/finalize protocol.
|
|
102
|
+
claim/execute/finalize protocol. `loops-serve migrate` applies the Postgres
|
|
103
|
+
schema and `api_keys` table for a self-hosted control-plane host; the standalone
|
|
104
|
+
CLI migration previews still do not perform id-preserving remote apply.
|
|
97
105
|
Runner registration is preview-only unless `--apply` is present.
|
|
98
106
|
|
|
99
107
|
## Install
|
|
100
108
|
|
|
101
109
|
**OpenLoops requires the [Bun](https://bun.sh) runtime (`bun >= 1.0`).** The
|
|
102
|
-
installed binaries are `loops`, `loops-daemon`, `loops-api`, `loops-
|
|
103
|
-
`loops-mcp`; each uses a `#!/usr/bin/env bun` shebang.
|
|
110
|
+
installed binaries are `loops`, `loops-daemon`, `loops-api`, `loops-serve`,
|
|
111
|
+
`loops-runner`, and `loops-mcp`; each uses a `#!/usr/bin/env bun` shebang.
|
|
104
112
|
|
|
105
113
|
From npm:
|
|
106
114
|
|
|
@@ -208,6 +216,23 @@ failures are recorded as failed loop runs with a `runtime preflight failed`
|
|
|
208
216
|
error, so health/routing checks can create follow-up tasks without spawning the
|
|
209
217
|
worker.
|
|
210
218
|
|
|
219
|
+
For controlled launches, scheduled todos-task drains can be guarded by blocker
|
|
220
|
+
tasks:
|
|
221
|
+
|
|
222
|
+
```bash
|
|
223
|
+
loops routes schedule todos-task platform-drain \
|
|
224
|
+
--every 5m \
|
|
225
|
+
--todos-project /path/to/source/todos-project \
|
|
226
|
+
--tags auto:route \
|
|
227
|
+
--launch-gate "pa19-controlled-launch" \
|
|
228
|
+
--launch-gate-blocker "/path/to/open-codewith::2d9d931b" \
|
|
229
|
+
--launch-gate-blocker "/path/to/open-loops::816e99db" \
|
|
230
|
+
--worktree-mode required
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
The drain creates zero worker loops while any blocker task is not completed.
|
|
234
|
+
Each blocked tick writes launch-gate evidence and leaves source tasks untouched.
|
|
235
|
+
|
|
211
236
|
Run a Claude loop every morning:
|
|
212
237
|
|
|
213
238
|
```bash
|
|
@@ -418,6 +443,13 @@ for those. Set `"blockedExitCodes": []` on a gate-named step to opt out.
|
|
|
418
443
|
|
|
419
444
|
## Templates And Task Events
|
|
420
445
|
|
|
446
|
+
OpenLoops is the runtime/scheduler/workflow engine for these flows — not the
|
|
447
|
+
automation domain model. Todos-task routes are OpenLoops-native admission; they
|
|
448
|
+
do not replace the OpenAutomations product queue. See
|
|
449
|
+
[Runtime Boundary](./RUNTIME_BOUNDARY.md) for ownership split and external
|
|
450
|
+
compiler handoff paths (`@hasna/automations` claim-queue,
|
|
451
|
+
`@hasna/actions` planned upsert-one-shot).
|
|
452
|
+
|
|
421
453
|
Built-in templates turn common orchestration flows into reusable workflow JSON.
|
|
422
454
|
`todos-task-worker-verifier` performs one todos task and then verifies it.
|
|
423
455
|
`event-worker-verifier` handles any Hasna event envelope and then verifies the
|
|
@@ -425,7 +457,11 @@ handling. `bounded-agent-worker-verifier` is for recurring bounded agent work:
|
|
|
425
457
|
one worker runs a narrow objective, then a fresh verifier audits the result.
|
|
426
458
|
The catalog also includes `task-lifecycle`, `pr-review`, `scheduled-audit`,
|
|
427
459
|
`knowledge-refresh`, `report-only`, `incident-response`, and
|
|
428
|
-
`deterministic-check-create-task` for common operator workflows.
|
|
460
|
+
`deterministic-check-create-task` for common operator workflows. Use
|
|
461
|
+
`routing-remediation` for bounded routing-doctor repair runs: it dry-runs by
|
|
462
|
+
default, gates `safe_auto` capacity, applies only supported
|
|
463
|
+
`todos doctor routing --apply` repairs when explicitly enabled, and files
|
|
464
|
+
blocker tasks for human/cross-repo/unsupported findings.
|
|
429
465
|
|
|
430
466
|
```bash
|
|
431
467
|
loops templates list
|
|
@@ -459,6 +495,12 @@ loops templates render pr-review \
|
|
|
459
495
|
loops templates render deterministic-check-create-task \
|
|
460
496
|
--var projectPath=/path/to/repo \
|
|
461
497
|
--var checkCommand='your deterministic check and todos upsert command'
|
|
498
|
+
loops templates render routing-remediation \
|
|
499
|
+
--var projectPath=/path/to/repo \
|
|
500
|
+
--var todosProjectPath=/path/to/todos-project \
|
|
501
|
+
--var shard=0/6 \
|
|
502
|
+
--var maxRepairs=25 \
|
|
503
|
+
--var idempotencyKey=routing-health:repo:shard0
|
|
462
504
|
```
|
|
463
505
|
|
|
464
506
|
Custom reusable workflow templates live under the OpenLoops app data directory:
|
|
@@ -579,12 +621,14 @@ on the remote machine. The generated agent target includes worktree metadata
|
|
|
579
621
|
inspection expose the exact checkout.
|
|
580
622
|
|
|
581
623
|
Before a worker starts, worktree preparation verifies that any existing
|
|
582
|
-
managed path is a real git worktree with the same top-level checkout
|
|
583
|
-
git common directory as the source repo
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
624
|
+
managed path is a real git worktree with the same top-level checkout and the
|
|
625
|
+
same git common directory as the source repo. If the checkout is on a detached
|
|
626
|
+
HEAD or unexpected branch, OpenLoops only reattaches it to the expected
|
|
627
|
+
generated branch when the worktree is clean. Dirty worktrees, symlinked paths,
|
|
628
|
+
non-worktree paths, different common dirs, and unrecoverable branch switches
|
|
629
|
+
fail closed with cleanup evidence (`worktreeMode=required`) instead of
|
|
630
|
+
silently running a mutating workflow in the wrong state; `worktreeMode=auto`
|
|
631
|
+
falls back to the original checkout and records the fallback.
|
|
588
632
|
|
|
589
633
|
Use explicit main/default checkout mode only when the task truly requires it:
|
|
590
634
|
|
|
@@ -628,6 +672,9 @@ blocked, completed/done, cancelled/canceled, failed, archived, manual,
|
|
|
628
672
|
approval-required, or `no-auto` tasks. This guard exists even when the upstream
|
|
629
673
|
`@hasna/events` webhook filter is misconfigured, so task existence alone is not
|
|
630
674
|
permission to execute agent work.
|
|
675
|
+
Route work items are the durable reservation ledger: they include the stable
|
|
676
|
+
idempotency key, task/event references, project/group keys, admitting machine
|
|
677
|
+
ID, route scope, workflow/loop IDs, and the current terminal or active status.
|
|
631
678
|
|
|
632
679
|
Task route drains can select providers from task metadata instead of running one
|
|
633
680
|
fixed provider/account pool for the whole drain. Add one or more
|
|
@@ -637,13 +684,20 @@ matching rule wins. Rule profiles become a Codewith auth-profile pool for
|
|
|
637
684
|
can also carry `provider_hint`/`route_provider`, `auth_profile_pool`, or
|
|
638
685
|
`account_pool` metadata. Dry-run, drain evidence, and route invocation scope
|
|
639
686
|
include `providerRouting` so operators can see why a provider/account was
|
|
640
|
-
selected.
|
|
687
|
+
selected. Selector values may contain `:`; the parser treats the colon before a
|
|
688
|
+
supported provider id as the route delimiter, so exact tag selectors such as
|
|
689
|
+
`tags=area:frontend:claude:account003,account015` and
|
|
690
|
+
`tags=provider:claude-code:claude:account003,account015` match literal task
|
|
691
|
+
tags `area:frontend` and `provider:claude-code`.
|
|
641
692
|
|
|
642
693
|
```bash
|
|
643
694
|
loops routes drain todos-task \
|
|
644
695
|
--dry-run \
|
|
696
|
+
--provider-rule tags=area:frontend:claude:account003,account015 \
|
|
697
|
+
--provider-rule tags=provider:claude-code:claude:account003,account015 \
|
|
645
698
|
--provider-rule area=frontend:claude:claude-ui-a,claude-ui-b \
|
|
646
699
|
--provider-rule area=backend:codewith:account001,account002 \
|
|
700
|
+
--provider-rule tags=task-lifecycle:codewith:account001,account002 \
|
|
647
701
|
--worktree-mode required
|
|
648
702
|
```
|
|
649
703
|
|
|
@@ -721,6 +775,10 @@ equivalent directory-scoped write controls.
|
|
|
721
775
|
Inspect route state with:
|
|
722
776
|
|
|
723
777
|
```bash
|
|
778
|
+
loops routes policies list
|
|
779
|
+
loops routes policies show oss
|
|
780
|
+
loops routes policies render oss
|
|
781
|
+
loops routes policies validate
|
|
724
782
|
cat task-created-event.json | loops routes preview todos-task --sandbox workspace-write
|
|
725
783
|
cat task-created-event.json | loops routes create todos-task --sandbox workspace-write
|
|
726
784
|
loops routes drain todos-task --task-list oss --max-dispatch 2 --compact
|
|
@@ -731,6 +789,29 @@ loops routes requeue <work-item-id> --reason "fixed upstream blocker"
|
|
|
731
789
|
loops routes invocations
|
|
732
790
|
```
|
|
733
791
|
|
|
792
|
+
Compact drain output includes route reservation IDs, work-item status, machine
|
|
793
|
+
ID, workflow ID, loop ID, and route scope for each considered task.
|
|
794
|
+
|
|
795
|
+
Named route policies expand the long recurring drain commands used by the live
|
|
796
|
+
task routers into explicit, replayable options. `repoops-pr-queue`, `oss`,
|
|
797
|
+
`pilot`, and `machine-sync` are built in. Operators can inspect the policy,
|
|
798
|
+
render the exact `loops --json routes drain todos-task ...` command, and validate
|
|
799
|
+
that the rendered args no longer depend on `--policy`/`--preset`:
|
|
800
|
+
|
|
801
|
+
```bash
|
|
802
|
+
loops routes policies render oss
|
|
803
|
+
loops routes schedule todos-task machine-oss-task-lifecycle-router --policy oss
|
|
804
|
+
```
|
|
805
|
+
|
|
806
|
+
Scheduled policy routes store explicit drain args plus
|
|
807
|
+
`--route-policy-evidence <id>`, so future runs remain auditable even if a policy
|
|
808
|
+
definition changes later. Policy drains and dry-runs include `routePolicy`
|
|
809
|
+
evidence with the source, safety class, guards, expanded options, and rendered
|
|
810
|
+
args. Passing a conflicting explicit option fails before the route is created.
|
|
811
|
+
The `pilot` policy uses `sandbox=danger-full-access` and is treated as a paused
|
|
812
|
+
manual break-glass lane; applying it requires the operator to pass
|
|
813
|
+
`--manual-break-glass` explicitly.
|
|
814
|
+
|
|
734
815
|
When a workflow run starts from an admitted work item, OpenLoops writes a
|
|
735
816
|
manifest under:
|
|
736
817
|
|
|
@@ -762,6 +843,8 @@ loops routes drain todos-task \
|
|
|
762
843
|
--max-active-per-project 1 \
|
|
763
844
|
--max-active-per-project-group 4 \
|
|
764
845
|
--max-active 12 \
|
|
846
|
+
--provider-active-cap 6 \
|
|
847
|
+
--provider-admission-check \
|
|
765
848
|
--worktree-mode required \
|
|
766
849
|
--evidence-dir "$HOME/.hasna/loops/reports/task-drain"
|
|
767
850
|
```
|
|
@@ -779,6 +862,16 @@ when requested, and leaves excess ready tasks in todos for a later drain pass.
|
|
|
779
862
|
Use `--dry-run` to preview candidates and rendered workflows without mutating
|
|
780
863
|
OpenLoops state.
|
|
781
864
|
|
|
865
|
+
The route throttle flags count OpenLoops routed workflow work items, not the
|
|
866
|
+
live background-agent slots inside a provider. For Codewith drains, add
|
|
867
|
+
`--provider-active-cap <n>` (or `--codewith-active-cap <n>`) so each candidate
|
|
868
|
+
checks `codewith agent diagnostics --json` before workflow-loop creation and
|
|
869
|
+
defers when `activeRunCount >= n`. Use `--provider-admission-check` when the
|
|
870
|
+
drain should also fail closed on diagnostics errors, unsupported providers, or
|
|
871
|
+
Codewith reports with no available admission slots. Backlog prioritizer and
|
|
872
|
+
drain loops should use these first-class flags rather than a shell guard around
|
|
873
|
+
`codewith agent diagnostics`.
|
|
874
|
+
|
|
782
875
|
For an OSS task-created route, keep the drain deterministic and narrow:
|
|
783
876
|
|
|
784
877
|
```bash
|
|
@@ -797,6 +890,8 @@ loops routes schedule todos-task oss-task-route-drain \
|
|
|
797
890
|
--max-active-per-project 1 \
|
|
798
891
|
--max-active-per-project-group 4 \
|
|
799
892
|
--max-active 12 \
|
|
893
|
+
--provider-active-cap 6 \
|
|
894
|
+
--provider-admission-check \
|
|
800
895
|
--worktree-mode required \
|
|
801
896
|
--evidence-dir "$HOME/.hasna/loops/reports/oss-task-route-drain" \
|
|
802
897
|
--compact
|
|
@@ -807,11 +902,13 @@ in with the `auto:route` tag, `route_enabled=true`, or
|
|
|
807
902
|
`automation.allowed=true` should be routed. Keep repo-mutating worker/verifier
|
|
808
903
|
runs on a Codewith account pool with `--worktree-mode required`. Do not dispatch
|
|
809
904
|
or paste task prompts into tmux panes. Use max-active throttles and
|
|
810
|
-
`--max-dispatch` to bound agent fan-out, and
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
905
|
+
`--max-dispatch` to bound OpenLoops agent fan-out, and use
|
|
906
|
+
`--provider-active-cap` plus `--provider-admission-check` to bound live Codewith
|
|
907
|
+
background-agent admission. Write compact evidence into a bounded reports
|
|
908
|
+
directory so operators can audit each drain without unbounded stdout or loop
|
|
909
|
+
history growth. Keep `--scan-limit` large enough for the current ready-task
|
|
910
|
+
backlog; if the scan is exhausted before matching tasks appear, the drain will
|
|
911
|
+
correctly do no work.
|
|
815
912
|
|
|
816
913
|
Generated task/event route workflow specs are lifecycle-managed. After a
|
|
817
914
|
generated one-shot route workflow run reaches `succeeded`, `failed`,
|
|
@@ -847,6 +944,27 @@ is requested without `manualBreakGlass=true`. Use `workspace-write` for
|
|
|
847
944
|
unattended task/event routes. Full access is an explicit manual emergency path,
|
|
848
945
|
not a default automation mode.
|
|
849
946
|
|
|
947
|
+
## Run Receipts
|
|
948
|
+
|
|
949
|
+
Use run receipts when an agent, scheduler, route, or external workflow needs a
|
|
950
|
+
stable run outcome without parsing raw stdout or wrapper-script text. Receipts
|
|
951
|
+
are scheduler-neutral JSON records with these public snake_case fields:
|
|
952
|
+
`loop_id`, `run_id`, `machine`, `repo`, `task_ids`, `knowledge_ids`,
|
|
953
|
+
`digest_id`, `started_at`, `finished_at`, `status`, `exit_code`, `summary`, and
|
|
954
|
+
`evidence_paths`. The summary contains bounded, scrubbed excerpts and byte
|
|
955
|
+
counts; raw unbounded stdout/stderr stays out of the receipt contract.
|
|
956
|
+
|
|
957
|
+
```bash
|
|
958
|
+
cat receipt.json | loops --json receipts write --file -
|
|
959
|
+
loops --json receipts read run_123
|
|
960
|
+
loops --json receipts list --loop-id loop_123 --task-id task_123
|
|
961
|
+
```
|
|
962
|
+
|
|
963
|
+
MCP clients can use `loops_receipt_read`, `loops_receipts_list`, and the
|
|
964
|
+
mutation-gated `loops_receipt_write`. The SDK exposes `writeReceipt`,
|
|
965
|
+
`receipt`, and `receipts`; the HTTP API exposes `POST /v1/receipts`,
|
|
966
|
+
`GET /v1/receipts/{runId}`, and `GET /v1/receipts`.
|
|
967
|
+
|
|
850
968
|
## Transcript-Driven Loops
|
|
851
969
|
|
|
852
970
|
OpenLoops can turn long-form media or meeting transcripts into recurring workflow work when paired with `iapp-transcriber`. The template at `docs/workflows/transcript-feedback-to-loops.json` transcribes an authorized media URL, asks an agent to extract recurring loop candidates, authors workflow specs, and validates generated workflows before scheduling. Copy it into the target repo, replace `/path/to/repo` with that repo's absolute path, and provide `TRANSCRIBER_SOURCE_URL` through the runner environment or a private, uncommitted workflow copy before storing or scheduling it. Do not commit private or signed media URLs.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hasna/loops",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.22",
|
|
4
4
|
"description": "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -110,7 +110,6 @@
|
|
|
110
110
|
"bun": ">=1.0.0"
|
|
111
111
|
},
|
|
112
112
|
"dependencies": {
|
|
113
|
-
"@hasna/contracts": "^0.4.1",
|
|
114
113
|
"@hasna/events": "^0.1.9",
|
|
115
114
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
116
115
|
"@openrouter/ai-sdk-provider": "2.9.1",
|
|
@@ -120,7 +119,8 @@
|
|
|
120
119
|
"zod": "4.4.3"
|
|
121
120
|
},
|
|
122
121
|
"optionalDependencies": {
|
|
123
|
-
"@hasna/machines": "0.0.49"
|
|
122
|
+
"@hasna/machines": "0.0.49",
|
|
123
|
+
"@hasna/contracts": "^0.4.2"
|
|
124
124
|
},
|
|
125
125
|
"devDependencies": {
|
|
126
126
|
"@types/bun": "latest",
|