@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.
- package/CHANGELOG.md +74 -3
- package/README.md +54 -12
- package/dist/api/index.d.ts +35 -0
- package/dist/api/index.js +695 -10
- package/dist/cli/index.js +1633 -366
- 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 +2 -2
- package/dist/index.js +3567 -2010
- package/dist/lib/health.d.ts +83 -3
- package/dist/lib/mode.js +14 -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 +1067 -306
- package/dist/runner/index.js +124 -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 +1052 -312
- 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/USAGE.md +50 -11
- 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
|
|
444
|
-
|
|
445
|
-
|
|
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
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
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
|
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>;
|