@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
package/CHANGELOG.md CHANGED
@@ -5,6 +5,31 @@ 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
+
8
33
  ## 0.4.14 (2026-07-06)
9
34
 
10
35
  Self-hosted control-plane service brought to the full Hasna standard: all four
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
 
@@ -393,13 +415,20 @@ Use `shell: true` only when you intentionally want shell parsing:
393
415
  ## Templates And Task Events
394
416
 
395
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.
396
421
  `todos-task-worker-verifier` performs one todos task and then verifies it.
397
422
  `event-worker-verifier` handles any Hasna event envelope and then verifies the
398
423
  handling. `bounded-agent-worker-verifier` is for recurring bounded agent work:
399
424
  one worker runs a narrow objective, then a fresh verifier audits the result.
400
425
  The catalog also includes `task-lifecycle`, `pr-review`, `scheduled-audit`,
401
426
  `knowledge-refresh`, `report-only`, `incident-response`, and
402
- `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.
403
432
 
404
433
  ```bash
405
434
  loops templates list
@@ -434,6 +463,12 @@ loops templates render pr-review \
434
463
  loops templates render deterministic-check-create-task \
435
464
  --var projectPath=/path/to/repo \
436
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
437
472
  ```
438
473
 
439
474
  Custom reusable workflow templates live under the OpenLoops app data directory:
@@ -569,6 +604,9 @@ redispatch cap has not been reached. Operators can still force a retry with
569
604
  `loops routes requeue <work-item-id> --reason "<cause fixed>"`. The next
570
605
  route-created output records `requeue` evidence with the previous work item id,
571
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.
572
610
 
573
611
  Task route drains can select providers from task metadata instead of running one
574
612
  fixed provider/account pool for the whole drain. Add one or more
@@ -578,13 +616,20 @@ matching rule wins. Rule profiles become a Codewith auth-profile pool for
578
616
  can also carry `provider_hint`/`route_provider`, `auth_profile_pool`, or
579
617
  `account_pool` metadata. Dry-run, drain evidence, and route invocation scope
580
618
  include `providerRouting` so operators can see why a provider/account was
581
- 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`.
582
624
 
583
625
  ```bash
584
626
  loops routes drain todos-task \
585
627
  --dry-run \
628
+ --provider-rule tags=area:frontend:claude:account003,account015 \
629
+ --provider-rule tags=provider:claude-code:claude:account003,account015 \
586
630
  --provider-rule area=frontend:claude:claude-ui-a,claude-ui-b \
587
631
  --provider-rule area=backend:codewith:account001,account002 \
632
+ --provider-rule tags=task-lifecycle:codewith:account001,account002 \
588
633
  --worktree-mode required
589
634
  ```
590
635
 
@@ -663,6 +708,10 @@ directory-scoped write controls.
663
708
  Inspect route state with:
664
709
 
665
710
  ```bash
711
+ loops routes policies list
712
+ loops routes policies show oss
713
+ loops routes policies render oss
714
+ loops routes policies validate
666
715
  cat task-created-event.json | loops routes preview todos-task --sandbox workspace-write
667
716
  cat task-created-event.json | loops routes create todos-task --sandbox workspace-write
668
717
  loops routes drain todos-task --task-list oss --max-dispatch 2 --compact
@@ -673,6 +722,22 @@ loops routes requeue <work-item-id> --reason "fixed upstream blocker"
673
722
  loops routes invocations
674
723
  ```
675
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
+
676
741
  For OSS task-created routing, use a deterministic drain instead of tmux
677
742
  dispatch:
678
743
 
@@ -692,21 +757,45 @@ loops routes schedule todos-task oss-task-route-drain \
692
757
  --max-active-per-project 1 \
693
758
  --max-active-per-project-group 4 \
694
759
  --max-active 12 \
760
+ --provider-active-cap 6 \
761
+ --provider-admission-check \
695
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" \
696
766
  --worktree-mode required \
