@hasna/loops 0.4.0 → 0.4.1

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.
@@ -0,0 +1,140 @@
1
+ # OpenLoops Deployment Modes
2
+
3
+ OpenLoops supports one active source of truth at a time. The public package
4
+ defines the mode vocabulary, local cache behavior, API shape, and runner
5
+ contract. Hosted multi-tenant operation is implemented outside this public
6
+ package.
7
+
8
+ ## Modes
9
+
10
+ | Mode | Source of truth | Local storage role | Executor |
11
+ | --- | --- | --- | --- |
12
+ | `local` | SQLite in `LOOPS_DATA_DIR` | Authoritative | `loops-daemon` |
13
+ | `self_hosted` | A user-operated `loops-api` control plane contract | Cache and offline spool | `loops-runner` foundation |
14
+ | `cloud` | A configured hosted control plane contract | Cache and offline spool | `loops-runner` foundation |
15
+
16
+ `local` remains the default. It must keep working without network access,
17
+ tokens, Postgres, or hosted infrastructure.
18
+
19
+ `self_hosted` is for users or teams running their own control plane. The public
20
+ `@hasna/loops` package owns the API and runner contract for this mode.
21
+
22
+ `cloud` is the hosted control-plane contract. The public package exposes the
23
+ client and runner contract, but tenant auth, account administration, and hosted
24
+ infrastructure stay outside this package. The public package must not depend on
25
+ private hosted packages or resource names. This release exposes status
26
+ surfaces only; non-local claim/lease execution is follow-up work.
27
+
28
+ ## Mode Resolution
29
+
30
+ `LOOPS_MODE` or `HASNA_LOOPS_MODE` may be set to `local`, `self_hosted`, or
31
+ `cloud`. Hyphenated `self-hosted` is normalized to `self_hosted`.
32
+
33
+ When no explicit mode is set, OpenLoops resolves the mode from configuration:
34
+
35
+ 1. `LOOPS_CLOUD_API_URL` or `HASNA_LOOPS_CLOUD_API_URL` selects `cloud`.
36
+ 2. `LOOPS_API_URL`, `HASNA_LOOPS_API_URL`, `LOOPS_DATABASE_URL`, or
37
+ `HASNA_LOOPS_DATABASE_URL` selects `self_hosted`.
38
+ 3. Otherwise OpenLoops uses `local`.
39
+
40
+ `LOOPS_API_URL` and `HASNA_LOOPS_API_URL` belong to `self_hosted`.
41
+ `cloud` uses only `LOOPS_CLOUD_API_URL` or `HASNA_LOOPS_CLOUD_API_URL`.
42
+
43
+ Tokens are represented only as presence signals in status output. Self-hosted
44
+ status uses `LOOPS_API_TOKEN` or `HASNA_LOOPS_API_TOKEN`. Cloud status uses
45
+ `LOOPS_CLOUD_TOKEN` or `HASNA_LOOPS_CLOUD_TOKEN`. URL credentials, query
46
+ strings, and fragments are not returned in status output.
47
+
48
+ ## Commands
49
+
50
+ ```bash
51
+ loops mode
52
+ loops --json mode
53
+ loops self-hosted status
54
+ loops cloud status
55
+ loops-api status
56
+ loops-runner status
57
+ ```
58
+
59
+ Human status output is intentionally compact:
60
+
61
+ ```text
62
+ deploymentMode=local active source=default truth=local_sqlite local=authoritative control_plane=none
63
+ ```
64
+
65
+ JSON uses these field names:
66
+
67
+ - `deploymentMode`: the requested status perspective.
68
+ - `activeDeploymentMode`: the mode selected from the current environment.
69
+ - `deploymentModeSource`: the env var or default that selected the active mode.
70
+ - `sourceOfTruth`: `local_sqlite`, `self_hosted_control_plane`, or
71
+ `cloud_control_plane`.
72
+ - `localStore.role`: `authoritative` in local mode, `cache_and_spool` in
73
+ non-local modes.
74
+ - `controlPlane.configured`: true only when the current mode has enough
75
+ configuration to be usable. Cloud requires both a cloud URL and a cloud token
76
+ presence signal.
77
+ - `controlPlane.apiUrl`: a display-safe URL without credentials, query string,
78
+ or fragment.
79
+
80
+ `loops-api` is a separate process in the same public package. It is not a
81
+ separate package at this stage because self-hosted users and the hosted service
82
+ must share the same public contract.
83
+
84
+ `loops-runner` is the process that connects a machine to a non-local control
85
+ plane. The initial foundation exposes status only. The Postgres schema, claim
86
+ protocol, runner heartbeat/finalization protocol, and import/export migration
87
+ must land before `loops-runner` is advertised as an executing worker.
88
+
89
+ ## Machine Placement
90
+
91
+ A loop can eventually target:
92
+
93
+ - one specific machine,
94
+ - a machine pool selected by capability labels,
95
+ - or multiple machines intentionally.
96
+
97
+ Single-run loops need a lease so only one runner executes each scheduled slot.
98
+ Multi-machine loops must record per-machine run evidence so duplicate work is
99
+ distinguishable from intentional fan-out.
100
+
101
+ This is separate from existing local OpenMachines dispatch. In `local` mode,
102
+ `loops-daemon` can still dispatch a loop target to a configured remote machine
103
+ through the existing OpenMachines transport. In `self_hosted` or `cloud`,
104
+ machine execution is runner-pull: a registered `loops-runner` claims work from
105
+ the control plane. Operators should not treat local remote dispatch as cloud
106
+ mode.
107
+
108
+ ## Follow-Up Work
109
+
110
+ Non-local execution needs these follow-up releases before it is live:
111
+
112
+ - Postgres/control-plane persistence for loops, schedules, runners, claims, and
113
+ evidence.
114
+ - Runner placement, capability labels, leases, heartbeat, and finalization.
115
+ - Import/export migration between local SQLite state and a control plane.
116
+ - Hosted product integration outside the public package.
117
+
118
+ ## Public Package Boundary
119
+
120
+ The public package owns:
121
+
122
+ - local SQLite scheduling and daemon execution,
123
+ - the mode resolver and status surfaces,
124
+ - the self-hosted API contract,
125
+ - the runner contract,
126
+ - local cache/spool semantics,
127
+ - import/export and migration commands,
128
+ - SDK, MCP, and CLI primitives.
129
+
130
+ Hosted product code owns:
131
+
132
+ - tenant account management,
133
+ - hosted authentication and invitations,
134
+ - hosted observability and admin operations,
135
+ - cloud infrastructure deployment,
136
+ - product UI and customer lifecycle flows.
137
+
138
+ The public package must document cloud behavior as a contract unless a hosted
139
+ URL and token are explicitly configured. It must not claim that cloud service is
140
+ live by default.
package/docs/USAGE.md CHANGED
@@ -11,8 +11,36 @@ It supports deterministic command loops, JSON-defined workflows, and guarded CLI
11
11
  - `opencode run`
