@hasna/loops 0.4.27 → 0.4.29

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 (79) hide show
  1. package/CHANGELOG.md +205 -1
  2. package/README.md +232 -49
  3. package/dist/api/index.d.ts +20 -19
  4. package/dist/api/index.js +7770 -1342
  5. package/dist/cli/index.js +2976 -966
  6. package/dist/cli/safe-error-context.d.ts +1 -0
  7. package/dist/daemon/daemon.d.ts +1 -0
  8. package/dist/daemon/index.js +1899 -499
  9. package/dist/generated/storage-kit/index.d.ts +1 -1
  10. package/dist/generated/storage-kit/mode.d.ts +6 -12
  11. package/dist/index.d.ts +3 -3
  12. package/dist/index.js +5319 -1096
  13. package/dist/lib/advancement.d.ts +52 -0
  14. package/dist/lib/agent-adapter.d.ts +20 -2
  15. package/dist/lib/auth/route-policy.d.ts +14 -0
  16. package/dist/lib/auth/tenant-auth.d.ts +38 -0
  17. package/dist/lib/cloud/mode.d.ts +2 -8
  18. package/dist/lib/cloud/storage.d.ts +1 -2
  19. package/dist/lib/cloud/transport.d.ts +4 -18
  20. package/dist/lib/errors.d.ts +43 -1
  21. package/dist/lib/executor.d.ts +9 -0
  22. package/dist/lib/format.d.ts +2 -2
  23. package/dist/lib/goal/runner.d.ts +36 -3
  24. package/dist/lib/health.d.ts +1 -1
  25. package/dist/lib/labels.d.ts +4 -0
  26. package/dist/lib/loop-status.d.ts +4 -0
  27. package/dist/lib/migration.d.ts +70 -17
  28. package/dist/lib/mode.d.ts +2 -5
  29. package/dist/lib/mode.js +28 -37
  30. package/dist/lib/route/fields.d.ts +8 -0
  31. package/dist/lib/route/index.d.ts +2 -2
  32. package/dist/lib/route/throttle.d.ts +7 -0
  33. package/dist/lib/route/types.d.ts +3 -0
  34. package/dist/lib/run-completion.d.ts +18 -0
  35. package/dist/lib/scheduler.d.ts +5 -11
  36. package/dist/lib/storage/contract.d.ts +15 -1
  37. package/dist/lib/storage/index.d.ts +1 -1
  38. package/dist/lib/storage/index.js +4083 -486
  39. package/dist/lib/storage/pg-executor.d.ts +10 -5
  40. package/dist/lib/storage/postgres-loop-storage.d.ts +52 -26
  41. package/dist/lib/storage/postgres-schema.d.ts +8 -0
  42. package/dist/lib/storage/postgres-schema.js +1326 -1
  43. package/dist/lib/storage/postgres.d.ts +3 -1
  44. package/dist/lib/storage/postgres.js +1356 -27
  45. package/dist/lib/storage/provider-credentials.d.ts +84 -0
  46. package/dist/lib/storage/shared-database-transfer.d.ts +136 -0
  47. package/dist/lib/storage/sqlite.d.ts +15 -2
  48. package/dist/lib/storage/sqlite.js +1379 -217
  49. package/dist/lib/storage/tenant-backfill-s3.d.ts +53 -0
  50. package/dist/lib/storage/tenant-backfill.d.ts +40 -0
  51. package/dist/lib/store/index.d.ts +14 -8
  52. package/dist/lib/store.d.ts +129 -7
  53. package/dist/lib/store.js +1352 -218
  54. package/dist/lib/templates.d.ts +11 -0
  55. package/dist/lib/workflow-events.d.ts +6 -0
  56. package/dist/lib/workflow-provenance.d.ts +8 -0
  57. package/dist/lib/workflow-runner.d.ts +52 -4
  58. package/dist/mcp/index.js +2074 -657
  59. package/dist/runner/index.d.ts +20 -2
  60. package/dist/runner/index.js +2146 -183
  61. package/dist/sdk/http.d.ts +364 -4
  62. package/dist/sdk/http.js +379 -1
  63. package/dist/sdk/index.d.ts +7 -2
  64. package/dist/sdk/index.js +2341 -742
  65. package/dist/serve/index.d.ts +14 -0
  66. package/dist/serve/index.js +13269 -1830
  67. package/dist/types.d.ts +75 -3
  68. package/docs/AUTOMATION_RUNTIME_DESIGN.md +32 -32
  69. package/docs/CUTOVER-RUNBOOK.md +158 -31
  70. package/docs/DEPLOYMENT_MODES.md +84 -56
  71. package/docs/RUNTIME_BOUNDARY.md +26 -26
  72. package/docs/SHARED-DATABASE-TRANSFER.md +95 -0
  73. package/docs/SHARED_KIT_EXTRACTION_INVENTORY.md +631 -0
  74. package/docs/TRANSCRIPT_LOOP_PATTERNS.md +3 -3
  75. package/docs/UNIFIED_PRODUCT_CONTRACT.md +365 -0
  76. package/docs/USAGE.md +150 -56
  77. package/docs/workflows/transcript-feedback-to-loops.json +2 -2
  78. package/package.json +8 -4
  79. package/dist/lib/storage/pg-runner-claim.d.ts +0 -40
