@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.
Files changed (48) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +119 -29
  3. package/dist/api/index.d.ts +1 -0
  4. package/dist/api/index.js +825 -7
  5. package/dist/cli/index.js +5419 -2619
  6. package/dist/daemon/index.js +478 -84
  7. package/dist/generated/storage-kit/index.d.ts +1 -1
  8. package/dist/index.js +2521 -226
  9. package/dist/lib/cloud/mode.d.ts +17 -0
  10. package/dist/lib/cloud/resolve.d.ts +16 -0
  11. package/dist/lib/cloud/storage.d.ts +82 -0
  12. package/dist/lib/cloud/transport.d.ts +148 -0
  13. package/dist/lib/format.d.ts +2 -1
  14. package/dist/lib/migration.d.ts +40 -0
  15. package/dist/lib/mode.js +3 -3
  16. package/dist/lib/route/index.d.ts +2 -0
  17. package/dist/lib/route/policies.d.ts +51 -0
  18. package/dist/lib/route/provider-admission.d.ts +37 -0
  19. package/dist/lib/route/throttle.d.ts +4 -0
  20. package/dist/lib/route/types.d.ts +8 -0
  21. package/dist/lib/run-receipts.d.ts +14 -0
  22. package/dist/lib/storage/contract.d.ts +7 -1
  23. package/dist/lib/storage/index.js +710 -31
  24. package/dist/lib/storage/postgres-loop-storage.d.ts +6 -0
  25. package/dist/lib/storage/postgres-schema.js +29 -0
  26. package/dist/lib/storage/postgres.js +29 -0
  27. package/dist/lib/storage/sqlite.d.ts +6 -0
  28. package/dist/lib/storage/sqlite.js +412 -18
  29. package/dist/lib/store/index.d.ts +268 -0
  30. package/dist/lib/store.d.ts +48 -1
  31. package/dist/lib/store.js +396 -18
  32. package/dist/lib/template-kit.d.ts +25 -0
  33. package/dist/lib/templates.d.ts +16 -1
  34. package/dist/mcp/http.d.ts +16 -0
  35. package/dist/mcp/index.js +1658 -168
  36. package/dist/runner/index.js +76 -9
  37. package/dist/sdk/http.d.ts +67 -0
  38. package/dist/sdk/http.js +21 -0
  39. package/dist/sdk/index.d.ts +50 -17
  40. package/dist/sdk/index.js +1509 -132
  41. package/dist/serve/index.js +1598 -122
  42. package/dist/types.d.ts +49 -0
  43. package/docs/AUTOMATION_RUNTIME_DESIGN.md +442 -1
  44. package/docs/CUTOVER-RUNBOOK.md +73 -56
  45. package/docs/DEPLOYMENT_MODES.md +35 -18
  46. package/docs/RUNTIME_BOUNDARY.md +203 -0
  47. package/docs/USAGE.md +145 -27
  48. package/package.json +3 -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.
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 future non-local control
19
- planes:
18
+ defines `self_hosted` and `cloud` contracts for non-local control planes:
20
19
 
21
- - `self_hosted`: user-operated `loops-api` control-plane contract; this
22
- release exposes storage-backed API/runner foundations plus local migration
23
- previews.
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`) and reports `applySupported=false` because this
33
- public package does not directly mutate remote Postgres, S3/object storage, AWS
34
- resources, or hosted credentials. Route admission remains bounded by
35
- `max_dispatch`, `max_active`, `max_active_per_project`,
36
- `max_active_per_project_group`, `max_active_scope`, and `max_per_profile`.
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-runner`, and
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, the same
583
- git common directory as the source repo, and the expected generated branch. A
584
- detached HEAD or unexpected branch fails closed with evidence
585
- (`worktreeMode=required`) instead of silently running a mutating workflow in
586
- the wrong state; `worktreeMode=auto` falls back to the original checkout and
587
- records the fallback.
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 write compact evidence into a
811
- bounded reports directory so operators can audit each drain without unbounded
812
- stdout or loop history growth. Keep `--scan-limit` large enough for the current
813
- ready-task backlog; if the scan is exhausted before matching tasks appear, the
814
- drain will correctly do no work.
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.14",
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",