@hasna/loops 0.3.60 → 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.
Files changed (63) hide show
  1. package/CHANGELOG.md +265 -0
  2. package/LICENSE +197 -13
  3. package/README.md +95 -85
  4. package/dist/api/index.d.ts +11 -0
  5. package/dist/api/index.js +333 -0
  6. package/dist/cli/index.js +8013 -6659
  7. package/dist/daemon/control.d.ts +21 -1
  8. package/dist/daemon/daemon.d.ts +5 -0
  9. package/dist/daemon/index.js +2836 -1222
  10. package/dist/daemon/install.d.ts +1 -1
  11. package/dist/index.d.ts +23 -8
  12. package/dist/index.js +5715 -4541
  13. package/dist/lib/accounts.d.ts +6 -1
  14. package/dist/lib/agent-adapter.d.ts +74 -0
  15. package/dist/lib/backup.d.ts +25 -0
  16. package/dist/lib/errors.d.ts +21 -0
  17. package/dist/lib/executor.d.ts +10 -1
  18. package/dist/lib/goal/metadata.d.ts +16 -0
  19. package/dist/lib/goal/model-factory.d.ts +1 -0
  20. package/dist/lib/goal/prompts.d.ts +3 -1
  21. package/dist/lib/goal/types.d.ts +3 -0
  22. package/dist/lib/health.d.ts +1 -1
  23. package/dist/lib/ids.d.ts +8 -1
  24. package/dist/lib/machines.d.ts +6 -0
  25. package/dist/lib/mode.d.ts +48 -0
  26. package/dist/lib/mode.js +260 -0
  27. package/dist/lib/process-identity.d.ts +16 -0
  28. package/dist/lib/{schedule.d.ts → recurrence.d.ts} +1 -0
  29. package/dist/lib/redact.d.ts +21 -0
  30. package/dist/lib/route/cursors.d.ts +18 -0
  31. package/dist/lib/route/drain.d.ts +6 -0
  32. package/dist/lib/route/fields.d.ts +27 -0
  33. package/dist/lib/route/gates.d.ts +26 -0
  34. package/dist/lib/route/index.d.ts +18 -0
  35. package/dist/lib/route/options.d.ts +31 -0
  36. package/dist/lib/route/parse.d.ts +8 -0
  37. package/dist/lib/route/pr-review.d.ts +11 -0
  38. package/dist/lib/route/provider.d.ts +41 -0
  39. package/dist/lib/route/route-event.d.ts +23 -0
  40. package/dist/lib/route/route-tasks.d.ts +75 -0
  41. package/dist/lib/route/throttle.d.ts +47 -0
  42. package/dist/lib/route/todos-cli.d.ts +21 -0
  43. package/dist/lib/route/types.d.ts +88 -0
  44. package/dist/lib/run-artifacts.d.ts +17 -2
  45. package/dist/lib/run-envelope.d.ts +41 -0
  46. package/dist/lib/scheduler.d.ts +78 -2
  47. package/dist/lib/store.d.ts +63 -0
  48. package/dist/lib/store.js +1007 -287
  49. package/dist/lib/template-kit.d.ts +70 -0
  50. package/dist/lib/templates-custom.d.ts +34 -0
  51. package/dist/lib/templates.d.ts +13 -81
  52. package/dist/lib/workflow-runner.d.ts +2 -0
  53. package/dist/mcp/index.d.ts +3 -0
  54. package/dist/mcp/index.js +3231 -1382
  55. package/dist/runner/index.d.ts +9 -0
  56. package/dist/runner/index.js +295 -0
  57. package/dist/sdk/index.d.ts +29 -3
  58. package/dist/sdk/index.js +2743 -1044
  59. package/dist/test-helpers.d.ts +37 -0
  60. package/dist/types.d.ts +3 -1
  61. package/docs/DEPLOYMENT_MODES.md +140 -0
  62. package/docs/USAGE.md +102 -46
  63. package/package.json +23 -5