package/docs/USAGE.md CHANGED
@@ -1,6 +1,6 @@
1
- # OpenLoops
1
+ # Loops
2
2
 
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.
3
+ Loops 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
5
  It supports deterministic command loops, JSON-defined workflows, and guarded CLI adapters for headless coding agents:
6
6
 
@@ -13,18 +13,19 @@ It supports deterministic command loops, JSON-defined workflows, and guarded CLI
13
13
 
14
14
  ## Deployment Modes
15
15
 
16
- OpenLoops defaults to `local`, where SQLite in `LOOPS_DATA_DIR` is
16
+ Loops defaults to `local`, where SQLite in `LOOPS_DATA_DIR` is
17
17
  authoritative and `loops-daemon` executes scheduled work. The package also
18
18
  defines `self_hosted` and `cloud` contracts for non-local control planes:
19
19
 
20
20
  - `self_hosted`: the Hasna-owned AWS/RDS control-plane deployment, served by
21
21
  `loops-serve` and backed by Postgres, with the embeddable `loops-api` contract
22
22
  shared by serve, SDK, and tests. This release exposes storage-backed `/v1`
23
- loop CRUD and run listing, runner registration/claim/heartbeat/finalize
24
- foundations, and local migration previews.
23
+ loop CRUD and run listing, runner claim/lease heartbeat/finalize foundations,
24
+ and local migration previews. Durable runner registration is not exposed
25
+ until the machine-record lifecycle is implemented.
25
26
  - `cloud`: hosted control-plane contract; this release exposes client/runner
26
- status only, and requires `LOOPS_CLOUD_API_URL` plus `LOOPS_CLOUD_TOKEN` or
27
- `HASNA_LOOPS_CLOUD_TOKEN` before status can report ready.
27
+ status only, and requires `HASNA_LOOPS_API_URL` plus
28
+ `HASNA_LOOPS_API_KEY` before status can report ready.
28
29
 
29
30
  Scheduler state is explicit in status JSON. `schedulerState.localStore` is
30
31
  SQLite plus local run artifact files: authoritative in `local`, cache/spool in
@@ -48,12 +49,11 @@ loops self-hosted status
48
49
  loops self-hosted migrate --dry-run
49
50
  loops self-hosted push --dry-run
50
51
  loops self-hosted pull --dry-run
51
- loops self-hosted runner-register --runner-id <id> --machine-id <machine>
52
- loops self-hosted runner-register --runner-id <id> --machine-id <machine> --apply
53
52
  loops cloud status
54
53
  loops-api status
55
54
  loops-serve version
56
- HASNA_LOOPS_DATABASE_URL=... loops-serve migrate --dry-run
55
+ HASNA_LOOPS_MIGRATOR_DATABASE_URL=... loops-serve migrate --dry-run
56
+ HASNA_LOOPS_DATABASE_URL=... HASNA_LOOPS_AUTH_DATABASE_URL=... loops-serve serve
57
57
  loops-runner status
58
58
  ```
59
59
 
@@ -86,27 +86,30 @@ leases, running runs, leased work items). Inline command env values are not
86
86
  exported as secrets; bundles with redacted env values require
87
87
  `--allow-redacted` and are marked non-importable.
88
88
 
89
- Self-hosted sync commands are preview-only until the control-plane API exposes
90
- id-preserving import endpoints:
89
+ Self-hosted sync commands compare local definitions with the control-plane API.
90
+ `push` can apply through the id-preserving `/v1/import` endpoint; `migrate` and
91
+ `pull` remain preview/blocked for remote state that lacks a full export surface:
91
92
 
92
93
  ```bash
93
94
  loops self-hosted migrate --dry-run
94
95
  loops self-hosted push --dry-run
96
+ loops self-hosted push --apply --manifest-file ./self-hosted-push.json
95
97
  loops self-hosted pull --dry-run
96
98
  ```
97
99
 
98
- The preview may inspect `LOOPS_API_URL`/`HASNA_LOOPS_API_URL`, but it refuses
99
- remote apply because normal loop CRUD would generate new ids. Use
100
- `loops self-hosted runner-register` to verify runner registration against an
101
- API, then use `loops-runner run-once` for the current bounded non-workflow
102
- claim/execute/finalize protocol. `loops-serve migrate` applies the Postgres
103
- schema and `api_keys` table for a self-hosted control-plane host; the standalone
104
- CLI migration previews still do not perform id-preserving remote apply.
105
- Runner registration is preview-only unless `--apply` is present.
100
+ Self-hosted push is safe by default: workflows are archived and loops are
101
+ paused with scheduling pointers cleared, including existing same-id rows that
102
+ need re-neutralizing. `--replace` permits broader same-id data updates, but is
103
+ not required for that safety normalization. `loops-runner run-once` uses the
104
+ current bounded non-workflow
105
+ claim/execute/finalize protocol. Durable runner registration is intentionally
106
+ absent until the control plane persists and verifies machine records.
107
+ `loops-serve migrate` applies the Postgres schema and `api_keys` table for a
108
+ self-hosted control-plane host.
106
109
 
107
110
  ## Install
108
111
 
109
- **OpenLoops requires the [Bun](https://bun.sh) runtime (`bun >= 1.0`).** The
112
+ **Loops requires the [Bun](https://bun.sh) runtime (`bun >= 1.0`).** The
110
113
  installed binaries are `loops`, `loops-daemon`, `loops-api`, `loops-serve`,
111
114
  `loops-runner`, and `loops-mcp`; each uses a `#!/usr/bin/env bun` shebang.