697
767
  --evidence-dir "$HOME/.hasna/loops/reports/oss-task-route-drain" \
698
768
  --compact
699
769
  ```
700
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
+
701
778
  `--max-active` counts active routed workflows **per route**. Scope precedence:
702
779
  an explicit `--max-active-scope <key>` wins, else the running loop's
703
780
  `LOOPS_LOOP_NAME`, else the route key — so each router's limit is its own
704
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.
705
784
  `--max-active-per-project` and `--max-active-per-project-group` remain cross-route
706
785
  anti-hog caps counted over all routes. Raise a router's `--max-active`
707
786
  deliberately once counting is per-route; keep `--max-per-profile` set so the
708
787
  extra concurrency spreads across subscription accounts.
709
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
+
710
799
  Only route tasks that explicitly opt in with `auto:route`, `route_enabled=true`,
711
800
  or `automation.allowed=true`. Use Codewith account pools, required worktrees,
712
801
  max-active throttles, and bounded evidence directories. Do not dispatch or paste
@@ -722,30 +811,25 @@ Workflow run manifests are written under
722
811
 
723
812
  ## OpenAutomations Runtime Binding
724
813
 
725
- OpenLoops can be used as an execution runtime for deterministic OpenAutomations
726
- product automations, but it does not own the automation product surface.
727
- `@hasna/automations` owns automation specs, trigger materialization, product
728
- automation action queues, DLQ/replay, idempotency, approvals, and audit
729
- evidence. OpenLoops owns agent workflow invocation/admission/runs. OpenLoops
730
- only executes work that OpenAutomations has explicitly handed off; it does not
731
- 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.
732
820
 
733
- The SDK exposes the boundary descriptor:
821
+ Quick reference:
734
822
 
735
823
  ```ts
736
824
  import { openAutomationsRuntimeBinding } from "@hasna/loops";
737
-
738
825
  const binding = openAutomationsRuntimeBinding();
739
- console.log(binding.handoff); // "claim-queue"
740
- console.log(binding.eventHandoff.handlerCommand); // "loops routes create generic"
826
+ // binding.handoff === "claim-queue"
827
+ // binding.eventHandoff.handlerCommand === "loops routes create generic"
741
828
  ```
742
829
 
743
- The claim-queue handoff uses the OpenAutomations CLI or SDK:
744
-
745
830
  ```bash
746
831
  automations queue claim --runner open-loops:<worker-id>
747
832
  automations queue complete <action-id> --runner open-loops:<worker-id>
748
- automations queue fail <action-id> --runner open-loops:<worker-id> --code <code> --message <message>
749
833
  ```
750
834
 
751
835
  For explicit event workflow routing, OpenAutomations can export the normalized
@@ -782,6 +866,12 @@ required semantics (dry-run/preflight/commit modes, `idempotencyKey` +
782
866
  are specified in `docs/AUTOMATION_RUNTIME_DESIGN.md`; this README intentionally
783
867
  does not duplicate that spec.
784
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
+
785
875
  The same design doc covers the planned DLQ/dead-letter lifecycle, including
786
876
  `loops dlq list/show/replay/resolve`, idempotent replay keys, and compatibility
787
877
  rules for `@hasna/actions`.
@@ -964,4 +1054,4 @@ The adapters intentionally use provider command surfaces instead of pretending e
964
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`.
965
1055
  - Daemon and scheduled runs prepend common user executable directories such as `~/.local/bin` and `~/.bun/bin` before resolving provider CLIs.
966
1056
 
967
- 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.
@@ -31,6 +31,7 @@ export interface LoopsApiServerOptions {
31
31
  storage?: LoopStorageContract;
32
32
  bodyLimitBytes?: number;
33
33
  evidenceLimitBytes?: number;
34
+ importLimitBytes?: number;
34
35
  now?: () => Date;
35
36
  /**
36
37
  * API-key verifier (from `@hasna/contracts/auth`). When present, every