12
12
  - `codex exec`
13
13
 
14
+ ## Deployment Modes
15
+
16
+ OpenLoops defaults to `local`, where SQLite in `LOOPS_DATA_DIR` is
17
+ authoritative and `loops-daemon` executes scheduled work. The package also
18
+ defines `self_hosted` and `cloud` contracts for future non-local control
19
+ planes:
20
+
21
+ - `self_hosted`: user-operated `loops-api` control-plane contract; this
22
+ release exposes API/runner status foundations only.
23
+ - `cloud`: hosted control-plane contract; this release exposes client/runner
24
+ status only, and requires `LOOPS_CLOUD_API_URL` plus `LOOPS_CLOUD_TOKEN` or
25
+ `HASNA_LOOPS_CLOUD_TOKEN` before status can report ready.
26
+
27
+ Useful status commands:
28
+
29
+ ```bash
30
+ loops mode
31
+ loops --json mode
32
+ loops self-hosted status
33
+ loops cloud status
34
+ loops-api status
35
+ loops-runner status
36
+ ```
37
+
14
38
  ## Install
15
39
 
40
+ **OpenLoops requires the [Bun](https://bun.sh) runtime (`bun >= 1.0`).** The
41
+ installed binaries are `loops`, `loops-daemon`, `loops-api`, `loops-runner`, and
42
+ `loops-mcp`; each uses a `#!/usr/bin/env bun` shebang.
43
+
16
44
  From npm:
17
45
 
18
46
  ```bash
@@ -57,8 +85,9 @@ The package also exports the server factory for embedded hosts:
57
85
  import { createLoopsMcpServer } from "@hasna/loops/mcp";
58
86
  ```
59
87
 
60
- Available read tools include `loops_list`, `loops_show`, `loop_runs`,
61
- `loops_doctor`, `workflows_list`, `workflow_read`, and `workflow_validate`.
88
+ Available read tools include `loops_list`, `loops_show`, `loops_runs`,
89
+ `loops_doctor`, `loops_workflows_list`, `loops_workflow_read`, and
90
+ `loops_workflow_validate`.
62
91
  Resources are available at `loops://runtime` and `loops://tools`.
63
92
  Those tools use the same `Store`, public redaction helpers, and workflow parser
64
93
  as the CLI and SDK, so read output and validation behavior stay aligned across
@@ -66,15 +95,19 @@ surfaces.
66
95
 
67
96
  Mutation tools are disabled by default. Start the server with
68
97
  `LOOPS_MCP_ALLOW_MUTATIONS=true` only for a trusted local MCP host that should be
69
- allowed to change loop state. Even then, mutation tools require exact
70
- confirmation strings: `loop_pause`, `loop_resume`, `loop_run_now`,
71
- `loop_create_command`, and `loop_create_workflow`. MCP `loop_run_now` schedules
72
- the loop for immediate daemon pickup; inline execution remains CLI-only.
98
+ allowed to change loop state. The guarded mutation tools use canonical names:
99
+ `loops_pause`, `loops_resume`, `loops_stop`, `loops_run_now`, `loops_archive`,
100
+ `loops_unarchive`, `loops_create_command`, and `loops_create_workflow`.
101
+ Deprecated `loop_*` aliases are still registered where compatibility needs them,
102
+ but callers should use the `loops_*` names. Mutation tools do not require or
103
+ accept confirmation-string parameters; the server-side environment opt-in is the
104
+ gate. MCP `loops_run_now` schedules the loop for immediate daemon pickup; inline
105
+ execution remains CLI-only.
73
106
 
74
107
  Keep host-affecting or long-running operations on the CLI: daemon
75
- start/stop/install/logs, inline `run-now`, `tick`, loop removal/archive
76
- maintenance, workflow create/migrate/cancel/recover, agent loop creation,
77
- template materialization, and event-route drains.
108
+ start/stop/install/logs, inline `run-now`, `tick`, loop deletion, workflow
109
+ create/migrate/cancel/recover, agent loop creation, template materialization,
110
+ and event-route drains.
78
111
 
79
112
  ## Create Loops
80
113
 
@@ -95,8 +128,8 @@ loops create command repo-status \
95
128
  ```
96
129
 
97
130
  `--preflight` is available on `loops create command`, `loops create agent`,
98
- `loops create workflow`, `loops workflows create`, and event-router commands such
99
- as `loops events handle todos-task` and `loops events handle generic`. It checks
131
+ `loops create workflow`, `loops workflows create`, and route commands such as
132
+ `loops routes create todos-task` and `loops routes create generic`. It checks
100
133
  target executables and configured account profiles before loop or workflow rows
101
134
  are stored, so a missing command, provider binary, OpenAccounts profile, native
102
135
  Codewith auth profile, or workflow step dependency fails without creating a
@@ -340,11 +373,11 @@ loops templates render todos-task-worker-verifier \
340
373
  --var taskTitle="Fix parser" \
341
374
  --var projectPath=/path/to/repo \
342
375
  --var provider=codewith \
343
- --var authProfilePool=account004,account005,account006 \
376
+ --var authProfilePool=account001,account002,account003 \
344
377
  --var sandbox=workspace-write \
345
378
  --var todosProjectPath=$HOME/.hasna/loops \
346
379
  --var addDirs=$HOME/.hasna/todos,$HOME/.hasna/loops
347
- loops templates create-workflow todos-task-worker-verifier \
380
+ loops workflows create --template todos-task-worker-verifier \
348
381
  --var taskId=<task-id> \
349
382
  --var projectPath=/path/to/repo
350
383
  loops templates render event-worker-verifier \
@@ -357,7 +390,7 @@ loops templates render bounded-agent-worker-verifier \
357
390
  --var objective="Check docs drift and queue tasks for gaps" \
358
391
  --var projectPath=/path/to/repo \
359
392
  --var provider=codewith \
360
- --var authProfilePool=account004,account005 \
393
+ --var authProfilePool=account001,account002 \
361
394
  --var sandbox=workspace-write
362
395
  loops templates render pr-review \
363
396
  --var prUrl=https://github.com/hasna/loops/pull/123 \
@@ -441,14 +474,15 @@ loops templates show custom-report
441
474
  loops templates render custom-report \
442
475
  --var objective="Check docs drift" \
443
476
  --var projectPath=/path/to/repo
444
- loops templates create-workflow custom-report \
477
+ loops workflows create --template custom-report \
445
478
  --var objective="Check docs drift" \
446
479
  --var projectPath=/path/to/repo
447
480
  ```
448
481
 
449
482
  Use `--source builtin`, `--source custom`, or `--source all` on
450
- `list`, `show`, `render`, and `create-workflow` when automation needs an
451
- explicit source. Custom template ids and names cannot override built-ins.
483
+ `templates list`, `templates show`, `templates render`, and
484
+ `workflows create --template` when automation needs an explicit source. Custom
485
+ template ids and names cannot override built-ins.
452
486
  Custom templates fail closed for `danger-full-access`, dangerous passthrough
453
487
  arguments, and implicit Codewith/Codex full-access defaults. If a custom
454
488
  Codewith/Codex template uses `permissionMode: "bypass"`, it must also set
@@ -491,16 +525,16 @@ PR review and merge route workers may fetch, rebase, or merge base branches
491
525
  inside the isolated worktree when the task requires it, but they must not
492
526
  mutate the primary main checkout.
493
527
 
494
- For event-driven task automation, `loops events handle todos-task` reads a
528
+ For event-driven task automation, `loops routes create todos-task` reads a
495
529
  Hasna event envelope from stdin or `HASNA_EVENT_JSON`, records a
496
530
  `WorkflowInvocation`, upserts an admission work item, and admits that work item
497
531
  into a deduped one-shot workflow loop when route capacity allows:
498
532
 
499
533
  ```bash
500
- cat task-created-event.json | loops events handle todos-task \
534
+ cat task-created-event.json | loops routes create todos-task \
501
535
  --template task-lifecycle \
502
536
  --provider codewith \
503
- --auth-profile-pool account004,account005,account006 \
537
+ --auth-profile-pool account001,account002,account003 \
504
538
  --permission-mode bypass \
505
539
  --sandbox workspace-write \
506
540
  --todos-project "$HOME/.hasna/loops" \
@@ -530,7 +564,7 @@ selected.
530
564
  loops routes drain todos-task \
531
565
  --dry-run \
532
566
  --provider-rule area=frontend:claude:claude-ui-a,claude-ui-b \
533
- --provider-rule area=backend:codewith:account004,account005 \
567
+ --provider-rule area=backend:codewith:account001,account002 \
534
568
  --worktree-mode required
535
569
  ```
536
570
 
@@ -568,9 +602,9 @@ Use route throttles to avoid stampeding agents when a producer creates many
568
602
  tasks at once:
569
603
 
570
604
  ```bash
571
- cat task-created-event.json | loops events handle todos-task \
605
+ cat task-created-event.json | loops routes create todos-task \
572
606
  --provider codewith \
573
- --auth-profile-pool account004,account005,account006 \
607
+ --auth-profile-pool account001,account002,account003 \
574
608
  --project-group oss \
575
609
  --max-active-per-project 1 \
576
610
  --max-active-per-project-group 4 \
@@ -633,14 +667,14 @@ hand. It scans `todos ready --json`, so tasks with incomplete dependencies,
633
667
  locks, or non-pending states stay queued in todos and are not routed:
634
668
 
635
669
  ```bash
636
- loops events drain todos-task \
670
+ loops routes drain todos-task \
637
671
  --todos-project "$HOME/.hasna/loops" \
638
672
  --template task-lifecycle \
639
673
  --task-list repoops-pr-queue \
640
674
  --tags auto:route \
641
- --project-path-prefix /home/hasna/workspace/hasna/opensource \
675
+ --project-path-prefix "$HOME/workspace/example/opensource" \
642
676
  --provider codewith \
643
- --auth-profile-pool account004,account005,account006 \
677
+ --auth-profile-pool account001,account002,account003 \
644
678
  --add-dir "$HOME/.hasna/todos,$HOME/.hasna/loops" \
645
679
  --project-group oss \
646
680
  --max-dispatch 2 \
@@ -665,17 +699,17 @@ when requested, and leaves excess ready tasks in todos for a later drain pass.
665
699
  Use `--dry-run` to preview candidates and rendered workflows without mutating
666
700
  OpenLoops state.
667
701
 
668
- For the Hasna OSS task-created route, keep the drain deterministic and narrow:
702
+ For an OSS task-created route, keep the drain deterministic and narrow:
669
703
 
670
704
  ```bash
671
705
  loops routes schedule todos-task oss-task-route-drain \
672
706
  --every 5m \
673
707
  --todos-project "$HOME/.hasna/loops" \
674
708
  --template task-lifecycle \
675
- --project-path-prefix /home/hasna/workspace/hasna/opensource \
709
+ --project-path-prefix "$HOME/workspace/example/opensource" \
676
710
  --tags auto:route \
677
711
  --provider codewith \
678
- --auth-profile-pool account004,account005,account006 \
712
+ --auth-profile-pool account001,account002,account003 \
679
713
  --add-dir "$HOME/.hasna/todos,$HOME/.hasna/loops" \
680
714
  --project-group oss \
681
715
  --max-dispatch 2 \
@@ -688,7 +722,7 @@ loops routes schedule todos-task oss-task-route-drain \
688
722
  --compact
689
723
  ```
690
724
 
691
- Only tasks under `/home/hasna/workspace/hasna/opensource` that explicitly opt
725
+ Only tasks under `$HOME/workspace/example/opensource` that explicitly opt
692
726
  in with the `auto:route` tag, `route_enabled=true`, or
693
727
  `automation.allowed=true` should be routed. Keep repo-mutating worker/verifier
694
728
  runs on a Codewith account pool with `--worktree-mode required`. Do not dispatch
@@ -709,9 +743,9 @@ For other Hasna apps that expose `@hasna/events` webhooks, use the generic
709
743
  handler:
710
744
 
711
745
  ```bash
712
- cat event.json | loops events handle generic \
746
+ cat event.json | loops routes create generic \
713
747
  --provider codewith \
714
- --auth-profile-pool account004,account005,account006 \
748
+ --auth-profile-pool account001,account002,account003 \
715
749
  --permission-mode bypass \
716
750
  --sandbox workspace-write \
717
751
  --project-path /path/to/repo \
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/loops",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -8,6 +8,8 @@
8
8
  "bin": {
9
9
  "loops": "dist/cli/index.js",
10
10
  "loops-daemon": "dist/daemon/index.js",
11
+ "loops-api": "dist/api/index.js",
12
+ "loops-runner": "dist/runner/index.js",
11
13
  "loops-mcp": "dist/mcp/index.js"
12
14
  },
13
15
  "exports": {
@@ -23,6 +25,18 @@
23
25
  "types": "./dist/mcp/index.d.ts",
24
26
  "import": "./dist/mcp/index.js"
25
27
  },
28
+ "./api": {
29
+ "types": "./dist/api/index.d.ts",
30
+ "import": "./dist/api/index.js"
31
+ },
32
+ "./runner": {
33
+ "types": "./dist/runner/index.d.ts",
34
+ "import": "./dist/runner/index.js"
35
+ },
36
+ "./mode": {
37
+ "types": "./dist/lib/mode.d.ts",
38
+ "import": "./dist/lib/mode.js"
39
+ },
26
40
  "./storage": {
27
41
  "types": "./dist/lib/store.d.ts",
28
42
  "import": "./dist/lib/store.js"
@@ -36,14 +50,15 @@
36
50
  "LICENSE"
37
51
  ],
38
52
  "scripts": {
39
- "build": "rm -rf dist && bun build src/cli/index.ts src/daemon/index.ts src/mcp/index.ts src/index.ts src/sdk/index.ts src/lib/store.ts --outdir dist --target bun --packages external && chmod +x dist/cli/index.js dist/daemon/index.js dist/mcp/index.js && tsc -p tsconfig.build.json --emitDeclarationOnly --outDir dist",
53
+ "build": "rm -rf dist && bun build src/cli/index.ts src/daemon/index.ts src/api/index.ts src/runner/index.ts src/mcp/index.ts src/index.ts src/sdk/index.ts src/lib/store.ts src/lib/mode.ts --root src --outdir dist --target bun --packages external && chmod +x dist/cli/index.js dist/daemon/index.js dist/api/index.js dist/runner/index.js dist/mcp/index.js && tsc -p tsconfig.build.json --emitDeclarationOnly --outDir dist",
40
54
  "build:bin": "bun build src/cli/index.ts --compile --outfile dist/loops",
41
55
  "typecheck": "tsc --noEmit",
42
56
  "test": "bun test",
57
+ "test:boundary": "bun run scripts/no-private-cloud-boundary.mjs",
43
58
  "prepare": "test -d dist || bun run build",
44
59
  "dev:cli": "bun run src/cli/index.ts",
45
60
  "dev:daemon": "bun run src/daemon/index.ts",
46
- "prepublishOnly": "bun run build"
61
+ "prepublishOnly": "bun run build && bun run test:boundary"
47
62
  },
48
63
  "keywords": [
49
64
  "loops",