112
115
 
@@ -140,7 +143,7 @@ The CLI stores state in `~/.hasna/loops` by default. Set `LOOPS_DATA_DIR` to iso
140
143
 
141
144
  ## MCP Server
142
145
 
143
- OpenLoops ships a stdio MCP server for safe loop and workflow inspection from
146
+ Loops ships a stdio MCP server for safe loop and workflow inspection from
144
147
  MCP-capable agents:
145
148
 
146
149
  ```bash
@@ -157,6 +160,7 @@ import { createLoopsMcpServer } from "@hasna/loops/mcp";
157
160
  Available read tools include `loops_list`, `loops_show`, `loops_runs`,
158
161
  `loops_doctor`, `loops_workflows_list`, `loops_workflow_read`, and
159
162
  `loops_workflow_validate`.
163
+ `loops_list` and `loops_runs` accept label arrays with AND semantics.
160
164
  Resources are available at `loops://runtime` and `loops://tools`.
161
165
  Those tools use the same `Store`, public redaction helpers, and workflow parser
162
166
  as the CLI and SDK, so read output and validation behavior stay aligned across
@@ -166,12 +170,16 @@ Mutation tools are disabled by default. Start the server with
166
170
  `LOOPS_MCP_ALLOW_MUTATIONS=true` only for a trusted local MCP host that should be
167
171
  allowed to change loop state. The guarded mutation tools use canonical names:
168
172
  `loops_pause`, `loops_resume`, `loops_stop`, `loops_run_now`, `loops_archive`,
169
- `loops_unarchive`, `loops_create_command`, and `loops_create_workflow`.
173
+ `loops_unarchive`, `loops_labels_update`, `loops_create_command`, and
174
+ `loops_create_workflow`.
170
175
  Deprecated `loop_*` aliases are still registered where compatibility needs them,
171
176
  but callers should use the `loops_*` names. Mutation tools do not require or
172
177
  accept confirmation-string parameters; the server-side environment opt-in is the
173
178
  gate. MCP `loops_run_now` schedules the loop for immediate daemon pickup; inline
174
179
  execution remains CLI-only.
180
+ When `showOutput` is enabled, `loops_runs` caps each stdout/stderr field at
181
+ 32,000 characters, caps output-bearing pages at 25 runs, and caps the aggregate
182
+ MCP JSON response at 128,000 characters.
175
183
 
176
184
  Keep host-affecting or long-running operations on the CLI: daemon
177
185
  start/stop/install/logs, inline `run-now`, `tick`, loop deletion, workflow
@@ -205,6 +213,16 @@ Codewith auth profile, or workflow step dependency fails without creating a
205
213
  scheduled loop. Use `--json` with `--preflight` to capture stable machine-readable
206
214
  preflight evidence.
207
215
 
216
+ For Codewith `--auth-profile` and workflow `authProfile`, local and remote
217
+ preflight first request `codewith profile list --json` and match the requested
218
+ name exactly against the root `data` or `profiles` inventory. That inventory is
219
+ authoritative: `usable: false` rejects a profile, legacy entries without
220
+ `usable` remain usable, and `currentProfile` does not add inventory membership.
221
+ Loops falls back to the human-readable table, including active `*` rows,
222
+ only when JSON mode is unsupported or its inventory is structurally
223
+ unavailable. Embedded NUL bytes, missing profiles, and non-fallback
224
+ profile-list failures fail closed.
225
+
208
226
  For shell command loops, preflight can only verify the shell plus configured
209
227
  accounts because the command string is interpreted later by the shell. Use
210
228
  `--no-shell` or workflow command `args` when you need executable-level
@@ -278,8 +296,34 @@ loops create agent supply-chain-watch \
278
296
  --prompt "Check for suspicious dependency or supply-chain changes. Report only concrete findings."
279
297
  ```
280
298
 
299
+ Codewith `--auth-profile` is provider-native, not an OpenAccounts selector.
300
+ Use `--account` only when you want OpenAccounts environment isolation.
301
+
302
+ ## Labels
303
+
304
+ Command, agent, and workflow loops accept repeatable labels:
305
+
306
+ ```bash
307
+ loops create command repo-status --every 1m --cmd "git status --short" \
308
+ --label BrowserPlan --label maintenance
309
+ loops list --label browserplan --label maintenance
310
+ loops runs --label browserplan
311
+ ```
312
+
313
+ Labels are normalized to lowercase, deduplicated, limited to 32 per loop, and
314
+ must match `[a-z0-9][a-z0-9._-]{0,63}` after normalization. Repeated filters use
315
+ AND semantics. Run filtering uses the loop's current labels rather than a
316
+ historical snapshot on each run.
317
+
318
+ ```bash
319
+ loops labels add repo-status urgent
320
+ loops labels remove repo-status maintenance
321
+ loops labels set repo-status browserplan nightly
322
+ loops labels clear repo-status
323
+ ```
324
+
281
325
  Run an OpenCode loop with an explicit provider/model. OpenCode reads
282
- `~/.config/opencode/config.json` when no model is supplied, so OpenLoops rejects
326
+ `~/.config/opencode/config.json` when no model is supplied, so Loops rejects
283
327
  OpenCode agent targets without `--model` instead of inheriting a stale or
284
328
  machine-specific default.
285
329
 
@@ -292,7 +336,7 @@ loops create agent opencode-smoke \
292
336
  --prompt "Reply with exactly OK."
293
337
  ```
