@hasna/loops 0.3.56 → 0.3.57

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -41,6 +41,41 @@ bun link
41
41
 
42
42
  The CLI stores state in `~/.hasna/loops` by default. Set `LOOPS_DATA_DIR` to isolate state for tests or another profile.
43
43
 
44
+ ## MCP Server
45
+
46
+ OpenLoops ships a stdio MCP server for safe loop and workflow inspection from
47
+ MCP-capable agents:
48
+
49
+ ```bash
50
+ loops-mcp list-tools
51
+ loops-mcp
52
+ ```
53
+
54
+ The package also exports the server factory for embedded hosts:
55
+
56
+ ```ts
57
+ import { createLoopsMcpServer } from "@hasna/loops/mcp";
58
+ ```
59
+
60
+ Available read tools include `loops_list`, `loops_show`, `loop_runs`,
61
+ `loops_doctor`, `workflows_list`, `workflow_read`, and `workflow_validate`.
62
+ Resources are available at `loops://runtime` and `loops://tools`.
63
+ Those tools use the same `Store`, public redaction helpers, and workflow parser
64
+ as the CLI and SDK, so read output and validation behavior stay aligned across
65
+ surfaces.
66
+
67
+ Mutation tools are disabled by default. Start the server with
68
+ `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.
73
+
74
+ 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.
78
+
44
79
  ## Create Loops
45
80
 
46
81
  Run a deterministic command every minute:
@@ -94,6 +129,20 @@ loops create agent supply-chain-watch \
94
129
  --prompt "Check for suspicious dependency or supply-chain changes. Report only concrete findings."
95
130
  ```
96
131
 
132
+ Run an OpenCode loop with an explicit provider/model. OpenCode reads
133
+ `~/.config/opencode/config.json` when no model is supplied, so OpenLoops rejects
134
+ OpenCode agent targets without `--model` instead of inheriting a stale or
135
+ machine-specific default.
136
+
137
+ ```bash
138
+ loops create agent opencode-smoke \
139
+ --provider opencode \
140
+ --model openrouter/google/gemini-2.5-flash \
141
+ --at "$(date -u -d '+1 minute' +%Y-%m-%dT%H:%M:%SZ)" \
142
+ --cwd /path/to/repo \
143
+ --prompt "Reply with exactly OK."
144
+ ```
145
+
97
146
  For `codewith` and `aicopilot` account isolation, register matching OpenAccounts tools first if they are not built in on the machine:
98
147
 
99
148
  ```bash
@@ -166,6 +215,11 @@ loops create agent repo-fixer \
166
215
 
167
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`.
168
217
 
218
+ If a resumed goal plan has no runnable nodes, failed run output includes a
219
+ structured `diagnostics` object with the concrete blocker and `owner`
220
+ (`goal`, `goal-plan`, or `goal-plan-node`) so scheduled workflow loops do not
221
+ end as opaque blocked plans.
222
+
169
223
  Inspect configured and runtime goal state:
170
224
 
171
225
  ```bash
@@ -238,8 +292,8 @@ loops create workflow repo-morning-loop --workflow repo-morning --cron "0 8 * *
238
292
 
239
293
  Use `recover` only for interrupted `running` workflow runs whose recorded child
240
294
  process is gone. Terminal `timed_out` task/event workflow runs are audit
241
- history; requeue them through the original task/event route after fixing the
242
- cause.
295
+ history; use `loops routes requeue <work-item-id> --reason "<cause fixed>"`
296
+ after fixing the cause, then redeliver or drain the original task/event route.
243
297
 
244
298
  Workflow specs are stored separately from loops. A loop can schedule a workflow, but workflow runs and step runs have their own durable rows and events. Steps run in dependency order and a scheduled workflow run is idempotent per loop slot.
245
299
 
@@ -309,11 +363,14 @@ registry.
309
363
 
310
364
  Timeout policy is explicit. Deterministic command/check steps should normally
311
365
  keep finite `timeoutMs`/`idleTimeoutMs` guards so broken shell work cannot run
312
- forever. Agentic work steps default to no timeout in built-in worker/verifier
313
- and task-lifecycle templates; use `timeoutMs: null` in workflow JSON, or
314
- `--timeout none` / `--timeout unlimited` for CLI-created targets, when a step
315
- may need hours or days. Use a positive numeric `timeoutMs` only when an agentic
316
- step is intentionally bounded.
366
+ forever. Agentic work steps default to no wall-clock timeout in built-in
367
+ worker/verifier and task-lifecycle templates; use `timeoutMs: null` in workflow
368
+ JSON, or `--timeout none` / `--timeout unlimited` for CLI-created targets, when
369
+ a step may need hours or days. Verifier/evaluator steps add a 15 minute
370
+ `idleTimeoutMs` watchdog by default so a verifier cannot hang silently after the
371
+ worker finishes; pass `--verifier-idle-timeout none` or template variable
372
+ `verifierIdleTimeoutMs=none` only when another heartbeat is guaranteed. Use a
373
+ positive numeric `timeoutMs` only when an agentic step is intentionally bounded.
317
374
 
318
375
  To migrate existing workflow loops, do not edit `workflow_specs.steps_json`
319
376
  directly because historical workflow runs must keep pointing at their original
@@ -329,7 +386,8 @@ with the requested agent timeout policy, retargets only future executions of
329
386
  eligible non-running workflow loops, and leaves terminal timed-out workflow runs
330
387
  as audit history. Use `loops workflows recover` only for interrupted `running`
331
388
  workflow runs whose recorded child process is gone; terminal `timed_out` runs
332
- must be requeued by re-delivering or draining the original task/event route.
389
+ must be requeued with `loops routes requeue <work-item-id> --reason "<cause fixed>"` before
390
+ re-delivering or draining the original task/event route.
333
391
 
334
392
  ```json
