@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/dist/types.d.ts CHANGED
@@ -365,6 +365,9 @@ export interface Loop {
365
365
  retryDelayMs: number;
366
366
  leaseMs: number;
367
367
  expiresAt?: string;
368
+ latestRunId?: string;
369
+ latestRunStatus?: RunStatus;
370
+ lastRunAt?: string;
368
371
  createdAt: string;
369
372
  updatedAt: string;
370
373
  }
@@ -0,0 +1,61 @@
1
+ # Loops Postgres Cutover Runbook
2
+
3
+ Status: **backend foundation landed; daemon still runs on local sqlite.** Do NOT
4
+ flip the running daemon to Postgres until every step below is green.
5
+
6
+ PURE REMOTE (Amendment A1): in cloud mode, reads AND writes hit cloud Postgres
7
+ directly. There is no hybrid/sync/cache mode. After a verified migration the
8
+ local sqlite file is renamed to `<name>.db.pre-cloud-2026-07-06.bak`.
9
+
10
+ ## What shipped in this PR
11
+
12
+ - Vendored `@hasna/contracts` storage kit → `src/generated/storage-kit/`
13
+ (pool/query/tls/mode/migrations/health). Regenerate: `bunx @hasna/contracts vendor-kit`.
14
+ - `src/lib/storage/pg-executor.ts` — `PgPoolExecutor` adapting the vendored
15
+ `pg.Pool` client to `PostgresQueryExecutor` (drives `PostgresStorage.migrate`).
16
+ - `src/lib/storage/pg-runner-claim.ts` — `claimNextRun` / `heartbeatRunLease`
17
+ using `SELECT ... FOR UPDATE SKIP LOCKED` for concurrent runners.
18
+ - `src/lib/storage/postgres-concurrency.test.ts` — two-connection race test
19
+ (skips unless `LOOPS_TEST_DATABASE_URL` is set; verified green against a
20
+ throwaway local Postgres 16).
21
+ - `pg` promoted to a direct dependency; `@types/pg` added as devDependency.
22
+
23
+ ## Remaining before daemon cutover (tracked, NOT done here)
24
+
25
+ 1. **Full `PostgresLoopStorage` implementing `LoopStorageContract`** (60+ methods
26
+ in `src/lib/storage/contract.ts`). Only the migration ledger + claim/lease
27
+ primitives exist today. The claim primitive here is the correctness core to
28
+ build the rest on.
29
+ 2. **Async consumer wiring.** All call sites construct `new Store()` synchronously
30
+ (`src/cli`, `src/daemon`, `src/sdk`, `src/mcp`, `src/lib/route`). PURE REMOTE
31
+ requires routing them through the async `LoopStorageContract` when
32
+ `LOOPS_DATABASE_URL` is set (mode resolved by `src/lib/mode.ts`, which stays
33
+ authoritative). `SqliteLoopStorage` already wraps the sync store for the
34
+ local path.
35
+ 3. **`loops migrate-local` command** (data migration TOOLING only): copy
36
+ definitions + schedules + last 30 days of runs from local sqlite into cloud
37
+ Postgres, then rename the sqlite file to the `.pre-cloud-2026-07-06.bak`
38
+ backup. Does NOT enable cloud mode.
39
+ 4. **Apply schema to shared RDS** via SSM tunnel on local port 15438 using the
40
+ `hasna/oss/loops/database-url-owner` secret, then run
41
+ `PostgresStorage(new PgPoolExecutor(...)).migrate()` (dry-run first). Kill the
42
+ tunnel and remove temp files afterward. Never run the test suite against RDS.
43
+
44
+ ## Enable steps (run only after 1–4 are green)
45
+
46
+ 1. Confirm `PostgresStorage.migrate({ dryRun: true })` reports all four
47
+ migrations already applied on the loops RDS database.
48
+ 2. Stop the loops daemon: `loops daemon stop`.
49
+ 3. `loops migrate-local --database-url "$LOOPS_DATABASE_URL"` (moves defs +
50
+ schedules + 30d of runs; verify counts; sqlite renamed to backup).
51
+ 4. Export `LOOPS_DATABASE_URL` (from `hasna/oss/loops/database-url`) and the
52
+ cloud storage-mode env for the daemon unit; restart the daemon.
53
+ 5. Verify `loops status` reports `deploymentMode: cloud`, `sourceOfTruth:
54
+ cloud_control_plane`, and that a test loop run claims + finalizes against
55
+ Postgres.
56
+
57
+ ## Rollback
58
+
59
+ Cloud mode is off until `LOOPS_DATABASE_URL` is present in the daemon env.
60
+ To roll back: unset it, restore the sqlite backup
61
+ (`mv <name>.db.pre-cloud-2026-07-06.bak <name>.db`), restart the daemon.
package/docs/USAGE.md CHANGED
@@ -478,21 +478,39 @@ worker finishes; pass `--verifier-idle-timeout none` or template variable
478
478
  `verifierIdleTimeoutMs=none` only when another heartbeat is guaranteed. Use a