294
338
 
295
- Agent loops can also carry advisory per-session allowlist metadata:
339
+ Agent loops can also carry an auditable advisory session contract:
296
340
 
297
341
  ```bash
298
342
  loops create agent repo-check \
@@ -301,13 +345,25 @@ loops create agent repo-check \
301
345
  --cwd /path/to/repo \
302
346
  --prompt "Check the repo and report concrete failures." \
303
347
  --allow-tool functions.exec_command \
304
- --allow-command git,bun
348
+ --allow-command git,bun \
349
+ --safety-reason "isolated repository status inspection"
305
350
  ```
306
351
 
307
- These fields are stored on the loop target and exposed to the run environment
308
- as `LOOPS_AGENT_ALLOWED_TOOLS`, `LOOPS_AGENT_ALLOWED_COMMANDS`, and
309
- `LOOPS_AGENT_ALLOWLIST_ENFORCEMENT=metadata_only`. They are not enforced by
310
- OpenLoops yet; provider-native enforcement will be added separately.
352
+ These fields are stored on the loop target, appended to provider stdin, and
353
+ exposed as `LOOPS_AGENT_ALLOWED_TOOLS`, `LOOPS_AGENT_ALLOWED_COMMANDS`,
354
+ `LOOPS_AGENT_ALLOWLIST_SAFETY_REASON`,
355
+ `LOOPS_AGENT_ALLOWLIST_ENFORCEMENT=metadata_only`, and
356
+ `LOOPS_AGENT_SESSION_CONTRACT`. A direct agent loop has no workflow run and
357
+ does not create a workflow event. Agent steps inside workflow runs that carry
358
+ an audit contract additionally persist one server-derived
359
+ `agent_session_contract` event per step and run.
360
+ Tool/command restrictions remain advisory metadata: current provider adapters
361
+ do not claim native enforcement.
362
+
363
+ Codewith/Codex `danger-full-access`, Cursor `sandbox=disabled`, and provider
364
+ bypass modes (Claude, Cursor, AI Copilot, and OpenCode) require both a non-empty
365
+ `--safety-reason` and explicit `--manual-break-glass`. A reason alone never
366
+ substitutes for operator break-glass approval.
311
367
 
312
368
  For `codewith` and `aicopilot` account isolation, register matching OpenAccounts tools first if they are not built in on the machine:
313
369
 
@@ -354,7 +410,7 @@ from the workflow JSON file's directory:
354
410
  }
355
411
  ```
356
412
 
357
- OpenLoops stores the resolved prompt text for execution and records
413
+ Loops stores the resolved prompt text for execution and records
358
414
  `promptSource` metadata with the absolute file path. Public CLI output from
359
415
  `show`, `list`, `workflows list`, `workflows show`, `workflows validate`,
360
416
  `workflows create`, and `templates render` redacts prompt bodies by default
@@ -443,8 +499,8 @@ for those. Set `"blockedExitCodes": []` on a gate-named step to opt out.
443
499
 
444
500
  ## Templates And Task Events
445
501
 
446
- OpenLoops is the runtime/scheduler/workflow engine for these flows — not the
447
- automation domain model. Todos-task routes are OpenLoops-native admission; they
502
+ Loops is the runtime/scheduler/workflow engine for these flows — not the
503
+ automation domain model. Todos-task routes are Loops-native admission; they
448
504
  do not replace the OpenAutomations product queue. See
449
505
  [Runtime Boundary](./RUNTIME_BOUNDARY.md) for ownership split and external
450
506
  compiler handoff paths (`@hasna/automations` claim-queue,
@@ -503,7 +559,7 @@ loops templates render routing-remediation \
503
559
  --var idempotencyKey=routing-health:repo:shard0
504
560
  ```
505
561
 
506
- Custom reusable workflow templates live under the OpenLoops app data directory:
562
+ Custom reusable workflow templates live under the Loops app data directory:
507
563
  `~/.hasna/loops/templates` by default, or `$LOOPS_DATA_DIR/templates` when
508
564
  `LOOPS_DATA_DIR` is set. Store templates as declarative JSON files; listing,
509
565
  showing, and rendering templates never executes workflow steps or mutates the
@@ -609,6 +665,8 @@ arguments, and implicit Codewith/Codex full-access defaults. If a custom
609
665
  Codewith/Codex template uses `permissionMode: "bypass"`, it must also set
610
666
  `sandbox` to `workspace-write` or `read-only`. Use built-in templates with
611
667
  explicit break-glass handling for emergency workflows that need full access.
