@hasna/loops 0.4.12 → 0.4.14

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 (41) hide show
  1. package/CHANGELOG.md +106 -3
  2. package/README.md +70 -17
  3. package/dist/api/index.d.ts +35 -0
  4. package/dist/api/index.js +750 -10
  5. package/dist/cli/index.js +1744 -361
  6. package/dist/cli/ui.d.ts +44 -0
  7. package/dist/daemon/index.js +948 -212
  8. package/dist/generated/storage-kit/health.d.ts +19 -0
  9. package/dist/generated/storage-kit/index.d.ts +7 -0
  10. package/dist/generated/storage-kit/migrations.d.ts +47 -0
  11. package/dist/generated/storage-kit/mode.d.ts +47 -0
  12. package/dist/generated/storage-kit/pool.d.ts +33 -0
  13. package/dist/generated/storage-kit/query.d.ts +35 -0
  14. package/dist/generated/storage-kit/tls.d.ts +25 -0
  15. package/dist/index.d.ts +3 -3
  16. package/dist/index.js +6513 -4840
  17. package/dist/lib/health.d.ts +83 -3
  18. package/dist/lib/mode.d.ts +27 -0
  19. package/dist/lib/mode.js +69 -2
  20. package/dist/lib/scheduler.d.ts +5 -2
  21. package/dist/lib/storage/index.d.ts +1 -0
  22. package/dist/lib/storage/index.js +1329 -211
  23. package/dist/lib/storage/pg-executor.d.ts +27 -0
  24. package/dist/lib/storage/pg-runner-claim.d.ts +40 -0
  25. package/dist/lib/storage/postgres-loop-storage.d.ts +114 -0
  26. package/dist/lib/storage/sqlite.js +336 -8
  27. package/dist/lib/store.d.ts +234 -0
  28. package/dist/lib/store.js +350 -8
  29. package/dist/mcp/index.js +1447 -479
  30. package/dist/runner/index.js +179 -25
  31. package/dist/sdk/http.d.ts +115 -0
  32. package/dist/sdk/http.js +145 -0
  33. package/dist/sdk/index.d.ts +5 -1
  34. package/dist/sdk/index.js +1106 -296
  35. package/dist/serve/index.d.ts +4 -0
  36. package/dist/serve/index.js +7619 -0
  37. package/dist/types.d.ts +3 -0
  38. package/docs/CUTOVER-RUNBOOK.md +61 -0
  39. package/docs/DEPLOYMENT_MODES.md +23 -1
  40. package/docs/USAGE.md +66 -16
  41. package/package.json +14 -2
package/CHANGELOG.md CHANGED
@@ -5,6 +5,59 @@ documented in this file. Version entries are generated from the
5
5
  conventional-commit git history; one commit maps to one released patch version
6
6
  unless noted.
7
7
 