479
479
  positive numeric `timeoutMs` only when an agentic step is intentionally bounded.
480
480
 
481
- To migrate existing workflow loops, do not edit `workflow_specs.steps_json`
482
- directly because historical workflow runs must keep pointing at their original
483
- spec. Use the append-only migrator:
481
+ To migrate existing agentic loops, use the timeout migrator instead of editing
482
+ the database directly. Workflow loops are migrated append-only because
483
+ historical workflow runs must keep pointing at their original spec; direct
484
+ agent loops selected with `--loop` update their stored target in place for
485
+ future executions:
484
486
 
485
487
  ```bash
486
488
  loops workflows migrate-agent-timeouts --loop <loop-id-or-name>
487
489
  loops workflows migrate-agent-timeouts --loop <loop-id-or-name> --apply
490
+ loops workflows migrate-goal-wrappers --loop <loop-id-or-name>
491
+ loops workflows migrate-goal-wrappers --loop <loop-id-or-name> --apply
492
+ loops workflows migrate-goal-wrappers --loop <loop-id-or-name> --apply --archive-old
488
493
  ```
489
494
 
490
- The command dry-runs by default. With `--apply`, it creates a new workflow spec
491
- with the requested agent timeout policy, retargets only future executions of
492
- eligible non-running workflow loops, and leaves terminal timed-out workflow runs
493
- as audit history. Use `loops workflows recover` only for interrupted `running`
494
- workflow runs whose recorded child process is gone; terminal `timed_out` runs
495
- must be requeued with `loops routes requeue <work-item-id> --reason "<cause fixed>"` before
495
+ Both migrators dry-run by default. For eligible non-running workflow loops,
496
+ `--apply` creates a new workflow spec and retargets only future executions;
497
+ historical workflow runs keep pointing at their original spec. For direct agent
498
+ loops selected with `--loop`, `migrate-agent-timeouts --apply` updates the
499
+ stored target in place for future executions. Use `--archive-old` to archive
500
+ superseded workflow specs when no active loops still reference them.
501
+
502
+ `migrate-agent-timeouts` clones the workflow with the requested agent timeout
503
+ policy (`--timeout none` by default). `migrate-goal-wrappers` targets loops that
504
+ define both a loop-level goal and a redundant workflow-level top-level goal: it
505
+ clones a goal-free workflow spec, retargets the loop, and leaves the loop-level
506
+ goal as the sole orchestration wrapper. Loops with only a workflow-level goal or
507
+ only a loop-level goal are skipped. New workflow loops cannot combine both
508
+ wrappers; use loop-level `--goal` on `loops create workflow` instead of nesting
509
+ a top-level `"goal"` in the workflow JSON.
510
+
511
+ Use `loops workflows recover` only for interrupted `running` workflow runs whose
512
+ recorded child process is gone; terminal `timed_out` runs must be requeued with
513
+ `loops routes requeue <work-item-id> --reason "<cause fixed>"` before
496
514
  re-delivering or draining the original task/event route.
497
515
 
498
516
  ```json
@@ -865,25 +883,46 @@ agent-run failures for default-loop SLOs:
865
883
 
866
884
  ```bash
867
885
  loops health --json
886
+ loops health scan --include active,paused --latest-run --doctor --daemon --json
868
887
  loops expectations <loop-id-or-name> --json
869
888
  ```
870
889
 
871
890
  The JSON contains the expectation result, bounded error/stdout/stderr evidence,
872
891
  a stable failure fingerprint, route metadata, and recommended task fields.
873
- OpenLoops does not mutate Todos from `health` or `expectations`. To turn failed
874
- expectations into deduped tasks, use the explicit routing command:
892
+ OpenLoops does not mutate Todos from `health`, `expectations`, or read-only
893
+ `health scan`. To turn failed expectations or scan findings into deduped tasks,
894
+ use an explicit mutating command:
875
895
 
876
896
  ```bash
877
897
  loops health route-tasks \
878
898
  --project ~/.hasna/loops \
879
899
  --task-list loop-error-self-heal \
880
900
  --max-actions 5