668
+ Claude, Cursor, AI Copilot, and OpenCode bypass modes always require explicit
669
+ break-glass plus a non-empty safety reason.
612
670
 
613
671
  Repo-mutating task/event routes should set `worktreeMode=required` so the
614
672
  workflow fails fast instead of falling back to the main checkout. When
@@ -623,7 +681,7 @@ inspection expose the exact checkout.
623
681
  Before a worker starts, worktree preparation verifies that any existing
624
682
  managed path is a real git worktree with the same top-level checkout and the
625
683
  same git common directory as the source repo. If the checkout is on a detached
626
- HEAD or unexpected branch, OpenLoops only reattaches it to the expected
684
+ HEAD or unexpected branch, Loops only reattaches it to the expected
627
685
  generated branch when the worktree is clean. Dirty worktrees, symlinked paths,
628
686
  non-worktree paths, different common dirs, and unrecoverable branch switches
629
687
  fail closed with cleanup evidence (`worktreeMode=required`) instead of
@@ -744,13 +802,26 @@ cat task-created-event.json | loops routes create todos-task \
744
802
  --max-active 12
745
803
  ```
746
804
 
747
- The limits count active admitted/running OpenLoops work items once per workflow.
805
+ The limits count active admitted/running Loops work items once per workflow.
748
806
  `--max-active-per-project` gates new work for the same project path,
749
807
  `--max-active-per-project-group` shares a pool across related projects such as
750
808
  `oss`, and `--max-active` is the global routed-workflow cap. Project matching
751
809
  uses the canonical git top-level path when available, so repo subdirectories
752
- share the same project cap. A throttled event records a deferred OpenLoops
810
+ share the same project cap. A throttled event records a deferred Loops
753
811
  admission work item with JSON evidence instead of creating another worker loop.
812
+ Route flags or an expanded named policy are authoritative ceilings. Positive
813
+ integer task/event fields (`max_active`, `max_active_per_project`, and
814
+ `max_active_per_project_group`, including camel-case aliases) may only lower an
815
+ already configured ceiling for that admission attempt; metadata cannot create a
816
+ cap, raise it, or override an explicit `--project-group`. The resolved route
817
+ scope, project group, and limits are copied into workflow prompts and invocation
818
+ evidence.
819
+
820
+ Admission caps coordinate only through the active local SQLite store. The count
821
+ and admission write are one transaction, so concurrent routers sharing the same
822
+ `loops.db` serialize correctly. Separate `LOOPS_DATA_DIR` databases or machines
823
+ do not share counts; these flags do not provide distributed coordination.
824
+
754
825
  Re-delivering the event later is safe because handlers dedupe by the work-item
755
826
  idempotency key before rendering worktree plans or checking route limits. In
756
827
  dry-run mode, throttle counts are not evaluated because opening the live loop
@@ -812,7 +883,7 @@ The `pilot` policy uses `sandbox=danger-full-access` and is treated as a paused
812
883
  manual break-glass lane; applying it requires the operator to pass
813
884
  `--manual-break-glass` explicitly.
814
885
 
815
- When a workflow run starts from an admitted work item, OpenLoops writes a
886
+ When a workflow run starts from an admitted work item, Loops writes a
816
887
  manifest under:
817
888
 
818
889
  ```text
@@ -860,9 +931,9 @@ every candidate, so a drain can safely run every few minutes as a deterministic
860
931
  command loop: it fills only available capacity, writes compact JSON evidence
861
932
  when requested, and leaves excess ready tasks in todos for a later drain pass.
862
933
  Use `--dry-run` to preview candidates and rendered workflows without mutating
863
- OpenLoops state.
934
+ Loops state.
864
935
 
865
- The route throttle flags count OpenLoops routed workflow work items, not the
936
+ The route throttle flags count Loops routed workflow work items, not the
866
937
  live background-agent slots inside a provider. For Codewith drains, add
867
938
  `--provider-active-cap <n>` (or `--codewith-active-cap <n>`) so each candidate
868
939
  checks `codewith agent diagnostics --json` before workflow-loop creation and
@@ -902,7 +973,7 @@ in with the `auto:route` tag, `route_enabled=true`, or
902
973
  `automation.allowed=true` should be routed. Keep repo-mutating worker/verifier
903
974
  runs on a Codewith account pool with `--worktree-mode required`. Do not dispatch
904
975
  or paste task prompts into tmux panes. Use max-active throttles and
905
- `--max-dispatch` to bound OpenLoops agent fan-out, and use
976
+ `--max-dispatch` to bound Loops agent fan-out, and use
906
977
  `--provider-active-cap` plus `--provider-admission-check` to bound live Codewith
907
978
  background-agent admission. Write compact evidence into a bounded reports
908
979
  directory so operators can audit each drain without unbounded stdout or loop
@@ -912,7 +983,7 @@ correctly do no work.
912
983
 
913
984
  Generated task/event route workflow specs are lifecycle-managed. After a
914
985
  generated one-shot route workflow run reaches `succeeded`, `failed`,
915
- `timed_out`, or `cancelled`, OpenLoops archives the generated workflow spec while
986
+ `timed_out`, or `cancelled`, Loops archives the generated workflow spec while
916
987
  preserving workflow run, step, event, manifest, loop, invocation, and work-item
