@hasna/loops 0.3.59 → 0.4.0

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 (56) hide show
  1. package/CHANGELOG.md +247 -0
  2. package/LICENSE +197 -13
  3. package/README.md +28 -60
  4. package/dist/cli/index.js +7269 -5776
  5. package/dist/daemon/control.d.ts +21 -1
  6. package/dist/daemon/daemon.d.ts +5 -0
  7. package/dist/daemon/index.js +2985 -1072
  8. package/dist/daemon/install.d.ts +1 -1
  9. package/dist/index.d.ts +21 -8
  10. package/dist/index.js +5906 -4580
  11. package/dist/lib/accounts.d.ts +6 -1
  12. package/dist/lib/agent-adapter.d.ts +74 -0
  13. package/dist/lib/backup.d.ts +25 -0
  14. package/dist/lib/errors.d.ts +21 -0
  15. package/dist/lib/executor.d.ts +10 -1
  16. package/dist/lib/goal/metadata.d.ts +16 -0
  17. package/dist/lib/goal/model-factory.d.ts +1 -0
  18. package/dist/lib/goal/prompts.d.ts +3 -1
  19. package/dist/lib/goal/types.d.ts +3 -0
  20. package/dist/lib/health.d.ts +1 -1
  21. package/dist/lib/ids.d.ts +8 -1
  22. package/dist/lib/machines.d.ts +6 -0
  23. package/dist/lib/process-identity.d.ts +16 -0
  24. package/dist/lib/{schedule.d.ts → recurrence.d.ts} +1 -0
  25. package/dist/lib/redact.d.ts +21 -0
  26. package/dist/lib/route/cursors.d.ts +18 -0
  27. package/dist/lib/route/drain.d.ts +6 -0
  28. package/dist/lib/route/fields.d.ts +27 -0
  29. package/dist/lib/route/gates.d.ts +26 -0
  30. package/dist/lib/route/index.d.ts +18 -0
  31. package/dist/lib/route/options.d.ts +31 -0
  32. package/dist/lib/route/parse.d.ts +8 -0
  33. package/dist/lib/route/pr-review.d.ts +11 -0
  34. package/dist/lib/route/provider.d.ts +41 -0
  35. package/dist/lib/route/route-event.d.ts +23 -0
  36. package/dist/lib/route/route-tasks.d.ts +75 -0
  37. package/dist/lib/route/throttle.d.ts +47 -0
  38. package/dist/lib/route/todos-cli.d.ts +21 -0
  39. package/dist/lib/route/types.d.ts +88 -0
  40. package/dist/lib/run-artifacts.d.ts +17 -2
  41. package/dist/lib/run-envelope.d.ts +41 -0
  42. package/dist/lib/scheduler.d.ts +78 -2
  43. package/dist/lib/store.d.ts +63 -0
  44. package/dist/lib/store.js +1011 -266
  45. package/dist/lib/template-kit.d.ts +70 -0
  46. package/dist/lib/templates-custom.d.ts +34 -0
  47. package/dist/lib/templates.d.ts +13 -81
  48. package/dist/lib/workflow-runner.d.ts +2 -0
  49. package/dist/mcp/index.d.ts +3 -0
  50. package/dist/mcp/index.js +3313 -1165
  51. package/dist/sdk/index.d.ts +29 -3
  52. package/dist/sdk/index.js +2910 -897
  53. package/dist/test-helpers.d.ts +37 -0
  54. package/dist/types.d.ts +2 -0
  55. package/docs/USAGE.md +40 -18
  56. package/package.json +6 -3
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Shared deadline-based polling and process-kill verification helpers for the
3
+ * test suite. Test-only: nothing in src/ production code may import this file.
4
+ */
5
+ export interface WaitUntilOptions {
6
+ /** Total time to wait before failing (default 5s). */
7
+ timeoutMs?: number;
8
+ /** Poll interval (default 5ms). */
9
+ intervalMs?: number;
10
+ /** Included in the timeout error to identify the stuck condition. */
11
+ label?: string;
12
+ }
13
+ /**
14
+ * Poll `check` until it returns a truthy value, with a hard deadline.
15
+ * Returns the first truthy value; throws once the deadline passes so a stuck
16
+ * condition fails the test instead of silently falling through.
17
+ */
18
+ export declare function waitUntil<T>(check: () => T | undefined | null | false | Promise<T | undefined | null | false>, opts?: WaitUntilOptions): Promise<T>;
19
+ /**
20
+ * Shell command that blocks until `gate` exists, then writes `text` to
21
+ * `marker`. Replaces fixed `sleep N; printf ...` fixtures: the child stays
22
+ * alive (blocked on the gate) for exactly as long as the test needs, and a
23
+ * kill can be proven by opening the gate and observing that the marker never
24
+ * appears (see {@link expectMarkerNeverWritten}).
25
+ */
26
+ export declare function gatedWriteCommand(gate: string, marker: string, opts?: {
27
+ text?: string;
28
+ append?: boolean;
29
+ }): string;
30
+ /** Open a {@link gatedWriteCommand} gate so a still-alive child can finish. */
31
+ export declare function openGate(gate: string): void;
32
+ /**
33
+ * Prove a gated child process was killed: open the gate and verify the marker
34
+ * still does not appear. A surviving child polls the gate every 20ms, so the
35
+ * default 250ms settle window gives it >10x the time it would need to write.
36
+ */
37
+ export declare function expectMarkerNeverWritten(gate: string, marker: string, settleMs?: number): Promise<void>;
package/dist/types.d.ts CHANGED
@@ -370,6 +370,8 @@ export interface LoopRun {
370
370
  claimedBy?: string;
371
371
  leaseExpiresAt?: string;
372
372
  pid?: number;
373
+ pgid?: number;
374
+ processStartedAt?: string;
373
375
  exitCode?: number;
374
376
  durationMs?: number;
375
377
  stdout?: string;
package/docs/USAGE.md CHANGED
@@ -6,7 +6,7 @@ It supports deterministic command loops, JSON-defined workflows, and guarded CLI
6
6
 
7
7
  - `claude`
8
8
  - `agent` (Cursor Agent CLI)
9
- - `codewith exec`
9
+ - `codewith agent start`
10
10
  - `aicopilot run`
11
11
  - `opencode run`
12
12
  - `codex exec`
@@ -313,6 +313,15 @@ Use `shell: true` only when you intentionally want shell parsing:
313
313
  { "type": "command", "command": "git status --short", "shell": true }
314
314
  ```
315
315
 
316
+ Gate steps can end a workflow early without failing it. A step opts into
317
+ blocked-exit semantics either explicitly via `"blockedExitCodes": [12]` on the
318
+ step, or by naming convention: a step whose `id` or `name` contains "gate" as a
319
+ standalone word (for example `gate`, `triage-gate`, `gate_check`) treats exit
320
+ code 12 as "blocked" — the step and its dependents are recorded as skipped and
321
+ the workflow still succeeds. Substring matches such as `gateway`, `aggregate`,
322
+ or `delegate` do NOT inherit gate behavior; use `blockedExitCodes` explicitly
323
+ for those. Set `"blockedExitCodes": []` on a gate-named step to opt out.
324
+
316
325
  ## Templates And Task Events
317
326
 
318
327
  Built-in templates turn common orchestration flows into reusable workflow JSON.
@@ -448,18 +457,21 @@ explicit break-glass handling for emergency workflows that need full access.
448
457
 
449
458
  Repo-mutating task/event routes should set `worktreeMode=required` so the
450
459
  workflow fails fast instead of falling back to the main checkout. When
451
- `projectPath` is an existing git repository, OpenLoops inserts a
452
- `prepare-worktree` command step before the worker and runs the worker/verifier
453
- from a deterministic worktree under `~/.hasna/loops/worktrees/<repo>/<run>`.
454
- The generated agent target includes worktree metadata (`mode`, `cwd`, `path`,
455
- `branch`, `originalCwd`) so dry-runs and workflow inspection expose the exact
456
- checkout.
457
-
458
- Before a worker starts, `prepare-worktree` verifies that any existing managed
459
- path is a real git worktree with the same top-level checkout, the same git
460
- common directory as the source repo, and the expected generated branch. A
461
- detached HEAD or unexpected branch fails closed with evidence instead of
462
- silently running a mutating workflow in the wrong state.
460
+ `projectPath` is an existing git repository, the executor prepares and enters
461
+ a deterministic worktree under `~/.hasna/loops/worktrees/<repo>/<run>` before
462
+ spawning the worker/verifier locally and on machine-assigned (remote) loops,
463
+ where the dispatch script runs the equivalent `git worktree add`/reuse checks
464
+ on the remote machine. The generated agent target includes worktree metadata
465
+ (`mode`, `cwd`, `path`, `branch`, `originalCwd`) so dry-runs and workflow
466
+ inspection expose the exact checkout.
467
+
468
+ Before a worker starts, worktree preparation verifies that any existing
469
+ managed path is a real git worktree with the same top-level checkout, the same
470
+ git common directory as the source repo, and the expected generated branch. A
471
+ detached HEAD or unexpected branch fails closed with evidence
472
+ (`worktreeMode=required`) instead of silently running a mutating workflow in
473
+ the wrong state; `worktreeMode=auto` falls back to the original checkout and
474
+ records the fallback.
463
475
 
464
476
  Use explicit main/default checkout mode only when the task truly requires it:
465
477
 
@@ -785,8 +797,9 @@ record an `auto_route_skipped_reason`. Without `--auto-route`, route commands
785
797
  only upsert deduped tasks and do not launch agents.
786
798
 
787
799
  Failure classifications are: `rate_limit`, `auth`, `model_not_found`,
788
- `context_length`, `schema_response_format`, `node_init`, `timeout`, `sigsegv`,
789
- `skipped_previous_active`, and `unknown`.
800
+ `context_length`, `schema_response_format`, `node_init`, `preflight`,
801
+ `route_functional`, `timeout`, `sigsegv`, `skipped_previous_active`,
802
+ `circuit_breaker`, and `unknown`.
790
803
 
791
804
  ## Hygiene
792
805
 
@@ -893,17 +906,26 @@ On Linux this writes a user systemd service. On macOS it writes a LaunchAgent pl
893
906
  The adapters intentionally use provider command surfaces instead of pretending every agent has one SDK:
894
907
 
895
908
  - Claude uses `claude -p --output-format json` and safe-mode/local setting sources by default.
896
- - Codewith uses `codewith --ask-for-approval never exec --json --ephemeral --skip-git-repo-check`, with `--add-dir` for explicit extra writable directories.
909
+ - Codewith uses durable `codewith --ask-for-approval never agent start` background-agent runs by default, then polls `codewith agent read` until the run is terminal and records compact status/event evidence. Remote Codewith agent steps fail closed until remote durable readback is implemented, so workflows do not advance immediately after enqueue. OpenLoops rejects Codewith `extraArgs` that try to force `exec`, `--ephemeral`, or other non-durable exec-only flags for task-lifecycle, planner, worker, verifier, reviewer, release, or other long-running agentic work.
897
910
  - AI Copilot and OpenCode use `run --format json --pure`. OpenCode requires an explicit provider/model id because ambient OpenCode config is machine-specific.
898
911
  - Cursor is CLI-first for now via the standalone `agent -p` binary. OpenLoops no longer falls back to `cursor agent`; install the standalone Cursor Agent CLI so preflight and scheduled runs use the same executable.
899
912
  - Codex uses `codex --ask-for-approval never exec --json --ephemeral --skip-git-repo-check`, with `--add-dir` for explicit extra writable directories where supported.
900
- - Agent prompts are sent through child stdin instead of argv so prompt bodies do not appear in process listings.
913
+ - Agent prompts are sent through child stdin instead of argv where the provider supports stdin. Codewith durable background agents currently accept prompts as native `agent start` arguments, so OpenLoops stores only bounded status/event evidence and omits raw Codewith event payloads from workflow stdout.
901
914
  - When `--account` or a step `account` is set, OpenLoops resolves `accounts env <profile> --tool <tool>` before spawning the target, strips inherited tool home/API-key variables, and applies the selected profile only to that process. Missing account profiles fail before the provider binary receives the prompt.
902
- - `--auth-profile` and step `authProfile` are provider-native auth selectors. They currently apply to Codewith and are passed to Codewith as `--auth-profile <name>` before `exec`; they do not call OpenAccounts.
915
+ - `--auth-profile` and step `authProfile` are provider-native auth selectors. They currently apply to Codewith and are passed to Codewith as `--auth-profile <name>` before `agent start/read/logs`; they do not call OpenAccounts.
903
916
  - `--sandbox` maps to provider-native sandbox flags. Codewith/Codex accept `read-only`, `workspace-write`, or `danger-full-access`; Cursor accepts `enabled` or `disabled`.
904
917
  - `--permission-mode` maps `plan`, `auto`, and `bypass` where the provider supports it. Claude uses native permission modes, Cursor maps bypass to `--force`, and OpenCode/AICopilot map bypass to `--dangerously-skip-permissions`.
905
918
  - `--variant` is provider-specific reasoning/model effort. Claude maps it to `--effort`, Codewith/Codex map it to `model_reasoning_effort`, and OpenCode/AICopilot pass `--variant`.
906
919
  - Daemon and scheduled runs prepend common user executable directories such as `~/.local/bin` and `~/.bun/bin` before resolving provider CLIs.
920
+ - Agent targets that set neither `timeoutMs` nor `idleTimeoutMs` get a default
921
+ idle watchdog: 30 minutes without stdout/stderr for streaming providers
922
+ (codex, cursor), and 4 hours for providers whose CLIs buffer all output until
923
+ completion (claude, opencode, aicopilot) or whose durable progress
924
+ fingerprint can stay flat during long work (codewith). Override the default
925
+ with `LOOPS_AGENT_IDLE_TIMEOUT_MS=<ms>`, disable it with
926
+ `LOOPS_AGENT_IDLE_TIMEOUT_MS=0` (or `none`/`off`), or set explicit
927
+ `timeoutMs`/`idleTimeoutMs` per target — `"timeoutMs": null` opts a target
928
+ out of both the wall-clock default and the idle watchdog.
907
929
 
908
930
  For production loops that can mutate repos, prefer the built-in
909
931
  `worktreeMode=auto`/`required` path and explicit prompts that name allowed write
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/loops",
3
- "version": "0.3.59",
3
+ "version": "0.4.0",
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",
@@ -31,6 +31,7 @@
31
31
  "files": [
32
32
  "dist",
33
33
  "README.md",
34
+ "CHANGELOG.md",
34
35
  "docs",
35
36
  "LICENSE"
36
37
  ],
@@ -39,7 +40,7 @@
39
40
  "build:bin": "bun build src/cli/index.ts --compile --outfile dist/loops",
40
41
  "typecheck": "tsc --noEmit",
41
42
  "test": "bun test",
42
- "prepare": "bun run build",
43
+ "prepare": "test -d dist || bun run build",
43
44
  "dev:cli": "bun run src/cli/index.ts",
44
45
  "dev:daemon": "bun run src/daemon/index.ts",
45
46
  "prepublishOnly": "bun run build"
@@ -70,13 +71,15 @@
70
71
  },
71
72
  "dependencies": {
72
73
  "@hasna/events": "^0.1.9",
73
- "@hasna/machines": "0.0.49",
74
74
  "@modelcontextprotocol/sdk": "^1.29.0",
75
75
  "@openrouter/ai-sdk-provider": "2.9.1",
76
76
  "ai": "6.0.204",
77
77
  "commander": "^13.1.0",
78
78
  "zod": "4.4.3"
79
79
  },
80
+ "optionalDependencies": {
81
+ "@hasna/machines": "0.0.49"
82
+ },
80
83
  "devDependencies": {
81
84
  "@types/bun": "latest",
82
85
  "typescript": "^5.7.3"