8
+ ## 0.4.14 (2026-07-06)
9
+
10
+ Self-hosted control-plane service brought to the full Hasna standard: all four
11
+ surfaces (CLI, MCP, serve, SDK) are real over the Postgres backend, with
12
+ internet-facing API-key auth and a deployable ARM64 image.
13
+
14
+ ### Added
15
+
16
+ - **`loops-serve` HTTP control plane:** RDS-direct (Amendment A1) Postgres
17
+ storage wired into the serve; public `GET /health`, `/ready` (storage
18
+ reachable + fully migrated), `/version` (all `{status, version, mode}`) and
19
+ `/openapi.json`; the versioned `/v1` loops + runs API is gated behind
20
+ `@hasna/contracts` API-key auth (`verifyApiKey`, strict revocation via the
21
+ shared `api_keys` table). `loops-serve migrate` applies the ledger-tracked
22
+ schema + api_keys table.
23
+ - **Generated HTTP SDK:** `@hasna/loops/sdk/http` exports a typed dependency-free
24
+ `LoopsClient` generated from `openapi/loops.json` (the serve contract).
25
+ - **Deploy artifacts:** ARM64/bun `Dockerfile` (Amazon RDS CA baked for
26
+ verify-full TLS), `docker-compose.yml`, `hasna.contract.json` service
27
+ manifest, and a `migrations/` mirror of the ledger migrations.
28
+
29
+ ## 0.4.13 (2026-07-05)
30
+
31
+ `--pr-handoff` workflows whose worker pushes its own branch and opens the PR
32
+ directly (no handoff artifact — the cursor pattern) now complete instead of
33
+ failing the pr-handoff step and skipping the verifier.
34
+
35
+ ### Fixed
36
+
37
+ - **pr-handoff — no-artifact/direct-PR path exited 1 instead of 0:** the step
38
+ runs as `bash -lc` (a login shell). The no-artifact guard ended with an
39
+ explicit `exit 0` while `set -e` was active; under systemd (`SHLVL` unset →
40
+ bash sets 1) an explicit exit sources `~/.bash_logout`, whose `clear_console`
41
+ fails without a controlling TTY, and errexit handed that failure back as the
42
+ step's exit code. The workflow failed, the verifier (which depends on
43
+ pr-handoff) was skipped, and the source task was stranded `in_progress`.
44
+ Both guard branches now route through bun heredocs and fall through the `if`'s
45
+ natural end — no top-level shell `exit` — so the intended status is preserved
46
+ on the local and remote execution paths alike (gate steps already ended
47
+ naturally, which is why only pr-handoff failed). (#34)
48
+
49
+ ### Added
50
+
51
+ - **Worker-opened PR detection on the no-artifact path:** when no handoff
52
+ artifact exists, pr-handoff now looks up the worker's own open PR for the
53
+ workflow branch (`gh pr list --head <branch> --state open`), records the same
54
+ `openloops:pr-handoff=done task=… pr=… commit=… branch=…` comment the
55
+ artifact path records, and exits 0 — so direct-PR (cursor-style) completions
56
+ carry PR evidence into the verifier and merge lane. Best-effort and
57
+ fail-open on lookup: a missing PR or a `gh`/`git`/`todos` error is logged,
58
+ writes no done-evidence, and never fails the step. The artifact (codewith)
59
+ handoff path is unchanged. (#34)
60
+
8
61
  ## 0.4.12 (2026-07-05)
9
62
 
10
63
  Drain throughput: `--max-active` is now a per-route ceiling instead of a
@@ -445,13 +498,63 @@ CLI/MCP/SDK surface with deprecation aliases.
445
498
  npm installs), and `@hasna/machines` is now an optional dependency —
446
499
  installs without it simply disable remote-machine assignment.
447
500
 
501
+ ## 0.3.60 (2026-07-01)
502
+
503
+ Experimental Codewith durable-agent controller for long workflow steps.
504
+
505
+ ### Fixed
506
+
507
+ - **Codewith executor:** run Codewith agent steps through the durable
508
+ `codewith agent start` background-agent lifecycle with rollout progress
509
+ recording into workflow step runs.
510
+
511
+ Superseded in **0.4.9**: task-lifecycle and route workers now dispatch Codewith
512
+ via non-interactive `codewith exec --json` because `agent start` reloaded
513
+ multi-megabyte rollout history every turn and stalled workers with silent
514
+ `context_length_exceeded` completions.
515
+
516
+ ## 0.3.59 (2026-07-01)
517
+
518
+ Harden append-only workflow goal-wrapper migration.
519
+
520
+ ### Fixed
521
+
522
+ - **`migrate-goal-wrappers`:** dry-run and apply paths now use compact migration
523
+ summaries, block retarget while a loop run is active, and route failures
524
+ through `cloneWorkflowWithoutGoalAndRetargetLoop` so only loops with both a
525
+ loop-level goal and a workflow-level top-level goal migrate.
526
+ - **Store:** `cloneWorkflowWithoutGoalAndRetargetLoop` inserts a goal-free
527
+ workflow spec, retargets the loop, and optionally archives the old spec when
528
+ unreferenced — matching the append-only semantics of
529
+ `migrate-agent-timeouts`.
530
+
531
+ ## 0.3.58 (2026-07-01)
532
+
533
+ Break nested workflow goal deadlocks and add a migration path.
534
+
535
+ ### Fixed
536
+
537
+ - **Workflow loops — nested top-level goals:** a loop-level goal wrapping a
538
+ workflow that also defined a top-level `"goal"` deadlocked because each layer
539
+ waited on the other. New workflow loops that combine both wrappers are
540
+ rejected at creation; retargeting onto a dual-goal workflow is blocked. When
541
+ only a legacy dual-wrapper loop remains, the runner strips the workflow goal
542
+ for execution so the loop-level goal can drive orchestration.
543
+ - **Workflow runner:** loop-level goals on workflow loops execute the underlying
544
+ workflow with the workflow-level goal removed when both were present.
545
+
546
+ ### Added
547
+
548
+ - **`loops workflows migrate-goal-wrappers`:** append-only migrator that clones
549
+ a goal-free workflow spec and retargets eligible non-running workflow loops
550
+ that still carry redundant workflow-level goal wrappers alongside a loop-level
551
+ goal. Supports `--loop`, `--apply`, and `--archive-old` like
552
+ `migrate-agent-timeouts`.
553
+
448
554
  ## 0.3.x
449
555
 
450
556
  Compact history for the 0.3 line, newest first (`version (date) commit subject`).
451
557
 
452
- - 0.3.60 (2026-07-01) fix: run Codewith loops as durable agents
453
- - 0.3.59 (2026-07-01) fix: harden workflow goal migration
454
- - 0.3.58 (2026-07-01) fix: avoid nested workflow goal deadlocks
455
558
  - 0.3.57 (2026-07-01) feat: harden loop routing and add MCP server
456
559
  - 0.3.56 (2026-06-30) fix: allow worktree agents to write git metadata
457
560
  - 0.3.55 (2026-06-30) fix: agent workflow timeout policy
package/README.md CHANGED
@@ -38,6 +38,16 @@ package. Cloud mode is a public contract until a cloud-specific hosted URL and
38
38
  cloud token are configured through `LOOPS_CLOUD_API_URL` plus
39
39
  `LOOPS_CLOUD_TOKEN` or `HASNA_LOOPS_CLOUD_TOKEN`.
40
40
 
41
+ Scheduler state is explicit in status JSON. `schedulerState.localStore` is
42
+ SQLite plus local run artifact files: authoritative in `local`, cache/spool in
43
+ non-local modes. `schedulerState.remoteStore` names the non-local contract
44
+ (`api_control_plane_contract`, `postgres_contract`, or
45
+ `hosted_control_plane_contract`) and reports `applySupported=false` because this
46
+ public package does not directly mutate remote Postgres, S3/object storage, AWS
47
+ resources, or hosted credentials. Route admission remains bounded by
48
+ `max_dispatch`, `max_active`, `max_active_per_project`,
49
+ `max_active_per_project_group`, `max_active_scope`, and `max_per_profile`.
50
+
41
51
  Useful status commands:
42
52
 
43
53
  ```bash
@@ -301,6 +311,19 @@ Workflow JSON can also embed goals at the workflow or step level:
301
311
  }
302
312
  ```
303
313
 
314
+ For **workflow loops** (`loops create workflow ...`), prefer a single loop-level
315
+ `--goal` wrapper around the scheduled workflow. Do not also define a top-level
316
+ `"goal"` on the workflow JSON spec: nested loop-level and workflow-level goal
317
+ wrappers deadlocked execution because each layer waited on the other. OpenLoops
318
+ now rejects new workflow loops that combine both wrappers at creation time, and
319
+ blocks retargeting a loop-level goal loop onto a workflow that also carries a
320
+ top-level goal.
321
+
322
+ Step-level goals inside workflow JSON remain valid. When a loop-level goal wraps
323
+ a workflow that still carries a legacy top-level goal, the runner strips the
324
+ workflow goal for that execution only; migrate away from the redundant wrapper
325
+ with the append-only migrator (see **Workflows** below).
326
+
304
327
  ## Workflows
305
328
 
306
329
  Create a workflow JSON file:
@@ -430,22 +453,38 @@ worker finishes; pass `--verifier-idle-timeout none` or template variable
430
453
  `verifierIdleTimeoutMs=none` only when another heartbeat is guaranteed. Use a
431
454
  positive numeric `timeoutMs` only when an agentic step is intentionally bounded.
432
455
 
433
- To migrate existing workflow loops, do not edit `workflow_specs.steps_json`
434
- directly because historical workflow runs must keep pointing at their original
435
- spec. Use the append-only migrator:
456
+ To migrate existing agentic loops, use the timeout migrator instead of editing
457
+ the database directly. Workflow loops are migrated append-only because
458
+ historical workflow runs must keep pointing at their original spec; direct
459
+ agent loops selected with `--loop` update their stored target in place for
460
+ future executions:
436
461
 
437
462
  ```bash
438
463
  loops workflows migrate-agent-timeouts --loop <loop-id-or-name>
439
464
  loops workflows migrate-agent-timeouts --loop <loop-id-or-name> --apply
440
- ```
441
-
442
- The command dry-runs by default. With `--apply`, it creates a new workflow spec
443
- with the requested agent timeout policy, retargets only future executions of
444
- eligible non-running workflow loops, and leaves terminal timed-out workflow runs
445
- as audit history. Use `loops workflows recover` only for interrupted `running`
446
- workflow runs whose recorded child process is gone; terminal `timed_out` runs
447
- must be requeued with `loops routes requeue <work-item-id> --reason "<cause fixed>"` before
448
- re-delivering or draining the original task/event route.
465
+ loops workflows migrate-goal-wrappers --loop <loop-id-or-name>
466
+ loops workflows migrate-goal-wrappers --loop <loop-id-or-name> --apply
467
+ loops workflows migrate-goal-wrappers --loop <loop-id-or-name> --apply --archive-old
468
+ ```
469
+
470
+ Both migrators dry-run by default. For eligible non-running workflow loops,
471
+ `--apply` creates a new workflow spec and retargets only future executions;
472
+ historical workflow runs keep pointing at their original spec. For direct agent
473
+ loops selected with `--loop`, `migrate-agent-timeouts --apply` updates the
474
+ stored target in place for future executions. Use `--archive-old` to archive
475
+ superseded workflow specs when no active loops still reference them.
476
+
477
+ `migrate-agent-timeouts` clones the workflow with the requested agent timeout
478
+ policy (`--timeout none` by default). `migrate-goal-wrappers` targets loops that
479
+ define both a loop-level goal and a redundant workflow-level top-level goal: it
480
+ clones a goal-free workflow spec, retargets the loop, and leaves the loop-level
481
+ goal as the sole orchestration wrapper. Loops with only a workflow-level goal or
482
+ only a loop-level goal are skipped.
483
+
484
+ Use `loops workflows recover` only for interrupted `running` workflow runs
485
+ whose recorded child process is gone; terminal `timed_out` runs must be
486
+ requeued with `loops routes requeue <work-item-id> --reason "<cause fixed>"`
487
+ before re-delivering or draining the original task/event route.
449
488
 
450
489
  ```json
451
490
  {
@@ -524,11 +563,12 @@ workflow unless the event data or metadata has `route_enabled=true`,
524
563
  `automation.allowed=true`, or a task tag containing `auto:route`. It also skips
525
564
  blocked, completed/done, cancelled/canceled, failed, archived, manual,
526
565
  approval-required, or `no-auto` tasks. Terminal route work items such as
527
- failed, dead-letter, cancelled, or succeeded history stay deduped until an
528
- operator runs `loops routes requeue <work-item-id> --reason "<cause fixed>"`.
529
- The next route-created output records `requeue` evidence with the previous work
530
- item id, previous attempts, operator reason, new attempt, workflow id, and loop
531
- id.
566
+ failed, dead-letter, cancelled, or succeeded history are re-admitted only when
567
+ the todos task is still actionable, the per-attempt backoff has elapsed, and the
568
+ redispatch cap has not been reached. Operators can still force a retry with
569
+ `loops routes requeue <work-item-id> --reason "<cause fixed>"`. The next
570
+ route-created output records `requeue` evidence with the previous work item id,
571
+ previous attempts, reason, new attempt, workflow id, and loop id.
532
572
 
533
573
  Task route drains can select providers from task metadata instead of running one
534
574
  fixed provider/account pool for the whole drain. Add one or more
@@ -785,6 +825,9 @@ Use `--json` for machine-readable output. Prompt bodies and run stdout/stderr ar
785
825
 
786
826
  ```bash
787
827
  loops health --json
828
+ loops health scan --include active,paused --latest-run --doctor --daemon --json
829
+ loops health scan --include active,paused --latest-run --doctor --daemon \
830
+ --upsert-todos --dry-run --max-actions 5 --evidence-dir ~/.hasna/loops/reports/health-scan
788
831
  loops expectations <loop-id-or-name> --json
789
832
  loops health route-tasks --project ~/.hasna/loops --task-list loop-error-self-heal --max-actions 5
790
833
  loops hygiene names --json
@@ -799,6 +842,16 @@ mutating path: it upserts deduped Todos tasks for failed expectations and marks
799
842
  them with `no_tmux_dispatch=true` metadata. Use `--dry-run --json` before
800
843
  turning it into a production loop.
801
844
 
845
+ `health scan` is the first-class replacement for local loop-error self-heal
846
+ scripts. It inventories active/paused loops by default, can include doctor,
847
+ daemon, preflight, latest-run, and stale-running findings, writes bounded
848
+ `summary.json` and `report.md` files under
849
+ `$LOOPS_DATA_DIR/reports/health-scan`, and prints compact human or stable JSON
850
+ output. It is read-only unless `--upsert-todos` is supplied. The only built-in
851
+ self-heal is `--start-daemon`, which attempts to start the daemon when status
852
+ proves it is not running; it never stops, resumes, deletes, archives, or reaps
853
+ loops.
854
+
802
855
  Add `--evidence-dir <dir>` to `health route-tasks` or `hygiene route-tasks`
803
856
  when the deterministic loop should write a durable JSON heartbeat/report in
804
857
  addition to loop stdout. Add `--auto-route` only for task lists that are
@@ -1,10 +1,30 @@
1
1
  #!/usr/bin/env bun
2
2
  import type { LoopStorageContract } from "../lib/storage/contract.js";
3
+ /** The serve OpenAPI document (source of the generated SDK), version-synced. */
4
+ export declare function openApiDocument(): Record<string, unknown>;
3
5
  export declare function apiStatus(): {
4
6
  ok: boolean;
5
7
  service: string;
6
8
  status: import("../index.js").LoopDeploymentStatus;
7
9
  };
10
+ /**
11
+ * Framework-agnostic API-key verifier shape (matches
12
+ * `@hasna/contracts/auth` `ApiKeyVerifier`). Kept structural so the api module
13
+ * has no hard dependency on the auth package: the serve entry injects the real
14
+ * verifier built from the vendored kit's client + the HMAC signing secret.
15
+ */
16
+ export interface ApiAuthenticator {
17
+ authenticate(headers: Headers, context?: {
18
+ method?: string | null;
19
+ path?: string | null;
20
+ requiredScopes?: readonly string[];
21
+ }): Promise<{
22
+ ok: boolean;
23
+ status: number;
24
+ reason?: string;
25
+ message?: string;
26
+ }>;
27
+ }
8
28
  export interface LoopsApiServerOptions {
9
29
  host?: string;
10
30
  port?: number;
@@ -12,6 +32,21 @@ export interface LoopsApiServerOptions {
12
32
  bodyLimitBytes?: number;
13
33
  evidenceLimitBytes?: number;
14
34
  now?: () => Date;
35
+ /**
36
+ * API-key verifier (from `@hasna/contracts/auth`). When present, every
37
+ * request outside the open foundation probes (`/health`, `/ready`,
38
+ * `/version`, `/status`) must present a valid `loops:*` scoped key. This is
39
+ * the internet-facing auth path (no bearer token, no loopback bypass).
40
+ */
41
+ authenticator?: ApiAuthenticator;
42
+ /**
43
+ * Readiness probe. Should prove the storage backend is reachable AND fully
44
+ * migrated. Returns `{ ready, detail? }`. Defaults to a storage list probe.
45
+ */
46
+ readyCheck?: () => Promise<{
47
+ ready: boolean;
48
+ detail?: string;
49
+ }>;
15
50
  }
16
51
  export declare function createLoopsApiServer(opts?: LoopsApiServerOptions): Bun.Server<undefined>;
17
52
  export declare function main(argv?: string[]): Promise<void>;