335
393
  {
@@ -402,15 +460,60 @@ cat task-created-event.json | loops events handle todos-task \
402
460
  --worktree-mode required
403
461
  ```
404
462
 
463
+ Task routing is explicit opt-in. The handler skips the event without creating a
464
+ workflow unless the event data or metadata has `route_enabled=true`,
465
+ `automation.allowed=true`, or a task tag containing `auto:route`. It also skips
466
+ blocked, completed/done, cancelled/canceled, failed, archived, manual,
467
+ approval-required, or `no-auto` tasks. Terminal route work items such as
468
+ failed, dead-letter, cancelled, or succeeded history stay deduped until an
469
+ operator runs `loops routes requeue <work-item-id> --reason "<cause fixed>"`.
470
+ The next route-created output records `requeue` evidence with the previous work
471
+ item id, previous attempts, operator reason, new attempt, workflow id, and loop
472
+ id.
473
+
474
+ Task route drains can select providers from task metadata instead of running one
475
+ fixed provider/account pool for the whole drain. Add one or more
476
+ `--provider-rule field=value:provider[:profile1,profile2]` flags; the first
477
+ matching rule wins. Rule profiles become a Codewith auth-profile pool for
478
+ `provider=codewith` and an OpenAccounts account pool for other providers. Tasks
479
+ can also carry `provider_hint`/`route_provider`, `auth_profile_pool`, or
480
+ `account_pool` metadata. Dry-run, drain evidence, and route invocation scope
481
+ include `providerRouting` so operators can see why a provider/account was
482
+ selected.
483
+
484
+ ```bash
485
+ loops routes drain todos-task \
486
+ --dry-run \
487
+ --provider-rule area=frontend:claude:claude-ui-a,claude-ui-b \
488
+ --provider-rule area=backend:codewith:account004,account005 \
489
+ --worktree-mode required
490
+ ```
491
+
492
+ PR approval or merge tasks that need a branch-protection review must carry
493
+ explicit non-author GitHub reviewer evidence before a worker is created. When a
494
+ task has a PR reference plus `reviewDecision=REVIEW_REQUIRED`,
495
+ `mergeStateStatus=BLOCKED`, branch-protection review language, or similar
496
+ approval/merge intent, routing is skipped unless `--github-reviewer`,
497
+ `--github-reviewer-pool`, or task metadata such as `github_reviewer` /
498
+ `github_reviewer_pool` names at least one GitHub login different from the PR
499
+ author. This blocks self-review routes before they can spawn an impossible
500
+ worker, and dry-run/admission JSON includes `prReviewRouting` evidence.
501
+
405
502
  By default, `todos-task` routes use `todos-task-worker-verifier` for backwards
406
503
  compatibility. Use `--template task-lifecycle` to run the full triage ->
407
504
  planner -> worker -> verifier lifecycle. The route rejects unrelated templates
408
505
  such as `pr-review` so a todos task cannot accidentally use the wrong contract.
506
+ The default worker/verifier template starts with a deterministic
507
+ `source-task-gate` command that runs `todos --project <source-store> --json
508
+ inspect <task-id>` before the worker. If the routed source task cannot be
509
+ resolved in the intended Todos store, the workflow fails before repo-mutating
510
+ agent work starts.
409
511
  The lifecycle template adds deterministic gates after triage and planning. If
410
512
  either step marks the task blocked, omits its contextual
411
513
  `openloops:triage=go task=<id> event=<event-id>` /
412
514
  `openloops:planner=go task=<id> event=<event-id>` marker comment, or the task
413
- is no-auto/manual/approval-required, the worker step is not started. Use
515
+ is blocked/completed/done/cancelled/failed/archived/no-auto/manual/
516
+ approval-required, the worker step is not started. Use
414
517
  `--triage-auth-profile`, `--planner-auth-profile`, `--worker-auth-profile`, and
415
518
  `--verifier-auth-profile` when exact Codewith profiles are needed, or use
416
519
  `--auth-profile-pool` for deterministic role rotation.
@@ -460,6 +563,7 @@ loops routes drain todos-task --task-list oss --max-dispatch 2 --compact
460
563
  loops routes schedule todos-task route-drain-oss-5m --every 5m --task-list oss --max-dispatch 1 --compact
461
564
  loops routes list --route-key todos-task
462
565
  loops routes show <work-item-id>
566
+ loops routes requeue <work-item-id> --reason "fixed upstream blocker"
463
567
  loops routes invocations
464
568
  ```
465
569
 
@@ -549,6 +653,72 @@ When a loop or workflow is used for execution, keep `HASNA_AUTOMATIONS_DIR`
549
653
  pointing at the owning OpenAutomations data root and preserve the runner id in
550
654
  completion/failure calls so OpenAutomations can enforce action leases.
551
655
 
656
+ ### Planned Workflow Upsert SDK
657
+
658
+ External compilers such as `@hasna/actions` and `@hasna/automations` should not
659
+ write OpenLoops SQLite rows directly. The stable contract should be an
660
+ idempotent CLI/SDK upsert that accepts a fully rendered one-shot workflow loop
661
+ request and returns durable refs.
662
+
663
+ Proposed SDK shape:
664
+
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`.
716
+
717
+ The same design doc also defines the planned strict automation execution mode:
718
+ minimal env inheritance, scoped secret refs, enforced allowlists,
719
+ redaction-before-persistence, and provider-safe defaults for automation-created
720
+ work.
721
+
552
722
  ## Transcript-Driven Loops
553
723
 
554
724
  OpenLoops can turn long-form media or meeting transcripts into recurring workflow work when paired with `iapp-transcriber`. The template at `docs/workflows/transcript-feedback-to-loops.json` transcribes an authorized media URL, asks an agent to extract recurring loop candidates, authors workflow specs, and validates generated workflows before scheduling. Copy it into the target repo, replace `/path/to/repo` with that repo's absolute path, and provide `TRANSCRIBER_SOURCE_URL` through the runner environment or a private, uncommitted workflow copy before storing or scheduling it. Do not commit private or signed media URLs.
@@ -608,10 +778,14 @@ directory stay as tasks and record an `auto_route_skipped_reason`. Without
608
778
  `--auto-route`, the command only creates or updates deduped tasks.
609
779
 
610
780
  `hygiene names` reports canonical `machine-*` or `repo-<name>-*` loop names and
611
- renames only with `--apply`. Apply mode writes a SQLite backup under
612
- `<LOOPS_DATA_DIR>/backups` before changing loop names. `hygiene duplicates`
613
- groups loops with the same normalized name, cwd, and schedule. `hygiene scripts`
614
- inventories loops whose command still references `~/.hasna/loops/scripts`.
781
+ renames only with `--apply`. Cadence/timer suffixes such as `-5m`, `-15m`,
782
+ `-6h`, `-hourly`, and `-daily` are removed from canonical names; cadence belongs
783
+ in the schedule metadata and the human `loops list` `cadence=` column. Apply
784
+ mode writes a SQLite backup under `<LOOPS_DATA_DIR>/backups` before changing loop
785
+ names. New loops get a compact default description with Why/How/Outcome text
786
+ unless the operator supplies `--description`. `hygiene duplicates` groups loops
787
+ with the same normalized name, cwd, and schedule. `hygiene scripts` inventories
788
+ loops whose command still references `~/.hasna/loops/scripts`.
615
789
  `hygiene route-tasks` upserts deduped Todos tasks for hygiene findings with
616
790
  stable fingerprints and `no_tmux_dispatch=true` metadata. Route commands use a
617
791
  package-managed cursor under `<LOOPS_DATA_DIR>/route-cursors.json` so bounded
@@ -694,7 +868,7 @@ The adapters intentionally use provider command surfaces instead of pretending e
694
868
 
695
869
  - Claude uses `claude -p --output-format json` and safe-mode/local setting sources by default.
696
870
  - Codewith uses `codewith --ask-for-approval never exec --json --ephemeral --skip-git-repo-check`, with `--add-dir` for explicit extra writable directories.
697
- - AI Copilot and OpenCode use `run --format json --pure`.
871
+ - AI Copilot and OpenCode use `run --format json --pure`. OpenCode requires an explicit provider/model id because ambient OpenCode config is machine-specific.
698
872
  - Cursor is CLI-first for now via the standalone `agent -p` binary. OpenLoops no longer falls back to `cursor agent`; install the standalone Cursor Agent CLI so preflight and scheduled runs use the same executable.
699
873
  - Codex uses `codex --ask-for-approval never exec --json --ephemeral --skip-git-repo-check`, with `--add-dir` for explicit extra writable directories where supported.
700
874
  - Agent prompts are sent through child stdin instead of argv so prompt bodies do not appear in process listings.