917
988
  history.
918
989
 
@@ -930,19 +1001,19 @@ cat event.json | loops routes create generic \
930
1001
  ```
931
1002
 
932
1003
  This is the intended deterministic-to-agentic path: a producer creates a todos
933
- task, `@hasna/events` delivers `task.created`, OpenLoops records the invocation
934
- and admission item, OpenLoops creates a worker/verifier workflow when admitted,
1004
+ task, `@hasna/events` delivers `task.created`, Loops records the invocation
1005
+ and admission item, Loops creates a worker/verifier workflow when admitted,
935
1006
  and the workflow updates todos with evidence. Use account pools so worker and
936
- verifier steps do not burn the same profile; OpenLoops picks deterministically
1007
+ verifier steps do not burn the same profile; Loops picks deterministically
937
1008
  and uses a different verifier profile when the pool has at least two entries.
938
1009
  Use `--dry-run` to inspect the rendered invocation, work item, workflow, and
939
1010
  loop input without storing anything, including the worktree path and branch for
940
1011
  git-backed tasks.
941
1012
 
942
1013
  Generated worker/verifier workflows fail closed when `sandbox=danger-full-access`
943
- is requested without `manualBreakGlass=true`. Use `workspace-write` for
944
- unattended task/event routes. Full access is an explicit manual emergency path,
945
- not a default automation mode.
1014
+ is requested without both `manualBreakGlass=true` and a non-empty
1015
+ `safetyReason`. Use `workspace-write` for unattended task/event routes. Full
1016
+ access is an explicit manual emergency path, not a default automation mode.
946
1017
 
947
1018
  ## Run Receipts
948
1019
 
@@ -967,7 +1038,7 @@ mutation-gated `loops_receipt_write`. The SDK exposes `writeReceipt`,
967
1038
 
968
1039
  ## Transcript-Driven Loops
969
1040
 
970
- 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.
1041
+ Loops 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.
971
1042
 
972
1043
  ```bash
973
1044
  loops workflows validate /path/to/repo/.openloops/transcript-feedback-to-loops.json --preflight
@@ -1007,7 +1078,7 @@ loops expectations <loop-id-or-name> --json
1007
1078
 
1008
1079
  The JSON contains the expectation result, bounded error/stdout/stderr evidence,
1009
1080
  a stable failure fingerprint, route metadata, and recommended task fields.
1010
- OpenLoops does not mutate Todos from `health`, `expectations`, or read-only
1081
+ Loops does not mutate Todos from `health`, `expectations`, or read-only
1011
1082
  `health scan`. To turn failed expectations or scan findings into deduped tasks,
1012
1083
  use an explicit mutating command:
1013
1084
 
@@ -1056,7 +1127,7 @@ Failure classifications are: `rate_limit`, `auth`, `model_not_found`,
1056
1127
 
1057
1128
  ## Hygiene
1058
1129
 
1059
- OpenLoops includes deterministic hygiene checks for replacing local maintenance
1130
+ Loops includes deterministic hygiene checks for replacing local maintenance
1060
1131
  scripts with package commands:
1061
1132
 
