@hasna/loops 0.4.13 → 0.4.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/CHANGELOG.md +99 -3
  2. package/README.md +171 -39
  3. package/dist/api/index.d.ts +36 -0
  4. package/dist/api/index.js +1518 -15
  5. package/dist/cli/index.js +7280 -3213
  6. package/dist/cli/ui.d.ts +44 -0
  7. package/dist/daemon/index.js +1433 -303
  8. package/dist/generated/storage-kit/health.d.ts +19 -0
  9. package/dist/generated/storage-kit/index.d.ts +7 -0
  10. package/dist/generated/storage-kit/migrations.d.ts +47 -0
  11. package/dist/generated/storage-kit/mode.d.ts +47 -0
  12. package/dist/generated/storage-kit/pool.d.ts +33 -0
  13. package/dist/generated/storage-kit/query.d.ts +35 -0
  14. package/dist/generated/storage-kit/tls.d.ts +25 -0
  15. package/dist/index.d.ts +2 -2
  16. package/dist/index.js +8689 -4837
  17. package/dist/lib/cloud/mode.d.ts +17 -0
  18. package/dist/lib/cloud/resolve.d.ts +16 -0
  19. package/dist/lib/cloud/storage.d.ts +82 -0
  20. package/dist/lib/cloud/transport.d.ts +148 -0
  21. package/dist/lib/format.d.ts +2 -1
  22. package/dist/lib/health.d.ts +83 -3
  23. package/dist/lib/migration.d.ts +40 -0
  24. package/dist/lib/mode.js +15 -3
  25. package/dist/lib/route/index.d.ts +2 -0
  26. package/dist/lib/route/policies.d.ts +51 -0
  27. package/dist/lib/route/provider-admission.d.ts +37 -0
  28. package/dist/lib/route/throttle.d.ts +4 -0
  29. package/dist/lib/route/types.d.ts +8 -0
  30. package/dist/lib/run-receipts.d.ts +14 -0
  31. package/dist/lib/scheduler.d.ts +5 -2
  32. package/dist/lib/storage/contract.d.ts +7 -1
  33. package/dist/lib/storage/index.d.ts +1 -0
  34. package/dist/lib/storage/index.js +2028 -231
  35. package/dist/lib/storage/pg-executor.d.ts +27 -0
  36. package/dist/lib/storage/pg-runner-claim.d.ts +40 -0
  37. package/dist/lib/storage/postgres-loop-storage.d.ts +120 -0
  38. package/dist/lib/storage/postgres-schema.js +29 -0
  39. package/dist/lib/storage/postgres.js +29 -0
  40. package/dist/lib/storage/sqlite.d.ts +6 -0
  41. package/dist/lib/storage/sqlite.js +748 -26
  42. package/dist/lib/store/index.d.ts +268 -0
  43. package/dist/lib/store.d.ts +282 -1
  44. package/dist/lib/store.js +746 -26
  45. package/dist/lib/template-kit.d.ts +25 -0
  46. package/dist/lib/templates.d.ts +16 -1
  47. package/dist/mcp/http.d.ts +16 -0
  48. package/dist/mcp/index.js +2885 -634
  49. package/dist/runner/index.js +198 -32
  50. package/dist/sdk/http.d.ts +182 -0
  51. package/dist/sdk/http.js +166 -0
  52. package/dist/sdk/index.d.ts +55 -18
  53. package/dist/sdk/index.js +2734 -617
  54. package/dist/serve/index.d.ts +4 -0
  55. package/dist/serve/index.js +9095 -0
  56. package/dist/types.d.ts +52 -0
  57. package/docs/AUTOMATION_RUNTIME_DESIGN.md +442 -1
  58. package/docs/CUTOVER-RUNBOOK.md +78 -0
  59. package/docs/DEPLOYMENT_MODES.md +35 -18
  60. package/docs/RUNTIME_BOUNDARY.md +203 -0
  61. package/docs/USAGE.md +195 -38
  62. package/package.json +15 -3
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-runner`, and
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`: a user-operated `loops-api` control plane contract. This release exposes status, storage-backed `/v1` loop CRUD and run listing, runner claim/heartbeat/finalize protocol endpoints, a one-shot `loops-runner run-once` execution path for embedded control-plane hosts, id-preserving local export/import, and preview-only self-hosted migration/sync commands; full fleet rollout and remote apply are follow-up work.
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`) and reports `applySupported=false` because this
46
- public package does not directly mutate remote Postgres, S3/object storage, AWS
47
- resources, or hosted credentials. Route admission remains bounded by
48
- `max_dispatch`, `max_active`, `max_active_per_project`,
49
- `max_active_per_project_group`, `max_active_scope`, and `max_per_profile`.
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-runner`, and `loops-mcp` binaries
74
- run with a `#!/usr/bin/env bun` shebang, so Bun must be on `PATH` even when the
75
- package is installed with npm. Node.js is not a supported runtime.
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 workflow loops, do not edit `workflow_specs.steps_json`
444
- directly because historical workflow runs must keep pointing at their original
445
- spec. Use the append-only migrator:
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
- The command dry-runs by default. With `--apply`, it creates a new workflow spec
453
- with the requested agent timeout policy, retargets only future executions of
454
- eligible non-running workflow loops, and leaves terminal timed-out workflow runs
455
- as audit history. Use `loops workflows recover` only for interrupted `running`
456
- workflow runs whose recorded child process is gone; terminal `timed_out` runs
457
- must be requeued with `loops routes requeue <work-item-id> --reason "<cause fixed>"` before
458
- re-delivering or draining the original task/event route.
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 can be used as an execution runtime for deterministic OpenAutomations
697
- product automations, but it does not own the automation product surface.
698
- `@hasna/automations` owns automation specs, trigger materialization, product
699
- automation action queues, DLQ/replay, idempotency, approvals, and audit
700
- evidence. OpenLoops owns agent workflow invocation/admission/runs. OpenLoops
701
- only executes work that OpenAutomations has explicitly handed off; it does not
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
- The SDK exposes the boundary descriptor:
821
+ Quick reference:
705
822
 
706
823
  ```ts
707
824
  import { openAutomationsRuntimeBinding } from "@hasna/loops";
708
-
709
825
  const binding = openAutomationsRuntimeBinding();
710
- console.log(binding.handoff); // "claim-queue"
711
- console.log(binding.eventHandoff.handlerCommand); // "loops routes create generic"
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 are verified fail-closed on the remote side before the target executes.
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.
@@ -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>;