@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.
@@ -1,10 +1,21 @@
1
1
  import type { LanguageModel } from "ai";
2
- import type { AgentTarget, ExecutableTarget, ExecutorResult, Loop, LoopMachineRef, LoopRun, PersistGuardOptions } from "../types.js";
2
+ import type { AgentProvider, AgentTarget, ExecutableTarget, ExecutorResult, Loop, LoopMachineRef, LoopRun, PersistGuardOptions } from "../types.js";
3
3
  export interface SpawnedProcessInfo {
4
4
  pid: number;
5
5
  pgid: number;
6
6
  processStartedAt: string;
7
7
  }
8
+ export interface AgentProgressInfo {
9
+ provider: AgentProvider;
10
+ agentId?: string;
11
+ status?: string;
12
+ summary?: string;
13
+ statusReason?: string;
14
+ threadId?: string;
15
+ rolloutPath?: string;
16
+ pid?: number;
17
+ lastEventSeq?: number;
18
+ }
8
19
  export interface ExecuteOptions extends PersistGuardOptions {
9
20
  maxOutputBytes?: number;
10
21
  env?: NodeJS.ProcessEnv;
@@ -14,6 +25,8 @@ export interface ExecuteOptions extends PersistGuardOptions {
14
25
  onSpawn?: (pid: number) => void;
15
26
  /** Children are spawned detached in their own process group, so pgid === pid. */
16
27
  onSpawnProcess?: (info: SpawnedProcessInfo) => void;
28
+ /** Progress from durable provider controllers, for example Codewith background agents. */
29
+ onAgentProgress?: (info: AgentProgressInfo) => void;
17
30
  machine?: LoopMachineRef;
18
31
  machineResolver?: (machine: LoopMachineRef) => LoopMachineRef;
19
32
  machineCommandResolver?: (machineId: string, command: string) => MachineCommandPlan;
package/dist/lib/mode.js CHANGED
@@ -2,7 +2,7 @@
2
2
  // package.json
3
3
  var package_default = {
4
4
  name: "@hasna/loops",
5
- version: "0.4.5",
5
+ version: "0.4.7",
6
6
  description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
7
7
  type: "module",
8
8
  main: "dist/index.js",
@@ -42,11 +42,15 @@ export interface TodosTaskRouteOptions {
42
42
  preflight?: boolean;
43
43
  dryRun?: boolean;
44
44
  todosProject?: string;
45
+ /** Internal drain context; never derived from CLI/event fields. */
46
+ sourceTodosProjectPath?: string;
45
47
  }
46
48
  export interface TodosReadyTask {
47
49
  id?: string;
48
50
  task_id?: string;
49
51
  taskId?: string;
52
+ source_project_path?: string;
53
+ sourceProjectPath?: string;
50
54
  title?: string;
51
55
  description?: string;
52
56
  body?: string;
@@ -70,6 +74,8 @@ export interface TodosReadyTask {
70
74
  }
71
75
  export interface TodosDrainOptions extends TodosTaskRouteOptions {
72
76
  todosProject?: string;
77
+ todosProjectsFromRegistry?: boolean;
78
+ todosProjectInclude?: string[];
73
79
  todosProjectId?: string;
74
80
  taskList?: string;
75
81
  tags?: string;
@@ -724,7 +724,7 @@ function buildAgentInvocation(target) {
724
724
  if (target.model)
725
725
  args.push("--model", target.model);
726
726
  args.push(...target.extraArgs ?? []);
727
- args.push("agent", "start");
727
+ args.push("agent", "start", "--json");
728
728
  if (target.cwd)
729
729
  args.push("--cwd", target.cwd);
730
730
  args.push(target.prompt);
@@ -1491,11 +1491,14 @@ function ensurePrivateStorePath(file) {
1491
1491
  class Store {
1492
1492
  db;
1493
1493
  rootDir;
1494
+ memoryRootDir;
1494
1495
  constructor(path) {
1495
1496
  const file = path ?? dbPath();
1496
1497
  if (file !== ":memory:")
1497
1498
  ensurePrivateStorePath(file);
1498
1499
  this.rootDir = file === ":memory:" ? mkdtempSync(join3(tmpdir(), "open-loops-store-")) : dirname2(file);
1500
+ if (file === ":memory:")
1501
+ this.memoryRootDir = this.rootDir;
1499
1502
  this.db = new Database(file);
1500
1503
  this.db.exec("PRAGMA foreign_keys = ON;");
1501
1504
  this.db.exec("PRAGMA busy_timeout = 5000;");
@@ -1948,9 +1951,12 @@ class Store {
1948
1951
  const rows = this.db.query("SELECT * FROM loops WHERE name = ? ORDER BY created_at DESC LIMIT 2").all(idOrName);
1949
1952
  if (rows.length === 0)
1950
1953
  throw new LoopNotFoundError(idOrName);
1951
- if (rows.length > 1)
1954
+ if (rows.length === 1)
1955
+ return rowToLoop(rows[0]);
1956
+ const active = this.db.query("SELECT * FROM loops WHERE name = ? AND archived_at IS NULL ORDER BY created_at DESC LIMIT 2").all(idOrName);
1957
+ if (active.length !== 1)
1952
1958
  throw new AmbiguousNameError(idOrName);
1953
- return rowToLoop(rows[0]);
1959
+ return rowToLoop(active[0]);
1954
1960
  }
1955
1961
  requireLoop(idOrName) {
1956
1962
  return this.getLoop(idOrName) ?? this.findLoopByName(idOrName) ?? (() => {
@@ -2291,6 +2297,7 @@ class Store {
2291
2297
  return this.transact(() => {
2292
2298
  const loop = this.requireLoop(idOrName);
2293
2299
  this.setWorkflowWorkItemsForLoop(loop.id, "cancelled", "loop deleted", nowIso());
2300
+ this.db.query("DELETE FROM loop_runs WHERE loop_id = ?").run(loop.id);
2294
2301
  const res = this.db.query("DELETE FROM loops WHERE id = ?").run(loop.id);
2295
2302
  return res.changes > 0;
2296
2303
  });
@@ -2775,7 +2782,10 @@ class Store {
2775
2782
  return rowToGoal(row);
2776
2783
  }
2777
2784
  if (context.sourceType && context.sourceId) {
2778
- const row = this.db.query("SELECT * FROM goals WHERE source_type = ? AND source_id = ? ORDER BY created_at DESC LIMIT 1").get(context.sourceType, context.sourceId);
2785
+ const terminalPlaceholders = GOAL_TERMINAL.map(() => "?").join(", ");
2786
+ const row = this.db.query(`SELECT * FROM goals
2787
+ WHERE source_type = ? AND source_id = ? AND status NOT IN (${terminalPlaceholders})
2788
+ ORDER BY created_at DESC LIMIT 1`).get(context.sourceType, context.sourceId, ...GOAL_TERMINAL);
2779
2789
  if (row)
2780
2790
  return rowToGoal(row);
2781
2791
  }
@@ -3195,6 +3205,41 @@ class Store {
3195
3205
  throw new Error(`workflow step run not found after pid update: ${workflowRunId}/${stepId}`);
3196
3206
  return run;
3197
3207
  }
3208
+ recordWorkflowStepProgress(workflowRunId, stepId, progress, opts = {}) {
3209
+ const now = (opts.now ?? new Date).toISOString();
3210
+ this.db.exec("BEGIN IMMEDIATE");
3211
+ try {
3212
+ const res = this.db.query(`UPDATE workflow_step_runs
3213
+ SET stdout=COALESCE($stdout, stdout),
3214
+ stderr=COALESCE($stderr, stderr),
3215
+ updated_at=$updated
3216
+ WHERE workflow_run_id=$workflowRunId AND step_id=$stepId AND status='running'
3217
+ AND ($daemonLeaseId IS NULL OR EXISTS (
3218
+ SELECT 1 FROM daemon_lease WHERE id=$daemonLeaseId AND expires_at > $now
3219
+ ))`).run({
3220
+ $workflowRunId: workflowRunId,
3221
+ $stepId: stepId,
3222
+ $stdout: progress.stdout === undefined ? null : scrubbedOrNull(progress.stdout),
3223
+ $stderr: progress.stderr === undefined ? null : scrubbedOrNull(progress.stderr),
3224
+ $updated: now,
3225
+ $daemonLeaseId: opts.daemonLeaseId ?? null,
3226
+ $now: now
3227
+ });
3228
+ if (res.changes === 1) {
3229
+ this.appendWorkflowEvent(workflowRunId, "step_progress", stepId, progress.payload);
3230
+ }
3231
+ this.db.exec("COMMIT");
3232
+ } catch (error) {
3233
+ try {
3234
+ this.db.exec("ROLLBACK");
3235
+ } catch {}
3236
+ throw error;
3237
+ }
3238
+ const run = this.getWorkflowStepRun(workflowRunId, stepId);
3239
+ if (!run)
3240
+ throw new Error(`workflow step run not found after progress update: ${workflowRunId}/${stepId}`);
3241
+ return run;
3242
+ }
3198
3243
  recoverWorkflowRun(workflowRunId, reason = "workflow run recovered for retry") {
3199
3244
  return this.transact(() => {
3200
3245
  const now = nowIso();
@@ -3632,7 +3677,8 @@ class Store {
3632
3677
  AND ($daemonLeaseId IS NULL OR EXISTS (
3633
3678
  SELECT 1 FROM daemon_lease WHERE id=$daemonLeaseId AND expires_at > $now
3634
3679
  ))`).run(params) : this.db.query(`UPDATE loop_runs SET status=$status, finished_at=$finished, claim_token=NULL, lease_expires_at=NULL, pid=$pid, exit_code=$exitCode,
3635
- duration_ms=$durationMs, stdout=$stdout, stderr=$stderr, error=$error, updated_at=$updated WHERE id=$id`).run(params);
3680
+ duration_ms=$durationMs, stdout=$stdout, stderr=$stderr, error=$error, updated_at=$updated
3681
+ WHERE id=$id AND status='running'`).run(params);
3636
3682
  const run = this.getRun(id);
3637
3683
  if (!run)
3638
3684
  throw new Error(`run not found after finalize: ${id}`);
@@ -4160,6 +4206,12 @@ class Store {
4160
4206
  }
4161
4207
  close() {
4162
4208
  this.db.close();
4209
+ if (this.memoryRootDir) {
4210
+ try {
4211
+ rmSync2(this.memoryRootDir, { recursive: true, force: true });
4212
+ } catch {}
4213
+ this.memoryRootDir = undefined;
4214
+ }
4163
4215
  }
4164
4216
  }
4165
4217
 
@@ -724,7 +724,7 @@ function buildAgentInvocation(target) {
724
724
  if (target.model)
725
725
  args.push("--model", target.model);
726
726
  args.push(...target.extraArgs ?? []);
727
- args.push("agent", "start");
727
+ args.push("agent", "start", "--json");
728
728
  if (target.cwd)
729
729
  args.push("--cwd", target.cwd);
730
730
  args.push(target.prompt);
@@ -1491,11 +1491,14 @@ function ensurePrivateStorePath(file) {
1491
1491
  class Store {
1492
1492
  db;
1493
1493
  rootDir;
1494
+ memoryRootDir;
1494
1495
  constructor(path) {
1495
1496
  const file = path ?? dbPath();
1496
1497
  if (file !== ":memory:")
1497
1498
  ensurePrivateStorePath(file);
1498
1499
  this.rootDir = file === ":memory:" ? mkdtempSync(join3(tmpdir(), "open-loops-store-")) : dirname2(file);
1500
+ if (file === ":memory:")
1501
+ this.memoryRootDir = this.rootDir;
1499
1502
  this.db = new Database(file);
1500
1503
  this.db.exec("PRAGMA foreign_keys = ON;");
1501
1504
  this.db.exec("PRAGMA busy_timeout = 5000;");
@@ -1948,9 +1951,12 @@ class Store {
1948
1951
  const rows = this.db.query("SELECT * FROM loops WHERE name = ? ORDER BY created_at DESC LIMIT 2").all(idOrName);
1949
1952
  if (rows.length === 0)
1950
1953
  throw new LoopNotFoundError(idOrName);
1951
- if (rows.length > 1)
1954
+ if (rows.length === 1)
1955
+ return rowToLoop(rows[0]);
1956
+ const active = this.db.query("SELECT * FROM loops WHERE name = ? AND archived_at IS NULL ORDER BY created_at DESC LIMIT 2").all(idOrName);
1957
+ if (active.length !== 1)
1952
1958
  throw new AmbiguousNameError(idOrName);
1953
- return rowToLoop(rows[0]);
1959
+ return rowToLoop(active[0]);
1954
1960
  }
1955
1961
  requireLoop(idOrName) {
1956
1962
  return this.getLoop(idOrName) ?? this.findLoopByName(idOrName) ?? (() => {
@@ -2291,6 +2297,7 @@ class Store {
2291
2297
  return this.transact(() => {
2292
2298
  const loop = this.requireLoop(idOrName);
2293
2299
  this.setWorkflowWorkItemsForLoop(loop.id, "cancelled", "loop deleted", nowIso());
2300
+ this.db.query("DELETE FROM loop_runs WHERE loop_id = ?").run(loop.id);
2294
2301
  const res = this.db.query("DELETE FROM loops WHERE id = ?").run(loop.id);
2295
2302
  return res.changes > 0;
2296
2303
  });
@@ -2775,7 +2782,10 @@ class Store {
2775
2782
  return rowToGoal(row);
2776
2783
  }
2777
2784
  if (context.sourceType && context.sourceId) {
2778
- const row = this.db.query("SELECT * FROM goals WHERE source_type = ? AND source_id = ? ORDER BY created_at DESC LIMIT 1").get(context.sourceType, context.sourceId);
2785
+ const terminalPlaceholders = GOAL_TERMINAL.map(() => "?").join(", ");
2786
+ const row = this.db.query(`SELECT * FROM goals
2787
+ WHERE source_type = ? AND source_id = ? AND status NOT IN (${terminalPlaceholders})
2788
+ ORDER BY created_at DESC LIMIT 1`).get(context.sourceType, context.sourceId, ...GOAL_TERMINAL);
2779
2789
  if (row)
2780
2790
  return rowToGoal(row);
2781
2791
  }
@@ -3195,6 +3205,41 @@ class Store {
3195
3205
  throw new Error(`workflow step run not found after pid update: ${workflowRunId}/${stepId}`);
3196
3206
  return run;
3197
3207
  }
3208
+ recordWorkflowStepProgress(workflowRunId, stepId, progress, opts = {}) {
3209
+ const now = (opts.now ?? new Date).toISOString();
3210
+ this.db.exec("BEGIN IMMEDIATE");
3211
+ try {
3212
+ const res = this.db.query(`UPDATE workflow_step_runs
3213
+ SET stdout=COALESCE($stdout, stdout),
3214
+ stderr=COALESCE($stderr, stderr),
3215
+ updated_at=$updated
3216
+ WHERE workflow_run_id=$workflowRunId AND step_id=$stepId AND status='running'
3217
+ AND ($daemonLeaseId IS NULL OR EXISTS (
3218
+ SELECT 1 FROM daemon_lease WHERE id=$daemonLeaseId AND expires_at > $now
3219
+ ))`).run({
3220
+ $workflowRunId: workflowRunId,
3221
+ $stepId: stepId,
3222
+ $stdout: progress.stdout === undefined ? null : scrubbedOrNull(progress.stdout),
3223
+ $stderr: progress.stderr === undefined ? null : scrubbedOrNull(progress.stderr),
3224
+ $updated: now,
3225
+ $daemonLeaseId: opts.daemonLeaseId ?? null,
3226
+ $now: now
3227
+ });
3228
+ if (res.changes === 1) {
3229
+ this.appendWorkflowEvent(workflowRunId, "step_progress", stepId, progress.payload);
3230
+ }
3231
+ this.db.exec("COMMIT");
3232
+ } catch (error) {
3233
+ try {
3234
+ this.db.exec("ROLLBACK");
3235
+ } catch {}
3236
+ throw error;
3237
+ }
3238
+ const run = this.getWorkflowStepRun(workflowRunId, stepId);
3239
+ if (!run)
3240
+ throw new Error(`workflow step run not found after progress update: ${workflowRunId}/${stepId}`);
3241
+ return run;
3242
+ }
3198
3243
  recoverWorkflowRun(workflowRunId, reason = "workflow run recovered for retry") {
3199
3244
  return this.transact(() => {
3200
3245
  const now = nowIso();
@@ -3632,7 +3677,8 @@ class Store {
3632
3677
  AND ($daemonLeaseId IS NULL OR EXISTS (
3633
3678
  SELECT 1 FROM daemon_lease WHERE id=$daemonLeaseId AND expires_at > $now
3634
3679
  ))`).run(params) : this.db.query(`UPDATE loop_runs SET status=$status, finished_at=$finished, claim_token=NULL, lease_expires_at=NULL, pid=$pid, exit_code=$exitCode,
3635
- duration_ms=$durationMs, stdout=$stdout, stderr=$stderr, error=$error, updated_at=$updated WHERE id=$id`).run(params);
3680
+ duration_ms=$durationMs, stdout=$stdout, stderr=$stderr, error=$error, updated_at=$updated
3681
+ WHERE id=$id AND status='running'`).run(params);
3636
3682
  const run = this.getRun(id);
3637
3683
  if (!run)
3638
3684
  throw new Error(`run not found after finalize: ${id}`);
@@ -4160,6 +4206,12 @@ class Store {
4160
4206
  }
4161
4207
  close() {
4162
4208
  this.db.close();
4209
+ if (this.memoryRootDir) {
4210
+ try {
4211
+ rmSync2(this.memoryRootDir, { recursive: true, force: true });
4212
+ } catch {}
4213
+ this.memoryRootDir = undefined;
4214
+ }
4163
4215
  }
4164
4216
  }
4165
4217
 
@@ -125,6 +125,8 @@ export interface RecordGoalEventInput {
125
125
  export declare class Store {
126
126
  private db;
127
127
  private rootDir;
128
+ /** Temp dir created for a `:memory:` store, removed in close() so tests/short-lived instances don't leak it. */
129
+ private memoryRootDir?;
128
130
  constructor(path?: string);
129
131
  private migrate;
130
132
  private migrations;
@@ -279,6 +281,11 @@ export declare class Store {
279
281
  isWorkflowRunTerminal(workflowRunId: string): boolean;
280
282
  startWorkflowStepRun(workflowRunId: string, stepId: string, opts?: DaemonLeaseFence): WorkflowStepRun;
281
283
  markWorkflowStepPid(workflowRunId: string, stepId: string, pid: number, opts?: DaemonLeaseFence): WorkflowStepRun;
284
+ recordWorkflowStepProgress(workflowRunId: string, stepId: string, progress: {
285
+ stdout?: string;
286
+ stderr?: string;
287
+ payload?: Record<string, unknown>;
288
+ }, opts?: DaemonLeaseFence): WorkflowStepRun;
282
289
  recoverWorkflowRun(workflowRunId: string, reason?: string): {
283
290
  run: WorkflowRun;
284
291
  recoveredSteps: WorkflowStepRun[];
package/dist/lib/store.js CHANGED
@@ -724,7 +724,7 @@ function buildAgentInvocation(target) {
724
724
  if (target.model)
725
725
  args.push("--model", target.model);
726
726
  args.push(...target.extraArgs ?? []);
727
- args.push("agent", "start");
727
+ args.push("agent", "start", "--json");
728
728
  if (target.cwd)
729
729
  args.push("--cwd", target.cwd);
730
730
  args.push(target.prompt);
@@ -1491,11 +1491,14 @@ function ensurePrivateStorePath(file) {
1491
1491
  class Store {
1492
1492
  db;
1493
1493
  rootDir;
1494
+ memoryRootDir;
1494
1495
  constructor(path) {
1495
1496
  const file = path ?? dbPath();
1496
1497
  if (file !== ":memory:")
1497
1498
  ensurePrivateStorePath(file);
1498
1499
  this.rootDir = file === ":memory:" ? mkdtempSync(join3(tmpdir(), "open-loops-store-")) : dirname2(file);
1500
+ if (file === ":memory:")
1501
+ this.memoryRootDir = this.rootDir;
1499
1502
  this.db = new Database(file);
1500
1503
  this.db.exec("PRAGMA foreign_keys = ON;");
1501
1504
  this.db.exec("PRAGMA busy_timeout = 5000;");
@@ -1948,9 +1951,12 @@ class Store {
1948
1951
  const rows = this.db.query("SELECT * FROM loops WHERE name = ? ORDER BY created_at DESC LIMIT 2").all(idOrName);
1949
1952
  if (rows.length === 0)
1950
1953
  throw new LoopNotFoundError(idOrName);
1951
- if (rows.length > 1)
1954
+ if (rows.length === 1)
1955
+ return rowToLoop(rows[0]);
1956
+ const active = this.db.query("SELECT * FROM loops WHERE name = ? AND archived_at IS NULL ORDER BY created_at DESC LIMIT 2").all(idOrName);
1957
+ if (active.length !== 1)
1952
1958
  throw new AmbiguousNameError(idOrName);
1953
- return rowToLoop(rows[0]);
1959
+ return rowToLoop(active[0]);
1954
1960
  }
1955
1961
  requireLoop(idOrName) {
1956
1962
  return this.getLoop(idOrName) ?? this.findLoopByName(idOrName) ?? (() => {
@@ -2291,6 +2297,7 @@ class Store {
2291
2297
  return this.transact(() => {
2292
2298
  const loop = this.requireLoop(idOrName);
2293
2299
  this.setWorkflowWorkItemsForLoop(loop.id, "cancelled", "loop deleted", nowIso());
2300
+ this.db.query("DELETE FROM loop_runs WHERE loop_id = ?").run(loop.id);
2294
2301
  const res = this.db.query("DELETE FROM loops WHERE id = ?").run(loop.id);
2295
2302
  return res.changes > 0;
2296
2303
  });
@@ -2775,7 +2782,10 @@ class Store {
2775
2782
  return rowToGoal(row);
2776
2783
  }
2777
2784
  if (context.sourceType && context.sourceId) {
2778
- const row = this.db.query("SELECT * FROM goals WHERE source_type = ? AND source_id = ? ORDER BY created_at DESC LIMIT 1").get(context.sourceType, context.sourceId);
2785
+ const terminalPlaceholders = GOAL_TERMINAL.map(() => "?").join(", ");
2786
+ const row = this.db.query(`SELECT * FROM goals
2787
+ WHERE source_type = ? AND source_id = ? AND status NOT IN (${terminalPlaceholders})
2788
+ ORDER BY created_at DESC LIMIT 1`).get(context.sourceType, context.sourceId, ...GOAL_TERMINAL);
2779
2789
  if (row)
2780
2790
  return rowToGoal(row);
2781
2791
  }
@@ -3195,6 +3205,41 @@ class Store {
3195
3205
  throw new Error(`workflow step run not found after pid update: ${workflowRunId}/${stepId}`);
3196
3206
  return run;
3197
3207
  }
3208
+ recordWorkflowStepProgress(workflowRunId, stepId, progress, opts = {}) {
3209
+ const now = (opts.now ?? new Date).toISOString();
3210
+ this.db.exec("BEGIN IMMEDIATE");
3211
+ try {
3212
+ const res = this.db.query(`UPDATE workflow_step_runs
3213
+ SET stdout=COALESCE($stdout, stdout),
3214
+ stderr=COALESCE($stderr, stderr),
3215
+ updated_at=$updated
3216
+ WHERE workflow_run_id=$workflowRunId AND step_id=$stepId AND status='running'
3217
+ AND ($daemonLeaseId IS NULL OR EXISTS (
3218
+ SELECT 1 FROM daemon_lease WHERE id=$daemonLeaseId AND expires_at > $now
3219
+ ))`).run({
3220
+ $workflowRunId: workflowRunId,
3221
+ $stepId: stepId,
3222
+ $stdout: progress.stdout === undefined ? null : scrubbedOrNull(progress.stdout),
3223
+ $stderr: progress.stderr === undefined ? null : scrubbedOrNull(progress.stderr),
3224
+ $updated: now,
3225
+ $daemonLeaseId: opts.daemonLeaseId ?? null,
3226
+ $now: now
3227
+ });
3228
+ if (res.changes === 1) {
3229
+ this.appendWorkflowEvent(workflowRunId, "step_progress", stepId, progress.payload);
3230
+ }
3231
+ this.db.exec("COMMIT");
3232
+ } catch (error) {
3233
+ try {
3234
+ this.db.exec("ROLLBACK");
3235
+ } catch {}
3236
+ throw error;
3237
+ }
3238
+ const run = this.getWorkflowStepRun(workflowRunId, stepId);
3239
+ if (!run)
3240
+ throw new Error(`workflow step run not found after progress update: ${workflowRunId}/${stepId}`);
3241
+ return run;
3242
+ }
3198
3243
  recoverWorkflowRun(workflowRunId, reason = "workflow run recovered for retry") {
3199
3244
  return this.transact(() => {
3200
3245
  const now = nowIso();
@@ -3632,7 +3677,8 @@ class Store {
3632
3677
  AND ($daemonLeaseId IS NULL OR EXISTS (
3633
3678
  SELECT 1 FROM daemon_lease WHERE id=$daemonLeaseId AND expires_at > $now
3634
3679
  ))`).run(params) : this.db.query(`UPDATE loop_runs SET status=$status, finished_at=$finished, claim_token=NULL, lease_expires_at=NULL, pid=$pid, exit_code=$exitCode,
3635
- duration_ms=$durationMs, stdout=$stdout, stderr=$stderr, error=$error, updated_at=$updated WHERE id=$id`).run(params);
3680
+ duration_ms=$durationMs, stdout=$stdout, stderr=$stderr, error=$error, updated_at=$updated
3681
+ WHERE id=$id AND status='running'`).run(params);
3636
3682
  const run = this.getRun(id);
3637
3683
  if (!run)
3638
3684
  throw new Error(`run not found after finalize: ${id}`);
@@ -4160,6 +4206,12 @@ class Store {
4160
4206
  }
4161
4207
  close() {
4162
4208
  this.db.close();
4209
+ if (this.memoryRootDir) {
4210
+ try {
4211
+ rmSync2(this.memoryRootDir, { recursive: true, force: true });
4212
+ } catch {}
4213
+ this.memoryRootDir = undefined;
4214
+ }
4163
4215
  }
4164
4216
  }
4165
4217
  export {
@@ -37,6 +37,12 @@ export declare function roleFragment(role: string, flow: BuiltinFlow): string;
37
37
  * that rendering is contract-tested, so composition must preserve both shapes.
38
38
  */
39
39
  export declare function goalHeaderFragment(goal: string, role: string, flow: BuiltinFlow): string[];
40
+ /**
41
+ * Bounded lifecycle steps must not open native Codewith `/goal` state. The
42
+ * workflow itself already owns durability and sequencing; each agent step only
43
+ * needs a finite role/objective header plus exact task evidence instructions.
44
+ */
45
+ export declare function boundedStepHeaderFragment(objective: string, role: string, flow: BuiltinFlow): string[];
40
46
  export declare function adversarialReviewFragment(inspect: string, focus: string): string;
41
47
  export declare const DEFAULT_VERIFIER_IDLE_TIMEOUT_MS: number;
42
48
  export declare function verifierIdleTimeoutMs(input: {
@@ -39,9 +39,19 @@ export interface TodosTaskWorkflowTemplateInput extends AgentWorkflowTemplateBas
39
39
  taskDescription?: string;
40
40
  todosProjectPath?: string;
41
41
  prHandoff?: boolean;
42
+ prReviewRouting?: PrReviewRoutingTemplateContext;
42
43
  eventId?: string;
43
44
  eventType?: string;
44
45
  }
46
+ export interface PrReviewRoutingTemplateContext {
47
+ required?: boolean;
48
+ allowed?: boolean;
49
+ reason?: string;
50
+ author?: string;
51
+ reviewers?: string[];
52
+ selectedReviewer?: string;
53
+ signals?: string[];
54
+ }
45
55
  export interface EventWorkflowTemplateInput extends AgentWorkflowTemplateBaseInput {
46
56
  eventId: string;
47
57
  eventType: string;
@@ -9,7 +9,7 @@ export interface LoopsMcpToolMetadata {
9
9
  guarded?: boolean;
10
10
  requiresEnv?: string;
11
11
  }
12
- /** Tool metadata derived from TOOL_REGISTRATIONS; served via loops://tools and `loops mcp list-tools`. */
12
+ /** Tool metadata derived from TOOL_REGISTRATIONS; served via loops://tools and the loops-mcp bin's `list-tools` argv. */
13
13
  export declare const LOOPS_MCP_TOOLS: LoopsMcpToolMetadata[];
14
14
  export declare function createLoopsMcpServer(): McpServer;
15
15
  export declare function listToolsForCli(): LoopsMcpToolMetadata[];