1062
1133
  ```bash
@@ -1110,7 +1181,7 @@ Archived loops are hidden from the default `loops list`, excluded from daemon sc
1110
1181
 
1111
1182
  `loops run-now` reports the manual run source:
1112
1183
 
1113
- - `source=ad_hoc`: the loop was not due yet, so OpenLoops created a one-off manual slot. This is a single immediate attempt and does not schedule retries or consume the future scheduled slot.
1184
+ - `source=ad_hoc`: the loop was not due yet, so Loops created a one-off manual slot. This is a single immediate attempt and does not schedule retries or consume the future scheduled slot.
1114
1185
  - `source=due_slot`: the persisted scheduled slot was already due, so the manual run claims that slot and advances or retries the loop using normal scheduler rules.
1115
1186
  - `source=retry_slot`: the loop was waiting on a failed slot retry, so the manual run claims that retry slot and advances the loop using normal retry rules.
1116
1187
 
@@ -1159,15 +1230,30 @@ On Linux this writes a user systemd service. On macOS it writes a LaunchAgent pl
1159
1230
  The adapters intentionally use provider command surfaces instead of pretending every agent has one SDK:
1160
1231
 
1161
1232
  - Claude uses `claude -p --output-format json` and safe-mode/local setting sources by default.
1162
- - Codewith runs non-interactive `codewith --ask-for-approval never exec --json` sessions by default. exec starts a fresh session per invocation, avoiding the multi-megabyte rollout history that `codewith agent start` reloaded every turn (which drove `context_length_exceeded` silent no-ops), and it keeps network egress for gh/git — the `workspace-write` sandbox opts back into `sandbox_workspace_write.network_access`. Codewith exec is remote-capable like codex. OpenLoops rejects Codewith `extraArgs` that try to force `exec`, `--ephemeral`, `--json`, or other exec launch flags that the adapter already manages.
1233
+ - Codewith runs non-interactive `codewith --ask-for-approval never exec --json` sessions by default. exec starts a fresh session per invocation, avoiding the multi-megabyte rollout history that `codewith agent start` reloaded every turn (which drove `context_length_exceeded` silent no-ops), and it keeps network egress for gh/git — the `workspace-write` sandbox opts back into `sandbox_workspace_write.network_access`. Codewith exec is remote-capable like codex.
1163
1234
  - AI Copilot and OpenCode use `run --format json --pure`. OpenCode requires an explicit provider/model id because ambient OpenCode config is machine-specific.
1164
- - 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.
1235
+ - Cursor is CLI-first for now via the standalone `agent -p` binary. Loops no longer falls back to `cursor agent`; install the standalone Cursor Agent CLI so preflight and scheduled runs use the same executable.
1165
1236
  - Codex uses `codex --ask-for-approval never exec --json --ephemeral --skip-git-repo-check`, with `--add-dir` for explicit extra writable directories where supported.
1166
1237
  - Agent prompts are sent through child stdin instead of argv where the provider supports stdin, including Codewith `exec` (which reads instructions from stdin when no positional prompt is given), so the prompt never lands on argv.
1167
- - When `--account` or a step `account` is set, OpenLoops resolves `accounts env <profile> --tool <tool>` before spawning the target, strips inherited tool home/API-key variables, and applies the selected profile only to that process. Missing account profiles fail before the provider binary receives the prompt.
1168
- - `--auth-profile` and step `authProfile` are provider-native auth selectors. They currently apply to Codewith and are passed to Codewith as `--auth-profile <name>` on the `exec` invocation; they do not call OpenAccounts.
1238
+ - When `--account` or a step `account` is set, Loops resolves `accounts env <profile> --tool <tool>` before spawning the target, strips inherited tool home/API-key variables, and applies the selected profile only to that process. Missing account profiles fail before the provider binary receives the prompt.
1239
+ - `--auth-profile` and step `authProfile` are provider-native Codewith selectors passed as `--auth-profile <name>` on the `exec` invocation; they do not call OpenAccounts. Local and remote preflight share the JSON-first, exact-name contract described above, use human-table parsing only as a compatibility fallback, and fail closed for unusable or missing profiles, NUL-containing names, and non-fallback list failures.
1169
1240
  - `--sandbox` maps to provider-native sandbox flags. Codewith/Codex accept `read-only`, `workspace-write`, or `danger-full-access`; Cursor accepts `enabled` or `disabled`.
1241
+ - `--allow-tool` and `--allow-command` declare advisory, metadata-only
1242
+ restrictions and require `--safety-reason`. The exact contract is persisted
1243
+ and emitted for audit, but Loops does not claim provider-side enforcement.
1244
+ Relaxed sandboxes and native provider bypass modes also require explicit
1245
+ `--manual-break-glass`.
1170
1246
  - `--permission-mode` maps `plan`, `auto`, and `bypass` where the provider supports it. Claude uses native permission modes, Cursor maps bypass to `--force`, and OpenCode/AICopilot map bypass to `--dangerously-skip-permissions`.
1247
+ - `extraArgs` is fail-closed: every provider currently rejects non-empty
1248
+ passthrough arguments, including unknown options, positional subcommands,
1249
+ split values, `--option=value`, and attached short-option forms. Configure
1250
+ execution, output, permissions, sandboxing, model, cwd, and other supported
1251
+ behavior through the modeled agent-target fields. A passthrough option must
1252
+ be explicitly reviewed and added to the provider allowlist before use.
1253
+ Legacy persisted targets are not silently accepted or rewritten: execution
1254
+ fails validation until `extraArgs` is removed and replaced with modeled
1255
+ fields. API and migration imports reject those targets instead of stripping
1256
+ the arguments; update the source record and retry the import.
1171
1257
  - `--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`.
1172
1258
  - Daemon and scheduled runs prepend common user executable directories such as `~/.local/bin` and `~/.bun/bin` before resolving provider CLIs.
1173
1259
  - Agent targets that set neither `timeoutMs` nor `idleTimeoutMs` get a default
@@ -1180,6 +1266,14 @@ The adapters intentionally use provider command surfaces instead of pretending e
1180
1266
  `timeoutMs`/`idleTimeoutMs` per target — `"timeoutMs": null` opts a target
1181
1267
  out of both the wall-clock default and the idle watchdog.
1182
1268
 
1269
+ For hosted workflow runs, the control plane derives any required agent session
1270
+ contract from the stored workflow step and persists it before execution. Reusing an
1271
+ older workflow run backfills a missing valid contract idempotently. Stored
1272
+ pre-contract workflows with unsafe/invalid agent options, mismatched stored
1273
+ contracts, duplicate contracts, command-step contracts, and client-fabricated
1274
+ contracts fail closed. Direct agent loops continue to expose their contract
1275
+ through prompt/environment metadata only.
1276
+
1183
1277
  For production loops that can mutate repos, prefer the built-in
1184
1278
  `worktreeMode=auto`/`required` path and explicit prompts that name allowed write
1185
1279
  scope. Use `main` or `off` only for operations that intentionally need the
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "transcript-feedback-to-loops",
3
- "description": "Turn an authorized media transcript into a reviewable backlog of recurring OpenLoops workflows.",
3
+ "description": "Turn an authorized media transcript into a reviewable backlog of recurring Loops workflows.",
4
4
  "version": 1,
5
5
  "steps": [
6
6
  {
@@ -49,7 +49,7 @@
49
49
  "provider": "codewith",
50
50
  "cwd": "/path/to/repo",
51
51
  "timeoutMs": null,
52
- "prompt": "Read .openloops/transcripts/latest-transcript.json and create or update docs/loop-backlog.md. Extract only recurring work that is useful enough to schedule through OpenLoops. For each candidate include cadence, target provider, allowed write scope, expected artifacts, verification command, stop condition, and the specific transcript insight that justifies it. Prefer loops for code review/security, customer feedback triage, maintenance PRs, CI optimization, knowledge capture, and workflow hygiene."
52
+ "prompt": "Read .openloops/transcripts/latest-transcript.json and create or update docs/loop-backlog.md. Extract only recurring work that is useful enough to schedule through Loops. For each candidate include cadence, target provider, allowed write scope, expected artifacts, verification command, stop condition, and the specific transcript insight that justifies it. Prefer loops for code review/security, customer feedback triage, maintenance PRs, CI optimization, knowledge capture, and workflow hygiene."
53
53
  }
54
54
  },
55
55
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/loops",
3
- "version": "0.4.27",
3
+ "version": "0.4.29",
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",
@@ -79,11 +79,14 @@
79
79
  "build:bin": "bun build src/cli/index.ts --compile --outfile dist/loops",
80
80
  "typecheck": "tsc --noEmit",
81
81
  "test": "bun test",
82
+ "check:branding": "bun test scripts/check-branding.test.mjs && bun run scripts/check-branding.mjs",
82
83
  "test:boundary": "bun run scripts/no-private-cloud-boundary.mjs",
84
+ "check:contracts": "bun run scripts/check-storage-kit.mjs && bun run scripts/check-contract-conformance.mjs",
85
+ "check:supply-chain": "bun audit && bun audit --production && bun run check:contracts && bun run check:branding && bun pm pack --dry-run --ignore-scripts && bun run test:boundary && bun run scripts/check-packed-boundary.mjs",
83
86
  "prepare": "test -d dist || bun run build",
84
87
  "dev:cli": "bun run src/cli/index.ts",
85
88
  "dev:daemon": "bun run src/daemon/index.ts",
86
- "prepublishOnly": "bun run build && bun run typecheck && bun test && bun run test:boundary"
89
+ "prepublishOnly": "bun run build && bun run typecheck && bun test && bun run test:boundary && bun run check:supply-chain"
87
90
  },
88
91
  "keywords": [
89
92
  "loops",
@@ -110,6 +113,8 @@
110
113
  "bun": ">=1.0.0"
111
114
  },
112
115
  "dependencies": {
116
+ "@aws-sdk/client-secrets-manager": "^3.1083.0",
117
+ "@hasna/contracts": "0.5.2",
113
118
  "@hasna/events": "^0.1.9",
114
119
  "@modelcontextprotocol/sdk": "^1.29.0",
115
120
  "@openrouter/ai-sdk-provider": "2.9.1",
@@ -119,8 +124,7 @@
119
124
  "zod": "4.4.3"
120
125
  },
121
126
  "optionalDependencies": {
122
- "@hasna/machines": "0.0.49",
123
- "@hasna/contracts": "^0.4.2"
127
+ "@hasna/machines": "0.0.49"
124
128
  },
125
129
  "devDependencies": {
126
130
  "@types/bun": "latest",
@@ -1,40 +0,0 @@
1
- import type { PoolQueryClient } from "../../generated/storage-kit/query.js";
2
- export interface ClaimedRunRow extends Record<string, unknown> {
3
- id: string;
4
- loop_id: string;
5
- loop_name: string;
6
- scheduled_for: string | Date;
7
- attempt: number;
8
- status: string;
9
- claimed_by: string | null;
10
- claim_token: string | null;
11
- lease_expires_at: string | Date | null;
12
- }
13
- export interface ClaimNextRunOptions {
14
- runnerId: string;
15
- leaseMs: number;
16
- claimToken: string;
17
- /** Statuses a run may be in to be claimable. Defaults to queued + lease-expired running. */
18
- now?: Date;
19
- }
20
- /**
21
- * Atomically claim the next claimable run for `runnerId`.
22
- *
23
- * A run is claimable when it is `queued`, or `running` with an expired lease
24
- * (crash recovery). Returns the claimed row, or `null` when nothing was
25
- * available. Two concurrent callers are guaranteed to receive distinct rows
26
- * (or one receives `null`) thanks to `FOR UPDATE SKIP LOCKED`.
27
- */
28
- export declare function claimNextRun(client: PoolQueryClient, options: ClaimNextRunOptions): Promise<ClaimedRunRow | null>;
29
- /**
30
- * Heartbeat a claimed run's lease. Returns true when the lease was extended
31
- * (the caller still owns the run), false when ownership was lost (claim token
32
- * mismatch, run finalized, or lease already stolen).
33
- */
34
- export declare function heartbeatRunLease(client: PoolQueryClient, input: {
35
- runId: string;
36
- runnerId: string;
37
- claimToken: string;
38
- leaseMs: number;
39
- now?: Date;
40
- }): Promise<boolean>;