@hasna/loops 0.4.13 → 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 (39) hide show
  1. package/CHANGELOG.md +74 -3
  2. package/README.md +54 -12
  3. package/dist/api/index.d.ts +35 -0
  4. package/dist/api/index.js +695 -10
  5. package/dist/cli/index.js +1633 -366
  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 +2 -2
  16. package/dist/index.js +3567 -2010
  17. package/dist/lib/health.d.ts +83 -3
  18. package/dist/lib/mode.js +14 -2
  19. package/dist/lib/scheduler.d.ts +5 -2
  20. package/dist/lib/storage/index.d.ts +1 -0
  21. package/dist/lib/storage/index.js +1329 -211
  22. package/dist/lib/storage/pg-executor.d.ts +27 -0
  23. package/dist/lib/storage/pg-runner-claim.d.ts +40 -0
  24. package/dist/lib/storage/postgres-loop-storage.d.ts +114 -0
  25. package/dist/lib/storage/sqlite.js +336 -8
  26. package/dist/lib/store.d.ts +234 -0
  27. package/dist/lib/store.js +350 -8
  28. package/dist/mcp/index.js +1067 -306
  29. package/dist/runner/index.js +124 -25
  30. package/dist/sdk/http.d.ts +115 -0
  31. package/dist/sdk/http.js +145 -0
  32. package/dist/sdk/index.d.ts +5 -1
  33. package/dist/sdk/index.js +1052 -312
  34. package/dist/serve/index.d.ts +4 -0
  35. package/dist/serve/index.js +7619 -0
  36. package/dist/types.d.ts +3 -0
  37. package/docs/CUTOVER-RUNBOOK.md +61 -0
  38. package/docs/USAGE.md +50 -11
  39. package/package.json +14 -2
package/CHANGELOG.md CHANGED
@@ -5,6 +5,27 @@ 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
+
8
29
  ## 0.4.13 (2026-07-05)
9
30
 
10
31
  `--pr-handoff` workflows whose worker pushes its own branch and opens the PR
@@ -477,13 +498,63 @@ CLI/MCP/SDK surface with deprecation aliases.
477
498
  npm installs), and `@hasna/machines` is now an optional dependency —
478
499
  installs without it simply disable remote-machine assignment.
479
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
+
480
554
  ## 0.3.x
481
555
 
482
556
  Compact history for the 0.3 line, newest first (`version (date) commit subject`).
483
557
 
484
- - 0.3.60 (2026-07-01) fix: run Codewith loops as durable agents
485
- - 0.3.59 (2026-07-01) fix: harden workflow goal migration
486
- - 0.3.58 (2026-07-01) fix: avoid nested workflow goal deadlocks
487
558
  - 0.3.57 (2026-07-01) feat: harden loop routing and add MCP server
488
559
  - 0.3.56 (2026-06-30) fix: allow worktree agents to write git metadata
489
560
  - 0.3.55 (2026-06-30) fix: agent workflow timeout policy
package/README.md CHANGED
@@ -311,6 +311,19 @@ Workflow JSON can also embed goals at the workflow or step level:
311
311
  }
312
312
  ```
313
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
+
314
327
  ## Workflows
315
328
 
316
329
  Create a workflow JSON file:
@@ -440,22 +453,38 @@ worker finishes; pass `--verifier-idle-timeout none` or template variable
440
453
  `verifierIdleTimeoutMs=none` only when another heartbeat is guaranteed. Use a
441
454
  positive numeric `timeoutMs` only when an agentic step is intentionally bounded.
442
455
 
443
- To migrate existing workflow loops, do not edit `workflow_specs.steps_json`
444
- directly because historical workflow runs must keep pointing at their original
445
- 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:
446
461
 
447
462
  ```bash
448
463
  loops workflows migrate-agent-timeouts --loop <loop-id-or-name>
449
464
  loops workflows migrate-agent-timeouts --loop <loop-id-or-name> --apply
450
- ```
451
-
452
- The command dry-runs by default. With `--apply`, it creates a new workflow spec
453
- with the requested agent timeout policy, retargets only future executions of
454
- eligible non-running workflow loops, and leaves terminal timed-out workflow runs
455
- as audit history. Use `loops workflows recover` only for interrupted `running`
456
- workflow runs whose recorded child process is gone; terminal `timed_out` runs
457
- must be requeued with `loops routes requeue <work-item-id> --reason "<cause fixed>"` before
458
- 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.
459
488
 
460
489
  ```json
461
490
  {
@@ -796,6 +825,9 @@ Use `--json` for machine-readable output. Prompt bodies and run stdout/stderr ar
796
825
 
797
826
  ```bash
798
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
799
831
  loops expectations <loop-id-or-name> --json
800
832
  loops health route-tasks --project ~/.hasna/loops --task-list loop-error-self-heal --max-actions 5
801
833
  loops hygiene names --json
@@ -810,6 +842,16 @@ mutating path: it upserts deduped Todos tasks for failed expectations and marks
810
842
  them with `no_tmux_dispatch=true` metadata. Use `--dry-run --json` before
811
843
  turning it into a production loop.
812
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
+
813
855
  Add `--evidence-dir <dir>` to `health route-tasks` or `hygiene route-tasks`
814
856
  when the deterministic loop should write a durable JSON heartbeat/report in
815
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>;