@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/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,52 @@ documented in this file. Version entries are generated from the
|
|
|
5
5
|
conventional-commit git history; one commit maps to one released patch version
|
|
6
6
|
unless noted.
|
|
7
7
|
|
|
8
|
+
## Unreleased
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- `loops-mcp` now serves a shared Streamable HTTP transport (default
|
|
13
|
+
`http://127.0.0.1:8890/mcp`, `GET /health`) in addition to stdio. Running
|
|
14
|
+
it as one long-lived daemon with the routing env baked in (e.g.
|
|
15
|
+
`HASNA_LOOPS_API_URL` + `HASNA_LOOPS_API_KEY`) makes every connecting agent
|
|
16
|
+
route CRUD deterministically to the same backend regardless of the caller's
|
|
17
|
+
own shell environment (including non-login SSH). Select the transport with
|
|
18
|
+
`--http`/`MCP_HTTP=1` (default) or `--stdio`/`MCP_STDIO=1`; the port is set
|
|
19
|
+
via `--port` or `MCP_HTTP_PORT`.
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- Documentation now names `loops-serve` as the Postgres-backed Hasna-owned
|
|
24
|
+
self-hosted control-plane host, keeps `loops-api` as the shared embeddable API
|
|
25
|
+
contract, and reserves `cloud` wording for the hosted SaaS contract rather
|
|
26
|
+
than the Hasna-owned self-hosted deployment.
|
|
27
|
+
- The cutover and migration docs now match the 0.4.14 self-hosted backend:
|
|
28
|
+
`PostgresLoopStorage`, API-key auth, HTTP SDK, ARM64 deploy artifacts, and
|
|
29
|
+
`loops-serve migrate` are shipped; long-running runner daemon mode, workflow
|
|
30
|
+
execution over the runner protocol, and id-preserving remote import remain
|
|
31
|
+
follow-up work.
|
|
32
|
+
|
|
33
|
+
## 0.4.14 (2026-07-06)
|
|
34
|
+
|
|
35
|
+
Self-hosted control-plane service brought to the full Hasna standard: all four
|
|
36
|
+
surfaces (CLI, MCP, serve, SDK) are real over the Postgres backend, with
|
|
37
|
+
internet-facing API-key auth and a deployable ARM64 image.
|
|
38
|
+
|
|
39
|
+
### Added
|
|
40
|
+
|
|
41
|
+
- **`loops-serve` HTTP control plane:** RDS-direct (Amendment A1) Postgres
|
|
42
|
+
storage wired into the serve; public `GET /health`, `/ready` (storage
|
|
43
|
+
reachable + fully migrated), `/version` (all `{status, version, mode}`) and
|
|
44
|
+
`/openapi.json`; the versioned `/v1` loops + runs API is gated behind
|
|
45
|
+
`@hasna/contracts` API-key auth (`verifyApiKey`, strict revocation via the
|
|
46
|
+
shared `api_keys` table). `loops-serve migrate` applies the ledger-tracked
|
|
47
|
+
schema + api_keys table.
|
|
48
|
+
- **Generated HTTP SDK:** `@hasna/loops/sdk/http` exports a typed dependency-free
|
|
49
|
+
`LoopsClient` generated from `openapi/loops.json` (the serve contract).
|
|
50
|
+
- **Deploy artifacts:** ARM64/bun `Dockerfile` (Amazon RDS CA baked for
|
|
51
|
+
verify-full TLS), `docker-compose.yml`, `hasna.contract.json` service
|
|
52
|
+
manifest, and a `migrations/` mirror of the ledger migrations.
|
|
53
|
+
|
|
8
54
|
## 0.4.13 (2026-07-05)
|
|
9
55
|
|
|
10
56
|
`--pr-handoff` workflows whose worker pushes its own branch and opens the PR
|
|
@@ -477,13 +523,63 @@ CLI/MCP/SDK surface with deprecation aliases.
|
|
|
477
523
|
npm installs), and `@hasna/machines` is now an optional dependency —
|
|
478
524
|
installs without it simply disable remote-machine assignment.
|
|
479
525
|
|
|
526
|
+
## 0.3.60 (2026-07-01)
|
|
527
|
+
|
|
528
|
+
Experimental Codewith durable-agent controller for long workflow steps.
|
|
529
|
+
|
|
530
|
+
### Fixed
|
|
531
|
+
|
|
532
|
+
- **Codewith executor:** run Codewith agent steps through the durable
|
|
533
|
+
`codewith agent start` background-agent lifecycle with rollout progress
|
|
534
|
+
recording into workflow step runs.
|
|
535
|
+
|
|
536
|
+
Superseded in **0.4.9**: task-lifecycle and route workers now dispatch Codewith
|
|
537
|
+
via non-interactive `codewith exec --json` because `agent start` reloaded
|
|
538
|
+
multi-megabyte rollout history every turn and stalled workers with silent
|
|
539
|
+
`context_length_exceeded` completions.
|
|
540
|
+
|
|
541
|
+
## 0.3.59 (2026-07-01)
|
|
542
|
+
|
|
543
|
+
Harden append-only workflow goal-wrapper migration.
|
|
544
|
+
|
|
545
|
+
### Fixed
|
|
546
|
+
|
|
547
|
+
- **`migrate-goal-wrappers`:** dry-run and apply paths now use compact migration
|
|
548
|
+
summaries, block retarget while a loop run is active, and route failures
|
|
549
|
+
through `cloneWorkflowWithoutGoalAndRetargetLoop` so only loops with both a
|
|
550
|
+
loop-level goal and a workflow-level top-level goal migrate.
|
|
551
|
+
- **Store:** `cloneWorkflowWithoutGoalAndRetargetLoop` inserts a goal-free
|
|
552
|
+
workflow spec, retargets the loop, and optionally archives the old spec when
|
|
553
|
+
unreferenced — matching the append-only semantics of
|
|
554
|
+
`migrate-agent-timeouts`.
|
|
555
|
+
|
|
556
|
+
## 0.3.58 (2026-07-01)
|
|
557
|
+
|
|
558
|
+
Break nested workflow goal deadlocks and add a migration path.
|
|
559
|
+
|
|
560
|
+
### Fixed
|
|
561
|
+
|
|
562
|
+
- **Workflow loops — nested top-level goals:** a loop-level goal wrapping a
|
|
563
|
+
workflow that also defined a top-level `"goal"` deadlocked because each layer
|
|
564
|
+
waited on the other. New workflow loops that combine both wrappers are
|
|
565
|
+
rejected at creation; retargeting onto a dual-goal workflow is blocked. When
|
|
566
|
+
only a legacy dual-wrapper loop remains, the runner strips the workflow goal
|
|
567
|
+
for execution so the loop-level goal can drive orchestration.
|
|
568
|
+
- **Workflow runner:** loop-level goals on workflow loops execute the underlying
|
|
569
|
+
workflow with the workflow-level goal removed when both were present.
|
|
570
|
+
|
|
571
|
+
### Added
|
|
572
|
+
|
|
573
|
+
- **`loops workflows migrate-goal-wrappers`:** append-only migrator that clones
|
|
574
|
+
a goal-free workflow spec and retargets eligible non-running workflow loops
|
|
575
|
+
that still carry redundant workflow-level goal wrappers alongside a loop-level
|
|
576
|
+
goal. Supports `--loop`, `--apply`, and `--archive-old` like
|
|
577
|
+
`migrate-agent-timeouts`.
|
|
578
|
+
|
|
480
579
|
## 0.3.x
|
|
481
580
|
|
|
482
581
|
Compact history for the 0.3 line, newest first (`version (date) commit subject`).
|
|
483
582
|
|
|
484
|
-
- 0.3.60 (2026-07-01) fix: run Codewith loops as durable agents
|
|
485
|
-
- 0.3.59 (2026-07-01) fix: harden workflow goal migration
|
|
486
|
-
- 0.3.58 (2026-07-01) fix: avoid nested workflow goal deadlocks
|
|
487
583
|
- 0.3.57 (2026-07-01) feat: harden loop routing and add MCP server
|
|
488
584
|
- 0.3.56 (2026-06-30) fix: allow worktree agents to write git metadata
|
|
489
585
|
- 0.3.55 (2026-06-30) fix: agent workflow timeout policy
|
package/README.md
CHANGED
|
@@ -5,8 +5,14 @@ OpenLoops is a local CLI and daemon for persistent loops and workflows: schedule
|
|
|
5
5
|
Naming: the product is **OpenLoops**, published on npm as
|
|
6
6
|
[`@hasna/loops`](https://www.npmjs.com/package/@hasna/loops) and developed in the
|
|
7
7
|
[`hasna/loops`](https://github.com/hasna/loops) repository. The installed
|
|
8
|
-
binaries are `loops`, `loops-daemon`, `loops-api`, `loops-
|
|
9
|
-
`loops-mcp`.
|
|
8
|
+
binaries are `loops`, `loops-daemon`, `loops-api`, `loops-serve`,
|
|
9
|
+
`loops-runner`, and `loops-mcp`.
|
|
10
|
+
|
|
11
|
+
OpenLoops is the **runtime, scheduler, and workflow engine** for automations —
|
|
12
|
+
not the automation domain model. Specs, triggers, queue ownership, approvals,
|
|
13
|
+
DLQ/replay, and audit live in `@hasna/automations` and `@hasna/actions`; see
|
|
14
|
+
[Runtime Boundary](docs/RUNTIME_BOUNDARY.md) for ownership split and external
|
|
15
|
+
compiler handoff examples.
|
|
10
16
|
|
|
11
17
|
It supports deterministic command loops, JSON-defined workflows, and guarded CLI adapters for headless coding agents:
|
|
12
18
|
|
|
@@ -22,7 +28,14 @@ It supports deterministic command loops, JSON-defined workflows, and guarded CLI
|
|
|
22
28
|
OpenLoops has three deployment modes:
|
|
23
29
|
|
|
24
30
|
- `local`: SQLite in `LOOPS_DATA_DIR` is authoritative and `loops-daemon` executes scheduled work.
|
|
25
|
-
- `self_hosted`:
|
|
31
|
+
- `self_hosted`: the Hasna-owned AWS/RDS control-plane deployment, served by
|
|
32
|
+
`loops-serve` and backed by Postgres, with the embeddable `loops-api` contract
|
|
33
|
+
shared by serve, SDK, and tests. This release exposes status, storage-backed
|
|
34
|
+
`/v1` loop CRUD and run listing, runner claim/heartbeat/finalize protocol
|
|
35
|
+
endpoints, a one-shot `loops-runner run-once` execution path for embedded
|
|
36
|
+
control-plane hosts, id-preserving local export/import, and preview-only
|
|
37
|
+
self-hosted migration/sync commands; full fleet rollout and id-preserving
|
|
38
|
+
remote apply are follow-up work.
|
|
26
39
|
- `cloud`: a hosted control-plane contract. This release exposes client/runner status only; hosted tenant auth and infrastructure live outside this package.
|
|
27
40
|
|
|
28
41
|
`local` is the default and requires no network, token, Postgres, or hosted
|
|
@@ -42,25 +55,33 @@ Scheduler state is explicit in status JSON. `schedulerState.localStore` is
|
|
|
42
55
|
SQLite plus local run artifact files: authoritative in `local`, cache/spool in
|
|
43
56
|
non-local modes. `schedulerState.remoteStore` names the non-local contract
|
|
44
57
|
(`api_control_plane_contract`, `postgres_contract`, or
|
|
45
|
-
`hosted_control_plane_contract`)
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
58
|
+
`hosted_control_plane_contract`). The standalone `loops` CLI reports
|
|
59
|
+
`applySupported=false` for non-local apply because it does not perform
|
|
60
|
+
id-preserving remote migration, S3/object storage mutation, AWS resource
|
|
61
|
+
mutation, or hosted credential mutation. `loops-serve` mutates self-hosted
|
|
62
|
+
Postgres for normal control-plane CRUD and runner protocol routes when it is
|
|
63
|
+
explicitly configured. Route admission remains bounded by `max_dispatch`,
|
|
64
|
+
`max_active`, `max_active_per_project`, `max_active_per_project_group`,
|
|
65
|
+
`max_active_scope`, and `max_per_profile`.
|
|
50
66
|
|
|
51
|
-
Useful status commands:
|
|
67
|
+
Useful status and setup commands:
|
|
52
68
|
|
|
53
69
|
```bash
|
|
54
70
|
loops mode
|
|
55
71
|
loops --json mode
|
|
56
72
|
loops self-hosted status
|
|
57
73
|
loops self-hosted migrate --dry-run
|
|
74
|
+
loops self-hosted push --dry-run
|
|
75
|
+
loops self-hosted pull --dry-run
|
|
58
76
|
loops self-hosted runner-register --runner-id <id> --machine-id <machine> --apply
|
|
59
77
|
loops export --file ./loops-export.json --dry-run
|
|
60
78
|
loops export --file ./loops-export.json
|
|
61
79
|
loops import ./loops-export.json
|
|
80
|
+
loops import ./loops-export.json --apply
|
|
62
81
|
loops cloud status
|
|
63
82
|
loops-api status
|
|
83
|
+
loops-serve version
|
|
84
|
+
HASNA_LOOPS_DATABASE_URL=... loops-serve migrate --dry-run
|
|
64
85
|
loops-runner status
|
|
65
86
|
```
|
|
66
87
|
|
|
@@ -70,9 +91,10 @@ and machine-placement contract.
|
|
|
70
91
|
## Install
|
|
71
92
|
|
|
72
93
|
**OpenLoops requires the [Bun](https://bun.sh) runtime (`bun >= 1.0`).** The
|
|
73
|
-
`loops`, `loops-daemon`, `loops-api`, `loops-
|
|
74
|
-
run with a `#!/usr/bin/env bun` shebang, so Bun must be on
|
|
75
|
-
package is installed with npm. Node.js is not a supported
|
|
94
|
+
`loops`, `loops-daemon`, `loops-api`, `loops-serve`, `loops-runner`, and
|
|
95
|
+
`loops-mcp` binaries run with a `#!/usr/bin/env bun` shebang, so Bun must be on
|
|
96
|
+
`PATH` even when the package is installed with npm. Node.js is not a supported
|
|
97
|
+
runtime.
|
|
76
98
|
|
|
77
99
|
From npm (with Bun installed):
|
|
78
100
|
|
|
@@ -311,6 +333,19 @@ Workflow JSON can also embed goals at the workflow or step level:
|
|
|
311
333
|
}
|
|
312
334
|
```
|
|
313
335
|
|
|
336
|
+
For **workflow loops** (`loops create workflow ...`), prefer a single loop-level
|
|
337
|
+
`--goal` wrapper around the scheduled workflow. Do not also define a top-level
|
|
338
|
+
`"goal"` on the workflow JSON spec: nested loop-level and workflow-level goal
|
|
339
|
+
wrappers deadlocked execution because each layer waited on the other. OpenLoops
|
|
340
|
+
now rejects new workflow loops that combine both wrappers at creation time, and
|
|
341
|
+
blocks retargeting a loop-level goal loop onto a workflow that also carries a
|
|
342
|
+
top-level goal.
|
|
343
|
+
|
|
344
|
+
Step-level goals inside workflow JSON remain valid. When a loop-level goal wraps
|
|
345
|
+
a workflow that still carries a legacy top-level goal, the runner strips the
|
|
346
|
+
workflow goal for that execution only; migrate away from the redundant wrapper
|
|
347
|
+
with the append-only migrator (see **Workflows** below).
|
|
348
|
+
|
|
314
349
|
## Workflows
|
|
315
350
|
|
|
316
351
|
Create a workflow JSON file:
|
|
@@ -380,13 +415,20 @@ Use `shell: true` only when you intentionally want shell parsing:
|
|
|
380
415
|
## Templates And Task Events
|
|
381
416
|
|
|
382
417
|
Built-in templates turn common orchestration flows into reusable workflow JSON.
|
|
418
|
+
Todos-task routes are OpenLoops-native runtime admission — see
|
|
419
|
+
[Runtime Boundary](docs/RUNTIME_BOUNDARY.md) for how this differs from
|
|
420
|
+
OpenAutomations queue ownership.
|
|
383
421
|
`todos-task-worker-verifier` performs one todos task and then verifies it.
|
|
384
422
|
`event-worker-verifier` handles any Hasna event envelope and then verifies the
|
|
385
423
|
handling. `bounded-agent-worker-verifier` is for recurring bounded agent work:
|
|
386
424
|
one worker runs a narrow objective, then a fresh verifier audits the result.
|
|
387
425
|
The catalog also includes `task-lifecycle`, `pr-review`, `scheduled-audit`,
|
|
388
426
|
`knowledge-refresh`, `report-only`, `incident-response`, and
|
|
389
|
-
`deterministic-check-create-task` for common operator workflows.
|
|
427
|
+
`deterministic-check-create-task` for common operator workflows. Use
|
|
428
|
+
`routing-remediation` for bounded routing-doctor repair runs: it dry-runs by
|
|
429
|
+
default, gates `safe_auto` capacity, applies only supported
|
|
430
|
+
`todos doctor routing --apply` repairs when explicitly enabled, and files
|
|
431
|
+
blocker tasks for human/cross-repo/unsupported findings.
|
|
390
432
|
|
|
391
433
|
```bash
|
|
392
434
|
loops templates list
|
|
@@ -421,6 +463,12 @@ loops templates render pr-review \
|
|
|
421
463
|
loops templates render deterministic-check-create-task \
|
|
422
464
|
--var projectPath=/path/to/repo \
|
|
423
465
|
--var checkCommand='your deterministic check and todos upsert command'
|
|
466
|
+
loops templates render routing-remediation \
|
|
467
|
+
--var projectPath=/path/to/repo \
|
|
468
|
+
--var todosProjectPath=/path/to/todos-project \
|
|
469
|
+
--var shard=0/6 \
|
|
470
|
+
--var maxRepairs=25 \
|
|
471
|
+
--var idempotencyKey=routing-health:repo:shard0
|
|
424
472
|
```
|
|
425
473
|
|
|
426
474
|
Custom reusable workflow templates live under the OpenLoops app data directory:
|
|
@@ -440,22 +488,38 @@ worker finishes; pass `--verifier-idle-timeout none` or template variable
|
|
|
440
488
|
`verifierIdleTimeoutMs=none` only when another heartbeat is guaranteed. Use a
|
|
441
489
|
positive numeric `timeoutMs` only when an agentic step is intentionally bounded.
|
|
442
490
|
|
|
443
|
-
To migrate existing
|
|
444
|
-
|
|
445
|
-
|
|
491
|
+
To migrate existing agentic loops, use the timeout migrator instead of editing
|
|
492
|
+
the database directly. Workflow loops are migrated append-only because
|
|
493
|
+
historical workflow runs must keep pointing at their original spec; direct
|
|
494
|
+
agent loops selected with `--loop` update their stored target in place for
|
|
495
|
+
future executions:
|
|
446
496
|
|
|
447
497
|
```bash
|
|
448
498
|
loops workflows migrate-agent-timeouts --loop <loop-id-or-name>
|
|
449
499
|
loops workflows migrate-agent-timeouts --loop <loop-id-or-name> --apply
|
|
500
|
+
loops workflows migrate-goal-wrappers --loop <loop-id-or-name>
|
|
501
|
+
loops workflows migrate-goal-wrappers --loop <loop-id-or-name> --apply
|
|
502
|
+
loops workflows migrate-goal-wrappers --loop <loop-id-or-name> --apply --archive-old
|
|
450
503
|
```
|
|
451
504
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
505
|
+
Both migrators dry-run by default. For eligible non-running workflow loops,
|
|
506
|
+
`--apply` creates a new workflow spec and retargets only future executions;
|
|
507
|
+
historical workflow runs keep pointing at their original spec. For direct agent
|
|
508
|
+
loops selected with `--loop`, `migrate-agent-timeouts --apply` updates the
|
|
509
|
+
stored target in place for future executions. Use `--archive-old` to archive
|
|
510
|
+
superseded workflow specs when no active loops still reference them.
|
|
511
|
+
|
|
512
|
+
`migrate-agent-timeouts` clones the workflow with the requested agent timeout
|
|
513
|
+
policy (`--timeout none` by default). `migrate-goal-wrappers` targets loops that
|
|
514
|
+
define both a loop-level goal and a redundant workflow-level top-level goal: it
|
|
515
|
+
clones a goal-free workflow spec, retargets the loop, and leaves the loop-level
|
|
516
|
+
goal as the sole orchestration wrapper. Loops with only a workflow-level goal or
|
|
517
|
+
only a loop-level goal are skipped.
|
|
518
|
+
|
|
519
|
+
Use `loops workflows recover` only for interrupted `running` workflow runs
|
|
520
|
+
whose recorded child process is gone; terminal `timed_out` runs must be
|
|
521
|
+
requeued with `loops routes requeue <work-item-id> --reason "<cause fixed>"`
|
|
522
|
+
before re-delivering or draining the original task/event route.
|
|
459
523
|
|
|
460
524
|
```json
|
|
461
525
|
{
|
|
@@ -540,6 +604,9 @@ redispatch cap has not been reached. Operators can still force a retry with
|
|
|
540
604
|
`loops routes requeue <work-item-id> --reason "<cause fixed>"`. The next
|
|
541
605
|
route-created output records `requeue` evidence with the previous work item id,
|
|
542
606
|
previous attempts, reason, new attempt, workflow id, and loop id.
|
|
607
|
+
Route work items are the durable reservation ledger: they include the stable
|
|
608
|
+
idempotency key, task/event references, project/group keys, admitting machine
|
|
609
|
+
ID, route scope, workflow/loop IDs, and the current terminal or active status.
|
|
543
610
|
|
|
544
611
|
Task route drains can select providers from task metadata instead of running one
|
|
545
612
|
fixed provider/account pool for the whole drain. Add one or more
|
|
@@ -549,13 +616,20 @@ matching rule wins. Rule profiles become a Codewith auth-profile pool for
|
|
|
549
616
|
can also carry `provider_hint`/`route_provider`, `auth_profile_pool`, or
|
|
550
617
|
`account_pool` metadata. Dry-run, drain evidence, and route invocation scope
|
|
551
618
|
include `providerRouting` so operators can see why a provider/account was
|
|
552
|
-
selected.
|
|
619
|
+
selected. Selector values may contain `:`; the parser treats the colon before a
|
|
620
|
+
supported provider id as the route delimiter, so exact tag selectors such as
|
|
621
|
+
`tags=area:frontend:claude:account003,account015` and
|
|
622
|
+
`tags=provider:claude-code:claude:account003,account015` match literal task
|
|
623
|
+
tags `area:frontend` and `provider:claude-code`.
|
|
553
624
|
|
|
554
625
|
```bash
|
|
555
626
|
loops routes drain todos-task \
|
|
556
627
|
--dry-run \
|
|
628
|
+
--provider-rule tags=area:frontend:claude:account003,account015 \
|
|
629
|
+
--provider-rule tags=provider:claude-code:claude:account003,account015 \
|
|
557
630
|
--provider-rule area=frontend:claude:claude-ui-a,claude-ui-b \
|
|
558
631
|
--provider-rule area=backend:codewith:account001,account002 \
|
|
632
|
+
--provider-rule tags=task-lifecycle:codewith:account001,account002 \
|
|
559
633
|
--worktree-mode required
|
|
560
634
|
```
|
|
561
635
|
|
|
@@ -634,6 +708,10 @@ directory-scoped write controls.
|
|
|
634
708
|
Inspect route state with:
|
|
635
709
|
|
|
636
710
|
```bash
|
|
711
|
+
loops routes policies list
|
|
712
|
+
loops routes policies show oss
|
|
713
|
+
loops routes policies render oss
|
|
714
|
+
loops routes policies validate
|
|
637
715
|
cat task-created-event.json | loops routes preview todos-task --sandbox workspace-write
|
|
638
716
|
cat task-created-event.json | loops routes create todos-task --sandbox workspace-write
|
|
639
717
|
loops routes drain todos-task --task-list oss --max-dispatch 2 --compact
|
|
@@ -644,6 +722,22 @@ loops routes requeue <work-item-id> --reason "fixed upstream blocker"
|
|
|
644
722
|
loops routes invocations
|
|
645
723
|
```
|
|
646
724
|
|
|
725
|
+
Named route policies let operators create and inspect recurring task-drain
|
|
726
|
+
routes without copying the full live command. Built-in policies are
|
|
727
|
+
`repoops-pr-queue`, `oss`, `pilot`, and `machine-sync`:
|
|
728
|
+
|
|
729
|
+
```bash
|
|
730
|
+
loops routes policies render oss
|
|
731
|
+
loops routes schedule todos-task machine-oss-task-lifecycle-router --policy oss
|
|
732
|
+
```
|
|
733
|
+
|
|
734
|
+
Scheduling with a policy stores explicit route drain args plus
|
|
735
|
+
`--route-policy-evidence <id>` instead of replaying `--policy`, so audits can see
|
|
736
|
+
the exact task list, filters, account pool, throttles, worktree mode, evidence
|
|
737
|
+
directory, and name prefix used for future runs. The `pilot` policy is a manual
|
|
738
|
+
break-glass lane using `danger-full-access`; applying it requires an explicit
|
|
739
|
+
`--manual-break-glass`.
|
|
740
|
+
|
|
647
741
|
For OSS task-created routing, use a deterministic drain instead of tmux
|
|
648
742
|
dispatch:
|
|
649
743
|
|
|
@@ -663,21 +757,45 @@ loops routes schedule todos-task oss-task-route-drain \
|
|
|
663
757
|
--max-active-per-project 1 \
|
|
664
758
|
--max-active-per-project-group 4 \
|
|
665
759
|
--max-active 12 \
|
|
760
|
+
--provider-active-cap 6 \
|
|
761
|
+
--provider-admission-check \
|
|
666
762
|
--max-per-profile 2 \
|
|
763
|
+
--launch-gate "pa19-controlled-launch" \
|
|
764
|
+
--launch-gate-blocker "$HOME/workspace/example/opensource/open-codewith::2d9d931b" \
|
|
765
|
+
--launch-gate-blocker "$HOME/workspace/example/opensource/open-loops::816e99db" \
|
|
667
766
|
--worktree-mode required \
|
|
668
767
|
--evidence-dir "$HOME/.hasna/loops/reports/oss-task-route-drain" \
|
|
669
768
|
--compact
|
|
670
769
|
```
|
|
671
770
|
|
|
771
|
+
Use `--launch-gate-blocker <todos-project-path>::<task-id>` for staged
|
|
772
|
+
launches where a scheduled drain must create zero worker loops until explicit
|
|
773
|
+
blocker tasks are completed. While any blocker is still pending, in progress,
|
|
774
|
+
failed, cancelled, missing, or unreadable, the drain fails closed before reading
|
|
775
|
+
the ready queue, writes launch-gate evidence, and leaves source tasks untouched.
|
|
776
|
+
Scheduled route drains preserve these gate flags in their stored argv.
|
|
777
|
+
|
|
672
778
|
`--max-active` counts active routed workflows **per route**. Scope precedence:
|
|
673
779
|
an explicit `--max-active-scope <key>` wins, else the running loop's
|
|
674
780
|
`LOOPS_LOOP_NAME`, else the route key — so each router's limit is its own
|
|
675
781
|
ceiling rather than a store-wide total shared by every router.
|
|
782
|
+
Compact drain output includes route reservation IDs, work-item status,
|
|
783
|
+
machine ID, workflow ID, loop ID, and route scope for each considered task.
|
|
676
784
|
`--max-active-per-project` and `--max-active-per-project-group` remain cross-route
|
|
677
785
|
anti-hog caps counted over all routes. Raise a router's `--max-active`
|
|
678
786
|
deliberately once counting is per-route; keep `--max-per-profile` set so the
|
|
679
787
|
extra concurrency spreads across subscription accounts.
|
|
680
788
|
|
|
789
|
+
`--max-active` and related route throttles count OpenLoops routed workflow work
|
|
790
|
+
items; they do not know whether Codewith is already at its live background-agent
|
|
791
|
+
admission limit. Add `--provider-active-cap <n>` (or the Codewith-specific alias
|
|
792
|
+
`--codewith-active-cap <n>`) to make the route run `codewith agent diagnostics
|
|
793
|
+
--json` before creating workflow loops and defer when `activeRunCount >= n`.
|
|
794
|
+
Add `--provider-admission-check` when drains should also fail closed on provider
|
|
795
|
+
diagnostics failure, unsupported providers, or Codewith reports with no
|
|
796
|
+
available admission slots. Backlog prioritizer and drain loops should use these
|
|
797
|
+
first-class flags instead of shell-wrapping a temporary diagnostics guard.
|
|
798
|
+
|
|
681
799
|
Only route tasks that explicitly opt in with `auto:route`, `route_enabled=true`,
|
|
682
800
|
or `automation.allowed=true`. Use Codewith account pools, required worktrees,
|
|
683
801
|
max-active throttles, and bounded evidence directories. Do not dispatch or paste
|
|
@@ -693,30 +811,25 @@ Workflow run manifests are written under
|
|
|
693
811
|
|
|
694
812
|
## OpenAutomations Runtime Binding
|
|
695
813
|
|
|
696
|
-
OpenLoops
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
make OpenAutomations the queue owner for todos task/PR/review agent workflows.
|
|
814
|
+
OpenLoops executes automations that external compilers have materialized; it
|
|
815
|
+
does not own the automation product surface. See
|
|
816
|
+
[Runtime Boundary](docs/RUNTIME_BOUNDARY.md) for the full ownership table,
|
|
817
|
+
three handoff paths (claim-queue, planned `@hasna/actions` upsert-one-shot, and
|
|
818
|
+
explicit event-envelope routing), todos-task route distinction, and
|
|
819
|
+
anti-patterns.
|
|
703
820
|
|
|
704
|
-
|
|
821
|
+
Quick reference:
|
|
705
822
|
|
|
706
823
|
```ts
|
|
707
824
|
import { openAutomationsRuntimeBinding } from "@hasna/loops";
|
|
708
|
-
|
|
709
825
|
const binding = openAutomationsRuntimeBinding();
|
|
710
|
-
|
|
711
|
-
|
|
826
|
+
// binding.handoff === "claim-queue"
|
|
827
|
+
// binding.eventHandoff.handlerCommand === "loops routes create generic"
|
|
712
828
|
```
|
|
713
829
|
|
|
714
|
-
The claim-queue handoff uses the OpenAutomations CLI or SDK:
|
|
715
|
-
|
|
716
830
|
```bash
|
|
717
831
|
automations queue claim --runner open-loops:<worker-id>
|
|
718
832
|
automations queue complete <action-id> --runner open-loops:<worker-id>
|
|
719
|
-
automations queue fail <action-id> --runner open-loops:<worker-id> --code <code> --message <message>
|
|
720
833
|
```
|
|
721
834
|
|
|
722
835
|
For explicit event workflow routing, OpenAutomations can export the normalized
|
|
@@ -753,6 +866,12 @@ required semantics (dry-run/preflight/commit modes, `idempotencyKey` +
|
|
|
753
866
|
are specified in `docs/AUTOMATION_RUNTIME_DESIGN.md`; this README intentionally
|
|
754
867
|
does not duplicate that spec.
|
|
755
868
|
|
|
869
|
+
That design doc also defines the planned `@hasna/actions` target binding. The
|
|
870
|
+
binding reuses `ActionManifest`, `ActionInvocation`, `ActionRunStatus`,
|
|
871
|
+
`ActionQueueStatus`, action-owned idempotency keys, action audit events, and
|
|
872
|
+
dead-letter/replay records. OpenLoops only admits and executes the handed-off
|
|
873
|
+
workflow, then returns workflow refs for action-owned evidence.
|
|
874
|
+
|
|
756
875
|
The same design doc covers the planned DLQ/dead-letter lifecycle, including
|
|
757
876
|
`loops dlq list/show/replay/resolve`, idempotent replay keys, and compatibility
|
|
758
877
|
rules for `@hasna/actions`.
|
|
@@ -796,6 +915,9 @@ Use `--json` for machine-readable output. Prompt bodies and run stdout/stderr ar
|
|
|
796
915
|
|
|
797
916
|
```bash
|
|
798
917
|
loops health --json
|
|
918
|
+
loops health scan --include active,paused --latest-run --doctor --daemon --json
|
|
919
|
+
loops health scan --include active,paused --latest-run --doctor --daemon \
|
|
920
|
+
--upsert-todos --dry-run --max-actions 5 --evidence-dir ~/.hasna/loops/reports/health-scan
|
|
799
921
|
loops expectations <loop-id-or-name> --json
|
|
800
922
|
loops health route-tasks --project ~/.hasna/loops --task-list loop-error-self-heal --max-actions 5
|
|
801
923
|
loops hygiene names --json
|
|
@@ -810,6 +932,16 @@ mutating path: it upserts deduped Todos tasks for failed expectations and marks
|
|
|
810
932
|
them with `no_tmux_dispatch=true` metadata. Use `--dry-run --json` before
|
|
811
933
|
turning it into a production loop.
|
|
812
934
|
|
|
935
|
+
`health scan` is the first-class replacement for local loop-error self-heal
|
|
936
|
+
scripts. It inventories active/paused loops by default, can include doctor,
|
|
937
|
+
daemon, preflight, latest-run, and stale-running findings, writes bounded
|
|
938
|
+
`summary.json` and `report.md` files under
|
|
939
|
+
`$LOOPS_DATA_DIR/reports/health-scan`, and prints compact human or stable JSON
|
|
940
|
+
output. It is read-only unless `--upsert-todos` is supplied. The only built-in
|
|
941
|
+
self-heal is `--start-daemon`, which attempts to start the daemon when status
|
|
942
|
+
proves it is not running; it never stops, resumes, deletes, archives, or reaps
|
|
943
|
+
loops.
|
|
944
|
+
|
|
813
945
|
Add `--evidence-dir <dir>` to `health route-tasks` or `hygiene route-tasks`
|
|
814
946
|
when the deterministic loop should write a durable JSON heartbeat/report in
|
|
815
947
|
addition to loop stdout. Add `--auto-route` only for task lists that are
|
|
@@ -922,4 +1054,4 @@ The adapters intentionally use provider command surfaces instead of pretending e
|
|
|
922
1054
|
- `--variant` is provider-specific reasoning/model effort. Claude maps it to `--effort`, Codewith/Codex map it to `model_reasoning_effort`, and OpenCode/AICopilot pass `--variant`.
|
|
923
1055
|
- Daemon and scheduled runs prepend common user executable directories such as `~/.local/bin` and `~/.bun/bin` before resolving provider CLIs.
|
|
924
1056
|
|
|
925
|
-
For production loops that can mutate repos, use disposable worktrees (`--worktree-mode required` / `worktreeMode=required`) and explicit prompts that name allowed write scope. Worktrees are executor-enforced: when a target carries worktree metadata, the executor prepares and enters the git worktree before spawning the child process, records the worktree it entered, and with `mode=required` fails the run closed instead of falling back to the original checkout when preparation fails. Remote machine runs with a required worktree
|
|
1057
|
+
For production loops that can mutate repos, use disposable worktrees (`--worktree-mode required` / `worktreeMode=required`) and explicit prompts that name allowed write scope. Worktrees are executor-enforced: when a target carries worktree metadata, the executor prepares and enters the git worktree before spawning the child process, records the worktree it entered, and with `mode=required` fails the run closed instead of falling back to the original checkout when preparation fails. Existing managed worktrees are reused only after top-level and git-common-dir checks; a clean detached or stale-branch checkout may be reattached to the expected generated branch, while dirty or unsafe mismatches fail with cleanup evidence. Remote machine runs with a required worktree apply the same checks on the remote side before the target executes.
|
package/dist/api/index.d.ts
CHANGED
|
@@ -1,17 +1,53 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import type { LoopStorageContract } from "../lib/storage/contract.js";
|
|
3
|
+
/** The serve OpenAPI document (source of the generated SDK), version-synced. */
|
|
4
|
+
export declare function openApiDocument(): Record<string, unknown>;
|
|
3
5
|
export declare function apiStatus(): {
|
|
4
6
|
ok: boolean;
|
|
5
7
|
service: string;
|
|
6
8
|
status: import("../index.js").LoopDeploymentStatus;
|
|
7
9
|
};
|
|
10
|
+
/**
|
|
11
|
+
* Framework-agnostic API-key verifier shape (matches
|
|
12
|
+
* `@hasna/contracts/auth` `ApiKeyVerifier`). Kept structural so the api module
|
|
13
|
+
* has no hard dependency on the auth package: the serve entry injects the real
|
|
14
|
+
* verifier built from the vendored kit's client + the HMAC signing secret.
|
|
15
|
+
*/
|
|
16
|
+
export interface ApiAuthenticator {
|
|
17
|
+
authenticate(headers: Headers, context?: {
|
|
18
|
+
method?: string | null;
|
|
19
|
+
path?: string | null;
|
|
20
|
+
requiredScopes?: readonly string[];
|
|
21
|
+
}): Promise<{
|
|
22
|
+
ok: boolean;
|
|
23
|
+
status: number;
|
|
24
|
+
reason?: string;
|
|
25
|
+
message?: string;
|
|
26
|
+
}>;
|
|
27
|
+
}
|
|
8
28
|
export interface LoopsApiServerOptions {
|
|
9
29
|
host?: string;
|
|
10
30
|
port?: number;
|
|
11
31
|
storage?: LoopStorageContract;
|
|
12
32
|
bodyLimitBytes?: number;
|
|
13
33
|
evidenceLimitBytes?: number;
|
|
34
|
+
importLimitBytes?: number;
|
|
14
35
|
now?: () => Date;
|
|
36
|
+
/**
|
|
37
|
+
* API-key verifier (from `@hasna/contracts/auth`). When present, every
|
|
38
|
+
* request outside the open foundation probes (`/health`, `/ready`,
|
|
39
|
+
* `/version`, `/status`) must present a valid `loops:*` scoped key. This is
|
|
40
|
+
* the internet-facing auth path (no bearer token, no loopback bypass).
|
|
41
|
+
*/
|
|
42
|
+
authenticator?: ApiAuthenticator;
|
|
43
|
+
/**
|
|
44
|
+
* Readiness probe. Should prove the storage backend is reachable AND fully
|
|
45
|
+
* migrated. Returns `{ ready, detail? }`. Defaults to a storage list probe.
|
|
46
|
+
*/
|
|
47
|
+
readyCheck?: () => Promise<{
|
|
48
|
+
ready: boolean;
|
|
49
|
+
detail?: string;
|
|
50
|
+
}>;
|
|
15
51
|
}
|
|
16
52
|
export declare function createLoopsApiServer(opts?: LoopsApiServerOptions): Bun.Server<undefined>;
|
|
17
53
|
export declare function main(argv?: string[]): Promise<void>;
|