@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.
- package/CHANGELOG.md +106 -3
- package/README.md +70 -17
- package/dist/api/index.d.ts +35 -0
- package/dist/api/index.js +750 -10
- package/dist/cli/index.js +1744 -361
- package/dist/cli/ui.d.ts +44 -0
- package/dist/daemon/index.js +948 -212
- package/dist/generated/storage-kit/health.d.ts +19 -0
- package/dist/generated/storage-kit/index.d.ts +7 -0
- package/dist/generated/storage-kit/migrations.d.ts +47 -0
- package/dist/generated/storage-kit/mode.d.ts +47 -0
- package/dist/generated/storage-kit/pool.d.ts +33 -0
- package/dist/generated/storage-kit/query.d.ts +35 -0
- package/dist/generated/storage-kit/tls.d.ts +25 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.js +6513 -4840
- package/dist/lib/health.d.ts +83 -3
- package/dist/lib/mode.d.ts +27 -0
- package/dist/lib/mode.js +69 -2
- package/dist/lib/scheduler.d.ts +5 -2
- package/dist/lib/storage/index.d.ts +1 -0
- package/dist/lib/storage/index.js +1329 -211
- package/dist/lib/storage/pg-executor.d.ts +27 -0
- package/dist/lib/storage/pg-runner-claim.d.ts +40 -0
- package/dist/lib/storage/postgres-loop-storage.d.ts +114 -0
- package/dist/lib/storage/sqlite.js +336 -8
- package/dist/lib/store.d.ts +234 -0
- package/dist/lib/store.js +350 -8
- package/dist/mcp/index.js +1447 -479
- package/dist/runner/index.js +179 -25
- package/dist/sdk/http.d.ts +115 -0
- package/dist/sdk/http.js +145 -0
- package/dist/sdk/index.d.ts +5 -1
- package/dist/sdk/index.js +1106 -296
- package/dist/serve/index.d.ts +4 -0
- package/dist/serve/index.js +7619 -0
- package/dist/types.d.ts +3 -0
- package/docs/CUTOVER-RUNBOOK.md +61 -0
- package/docs/DEPLOYMENT_MODES.md +23 -1
- package/docs/USAGE.md +66 -16
- 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
|
|
434
|
-
|
|
435
|
-
|
|
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
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
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
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
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
|
package/dist/api/index.d.ts
CHANGED
|
@@ -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>;
|