@hasna/loops 0.4.5 → 0.4.7

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 CHANGED
@@ -5,6 +5,46 @@ 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.7 (2026-07-04)
9
+
10
+ Routing hardening for PR merge/drain automation.
11
+
12
+ ### Fixed
13
+
14
+ - **Todos drain:** admit registered repo-project task sources while pinning
15
+ routed work to the scanned source project, preventing task-controlled nested
16
+ route paths from moving worker execution into another repository.
17
+ - **PR lifecycle routing:** propagate PR author and reviewer evidence into
18
+ reviewer and merger lifecycle prompts so merge routing can select a valid
19
+ non-author reviewer and keep reviewer/merger steps separate.
20
+
21
+ ## 0.4.6 (2026-07-03)
22
+
23
+ Reliability hardening from a full audit of the runtime, control surfaces, and
24
+ todos routing integration.
25
+
26
+ ### Fixed
27
+
28
+ - **Runtime:** repair a due slot that holds a terminal run (idempotent
29
+ `advanceLoop`) so a daemon death between `finalizeRun` and `advanceLoop` can no
30
+ longer wedge a loop into never running again.
31
+ - **Workflow runs:** recover dead-pid steps on resumed idempotent runs and wrap
32
+ the step loop so an unexpected error finalizes the run `failed` instead of
33
+ leaving it `running` forever.
34
+ - **Runner:** abort execution after repeated heartbeat failures on a lost lease.
35
+ - **Daemon:** `stopDaemon` terminates a live pidfile process even when its lease
36
+ is expired/mismatched; startup inline-owner check verifies kernel start time.
37
+ - **Store:** `deleteLoop` removes child run history; `finalizeRun` guards
38
+ `status='running'`; ambiguous-name resolution ignores archived namesakes;
39
+ rerunning a terminal manual goal creates a fresh goal.
40
+ - **CLI/API/SDK/MCP:** add `daemon logs --tail`; `PATCH /v1/loops/:id` no longer
41
+ wipes schedule fields or 500s; resume-from-stopped recomputes `nextRunAt` so the
42
+ loop is actually due; mutation paths reject ambiguous names; numeric flags are
43
+ validated.
44
+ - **Routing:** honor the `route:enabled` opt-in tag; surface fatal drain errors
45
+ (non-zero exit, `fatal` count) instead of a green route-nothing run; tag routed
46
+ failure tasks `loops` and emit the correct `todos task upsert` command.
47
+
8
48
  ## 0.4.5 (2026-07-03)
9
49
 
10
50
  Upgrade-path fix for existing `0.4.1`-era local stores.
package/README.md CHANGED
@@ -882,11 +882,11 @@ On Linux this writes a user systemd service. On macOS it writes a LaunchAgent pl
882
882
  The adapters intentionally use provider command surfaces instead of pretending every agent has one SDK:
883
883
 
884
884
  - Claude uses `claude -p --output-format json` and safe-mode/local setting sources by default.
885
- - 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.
885
+ - 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. While the step is running, OpenLoops records bounded agent id/status updates as `step_progress` workflow events and as the running step's compact stdout; on terminal completion it 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.
886
886
  - AI Copilot and OpenCode use `run --format json --pure`. OpenCode requires an explicit provider/model id because ambient OpenCode config is machine-specific.
887
887
  - 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.
888
888
  - Codex uses `codex --ask-for-approval never exec --json --ephemeral --skip-git-repo-check`, with `--add-dir` for explicit extra writable directories where supported.
889
- - 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.
889
+ - 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 prompt text and raw event payloads from workflow stdout.
890
890
  - 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.
891
891
  - `--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.
892
892
  - `--sandbox` maps to provider-native sandbox flags. Codewith/Codex accept `read-only`, `workspace-write`, or `danger-full-access`; Cursor accepts `enabled` or `disabled`.
package/dist/api/index.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // package.json
4
4
  var package_default = {
5
5
  name: "@hasna/loops",
6
- version: "0.4.5",
6
+ version: "0.4.7",
7
7
  description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
8
8
  type: "module",
9
9
  main: "dist/index.js",
@@ -889,12 +889,16 @@ async function handleLoopsRequest(ctx, segments) {
889
889
  }
890
890
  if (segments.length === 1 && ctx.request.method === "PATCH") {
891
891
  const body = await readJsonBody(ctx.request, ctx.bodyLimitBytes);
892
- const loop = await storage.updateLoop(id, {
893
- status: body.status,
894
- nextRunAt: body.nextRunAt === null ? undefined : body.nextRunAt,
895
- retryScheduledFor: body.retryScheduledFor === null ? undefined : body.retryScheduledFor,
896
- expiresAt: body.expiresAt === null ? undefined : body.expiresAt
897
- });
892
+ const patch = {};
893
+ if ("status" in body && body.status !== undefined)
894
+ patch.status = body.status;
895
+ if ("nextRunAt" in body)
896
+ patch.nextRunAt = body.nextRunAt === null ? undefined : body.nextRunAt;
897
+ if ("retryScheduledFor" in body)
898
+ patch.retryScheduledFor = body.retryScheduledFor === null ? undefined : body.retryScheduledFor;
899
+ if ("expiresAt" in body)
900
+ patch.expiresAt = body.expiresAt === null ? undefined : body.expiresAt;
901
+ const loop = await storage.updateLoop(id, patch);
898
902
  return ok({ loop: publicLoop(loop) });
899
903
  }
900
904
  if (segments.length === 1 && ctx.request.method === "DELETE") {
@@ -923,15 +927,20 @@ async function handleRunsRequest(ctx, segments) {
923
927
  if (segments.length === 2 && id && segments[1] === "recover" && ctx.request.method === "POST") {
924
928
  const storage2 = requireStorage(ctx.storage);
925
929
  const now = ctx.now();
930
+ const target = await storage2.getRun(id);
931
+ if (!target)
932
+ return fail("run_not_found", 404);
926
933
  const recovered = await storage2.recoverExpiredRunLeasesDetailed(now);
927
- for (const run of recovered.abandoned) {
934
+ const abandoned = recovered.abandoned.filter((run) => run.id === id);
935
+ const deferred = recovered.deferred.filter((run) => run.id === id);
936
+ for (const run of abandoned) {
928
937
  const loop = await storage2.getLoop(run.loopId);
929
938
  if (loop)
930
939
  await advanceLoopAfterRun(storage2, loop, run, new Date(run.finishedAt ?? now), false);
931
940
  }
932
941
  return ok({
933
- abandoned: recovered.abandoned.map((run) => publicRun(run, false, { redactError: true })),
934
- deferred: recovered.deferred.map((run) => publicRun(run, false, { redactError: true }))
942
+ abandoned: abandoned.map((run) => publicRun(run, false, { redactError: true })),
943
+ deferred: deferred.map((run) => publicRun(run, false, { redactError: true }))
935
944
  });
936
945
  }
937
946
  const storage = requireStorage(ctx.storage);