package/README.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  OpenLoops is a local CLI and daemon for persistent loops and workflows: scheduled or recurring work that survives process restarts and records every run.
4
4
 
5
+ Naming: the product is **OpenLoops**, published on npm as
6
+ [`@hasna/loops`](https://www.npmjs.com/package/@hasna/loops) and developed in the
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`.
10
+
5
11
  It supports deterministic command loops, JSON-defined workflows, and guarded CLI adapters for headless coding agents:
6
12
 
7
13
  - `claude`
@@ -11,9 +17,49 @@ It supports deterministic command loops, JSON-defined workflows, and guarded CLI
11
17
  - `opencode run`
12
18
  - `codex exec`
13
19
 
20
+ ## Deployment Modes
21
+
22
+ OpenLoops has three deployment modes:
23
+
24
+ - `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/API/runner foundations; claim protocol and non-local execution are follow-up work.
26
+ - `cloud`: a hosted control-plane contract. This release exposes client/runner status only; hosted tenant auth and infrastructure live outside this package.
27
+
28
+ `local` is the default and requires no network, token, Postgres, or hosted
29
+ service. Set `LOOPS_MODE` or `HASNA_LOOPS_MODE` to `local`, `self_hosted`, or
30
+ `cloud` to choose explicitly. Without an explicit mode, `LOOPS_CLOUD_API_URL`
31
+ selects `cloud`, while `LOOPS_API_URL` or `LOOPS_DATABASE_URL` selects
32
+ `self_hosted`.
33
+
34
+ The public `@hasna/loops` package owns the local runtime, mode resolver,
35
+ self-hosted API contract, runner contract, SDK, MCP server, and CLI. Hosted
36
+ tenant auth, account administration, and infrastructure live outside this
37
+ package. Cloud mode is a public contract until a cloud-specific hosted URL and
38
+ cloud token are configured through `LOOPS_CLOUD_API_URL` plus
39
+ `LOOPS_CLOUD_TOKEN` or `HASNA_LOOPS_CLOUD_TOKEN`.
40
+
41
+ Useful status commands:
42
+
43
+ ```bash
44
+ loops mode
45
+ loops --json mode
46
+ loops self-hosted status
47
+ loops cloud status
48
+ loops-api status
49
+ loops-runner status
50
+ ```
51
+
52
+ See [Deployment Modes](docs/DEPLOYMENT_MODES.md) for the full package boundary
53
+ and machine-placement contract.
54
+
14
55
  ## Install
15
56
 
16
- From npm:
57
+ **OpenLoops requires the [Bun](https://bun.sh) runtime (`bun >= 1.0`).** The
58
+ `loops`, `loops-daemon`, `loops-api`, `loops-runner`, and `loops-mcp` binaries
59
+ run with a `#!/usr/bin/env bun` shebang, so Bun must be on `PATH` even when the
60
+ package is installed with npm. Node.js is not a supported runtime.
61
+
62
+ From npm (with Bun installed):
17
63
 
18
64
  ```bash
19
65
  npm install -g @hasna/loops
@@ -57,8 +103,9 @@ The package also exports the server factory for embedded hosts:
57
103
  import { createLoopsMcpServer } from "@hasna/loops/mcp";
58
104
  ```
59
105
 
60
- Available read tools include `loops_list`, `loops_show`, `loop_runs`,
61
- `loops_doctor`, `workflows_list`, `workflow_read`, and `workflow_validate`.
106
+ Available read tools include `loops_list`, `loops_show`, `loops_runs`,
107
+ `loops_doctor`, `loops_workflows_list`, `loops_workflow_read`, and
108
+ `loops_workflow_validate`.
62
109
  Resources are available at `loops://runtime` and `loops://tools`.
63
110
  Those tools use the same `Store`, public redaction helpers, and workflow parser
64
111
  as the CLI and SDK, so read output and validation behavior stay aligned across
@@ -66,15 +113,19 @@ surfaces.
66
113
 
67
114
  Mutation tools are disabled by default. Start the server with
68
115
  `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.
116
+ allowed to change loop state. The guarded mutation tools use canonical names:
117
+ `loops_pause`, `loops_resume`, `loops_stop`, `loops_run_now`, `loops_archive`,
118
+ `loops_unarchive`, `loops_create_command`, and `loops_create_workflow`.
119
+ Deprecated `loop_*` aliases are still registered where compatibility needs them,
120
+ but callers should use the `loops_*` names. Mutation tools do not require or
121
+ accept confirmation-string parameters; the server-side environment opt-in is the
122
+ gate. MCP `loops_run_now` schedules the loop for immediate daemon pickup; inline
123
+ execution remains CLI-only.
73
124
 
74
125
  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.
126
+ start/stop/install/logs, inline `run-now`, `tick`, loop deletion, workflow
127
+ create/migrate/cancel/recover, agent loop creation, template materialization,
128
+ and event-route drains.
78
129
 
79
130
  ## Create Loops
80
131
 
@@ -197,7 +248,9 @@ non-secret routing/configuration data.
197
248
 
198
249
  ## Goals
199
250
 
200
- Add `--goal` to wrap a command, agent, or workflow loop in an AI-SDK orchestration layer. OpenLoops asks the configured model to create a flat DAG plan, executes ready nodes by calling the underlying target, then runs an adversarial achievement audit before marking the goal complete.
251
+ Add `--goal` to wrap a command, agent, or workflow loop in an AI-SDK orchestration layer. OpenLoops asks the configured model to create a flat DAG plan, then executes ready plan nodes by re-running the underlying target once per node. Each node run is parameterized with that node's objective: agent prompts get the goal and node objective appended, and every wrapped process receives `LOOPS_GOAL_NODE_KEY` and `LOOPS_GOAL_NODE_OBJECTIVE` in its environment. An adversarial achievement audit runs before the goal is marked complete.
252
+
253
+ The goal `autoExecute` mode is honored: `off` plans and persists the DAG without executing any nodes (the run reports the persisted plan), while `readyOnly` (the default) and `aiDirected` run the dependency-driven execution loop over ready nodes.
201
254
 
202
255
  ```bash
203
256
  export OPENROUTER_API_KEY=...
@@ -213,7 +266,7 @@ loops create agent repo-fixer \
213
266
  --goal-max-turns 5
214
267
  ```
215
268
 
216
- Goal planning and validation use the Vercel AI SDK with `@openrouter/ai-sdk-provider`. Set `OPENROUTER_API_KEY`; optionally set `LOOPS_GOAL_BASE_URL` to point at a local gateway compatible with OpenRouter. Goal context is passed to wrapped commands and agents as `LOOPS_GOAL_ID`, `LOOPS_GOAL_OBJECTIVE`, and `LOOPS_GOAL_NODE_KEY`.
269
+ Goal planning and validation use the Vercel AI SDK with `@openrouter/ai-sdk-provider`. Set `OPENROUTER_API_KEY`; optionally set `LOOPS_GOAL_BASE_URL` to point at a local gateway compatible with OpenRouter. Goal context is passed to wrapped commands and agents as `LOOPS_GOAL_ID`, `LOOPS_GOAL_OBJECTIVE`, `LOOPS_GOAL_NODE_KEY`, and `LOOPS_GOAL_NODE_OBJECTIVE`.
217
270
 
218
271
  If a resumed goal plan has no runnable nodes, failed run output includes a
219
272
  structured `diagnostics` object with the concrete blocker and `owner`
@@ -224,7 +277,7 @@ Inspect configured and runtime goal state:
224
277
 
225
278
  ```bash
226
279
  loops goal show <loop-or-goal-id>
227
- loops goal status <goal-run-id-or-loop-run-id>
280
+ loops goal show <goal-run-id-or-loop-run-id>
228
281
  ```
229
282
 
230
283
  Workflow JSON can also embed goals at the workflow or step level:
@@ -327,11 +380,11 @@ loops templates render todos-task-worker-verifier \
327
380
  --var taskTitle="Fix parser" \
328
381
  --var projectPath=/path/to/repo \
329
382
  --var provider=codewith \
330
- --var authProfilePool=account004,account005,account006 \
383
+ --var authProfilePool=account001,account002,account003 \
331
384
  --var sandbox=workspace-write \
332
385
  --var todosProjectPath=$HOME/.hasna/loops \
333
386
  --var addDirs=$HOME/.hasna/todos,$HOME/.hasna/loops
334
- loops templates create-workflow todos-task-worker-verifier \
387
+ loops workflows create --template todos-task-worker-verifier \
335
388
  --var taskId=<task-id> \
336
389
  --var projectPath=/path/to/repo
337
390
  loops templates render event-worker-verifier \
@@ -344,7 +397,7 @@ loops templates render bounded-agent-worker-verifier \
344
397
  --var objective="Check docs drift and queue tasks for gaps" \
345
398
  --var projectPath=/path/to/repo \
346
399
  --var provider=codewith \
347
- --var authProfilePool=account004,account005 \
400
+ --var authProfilePool=account001,account002 \
348
401
  --var sandbox=workspace-write \
349
402
  --var worktreeMode=required
350
403
  loops templates render pr-review \
@@ -429,30 +482,31 @@ loops templates show custom-report
429
482
  loops templates render custom-report \
430
483
  --var objective="Check docs drift" \
431
484
  --var projectPath=/path/to/repo
432
- loops templates create-workflow custom-report \
485
+ loops workflows create --template custom-report \
433
486
  --var objective="Check docs drift" \
434
487
  --var projectPath=/path/to/repo
435
488
  ```
436
489
 
437
490
  Use `--source builtin`, `--source custom`, or `--source all` on
438
- `list`, `show`, `render`, and `create-workflow` when automation needs an
439
- explicit source. Custom template ids and names cannot override built-ins.
491
+ `templates list`, `templates show`, `templates render`, and
492
+ `workflows create --template` when automation needs an explicit source. Custom
493
+ template ids and names cannot override built-ins.
440
494
  Custom templates fail closed for `danger-full-access`, dangerous passthrough
441
495
  arguments, and implicit Codewith/Codex full-access defaults. If a custom
442
496
  Codewith/Codex template uses `permissionMode: "bypass"`, it must also set
443
497
  `sandbox` to `workspace-write` or `read-only`. Use built-in templates with
444
498
  explicit break-glass handling for emergency workflows that need full access.
445
499
 
446
- For event-driven task automation, `loops events handle todos-task` reads a
500
+ For event-driven task automation, `loops routes create todos-task` reads a
447
501
  Hasna event envelope from stdin or `HASNA_EVENT_JSON`, records a
448
502
  `WorkflowInvocation`, upserts an admission work item, and admits that work item
449
503
  into a deduped one-shot workflow loop when route capacity allows:
450
504
 
451
505
  ```bash
452
- cat task-created-event.json | loops events handle todos-task \
506
+ cat task-created-event.json | loops routes create todos-task \
453
507
  --template task-lifecycle \
454
508
  --provider codewith \
455
- --auth-profile-pool account004,account005,account006 \
509
+ --auth-profile-pool account001,account002,account003 \
456
510
  --permission-mode bypass \
457
511
  --sandbox workspace-write \
458
512
  --todos-project "$HOME/.hasna/loops" \
@@ -485,7 +539,7 @@ selected.
485
539
  loops routes drain todos-task \
486
540
  --dry-run \
487
541
  --provider-rule area=frontend:claude:claude-ui-a,claude-ui-b \
488
- --provider-rule area=backend:codewith:account004,account005 \
542
+ --provider-rule area=backend:codewith:account001,account002 \
489
543
  --worktree-mode required
490
544
  ```
491
545
 
@@ -522,9 +576,9 @@ For other Hasna apps that expose `@hasna/events` webhooks, use the generic
522
576
  handler:
523
577
 
524
578
  ```bash
525
- cat event.json | loops events handle generic \
579
+ cat event.json | loops routes create generic \
526
580
  --provider codewith \
527
- --auth-profile-pool account004,account005,account006 \
581
+ --auth-profile-pool account001,account002,account003 \
528
582
  --permission-mode bypass \
529
583
  --sandbox workspace-write \
530
584
  --project-path /path/to/repo \
@@ -567,7 +621,7 @@ loops routes requeue <work-item-id> --reason "fixed upstream blocker"
567
621
  loops routes invocations
568
622
  ```
569
623
 
570
- For Hasna OSS task-created routing, use a deterministic drain instead of tmux
624
+ For OSS task-created routing, use a deterministic drain instead of tmux
571
625
  dispatch:
572
626
 
573
627
  ```bash
@@ -575,10 +629,10 @@ loops routes schedule todos-task oss-task-route-drain \
575
629
  --every 5m \
576
630
  --todos-project "$HOME/.hasna/loops" \
577
631
  --template task-lifecycle \
578
- --project-path-prefix /home/hasna/workspace/hasna/opensource \
632
+ --project-path-prefix "$HOME/workspace/example/opensource" \
579
633
  --tags auto:route \
580
634
  --provider codewith \
581
- --auth-profile-pool account004,account005,account006 \
635
+ --auth-profile-pool account001,account002,account003 \
582
636
  --add-dir "$HOME/.hasna/todos,$HOME/.hasna/loops" \
583
637
  --project-group oss \
584
638
  --max-dispatch 2 \
@@ -621,7 +675,7 @@ import { openAutomationsRuntimeBinding } from "@hasna/loops";
621
675
 
622
676
  const binding = openAutomationsRuntimeBinding();
623
677
  console.log(binding.handoff); // "claim-queue"
624
- console.log(binding.eventHandoff.handlerCommand); // "loops events handle generic"
678
+ console.log(binding.eventHandoff.handlerCommand); // "loops routes create generic"
625
679
  ```
626
680
 
627
681
  The claim-queue handoff uses the OpenAutomations CLI or SDK:
@@ -634,18 +688,18 @@ automations queue fail <action-id> --runner open-loops:<worker-id> --code <code>
634
688
 
635
689
  For explicit event workflow routing, OpenAutomations can export the normalized
636
690
  event envelope and OpenLoops can consume it through the existing generic event
637
- handler:
691
+ route:
638
692
 
639
693
  ```bash
640
694
  automations --json webhooks event <route> --body-json '<json>' \
641
- | loops --json events handle generic
695
+ | loops --json routes create generic
642
696
  ```
643
697
 
644
698
  This is not automation materialization in OpenLoops. It is an explicit
645
699
  event-envelope workflow handoff: OpenAutomations owns deterministic automation
646
700
  specs, webhook normalization, queue state, approvals, DLQ, and replay; OpenLoops
647
701
  owns agent workflow invocation after the operator routes the envelope to
648
- `loops events handle generic`.
702
+ `loops routes create generic`.
649
703
 
650
704
  Do not store automation specs in OpenLoops, infer automation triggers from event
651
705
  transport alone, or replace the OpenAutomations queue with loop/workflow rows.
@@ -660,59 +714,15 @@ write OpenLoops SQLite rows directly. The stable contract should be an
660
714
  idempotent CLI/SDK upsert that accepts a fully rendered one-shot workflow loop
661
715
  request and returns durable refs.
662
716
 
663
- Proposed SDK shape:
717
+ The canonical `WorkflowUpsertRequest` / `WorkflowUpsertResult` shapes and their
718
+ required semantics (dry-run/preflight/commit modes, `idempotencyKey` +
719
+ `specHash` idempotency, dispatch behavior, and redaction-before-persistence)
720
+ are specified in `docs/AUTOMATION_RUNTIME_DESIGN.md`; this README intentionally
721
+ does not duplicate that spec.
664
722
 
665
- ```ts
666
- type WorkflowUpsertRequest = {
667
- idempotencyKey: string;
668
- source: { kind: "action" | "automation" | "event"; id: string; dedupeKey?: string };
669
- subject: { kind: "repo" | "task" | "pr" | "run"; id?: string; path?: string; url?: string };
670
- workflow: { name: string; description?: string; steps: WorkflowStepInput[] };
671
- loop: { name: string; schedule: { type: "once"; at: string }; machine?: LoopMachineRef };
672
- route?: { projectPath?: string; projectGroup?: string; concurrencyGroup?: string };
673
- execution?: AutomationExecutionPolicy;
674
- mode?: "dry-run" | "preflight" | "commit";
675
- dispatch?: "schedule" | "run-now" | "none";
676
- };
677
-
678
- type WorkflowUpsertResult = {
679
- ok: boolean;
680
- dryRun: boolean;
681
- idempotencyKey: string;
682
- specHash: string;
683
- refs: {
684
- workflowId?: string;
685
- loopId?: string;
686
- invocationId?: string;
687
- workItemId?: string;
688
- runId?: string;
689
- manifestPath?: string;
690
- };
691
- action: "created" | "updated" | "reused" | "rejected";
692
- preflight?: { ok: boolean; checks: unknown[]; error?: string };
693
- };
694
- ```
695
-
696
- Required semantics:
697
-
698
- - `mode="dry-run"` validates, canonicalizes, hashes, and returns the same JSON
699
- shape without mutating OpenLoops state.
700
- - `mode="preflight"` additionally checks provider binaries, machine routing,
701
- accounts/auth profiles, prompt files, and workflow target compatibility before
702
- commit.
703
- - `mode="commit"` is idempotent on `idempotencyKey` plus `specHash`: identical
704
- requests return existing refs; changed specs create a new workflow version or
705
- one-shot loop while preserving previous run history.
706
- - `dispatch="schedule"` stores a one-shot loop for the daemon; `run-now` claims
707
- an immediate manual slot; `none` only materializes refs for another owner to
708
- trigger later.
709
- - All persisted output is redacted before storage, and returned refs are enough
710
- for the caller to inspect, cancel, replay, or resolve the run without querying
711
- SQLite directly.
712
-
713
- See `docs/AUTOMATION_RUNTIME_DESIGN.md` for the planned DLQ/dead-letter
714
- lifecycle, including `loops dlq list/show/replay/resolve`, idempotent replay
715
- keys, and compatibility rules for `@hasna/actions`.
723
+ The same design doc covers the planned DLQ/dead-letter lifecycle, including
724
+ `loops dlq list/show/replay/resolve`, idempotent replay keys, and compatibility
725
+ rules for `@hasna/actions`.
716
726
 
717
727
  The same design doc also defines the planned strict automation execution mode:
718
728
  minimal env inheritance, scoped secret refs, enforced allowlists,
@@ -879,4 +889,4 @@ The adapters intentionally use provider command surfaces instead of pretending e
879
889
  - `--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`.
880
890
  - Daemon and scheduled runs prepend common user executable directories such as `~/.local/bin` and `~/.bun/bin` before resolving provider CLIs.
881
891
 
882
- For production loops that can mutate repos, prefer disposable worktrees and explicit prompts that name allowed write scope.
892
+ 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.
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env bun
2
+ export declare function apiStatus(): {
3
+ ok: boolean;
4
+ service: string;
5
+ status: import("../index.js").LoopDeploymentStatus;
6
+ };
7
+ export declare function createLoopsApiServer(opts?: {
8
+ host?: string;
9
+ port?: number;
10
+ }): Bun.Server<undefined>;
11
+ export declare function main(argv?: string[]): Promise<void>;