901
+
902
+ loops health scan \
903
+ --include active,paused \
904
+ --latest-run \
905
+ --doctor \
906
+ --daemon \
907
+ --upsert-todos \
908
+ --dry-run \
909
+ --max-actions 5 \
910
+ --evidence-dir ~/.hasna/loops/reports/health-scan
881
911
  ```
882
912
 
883
913
  Use `--dry-run --json` first when testing a new automation path. Routed tasks
884
914
  include the stable failure fingerprint, classification, loop id/name, and
885
915
  `no_tmux_dispatch=true` metadata.
886
916
 
917
+ `health scan` replaces local loop-error self-heal scripts with package-owned
918
+ CLI/SDK/MCP primitives. It inventories included loop statuses, detects daemon,
919
+ doctor, preflight, latest-run, and stale-running issues, writes bounded
920
+ `summary.json` and `report.md` files under
921
+ `$LOOPS_DATA_DIR/reports/health-scan` or `--report-dir`/`--evidence-dir`, and
922
+ keeps output compact. It is read-only by default. The only safe self-heal is
923
+ `--start-daemon`, which starts the daemon only when status proves it is not
924
+ running; it does not stop, resume, archive, delete, or reap loops.
925
+
887
926
  Use `--evidence-dir <dir>` when a deterministic loop needs a compact JSON
888
927
  heartbeat/report on disk. Use `--auto-route` only on task lists that should feed
889
928
  the task-created headless worker/verifier workflow; it adds the `auto:route`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/loops",
3
- "version": "0.4.13",
3
+ "version": "0.4.14",
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",
@@ -9,6 +9,7 @@
9
9
  "loops": "dist/cli/index.js",
10
10
  "loops-daemon": "dist/daemon/index.js",
11
11
  "loops-api": "dist/api/index.js",
12
+ "loops-serve": "dist/serve/index.js",
12
13
  "loops-runner": "dist/runner/index.js",
13
14
  "loops-mcp": "dist/mcp/index.js"
14
15
  },
@@ -21,6 +22,14 @@
21
22
  "types": "./dist/sdk/index.d.ts",
22
23
  "import": "./dist/sdk/index.js"
23
24
  },
25
+ "./sdk/http": {
26
+ "types": "./dist/sdk/http.d.ts",
27
+ "import": "./dist/sdk/http.js"
28
+ },
29
+ "./serve": {
30
+ "types": "./dist/serve/index.d.ts",
31
+ "import": "./dist/serve/index.js"
32
+ },
24
33
  "./mcp": {
25
34
  "types": "./dist/mcp/index.d.ts",
26
35
  "import": "./dist/mcp/index.js"
@@ -66,7 +75,7 @@
66
75
  "LICENSE"
67
76
  ],
68
77
  "scripts": {
69
- "build": "rm -rf dist && bun build src/cli/index.ts src/daemon/index.ts src/api/index.ts src/runner/index.ts src/mcp/index.ts src/index.ts src/sdk/index.ts src/lib/store.ts src/lib/mode.ts src/lib/storage/index.ts src/lib/storage/contract.ts src/lib/storage/sqlite.ts src/lib/storage/postgres.ts src/lib/storage/postgres-schema.ts --root src --outdir dist --target bun --packages external && chmod +x dist/cli/index.js dist/daemon/index.js dist/api/index.js dist/runner/index.js dist/mcp/index.js && tsc -p tsconfig.build.json --emitDeclarationOnly --outDir dist",
78
+ "build": "rm -rf dist && bun build src/cli/index.ts src/daemon/index.ts src/api/index.ts src/serve/index.ts src/runner/index.ts src/mcp/index.ts src/index.ts src/sdk/index.ts src/sdk/http.ts src/lib/store.ts src/lib/mode.ts src/lib/storage/index.ts src/lib/storage/contract.ts src/lib/storage/sqlite.ts src/lib/storage/postgres.ts src/lib/storage/postgres-schema.ts --root src --outdir dist --target bun --packages external && chmod +x dist/cli/index.js dist/daemon/index.js dist/api/index.js dist/serve/index.js dist/runner/index.js dist/mcp/index.js && tsc -p tsconfig.build.json --emitDeclarationOnly --outDir dist",
70
79
  "build:bin": "bun build src/cli/index.ts --compile --outfile dist/loops",
71
80
  "typecheck": "tsc --noEmit",
72
81
  "test": "bun test",
@@ -101,11 +110,13 @@
101
110
  "bun": ">=1.0.0"
102
111
  },
103
112
  "dependencies": {
113
+ "@hasna/contracts": "^0.4.1",
104
114
  "@hasna/events": "^0.1.9",
105
115
  "@modelcontextprotocol/sdk": "^1.29.0",
106
116
  "@openrouter/ai-sdk-provider": "2.9.1",
107
117
  "ai": "6.0.204",
108
118
  "commander": "^13.1.0",
119
+ "pg": "^8.13.1",
109
120
  "zod": "4.4.3"
110
121
  },
111
122
  "optionalDependencies": {
@@ -113,6 +124,7 @@
113
124
  },
114
125
  "devDependencies": {
115
126
  "@types/bun": "latest",
127
+ "@types/pg": "^8.11.10",
116
128
  "typescript": "^5.7.3"
117
129
  },
118
130
  "publishConfig": {