@hasna/loops 0.3.53 → 0.3.55

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/dist/cli/index.js CHANGED
@@ -314,6 +314,8 @@ function updateReadyFlags(nodes, planStatus) {
314
314
  }
315
315
 
316
316
  // src/lib/workflow-spec.ts
317
+ import { readFileSync } from "fs";
318
+ import { isAbsolute, resolve } from "path";
317
319
  function assertObject(value, label) {
318
320
  if (!value || typeof value !== "object" || Array.isArray(value))
319
321
  throw new Error(`${label} must be an object`);
@@ -329,6 +331,15 @@ function optionalPositiveInteger(value, label) {
329
331
  throw new Error(`${label} must be a positive integer`);
330
332
  return value;
331
333
  }
334
+ function optionalTimeoutMs(value, label) {
335
+ if (value === undefined)
336
+ return;
337
+ if (value === null)
338
+ return null;
339
+ if (!Number.isInteger(value) || value <= 0)
340
+ throw new Error(`${label} must be a positive integer or null for unlimited`);
341
+ return value;
342
+ }
332
343
  function optionalBoolean(value, label) {
333
344
  if (value === undefined)
334
345
  return;
@@ -359,6 +370,38 @@ function optionalAccountRef(value, label) {
359
370
  tool: typeof value.tool === "string" ? value.tool.trim() : undefined
360
371
  };
361
372
  }
373
+ function promptFilePath(rawPath, opts) {
374
+ return isAbsolute(rawPath) ? resolve(rawPath) : resolve(opts.baseDir ?? process.cwd(), rawPath);
375
+ }
376
+ function readPromptFile(rawPath, label, opts) {
377
+ assertString(rawPath, `${label}.promptFile`);
378
+ const path = promptFilePath(rawPath.trim(), opts);
379
+ let prompt;
380
+ try {
381
+ prompt = readFileSync(path, "utf8");
382
+ } catch (error) {
383
+ const code = error && typeof error === "object" && "code" in error ? String(error.code) : undefined;
384
+ throw new Error(`${label}.promptFile could not be read${code ? `: ${code}` : ""}`);
385
+ }
386
+ if (prompt.trim() === "")
387
+ throw new Error(`${label}.promptFile must contain a non-empty prompt`);
388
+ return { prompt, promptSource: { type: "file", path } };
389
+ }
390
+ function matchingPromptSource(value, prompt, opts) {
391
+ if (!value || typeof value !== "object" || Array.isArray(value))
392
+ return;
393
+ if (value.type !== "file")
394
+ return;
395
+ const rawPath = value.path;
396
+ if (typeof rawPath !== "string" || rawPath.trim() === "")
397
+ return;
398
+ const path = promptFilePath(rawPath.trim(), opts);
399
+ try {
400
+ return readFileSync(path, "utf8") === prompt ? { type: "file", path } : undefined;
401
+ } catch {
402
+ return;
403
+ }
404
+ }
362
405
  function normalizeGoalSpec(value, label = "goal") {
363
406
  if (value === undefined)
364
407
  return;
@@ -381,11 +424,11 @@ function normalizeGoalSpec(value, label = "goal") {
381
424
  autoExecute
382
425
  };
383
426
  }
384
- function validateTarget(value, label) {
427
+ function validateTarget(value, label, opts) {
385
428
  assertObject(value, label);
386
429
  if (value.type === "command") {
387
430
  assertString(value.command, `${label}.command`);
388
- optionalPositiveInteger(value.timeoutMs, `${label}.timeoutMs`);
431
+ optionalTimeoutMs(value.timeoutMs, `${label}.timeoutMs`);
389
432
  optionalPositiveInteger(value.idleTimeoutMs, `${label}.idleTimeoutMs`);
390
433
  if (value.shell !== true && /\s/.test(value.command.trim())) {
391
434
  throw new Error(`${label}.command must be an executable without spaces when shell is false; put flags in args or set shell true`);
@@ -394,11 +437,17 @@ function validateTarget(value, label) {
394
437
  }
395
438
  if (value.type === "agent") {
396
439
  assertString(value.provider, `${label}.provider`);
397
- assertString(value.prompt, `${label}.prompt`);
440
+ const hasPrompt = typeof value.prompt === "string" && value.prompt.trim() !== "";
441
+ const hasPromptFile = typeof value.promptFile === "string" && value.promptFile.trim() !== "";
442
+ if (hasPrompt && hasPromptFile)
443
+ throw new Error(`${label} must use either prompt or promptFile, not both`);
444
+ if (!hasPrompt && !hasPromptFile)
445
+ throw new Error(`${label}.prompt must be a non-empty string or ${label}.promptFile must be set`);
446
+ const promptFields = hasPrompt ? { prompt: value.prompt, promptSource: matchingPromptSource(value.promptSource, value.prompt, opts) } : readPromptFile(value.promptFile, label, opts);
398
447
  const providers = ["claude", "cursor", "codewith", "aicopilot", "opencode", "codex"];
399
448
  if (!providers.includes(value.provider))
400
449
  throw new Error(`${label}.provider must be one of ${providers.join(", ")}`);
401
- optionalPositiveInteger(value.timeoutMs, `${label}.timeoutMs`);
450
+ optionalTimeoutMs(value.timeoutMs, `${label}.timeoutMs`);
402
451
  optionalPositiveInteger(value.idleTimeoutMs, `${label}.idleTimeoutMs`);
403
452
  if (value.authProfile !== undefined) {
404
453
  assertString(value.authProfile, `${label}.authProfile`);
@@ -495,11 +544,14 @@ function validateTarget(value, label) {
495
544
  if (value.routing.eventSource !== undefined)
496
545
  assertString(value.routing.eventSource, `${label}.routing.eventSource`);
497
546
  }
498
- return value;
547
+ const target = { ...value };
548
+ delete target.promptFile;
549
+ delete target.promptSource;
550
+ return { ...target, ...promptFields };
499
551
  }
500
552
  throw new Error(`${label}.type must be command or agent`);
501
553
  }
502
- function normalizeCreateWorkflowInput(input) {
554
+ function normalizeCreateWorkflowInput(input, opts = {}) {
503
555
  assertString(input.name, "workflow.name");
504
556
  const goal = normalizeGoalSpec(input.goal, "goal");
505
557
  if (!Array.isArray(input.steps) || input.steps.length === 0)
@@ -515,10 +567,10 @@ function normalizeCreateWorkflowInput(input) {
515
567
  ...step,
516
568
  id: step.id,
517
569
  goal: normalizeGoalSpec(step.goal, `workflow.steps[${index}].goal`),
518
- target: validateTarget(step.target, `workflow.steps[${index}].target`),
570
+ target: validateTarget(step.target, `workflow.steps[${index}].target`, opts),
519
571
  dependsOn: optionalStringArray(step.dependsOn, `workflow.steps[${index}].dependsOn`) ?? [],
520
572
  continueOnFailure: optionalBoolean(step.continueOnFailure, `workflow.steps[${index}].continueOnFailure`) ?? false,
521
- timeoutMs: optionalPositiveInteger(step.timeoutMs, `workflow.steps[${index}].timeoutMs`),
573
+ timeoutMs: optionalTimeoutMs(step.timeoutMs, `workflow.steps[${index}].timeoutMs`),
522
574
  account: optionalAccountRef(step.account, `workflow.steps[${index}].account`)
523
575
  };
524
576
  });
@@ -558,7 +610,7 @@ function workflowExecutionOrder(workflow) {
558
610
  visit(step);
559
611
  return order;
560
612
  }
561
- function workflowBodyFromJson(value, fallbackName) {
613
+ function workflowBodyFromJson(value, fallbackName, opts = {}) {
562
614
  assertObject(value, "workflow file");
563
615
  const rawName = fallbackName ?? value.name;
564
616
  assertString(rawName, "workflow.name");
@@ -570,7 +622,7 @@ function workflowBodyFromJson(value, fallbackName) {
570
622
  goal: normalizeGoalSpec(value.goal, "goal"),
571
623
  version: typeof value.version === "number" ? value.version : undefined,
572
624
  steps: value.steps
573
- });
625
+ }, opts);
574
626
  }
575
627
 
576
628
  // src/lib/run-artifacts.ts
@@ -1207,13 +1259,17 @@ class Store {
1207
1259
  }
1208
1260
  createLoop(input, from = new Date) {
1209
1261
  const now = nowIso();
1262
+ const target = input.target.type === "workflow" ? input.target : normalizeCreateWorkflowInput({
1263
+ name: "loop-target-validation",
1264
+ steps: [{ id: "target", target: input.target }]
1265
+ }).steps[0].target;
1210
1266
  const loop = {
1211
1267
  id: genId(),
1212
1268
  name: input.name,
1213
1269
  description: input.description,
1214
1270
  status: "active",
1215
1271
  schedule: input.schedule,
1216
- target: input.target,
1272
+ target,
1217
1273
  goal: input.goal,
1218
1274
  machine: input.machine,
1219
1275
  nextRunAt: initialNextRun(input.schedule, from),
@@ -1260,6 +1316,17 @@ class Store {
1260
1316
  const row = this.db.query("SELECT * FROM loops WHERE name = ? ORDER BY created_at DESC LIMIT 1").get(name);
1261
1317
  return row ? rowToLoop(row) : undefined;
1262
1318
  }
1319
+ requireUniqueLoop(idOrName) {
1320
+ const byId = this.getLoop(idOrName);
1321
+ if (byId)
1322
+ return byId;
1323
+ const rows = this.db.query("SELECT * FROM loops WHERE name = ? ORDER BY created_at DESC LIMIT 2").all(idOrName);
1324
+ if (rows.length === 0)
1325
+ throw new Error(`loop not found: ${idOrName}`);
1326
+ if (rows.length > 1)
1327
+ throw new Error(`ambiguous loop name: ${idOrName}; use a loop id`);
1328
+ return rowToLoop(rows[0]);
1329
+ }
1263
1330
  requireLoop(idOrName) {
1264
1331
  return this.getLoop(idOrName) ?? this.findLoopByName(idOrName) ?? (() => {
1265
1332
  throw new Error(`loop not found: ${idOrName}`);
@@ -1334,6 +1401,130 @@ class Store {
1334
1401
  throw new Error(`loop not found after update: ${id}`);
1335
1402
  return after;
1336
1403
  }
1404
+ activeLoopReferenceCount(workflowId) {
1405
+ const rows = this.db.query("SELECT target_json FROM loops WHERE archived_at IS NULL").all();
1406
+ let count = 0;
1407
+ for (const row of rows) {
1408
+ try {
1409
+ const target = JSON.parse(row.target_json);
1410
+ if (target.type === "workflow" && target.workflowId === workflowId)
1411
+ count += 1;
1412
+ } catch {}
1413
+ }
1414
+ return count;
1415
+ }
1416
+ archiveWorkflowIfUnreferenced(workflowId, updated) {
1417
+ if (this.activeLoopReferenceCount(workflowId) > 0)
1418
+ return;
1419
+ const workflow = this.getWorkflow(workflowId);
1420
+ if (!workflow || workflow.status !== "active")
1421
+ return;
1422
+ const res = this.db.query("UPDATE workflow_specs SET status='archived', updated_at=? WHERE id=? AND status='active'").run(updated, workflowId);
1423
+ if (res.changes !== 1)
1424
+ return;
1425
+ return this.getWorkflow(workflowId);
1426
+ }
1427
+ retargetWorkflowLoop(idOrName, workflowId, opts = {}) {
1428
+ const updated = (opts.now ?? new Date).toISOString();
1429
+ this.db.exec("BEGIN IMMEDIATE");
1430
+ try {
1431
+ const current = this.requireLoop(idOrName);
1432
+ if (current.target.type !== "workflow")
1433
+ throw new Error(`loop is not a workflow loop: ${idOrName}`);
1434
+ if (this.hasRunningRun(current.id))
1435
+ throw new Error(`refusing to retarget running loop: ${current.id}`);
1436
+ const workflow = this.requireWorkflow(workflowId);
1437
+ const target = { ...current.target, workflowId: workflow.id };
1438
+ if (opts.workflowTimeoutMs !== undefined)
1439
+ target.timeoutMs = opts.workflowTimeoutMs;
1440
+ const res = this.db.query(`UPDATE loops SET target_json=$target, updated_at=$updated
1441
+ WHERE id=$id
1442
+ AND ($daemonLeaseId IS NULL OR EXISTS (
1443
+ SELECT 1 FROM daemon_lease WHERE id=$daemonLeaseId AND expires_at > $now
1444
+ ))`).run({
1445
+ $id: current.id,
1446
+ $target: JSON.stringify(target),
1447
+ $updated: updated,
1448
+ $daemonLeaseId: opts.daemonLeaseId ?? null,
1449
+ $now: updated
1450
+ });
1451
+ if (res.changes !== 1)
1452
+ throw new Error("daemon lease lost");
1453
+ this.db.exec("COMMIT");
1454
+ const after = this.getLoop(current.id);
1455
+ if (!after)
1456
+ throw new Error(`loop not found after retarget: ${current.id}`);
1457
+ return after;
1458
+ } catch (error) {
1459
+ try {
1460
+ this.db.exec("ROLLBACK");
1461
+ } catch {}
1462
+ throw error;
1463
+ }
1464
+ }
1465
+ createAndRetargetWorkflowLoop(idOrName, workflowInput, opts = {}) {
1466
+ const normalized = normalizeCreateWorkflowInput(workflowInput);
1467
+ const updated = (opts.now ?? new Date).toISOString();
1468
+ this.db.exec("BEGIN IMMEDIATE");
1469
+ try {
1470
+ const current = this.requireUniqueLoop(idOrName);
1471
+ if (current.target.type !== "workflow")
1472
+ throw new Error(`loop is not a workflow loop: ${idOrName}`);
1473
+ if (this.hasRunningRun(current.id))
1474
+ throw new Error(`refusing to retarget running loop: ${current.id}`);
1475
+ const previousWorkflow = this.requireWorkflow(current.target.workflowId);
1476
+ const workflow = {
1477
+ id: genId(),
1478
+ name: normalized.name,
1479
+ description: normalized.description,
1480
+ version: normalized.version ?? 1,
1481
+ status: "active",
1482
+ goal: normalized.goal,
1483
+ steps: normalized.steps,
1484
+ createdAt: updated,
1485
+ updatedAt: updated
1486
+ };
1487
+ this.db.query(`INSERT INTO workflow_specs (id, name, description, version, status, goal_json, steps_json, created_at, updated_at)
1488
+ VALUES ($id, $name, $description, $version, $status, $goal, $steps, $created, $updated)`).run({
1489
+ $id: workflow.id,
1490
+ $name: workflow.name,
1491
+ $description: workflow.description ?? null,
1492
+ $version: workflow.version,
1493
+ $status: workflow.status,
1494
+ $goal: workflow.goal ? JSON.stringify(workflow.goal) : null,
1495
+ $steps: JSON.stringify(workflow.steps),
1496
+ $created: workflow.createdAt,
1497
+ $updated: workflow.updatedAt
1498
+ });
1499
+ const target = { ...current.target, workflowId: workflow.id };
1500
+ if (opts.workflowTimeoutMs !== undefined)
1501
+ target.timeoutMs = opts.workflowTimeoutMs;
1502
+ const res = this.db.query(`UPDATE loops SET target_json=$target, updated_at=$updated
1503
+ WHERE id=$id
1504
+ AND ($daemonLeaseId IS NULL OR EXISTS (
1505
+ SELECT 1 FROM daemon_lease WHERE id=$daemonLeaseId AND expires_at > $now
1506
+ ))`).run({
1507
+ $id: current.id,
1508
+ $target: JSON.stringify(target),
1509
+ $updated: updated,
1510
+ $daemonLeaseId: opts.daemonLeaseId ?? null,
1511
+ $now: updated
1512
+ });
1513
+ if (res.changes !== 1)
1514
+ throw new Error("daemon lease lost");
1515
+ const archivedOld = opts.archiveOld ? this.archiveWorkflowIfUnreferenced(previousWorkflow.id, updated) : undefined;
1516
+ this.db.exec("COMMIT");
1517
+ const loop = this.getLoop(current.id);
1518
+ if (!loop)
1519
+ throw new Error(`loop not found after retarget: ${current.id}`);
1520
+ return { loop, workflow, previousWorkflow, archivedOld };
1521
+ } catch (error) {
1522
+ try {
1523
+ this.db.exec("ROLLBACK");
1524
+ } catch {}
1525
+ throw error;
1526
+ }
1527
+ }
1337
1528
  renameLoop(id, name, opts = {}) {
1338
1529
  const current = this.getLoop(id);
1339
1530
  if (!current)
@@ -2914,9 +3105,9 @@ class Store {
2914
3105
 
2915
3106
  // src/cli/index.ts
2916
3107
  import { createHash as createHash3, randomUUID } from "crypto";
2917
- import { closeSync, existsSync as existsSync5, mkdirSync as mkdirSync7, mkdtempSync as mkdtempSync2, openSync as openSync2, readFileSync as readFileSync3, realpathSync, rmSync as rmSync3, writeFileSync as writeFileSync5 } from "fs";
3108
+ import { closeSync, existsSync as existsSync5, mkdirSync as mkdirSync7, mkdtempSync as mkdtempSync2, openSync as openSync2, readFileSync as readFileSync4, realpathSync, rmSync as rmSync3, writeFileSync as writeFileSync5 } from "fs";
2918
3109
  import { spawnSync as spawnSync5 } from "child_process";
2919
- import { join as join6, resolve as resolve2 } from "path";
3110
+ import { dirname as dirname4, join as join6, resolve as resolve3 } from "path";
2920
3111
  import { tmpdir as tmpdir2 } from "os";
2921
3112
  import { Database as Database2 } from "bun:sqlite";
2922
3113
  import { Command } from "commander";
@@ -3559,7 +3750,7 @@ function commandSpec(target) {
3559
3750
  cwd: commandTarget.cwd,
3560
3751
  shell: commandTarget.shell,
3561
3752
  env: commandTarget.env,
3562
- timeoutMs: commandTarget.timeoutMs ?? DEFAULT_TIMEOUT_MS,
3753
+ timeoutMs: commandTarget.timeoutMs === undefined ? DEFAULT_TIMEOUT_MS : commandTarget.timeoutMs,
3563
3754
  idleTimeoutMs: commandTarget.idleTimeoutMs,
3564
3755
  account: commandTarget.account,
3565
3756
  accountTool: commandTarget.account?.tool
@@ -3570,7 +3761,7 @@ function commandSpec(target) {
3570
3761
  command: providerCommand(agentTarget.provider),
3571
3762
  args: agentArgs(agentTarget),
3572
3763
  cwd: agentTarget.cwd,
3573
- timeoutMs: agentTarget.timeoutMs ?? DEFAULT_TIMEOUT_MS,
3764
+ timeoutMs: agentTarget.timeoutMs ?? null,
3574
3765
  idleTimeoutMs: agentTarget.idleTimeoutMs,
3575
3766
  account: agentTarget.account,
3576
3767
  accountTool: agentTarget.account?.tool ?? accountToolForProvider(agentTarget.provider),
@@ -3758,12 +3949,12 @@ async function executeRemoteSpec(spec, machine, metadata, opts) {
3758
3949
  if (opts.signal?.aborted)
3759
3950
  abortHandler();
3760
3951
  opts.signal?.addEventListener("abort", abortHandler, { once: true });
3761
- const timer = setTimeout(() => {
3952
+ const timer = typeof spec.timeoutMs === "number" ? setTimeout(() => {
3762
3953
  timedOut = true;
3763
3954
  if (child.pid)
3764
3955
  killProcessGroup(child.pid);
3765
- }, spec.timeoutMs);
3766
- timer.unref();
3956
+ }, spec.timeoutMs) : undefined;
3957
+ timer?.unref();
3767
3958
  let idleTimer;
3768
3959
  const resetIdleTimer = () => {
3769
3960
  if (!spec.idleTimeoutMs)
@@ -3795,7 +3986,8 @@ async function executeRemoteSpec(spec, machine, metadata, opts) {
3795
3986
  } catch (err) {
3796
3987
  error = err instanceof Error ? err.message : String(err);
3797
3988
  } finally {
3798
- clearTimeout(timer);
3989
+ if (timer)
3990
+ clearTimeout(timer);
3799
3991
  if (idleTimer)
3800
3992
  clearTimeout(idleTimer);
3801
3993
  opts.signal?.removeEventListener("abort", abortHandler);
@@ -3937,12 +4129,12 @@ async function executeTarget(target, metadata = {}, opts = {}) {
3937
4129
  if (opts.signal?.aborted)
3938
4130
  abortHandler();
3939
4131
  opts.signal?.addEventListener("abort", abortHandler, { once: true });
3940
- const timer = setTimeout(() => {
4132
+ const timer = typeof spec.timeoutMs === "number" ? setTimeout(() => {
3941
4133
  timedOut = true;
3942
4134
  if (child.pid)
3943
4135
  killProcessGroup(child.pid);
3944
- }, spec.timeoutMs);
3945
- timer.unref();
4136
+ }, spec.timeoutMs) : undefined;
4137
+ timer?.unref();
3946
4138
  let idleTimer;
3947
4139
  const resetIdleTimer = () => {
3948
4140
  if (!spec.idleTimeoutMs)
@@ -3974,7 +4166,8 @@ async function executeTarget(target, metadata = {}, opts = {}) {
3974
4166
  } catch (err) {
3975
4167
  error = err instanceof Error ? err.message : String(err);
3976
4168
  } finally {
3977
- clearTimeout(timer);
4169
+ if (timer)
4170
+ clearTimeout(timer);
3978
4171
  if (idleTimer)
3979
4172
  clearTimeout(idleTimer);
3980
4173
  opts.signal?.removeEventListener("abort", abortHandler);
@@ -4399,7 +4592,7 @@ ${result.stderr}`);
4399
4592
  // src/lib/workflow-runner.ts
4400
4593
  function targetWithStepAccount(step) {
4401
4594
  const account = step.account ?? step.target.account;
4402
- const timeoutMs = step.timeoutMs ?? step.target.timeoutMs;
4595
+ const timeoutMs = step.timeoutMs !== undefined ? step.timeoutMs : step.target.timeoutMs;
4403
4596
  if (!account && timeoutMs === step.target.timeoutMs)
4404
4597
  return step.target;
4405
4598
  return { ...step.target, account, timeoutMs };
@@ -4695,7 +4888,8 @@ async function executeLoopTarget(store, loop, run, opts = {}) {
4695
4888
  })
4696
4889
  });
4697
4890
  }
4698
- const controller = loop.target.timeoutMs ? new AbortController : undefined;
4891
+ const workflowTimeoutMs = typeof loop.target.timeoutMs === "number" ? loop.target.timeoutMs : undefined;
4892
+ const controller = workflowTimeoutMs !== undefined ? new AbortController : undefined;
4699
4893
  let workflowTimedOut = false;
4700
4894
  const externalAbort = () => controller?.abort();
4701
4895
  if (controller && opts.signal?.aborted)
@@ -4705,13 +4899,13 @@ async function executeLoopTarget(store, loop, run, opts = {}) {
4705
4899
  const timer = controller ? setTimeout(() => {
4706
4900
  workflowTimedOut = true;
4707
4901
  controller.abort();
4708
- }, loop.target.timeoutMs) : undefined;
4902
+ }, workflowTimeoutMs) : undefined;
4709
4903
  timer?.unref();
4710
4904
  try {
4711
4905
  return await executeWorkflow(store, workflow, {
4712
4906
  ...opts,
4713
4907
  signal: controller?.signal ?? opts.signal,
4714
- signalTimeoutMessage: () => workflowTimedOut && loop.target.type === "workflow" ? `workflow timed out after ${loop.target.timeoutMs}ms` : undefined,
4908
+ signalTimeoutMessage: () => workflowTimedOut && loop.target.type === "workflow" ? `workflow timed out after ${workflowTimeoutMs}ms` : undefined,
4715
4909
  loop,
4716
4910
  loopRun: run,
4717
4911
  scheduledFor: run.scheduledFor,
@@ -5031,13 +5225,13 @@ async function tick(deps) {
5031
5225
  }
5032
5226
 
5033
5227
  // src/daemon/control.ts
5034
- import { existsSync as existsSync3, mkdirSync as mkdirSync4, readFileSync, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
5228
+ import { existsSync as existsSync3, mkdirSync as mkdirSync4, readFileSync as readFileSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
5035
5229
  import { hostname } from "os";
5036
5230
  import { dirname as dirname2 } from "path";
5037
5231
 
5038
5232
  // src/daemon/loop.ts
5039
5233
  function realSleep(ms) {
5040
- return new Promise((resolve) => setTimeout(resolve, ms));
5234
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
5041
5235
  }
5042
5236
  async function runLoop(opts) {
5043
5237
  const sleep = opts.sleep ?? realSleep;
@@ -5062,7 +5256,7 @@ function readPid(path = pidFilePath()) {
5062
5256
  if (!existsSync3(path))
5063
5257
  return;
5064
5258
  try {
5065
- const pid = Number(readFileSync(path, "utf8").trim());
5259
+ const pid = Number(readFileSync2(path, "utf8").trim());
5066
5260
  return Number.isInteger(pid) && pid > 0 ? pid : undefined;
5067
5261
  } catch {
5068
5262
  return;
@@ -5487,7 +5681,11 @@ function runDoctor(store) {
5487
5681
  if (loop.target.type === "workflow") {
5488
5682
  const workflow = store.requireWorkflow(loop.target.workflowId);
5489
5683
  for (const step of workflowExecutionOrder(workflow)) {
5490
- preflightTarget({ ...step.target, account: step.account ?? step.target.account, timeoutMs: step.timeoutMs ?? step.target.timeoutMs }, { loopId: loop.id, loopName: loop.name, workflowId: workflow.id, workflowName: workflow.name, workflowStepId: step.id }, { machine: loop.machine });
5684
+ preflightTarget({
5685
+ ...step.target,
5686
+ account: step.account ?? step.target.account,
5687
+ timeoutMs: step.timeoutMs !== undefined ? step.timeoutMs : step.target.timeoutMs
5688
+ }, { loopId: loop.id, loopName: loop.name, workflowId: workflow.id, workflowName: workflow.name, workflowStepId: step.id }, { machine: loop.machine });
5491
5689
  }
5492
5690
  } else {
5493
5691
  preflightTarget(loop.target, { loopId: loop.id, loopName: loop.name }, { machine: loop.machine });
@@ -5967,7 +6165,7 @@ function buildScriptInventoryReport(store, opts = {}) {
5967
6165
  // package.json
5968
6166
  var package_default = {
5969
6167
  name: "@hasna/loops",
5970
- version: "0.3.53",
6168
+ version: "0.3.55",
5971
6169
  description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
5972
6170
  type: "module",
5973
6171
  main: "dist/index.js",
@@ -6057,9 +6255,9 @@ function packageVersion() {
6057
6255
 
6058
6256
  // src/lib/templates.ts
6059
6257
  import { execFileSync } from "child_process";
6060
- import { existsSync as existsSync4, lstatSync, mkdirSync as mkdirSync6, readdirSync, readFileSync as readFileSync2, writeFileSync as writeFileSync4 } from "fs";
6258
+ import { existsSync as existsSync4, lstatSync, mkdirSync as mkdirSync6, readdirSync, readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "fs";
6061
6259
  import { homedir as homedir3 } from "os";
6062
- import { basename as basename3, isAbsolute, join as join5, relative, resolve } from "path";
6260
+ import { basename as basename3, isAbsolute as isAbsolute2, join as join5, relative, resolve as resolve2 } from "path";
6063
6261
  var TODOS_TASK_WORKER_VERIFIER_TEMPLATE_ID = "todos-task-worker-verifier";
6064
6262
  var EVENT_WORKER_VERIFIER_TEMPLATE_ID = "event-worker-verifier";
6065
6263
  var BOUNDED_AGENT_WORKER_VERIFIER_TEMPLATE_ID = "bounded-agent-worker-verifier";
@@ -6097,7 +6295,8 @@ var TEMPLATE_SUMMARIES = [
6097
6295
  { name: "manualBreakGlass", default: "false", description: "Allow explicit danger-full-access in a generated workflow. Intended for manual emergency use only." },
6098
6296
  { name: "worktreeMode", default: "auto", description: "Worktree isolation mode: auto, required, off, or main." },
6099
6297
  { name: "worktreeRoot", default: "~/.hasna/loops/worktrees", description: "Base directory for OpenLoops-managed git worktrees." },
6100
- { name: "worktreeBranchPrefix", default: "openloops", description: "Branch prefix for generated task/event worktree branches." }
6298
+ { name: "worktreeBranchPrefix", default: "openloops", description: "Branch prefix for generated task/event worktree branches." },
6299
+ { name: "timeoutMs", default: "unlimited", description: "Agent step timeout in milliseconds, or unlimited/none/null for no timeout. Deterministic helper steps remain bounded." }
6101
6300
  ]
6102
6301
  },
6103
6302
  {
@@ -6127,7 +6326,8 @@ var TEMPLATE_SUMMARIES = [
6127
6326
  { name: "manualBreakGlass", default: "false", description: "Allow explicit danger-full-access in a generated workflow. Intended for manual emergency use only." },
6128
6327
  { name: "worktreeMode", default: "auto", description: "Worktree isolation mode: auto, required, off, or main." },
6129
6328
  { name: "worktreeRoot", default: "~/.hasna/loops/worktrees", description: "Base directory for OpenLoops-managed git worktrees." },
6130
- { name: "worktreeBranchPrefix", default: "openloops", description: "Branch prefix for generated event worktree branches." }
6329
+ { name: "worktreeBranchPrefix", default: "openloops", description: "Branch prefix for generated event worktree branches." },
6330
+ { name: "timeoutMs", default: "unlimited", description: "Agent step timeout in milliseconds, or unlimited/none/null for no timeout. Deterministic helper steps remain bounded." }
6131
6331
  ]
6132
6332
  },
6133
6333
  {
@@ -6155,7 +6355,7 @@ var TEMPLATE_SUMMARIES = [
6155
6355
  { name: "worktreeMode", default: "auto", description: "Worktree isolation mode: auto, required, off, or main." },
6156
6356
  { name: "worktreeRoot", default: "~/.hasna/loops/worktrees", description: "Base directory for OpenLoops-managed git worktrees." },
6157
6357
  { name: "worktreeBranchPrefix", default: "openloops", description: "Branch prefix for generated bounded-agent worktree branches." },
6158
- { name: "timeoutMs", default: "2700000", description: "Step timeout in milliseconds." }
6358
+ { name: "timeoutMs", default: "unlimited", description: "Agent step timeout in milliseconds, or unlimited/none/null for no timeout. Deterministic helper steps remain bounded." }
6159
6359
  ]
6160
6360
  },
6161
6361
  {
@@ -6174,7 +6374,8 @@ var TEMPLATE_SUMMARIES = [
6174
6374
  { name: "accountPool", description: "Comma-separated OpenAccounts profiles for non-Codewith providers." },
6175
6375
  { name: "provider", default: "codewith", description: "Agent provider." },
6176
6376
  { name: "sandbox", default: "workspace-write", description: "Provider sandbox mode." },
6177
- { name: "worktreeMode", default: "required", description: "Worktree isolation mode." }
6377
+ { name: "worktreeMode", default: "required", description: "Worktree isolation mode." },
6378
+ { name: "timeoutMs", default: "unlimited", description: "Agent step timeout in milliseconds, or unlimited/none/null for no timeout. Deterministic helper steps remain bounded." }
6178
6379
  ]
6179
6380
  },
6180
6381
  {
@@ -6279,6 +6480,30 @@ function taskLabel(input) {
6279
6480
  const head = input.taskTitle?.trim() || input.taskId;
6280
6481
  return head.length > 160 ? `${head.slice(0, 157)}...` : head;
6281
6482
  }
6483
+ var UNLIMITED_AGENT_TIMEOUT_MS = null;
6484
+ function agentTimeoutMs(input) {
6485
+ return input.timeoutMs === undefined ? UNLIMITED_AGENT_TIMEOUT_MS : input.timeoutMs;
6486
+ }
6487
+ function parseTemplateTimeoutMs(raw) {
6488
+ if (raw === undefined || raw.trim() === "")
6489
+ return;
6490
+ const normalized = raw.trim().toLowerCase();
6491
+ if (["unlimited", "none", "null", "never", "off", "false"].includes(normalized))
6492
+ return null;
6493
+ const value = Number(raw);
6494
+ if (!Number.isInteger(value) || value <= 0) {
6495
+ throw new Error("timeoutMs must be a positive integer number of milliseconds, or unlimited/none/null");
6496
+ }
6497
+ return value;
6498
+ }
6499
+ function parseDeterministicTimeoutMs(raw, fallbackMs, label = "timeoutMs") {
6500
+ if (raw === undefined || raw.trim() === "")
6501
+ return fallbackMs;
6502
+ const value = Number(raw);
6503
+ if (!Number.isInteger(value) || value <= 0)
6504
+ throw new Error(`${label} must be a positive integer number of milliseconds`);
6505
+ return value;
6506
+ }
6282
6507
  function stableIndex(seed, size) {
6283
6508
  let hash = 2166136261;
6284
6509
  for (let i = 0;i < seed.length; i += 1) {
@@ -6341,7 +6566,7 @@ function normalizeWorktreeMode(mode) {
6341
6566
  function defaultWorktreeRoot(root) {
6342
6567
  if (root?.trim()) {
6343
6568
  const expanded = root.trim().replace(/^~(?=$|\/)/, homedir3());
6344
- return isAbsolute(expanded) ? expanded : resolve(expanded);
6569
+ return isAbsolute2(expanded) ? expanded : resolve2(expanded);
6345
6570
  }
6346
6571
  return join5(homedir3(), ".hasna", "loops", "worktrees");
6347
6572
  }
@@ -6448,7 +6673,7 @@ function worktreePlan(input, seed) {
6448
6673
  const seedSlug = `${slugSegment(seed, "run").slice(0, 48)}-${stableHex(`${repoRoot}:${seed}`)}`;
6449
6674
  const worktreePath = join5(root, repoSlug, seedSlug);
6450
6675
  const relativeCwd = relative(repoRoot, originalCwd);
6451
- const cwd = relativeCwd && !relativeCwd.startsWith("..") && !isAbsolute(relativeCwd) ? join5(worktreePath, relativeCwd) : worktreePath;
6676
+ const cwd = relativeCwd && !relativeCwd.startsWith("..") && !isAbsolute2(relativeCwd) ? join5(worktreePath, relativeCwd) : worktreePath;
6452
6677
  const branchPrefix = (input.worktreeBranchPrefix?.trim() || "openloops").replace(/^\/+|\/+$/g, "") || "openloops";
6453
6678
  const branch = `${branchPrefix}/${repoSlug}/${seedSlug}`;
6454
6679
  const prepareStep = {
@@ -6550,7 +6775,7 @@ function agentTarget(input, prompt, role, seed, plan) {
6550
6775
  ...input.projectGroup ? { projectGroup: input.projectGroup } : {}
6551
6776
  },
6552
6777
  account: accountForRole(input, role, seed),
6553
- timeoutMs: 45 * 60000
6778
+ timeoutMs: agentTimeoutMs(input)
6554
6779
  };
6555
6780
  }
6556
6781
  function workflowStepsWithWorktree(plan, steps) {
@@ -6683,9 +6908,24 @@ function assertNoImplicitDangerFullAccess(value, label) {
6683
6908
  assertNoImplicitDangerFullAccess(entry, `${label}.${key}`);
6684
6909
  }
6685
6910
  }
6911
+ function assertNoCustomTemplatePromptFiles(value, label) {
6912
+ if (!value || typeof value !== "object")
6913
+ return;
6914
+ if (Array.isArray(value)) {
6915
+ value.forEach((entry, index) => assertNoCustomTemplatePromptFiles(entry, `${label}[${index}]`));
6916
+ return;
6917
+ }
6918
+ for (const [key, entry] of Object.entries(value)) {
6919
+ if (key === "promptFile") {
6920
+ throw new Error(`${label}.${key} is not allowed in custom templates; use direct workflow JSON for prompt-file-backed workflows`);
6921
+ }
6922
+ assertNoCustomTemplatePromptFiles(entry, `${label}.${key}`);
6923
+ }
6924
+ }
6686
6925
  function assertCustomTemplateSafety(value, label) {
6687
6926
  assertNoDangerousCustomTemplateScalars(value, label);
6688
6927
  assertNoImplicitDangerFullAccess(value, label);
6928
+ assertNoCustomTemplatePromptFiles(value, label);
6689
6929
  }
6690
6930
  function customTemplateDefinitionFromJson(value, sourcePath) {
6691
6931
  assertRecord(value, sourcePath);
@@ -6715,7 +6955,7 @@ function customTemplateSummary(definition, sourcePath) {
6715
6955
  function readCustomTemplateFile(file) {
6716
6956
  let parsed;
6717
6957
  try {
6718
- parsed = JSON.parse(readFileSync2(file, "utf8"));
6958
+ parsed = JSON.parse(readFileSync3(file, "utf8"));
6719
6959
  } catch (error) {
6720
6960
  const message = error instanceof Error ? error.message : String(error);
6721
6961
  throw new Error(`failed to read custom template ${file}: ${message}`);
@@ -6854,19 +7094,19 @@ function renderCustomLoopTemplate(entry, values) {
6854
7094
  return workflow;
6855
7095
  }
6856
7096
  function validateCustomLoopTemplateFile(file) {
6857
- const source = resolve(file);
7097
+ const source = resolve2(file);
6858
7098
  const entry = readCustomTemplateFile(source);
6859
- const existing = loadCustomLoopTemplatesRaw().filter((template) => resolve(template.path) !== source);
7099
+ const existing = loadCustomLoopTemplatesRaw().filter((template) => resolve2(template.path) !== source);
6860
7100
  assertNoTemplateCollisions([...existing, entry]);
6861
7101
  return structuredClone(entry.summary);
6862
7102
  }
6863
7103
  function importCustomLoopTemplate(file, opts = {}) {
6864
- const source = resolve(file);
7104
+ const source = resolve2(file);
6865
7105
  const entry = readCustomTemplateFile(source);
6866
7106
  const dir = ensureCustomLoopTemplatesDir();
6867
7107
  const destination = join5(dir, `${entry.definition.id}.json`);
6868
7108
  const replaced = existsSync4(destination);
6869
- const existing = loadCustomLoopTemplatesRaw().filter((template) => resolve(template.path) !== resolve(destination));
7109
+ const existing = loadCustomLoopTemplatesRaw().filter((template) => resolve2(template.path) !== resolve2(destination));
6870
7110
  assertNoTemplateCollisions([...existing, { ...entry, path: destination, summary: customTemplateSummary(entry.definition, destination) }]);
6871
7111
  if (replaced) {
6872
7112
  const stat = lstatSync(destination);
@@ -6978,18 +7218,15 @@ function renderTodosTaskWorkerVerifierWorkflow(input) {
6978
7218
  name: "Worker",
6979
7219
  description: "Implement the todos task and record evidence.",
6980
7220
  target: agentTarget(input, workerPrompt, "worker", input.taskId, plan),
6981
- timeoutMs: 45 * 60000
7221
+ timeoutMs: agentTimeoutMs(input)
6982
7222
  },
6983
7223
  {
6984
7224
  id: "verifier",
6985
7225
  name: "Verifier",
6986
7226
  description: "Adversarially verify worker output and update todos.",
6987
7227
  dependsOn: ["worker"],
6988
- target: {
6989
- ...agentTarget(input, verifierPrompt, "verifier", input.taskId, plan),
6990
- idleTimeoutMs: 10 * 60000
6991
- },
6992
- timeoutMs: 30 * 60000
7228
+ target: agentTarget(input, verifierPrompt, "verifier", input.taskId, plan),
7229
+ timeoutMs: agentTimeoutMs(input)
6993
7230
  }
6994
7231
  ])
6995
7232
  };
@@ -7150,7 +7387,7 @@ function renderTaskLifecycleWorkflow(input) {
7150
7387
  name: "Triage",
7151
7388
  description: "Check task eligibility, duplicates, dependencies, and automation gates.",
7152
7389
  target: agentTarget(input, triagePrompt, "triage", input.taskId, plan),
7153
- timeoutMs: 20 * 60000
7390
+ timeoutMs: agentTimeoutMs(input)
7154
7391
  },
7155
7392
  {
7156
7393
  id: "triage-gate",
@@ -7172,7 +7409,7 @@ function renderTaskLifecycleWorkflow(input) {
7172
7409
  description: "Create a concise implementation plan and split unsafe scope before work starts.",
7173
7410
  dependsOn: ["triage-gate"],
7174
7411
  target: agentTarget(input, plannerPrompt, "planner", input.taskId, plan),
7175
- timeoutMs: 25 * 60000
7412
+ timeoutMs: agentTimeoutMs(input)
7176
7413
  },
7177
7414
  {
7178
7415
  id: "planner-gate",
@@ -7194,18 +7431,15 @@ function renderTaskLifecycleWorkflow(input) {
7194
7431
  description: "Implement the todos task according to triage and planner evidence.",
7195
7432
  dependsOn: ["planner-gate"],
7196
7433
  target: agentTarget(input, workerPrompt, "worker", input.taskId, plan),
7197
- timeoutMs: 45 * 60000
7434
+ timeoutMs: agentTimeoutMs(input)
7198
7435
  },
7199
7436
  {
7200
7437
  id: "verifier",
7201
7438
  name: "Verifier",
7202
7439
  description: "Adversarially verify worker output and update todos.",
7203
7440
  dependsOn: ["worker"],
7204
- target: {
7205
- ...agentTarget(input, verifierPrompt, "verifier", input.taskId, plan),
7206
- idleTimeoutMs: 10 * 60000
7207
- },
7208
- timeoutMs: 30 * 60000
7441
+ target: agentTarget(input, verifierPrompt, "verifier", input.taskId, plan),
7442
+ timeoutMs: agentTimeoutMs(input)
7209
7443
  }
7210
7444
  ])
7211
7445
  };
@@ -7275,18 +7509,15 @@ function renderEventWorkerVerifierWorkflow(input) {
7275
7509
  name: "Worker",
7276
7510
  description: "Handle the Hasna event and record evidence.",
7277
7511
  target: agentTarget(input, workerPrompt, "worker", seed, plan),
7278
- timeoutMs: 45 * 60000
7512
+ timeoutMs: agentTimeoutMs(input)
7279
7513
  },
7280
7514
  {
7281
7515
  id: "verifier",
7282
7516
  name: "Verifier",
7283
7517
  description: "Adversarially verify event handling.",
7284
7518
  dependsOn: ["worker"],
7285
- target: {
7286
- ...agentTarget(input, verifierPrompt, "verifier", seed, plan),
7287
- idleTimeoutMs: 10 * 60000
7288
- },
7289
- timeoutMs: 30 * 60000
7519
+ target: agentTarget(input, verifierPrompt, "verifier", seed, plan),
7520
+ timeoutMs: agentTimeoutMs(input)
7290
7521
  }
7291
7522
  ])
7292
7523
  };
@@ -7298,7 +7529,7 @@ function renderBoundedAgentWorkerVerifierWorkflow(input) {
7298
7529
  throw new Error("projectPath is required");
7299
7530
  const seed = `${input.projectPath}:${input.objective}`;
7300
7531
  const plan = worktreePlan(input, seed);
7301
- const timeoutMs = input.timeoutMs && Number.isFinite(input.timeoutMs) ? input.timeoutMs : 45 * 60000;
7532
+ const timeoutMs = agentTimeoutMs(input);
7302
7533
  const workerPrompt = [
7303
7534
  `/goal ${input.objective}`,
7304
7535
  "",
@@ -7336,11 +7567,8 @@ function renderBoundedAgentWorkerVerifierWorkflow(input) {
7336
7567
  name: "Verifier",
7337
7568
  description: "Adversarially verify the bounded objective result.",
7338
7569
  dependsOn: ["worker"],
7339
- target: {
7340
- ...agentTarget(input, verifierPrompt, "verifier", seed, plan),
7341
- idleTimeoutMs: 10 * 60000
7342
- },
7343
- timeoutMs: Math.min(timeoutMs, 30 * 60000)
7570
+ target: agentTarget(input, verifierPrompt, "verifier", seed, plan),
7571
+ timeoutMs
7344
7572
  }
7345
7573
  ])
7346
7574
  };
@@ -7369,7 +7597,7 @@ function renderLifecycleBoundedTemplate(id, values) {
7369
7597
  worktreeMode: values.worktreeMode ?? (id === REPORT_ONLY_TEMPLATE_ID ? "main" : "required"),
7370
7598
  worktreeRoot: values.worktreeRoot,
7371
7599
  worktreeBranchPrefix: values.worktreeBranchPrefix,
7372
- timeoutMs: values.timeoutMs ? Number(values.timeoutMs) : undefined
7600
+ timeoutMs: parseTemplateTimeoutMs(values.timeoutMs)
7373
7601
  };
7374
7602
  if (id === TASK_LIFECYCLE_TEMPLATE_ID) {
7375
7603
  const taskId = values.taskId ?? "";
@@ -7406,6 +7634,7 @@ function renderLifecycleBoundedTemplate(id, values) {
7406
7634
  worktreeMode: values.worktreeMode ?? "required",
7407
7635
  worktreeRoot: values.worktreeRoot,
7408
7636
  worktreeBranchPrefix: values.worktreeBranchPrefix,
7637
+ timeoutMs: parseTemplateTimeoutMs(values.timeoutMs),
7409
7638
  eventId: values.eventId,
7410
7639
  eventType: values.eventType
7411
7640
  });
@@ -7472,6 +7701,8 @@ function renderDeterministicCheckCreateTaskWorkflow(values) {
7472
7701
  if (!checkCommand.trim())
7473
7702
  throw new Error("checkCommand is required");
7474
7703
  const seed = `${projectPath}:${checkCommand}`;
7704
+ const timeoutMs = parseDeterministicTimeoutMs(values.timeoutMs, 5 * 60000);
7705
+ const idleTimeoutMs = parseDeterministicTimeoutMs(values.idleTimeoutMs, 60000, "idleTimeoutMs");
7475
7706
  return {
7476
7707
  name: values.name ?? `deterministic-check-${stableIndex(seed, 4294967295).toString(16).padStart(8, "0")}`,
7477
7708
  description: values.description ?? "Deterministic check that writes compact evidence and upserts one deduped todos task when the expectation is not met.",
@@ -7486,10 +7717,10 @@ function renderDeterministicCheckCreateTaskWorkflow(values) {
7486
7717
  command: "bash",
7487
7718
  args: ["-lc", checkCommand],
7488
7719
  cwd: projectPath,
7489
- timeoutMs: values.timeoutMs ? Number(values.timeoutMs) : 5 * 60000,
7490
- idleTimeoutMs: values.idleTimeoutMs ? Number(values.idleTimeoutMs) : 60000
7720
+ timeoutMs,
7721
+ idleTimeoutMs
7491
7722
  },
7492
- timeoutMs: values.timeoutMs ? Number(values.timeoutMs) : 5 * 60000
7723
+ timeoutMs
7493
7724
  }
7494
7725
  ]
7495
7726
  };
@@ -7527,6 +7758,7 @@ function renderBuiltinLoopTemplate(id, values) {
7527
7758
  worktreeMode: values.worktreeMode,
7528
7759
  worktreeRoot: values.worktreeRoot,
7529
7760
  worktreeBranchPrefix: values.worktreeBranchPrefix,
7761
+ timeoutMs: parseTemplateTimeoutMs(values.timeoutMs),
7530
7762
  eventId: values.eventId,
7531
7763
  eventType: values.eventType
7532
7764
  });
@@ -7558,7 +7790,8 @@ function renderBuiltinLoopTemplate(id, values) {
7558
7790
  manualBreakGlass: booleanVar(values.manualBreakGlass),
7559
7791
  worktreeMode: values.worktreeMode,
7560
7792
  worktreeRoot: values.worktreeRoot,
7561
- worktreeBranchPrefix: values.worktreeBranchPrefix
7793
+ worktreeBranchPrefix: values.worktreeBranchPrefix,
7794
+ timeoutMs: parseTemplateTimeoutMs(values.timeoutMs)
7562
7795
  });
7563
7796
  }
7564
7797
  if (id === BOUNDED_AGENT_WORKER_VERIFIER_TEMPLATE_ID) {
@@ -7586,7 +7819,7 @@ function renderBuiltinLoopTemplate(id, values) {
7586
7819
  worktreeMode: values.worktreeMode,
7587
7820
  worktreeRoot: values.worktreeRoot,
7588
7821
  worktreeBranchPrefix: values.worktreeBranchPrefix,
7589
- timeoutMs: values.timeoutMs ? Number(values.timeoutMs) : undefined
7822
+ timeoutMs: parseTemplateTimeoutMs(values.timeoutMs)
7590
7823
  });
7591
7824
  }
7592
7825
  throw new Error(`unknown template: ${id}`);
@@ -7672,12 +7905,13 @@ function validationFailed(error, context) {
7672
7905
  });
7673
7906
  process.exit(1);
7674
7907
  }
7675
- function validateLoopTargetForStorage(target, context) {
7908
+ function normalizeLoopTargetForStorage(target, context, opts = {}) {
7676
7909
  try {
7677
- workflowBodyFromJson({
7910
+ const workflow = workflowBodyFromJson({
7678
7911
  name: "loop-target-validation",
7679
7912
  steps: [{ id: "target", target }]
7680
- });
7913
+ }, undefined, opts);
7914
+ return workflow.steps[0].target;
7681
7915
  } catch (error) {
7682
7916
  validationFailed(error, context);
7683
7917
  }
@@ -7689,6 +7923,14 @@ function normalizeWorkflowForStorage(body, context) {
7689
7923
  validationFailed(error, context);
7690
7924
  }
7691
7925
  }
7926
+ function workflowBodyFromFile(file, fallbackName, context) {
7927
+ try {
7928
+ const resolved = resolve3(file);
7929
+ return workflowBodyFromJson(JSON.parse(readFileSync4(resolved, "utf8")), fallbackName, { baseDir: dirname4(resolved) });
7930
+ } catch (error) {
7931
+ validationFailed(error, context);
7932
+ }
7933
+ }
7692
7934
  function preflightLoopTarget(target, context, metadata, opts) {
7693
7935
  try {
7694
7936
  return preflightTarget(target, metadata, opts);
@@ -7717,6 +7959,48 @@ function workflowSpecForPreflight(body, id = "validation") {
7717
7959
  updatedAt: now
7718
7960
  };
7719
7961
  }
7962
+ function publicWorkflowBody(body) {
7963
+ const value = publicWorkflow(workflowSpecForPreflight(body, "render"));
7964
+ const { id: _id, status: _status, createdAt: _createdAt, updatedAt: _updatedAt, ...bodyOnly } = value;
7965
+ return bodyOnly;
7966
+ }
7967
+ function workflowWithAgentTimeouts(workflow, timeoutMs, opts = {}) {
7968
+ let changed = false;
7969
+ const agentStepIds = [];
7970
+ const steps = workflow.steps.map((step) => {
7971
+ if (step.target.type !== "agent")
7972
+ return step;
7973
+ agentStepIds.push(step.id);
7974
+ const target = { ...step.target, timeoutMs };
7975
+ if (timeoutMs === null && target.idleTimeoutMs !== undefined) {
7976
+ delete target.idleTimeoutMs;
7977
+ changed = true;
7978
+ }
7979
+ if (step.timeoutMs !== timeoutMs || step.target.timeoutMs !== timeoutMs)
7980
+ changed = true;
7981
+ return {
7982
+ ...step,
7983
+ target,
7984
+ timeoutMs
7985
+ };
7986
+ });
7987
+ return {
7988
+ body: {
7989
+ name: opts.name ?? workflow.name,
7990
+ description: workflow.description,
7991
+ version: workflow.version,
7992
+ goal: workflow.goal,
7993
+ steps
7994
+ },
7995
+ changed,
7996
+ agentStepIds
7997
+ };
7998
+ }
7999
+ function workflowTimeoutMigrationName(workflow, timeoutMs) {
8000
+ const policy = timeoutMs === null ? "agent-timeout-unlimited" : `agent-timeout-${timeoutMs}ms`;
8001
+ const suffix = randomUUID().replace(/-/g, "").slice(0, 8);
8002
+ return `${workflow.name}-${policy}-${suffix}`;
8003
+ }
7720
8004
  function printTextOutput(value) {
7721
8005
  for (const line of textOutputBlocks(value, { indent: " " }))
7722
8006
  console.log(line);
@@ -7757,6 +8041,17 @@ function positiveDuration(raw, label) {
7757
8041
  throw new Error(`${label} must be greater than zero`);
7758
8042
  return value;
7759
8043
  }
8044
+ function timeoutDuration(raw, label) {
8045
+ if (raw === undefined)
8046
+ return;
8047
+ const normalized = raw.trim().toLowerCase();
8048
+ if (["none", "unlimited", "null", "never", "off", "false"].includes(normalized))
8049
+ return null;
8050
+ const value = parseDuration(raw);
8051
+ if (!Number.isFinite(value) || value <= 0)
8052
+ throw new Error(`${label} must be a duration greater than zero, or none/unlimited`);
8053
+ return value;
8054
+ }
7760
8055
  function parsePolicy(opts) {
7761
8056
  const catchUp = opts.catchUp ?? "latest";
7762
8057
  if (!["none", "latest", "all"].includes(catchUp))
@@ -7913,7 +8208,7 @@ function runLocalCommandWithStdoutFile(command, args, opts = {}) {
7913
8208
  return {
7914
8209
  ok: result.status === 0,
7915
8210
  status: result.status,
7916
- stdout: readFileSync3(stdoutPath, "utf8"),
8211
+ stdout: readFileSync4(stdoutPath, "utf8"),
7917
8212
  stderr: typeof result.stderr === "string" ? result.stderr : result.stderr?.toString() || "",
7918
8213
  error: result.error ? String(result.error.message || result.error) : ""
7919
8214
  };
@@ -7957,7 +8252,7 @@ function readRouteCursors() {
7957
8252
  if (!existsSync5(path))
7958
8253
  return {};
7959
8254
  try {
7960
- const parsed = JSON.parse(readFileSync3(path, "utf8"));
8255
+ const parsed = JSON.parse(readFileSync4(path, "utf8"));
7961
8256
  return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
7962
8257
  } catch {
7963
8258
  return {};
@@ -8312,7 +8607,7 @@ function hasThrottleLimits(limits) {
8312
8607
  function normalizeRoutePath(value) {
8313
8608
  if (!value?.trim())
8314
8609
  return;
8315
- const resolved = resolve2(value.trim());
8610
+ const resolved = resolve3(value.trim());
8316
8611
  let canonical = resolved;
8317
8612
  try {
8318
8613
  canonical = realpathSync(resolved);
@@ -8324,7 +8619,7 @@ function normalizeRoutePath(value) {
8324
8619
  try {
8325
8620
  return realpathSync(gitRoot.stdout.trim());
8326
8621
  } catch {
8327
- return resolve2(gitRoot.stdout.trim());
8622
+ return resolve3(gitRoot.stdout.trim());
8328
8623
  }
8329
8624
  }
8330
8625
  return canonical;
@@ -8333,7 +8628,7 @@ function routeProjectGroup(optsGroup, data, metadata) {
8333
8628
  return optsGroup?.trim() || taskEventField(data, ["project_group", "projectGroup", "repo_group", "repoGroup", "workspace_group", "workspaceGroup"]) || taskEventField(metadata, ["project_group", "projectGroup", "repo_group", "repoGroup", "workspace_group", "workspaceGroup"]);
8334
8629
  }
8335
8630
  function routeThrottleDecision(store, args) {
8336
- const projectPath = normalizeRoutePath(args.projectPath) ?? resolve2(args.projectPath);
8631
+ const projectPath = normalizeRoutePath(args.projectPath) ?? resolve3(args.projectPath);
8337
8632
  const projectGroup = args.projectGroup?.trim() || undefined;
8338
8633
  const counts = store.countActiveWorkflowWorkItems({ projectKey: projectPath, projectGroup });
8339
8634
  const base = {
@@ -8358,7 +8653,7 @@ function routeThrottleDecision(store, args) {
8358
8653
  return { ...base, allowed: true };
8359
8654
  }
8360
8655
  function routeThrottleDryRunPreview(args) {
8361
- const projectPath = normalizeRoutePath(args.projectPath) ?? resolve2(args.projectPath);
8656
+ const projectPath = normalizeRoutePath(args.projectPath) ?? resolve3(args.projectPath);
8362
8657
  const projectGroup = args.projectGroup?.trim() || undefined;
8363
8658
  return {
8364
8659
  evaluated: false,
@@ -8380,7 +8675,7 @@ function todosTaskRouteTemplateId(opts) {
8380
8675
  return id;
8381
8676
  }
8382
8677
  async function readEventEnvelopeInput(opts = {}) {
8383
- const raw = opts.eventJson ?? (opts.eventFile ? readFileSync3(opts.eventFile, "utf8") : process.env.HASNA_EVENT_JSON || await Bun.stdin.text());
8678
+ const raw = opts.eventJson ?? (opts.eventFile ? readFileSync4(opts.eventFile, "utf8") : process.env.HASNA_EVENT_JSON || await Bun.stdin.text());
8384
8679
  const event = JSON.parse(raw);
8385
8680
  if (!event || typeof event !== "object" || Array.isArray(event))
8386
8681
  throw new Error("event JSON must be an object");
@@ -8421,7 +8716,7 @@ function routeTodosTaskEvent(event, opts) {
8421
8716
  "cwd"
8422
8717
  ]);
8423
8718
  const projectPath = dataProjectPath ?? metadataProjectPath ?? opts.projectPath ?? process.cwd();
8424
- const routeProjectPath = normalizeRoutePath(projectPath) ?? resolve2(projectPath);
8719
+ const routeProjectPath = normalizeRoutePath(projectPath) ?? resolve3(projectPath);
8425
8720
  const projectGroup = routeProjectGroup(opts.projectGroup, data, metadata);
8426
8721
  const throttleLimits = routeThrottleLimitsFromOpts(opts);
8427
8722
  const idempotencyKey = `todos-task:${taskId}`;
@@ -8487,6 +8782,7 @@ function routeTodosTaskEvent(event, opts) {
8487
8782
  variant: opts.variant,
8488
8783
  agent: opts.agent,
8489
8784
  addDirs: listFromRepeatedOpts(opts.addDir),
8785
+ timeoutMs: timeoutDuration(opts.timeout, "--timeout"),
8490
8786
  permissionMode,
8491
8787
  sandbox,
8492
8788
  manualBreakGlass: Boolean(opts.manualBreakGlass),
@@ -8671,7 +8967,7 @@ function routeGenericEvent(event, opts) {
8671
8967
  const data = eventData(event);
8672
8968
  const metadata = eventMetadata(event);
8673
8969
  const projectPath = opts.projectPath ?? taskEventField(data, ["working_dir", "workingDir", "project_path", "projectPath", "cwd", "repo_path", "repoPath"]) ?? taskEventField(metadata, ["working_dir", "workingDir", "project_path", "projectPath", "project_canonical_path", "cwd", "repo_path", "repoPath"]) ?? process.cwd();
8674
- const routeProjectPath = normalizeRoutePath(projectPath) ?? resolve2(projectPath);
8970
+ const routeProjectPath = normalizeRoutePath(projectPath) ?? resolve3(projectPath);
8675
8971
  const projectGroup = routeProjectGroup(opts.projectGroup, data, metadata);
8676
8972
  const throttleLimits = routeThrottleLimitsFromOpts(opts);
8677
8973
  const eventSuffix = event.id.slice(0, 8);
@@ -8709,6 +9005,7 @@ function routeGenericEvent(event, opts) {
8709
9005
  variant: opts.variant,
8710
9006
  agent: opts.agent,
8711
9007
  addDirs: listFromRepeatedOpts(opts.addDir),
9008
+ timeoutMs: timeoutDuration(opts.timeout, "--timeout"),
8712
9009
  permissionMode,
8713
9010
  sandbox,
8714
9011
  manualBreakGlass: Boolean(opts.manualBreakGlass),
@@ -9001,8 +9298,8 @@ function taskMatchesDrainFilters(task, filters) {
9001
9298
  const path = taskProjectPath(task);
9002
9299
  if (!path)
9003
9300
  return false;
9004
- const normalizedPath = normalizeRoutePath(path) ?? resolve2(path);
9005
- const normalizedPrefix = normalizeRoutePath(filters.projectPathPrefix) ?? resolve2(filters.projectPathPrefix);
9301
+ const normalizedPath = normalizeRoutePath(path) ?? resolve3(path);
9302
+ const normalizedPrefix = normalizeRoutePath(filters.projectPathPrefix) ?? resolve3(filters.projectPathPrefix);
9006
9303
  if (normalizedPath !== normalizedPrefix && !normalizedPath.startsWith(`${normalizedPrefix}/`))
9007
9304
  return false;
9008
9305
  }
@@ -9057,7 +9354,7 @@ function permissionModeFromOpts(opts, provider) {
9057
9354
  return mode;
9058
9355
  }
9059
9356
  var create = program.command("create").description("create loops");
9060
- addGoalOptions(addAccountOptions(addMachineOptions(addScheduleOptions(create.command("command <name>").description("create a deterministic shell command loop").requiredOption("--cmd <command>", "command string to execute").option("--cwd <dir>", "working directory").option("--timeout <duration>", "run timeout").option("--no-shell", "execute without a shell").option("--preflight-each-run", "check target executables/accounts before every scheduled run").option("--preflight", "check target executables/accounts before storing the loop"))))).action((name, opts) => {
9357
+ addGoalOptions(addAccountOptions(addMachineOptions(addScheduleOptions(create.command("command <name>").description("create a deterministic shell command loop").requiredOption("--cmd <command>", "command string to execute").option("--cwd <dir>", "working directory").option("--timeout <duration>", "run timeout; use none/unlimited for no timeout").option("--no-shell", "execute without a shell").option("--preflight-each-run", "check target executables/accounts before every scheduled run").option("--preflight", "check target executables/accounts before storing the loop"))))).action((name, opts) => {
9061
9358
  const store = new Store;
9062
9359
  try {
9063
9360
  const target = {
@@ -9065,7 +9362,7 @@ addGoalOptions(addAccountOptions(addMachineOptions(addScheduleOptions(create.com
9065
9362
  command: opts.cmd,
9066
9363
  cwd: opts.cwd,
9067
9364
  shell: opts.shell,
9068
- timeoutMs: opts.timeout ? parseDuration(opts.timeout) : undefined,
9365
+ timeoutMs: timeoutDuration(opts.timeout, "--timeout"),
9069
9366
  account: accountFromOpts(opts),
9070
9367
  preflight: runtimePreflightFromOpts(opts)
9071
9368
  };
@@ -9077,7 +9374,7 @@ addGoalOptions(addAccountOptions(addMachineOptions(addScheduleOptions(create.com
9077
9374
  store.close();
9078
9375
  }
9079
9376
  });
9080
- addGoalOptions(addAccountOptions(addMachineOptions(addScheduleOptions(create.command("agent <name>").description("create a headless coding-agent loop").requiredOption("--provider <provider>", "claude, cursor, codewith, aicopilot, opencode, or codex").requiredOption("--prompt <prompt>", "agent prompt").option("--cwd <dir>", "working directory").option("--model <model>", "model").option("--variant <variant>", "provider-specific model variant or reasoning effort").option("--agent <agent>", "provider-specific agent").option("--auth-profile <profile>", "provider-native auth profile; currently supported for codewith").option("--add-dir <dir>", "additional writable directory for provider sandboxes; may be repeated or comma-separated", collectValues, []).option("--timeout <duration>", "run timeout").option("--permission-mode <mode>", "provider permission mode: default, plan, auto, or bypass").option("--sandbox <mode>", "provider sandbox: codewith/codex use read-only/workspace-write/danger-full-access; cursor uses enabled/disabled").option("--allow-tool <name>", "advisory per-session tool allowlist metadata; may be repeated or comma-separated", collectValues, []).option("--allow-command <name>", "advisory per-session command allowlist metadata; may be repeated or comma-separated", collectValues, []).option("--config-isolation <mode>", "safe or none", "safe").option("--preflight-each-run", "check provider/account readiness before every scheduled run").option("--preflight", "check target executables/accounts before storing the loop"))))).action((name, opts) => {
9377
+ addGoalOptions(addAccountOptions(addMachineOptions(addScheduleOptions(create.command("agent <name>").description("create a headless coding-agent loop").requiredOption("--provider <provider>", "claude, cursor, codewith, aicopilot, opencode, or codex").option("--prompt <prompt>", "agent prompt").option("--prompt-file <file>", "read the agent prompt from a markdown/text file").option("--cwd <dir>", "working directory").option("--model <model>", "model").option("--variant <variant>", "provider-specific model variant or reasoning effort").option("--agent <agent>", "provider-specific agent").option("--auth-profile <profile>", "provider-native auth profile; currently supported for codewith").option("--add-dir <dir>", "additional writable directory for provider sandboxes; may be repeated or comma-separated", collectValues, []).option("--timeout <duration>", "run timeout; use none/unlimited for no timeout").option("--permission-mode <mode>", "provider permission mode: default, plan, auto, or bypass").option("--sandbox <mode>", "provider sandbox: codewith/codex use read-only/workspace-write/danger-full-access; cursor uses enabled/disabled").option("--allow-tool <name>", "advisory per-session tool allowlist metadata; may be repeated or comma-separated", collectValues, []).option("--allow-command <name>", "advisory per-session command allowlist metadata; may be repeated or comma-separated", collectValues, []).option("--config-isolation <mode>", "safe or none", "safe").option("--preflight-each-run", "check provider/account readiness before every scheduled run").option("--preflight", "check target executables/accounts before storing the loop"))))).action((name, opts) => {
9081
9378
  const provider = opts.provider;
9082
9379
  if (!["claude", "cursor", "codewith", "aicopilot", "opencode", "codex"].includes(provider)) {
9083
9380
  throw new Error("unsupported provider");
@@ -9087,26 +9384,26 @@ addGoalOptions(addAccountOptions(addMachineOptions(addScheduleOptions(create.com
9087
9384
  }
9088
9385
  const store = new Store;
9089
9386
  try {
9090
- const target = {
9387
+ const target = normalizeLoopTargetForStorage({
9091
9388
  type: "agent",
9092
9389
  provider,
9093
9390
  prompt: opts.prompt,
9391
+ promptFile: opts.promptFile,
9094
9392
  cwd: opts.cwd,
9095
9393
  model: opts.model,
9096
9394
  variant: opts.variant,
9097
9395
  agent: opts.agent,
9098
9396
  authProfile: providerAuthProfileFromOpts(opts, provider),
9099
9397
  addDirs: listFromRepeatedOpts(opts.addDir),
9100
- timeoutMs: opts.timeout ? parseDuration(opts.timeout) : undefined,
9398
+ timeoutMs: timeoutDuration(opts.timeout, "--timeout"),
9101
9399
  configIsolation: opts.configIsolation,
9102
9400
  permissionMode: permissionModeFromOpts(opts, provider),
9103
9401
  sandbox: sandboxFromOpts(opts, provider),
9104
9402
  allowlist: allowlistFromOpts(opts),
9105
9403
  account: accountFromOpts(opts),
9106
9404
  preflight: runtimePreflightFromOpts(opts)
9107
- };
9405
+ }, { name, type: "agent", provider }, { baseDir: process.cwd() });
9108
9406
  const input = baseCreateInput(name, opts, target);
9109
- validateLoopTargetForStorage(input.target, { name, type: "agent", provider });
9110
9407
  const preflight = opts.preflight ? preflightLoopTarget(input.target, { name, type: "agent", provider }, { loopName: name }, { machine: input.machine }) : undefined;
9111
9408
  const loop = store.createLoop(input);
9112
9409
  printCreatedLoop(loop, `created loop ${loop.id} (${loop.name}) next=${loop.nextRunAt}`, preflight);
@@ -9114,13 +9411,14 @@ addGoalOptions(addAccountOptions(addMachineOptions(addScheduleOptions(create.com
9114
9411
  store.close();
9115
9412
  }
9116
9413
  });
9117
- addGoalOptions(addMachineOptions(addScheduleOptions(create.command("workflow <name>").description("schedule a stored workflow").requiredOption("--workflow <idOrName>", "workflow id or name").option("--preflight-each-run", "check workflow steps before every scheduled run").option("--preflight", "check workflow step executables/accounts before storing the loop")))).action((name, opts) => {
9414
+ addGoalOptions(addMachineOptions(addScheduleOptions(create.command("workflow <name>").description("schedule a stored workflow").requiredOption("--workflow <idOrName>", "workflow id or name").option("--timeout <duration>", "workflow run timeout; use none/unlimited for no workflow-level timeout").option("--preflight-each-run", "check workflow steps before every scheduled run").option("--preflight", "check workflow step executables/accounts before storing the loop")))).action((name, opts) => {
9118
9415
  const store = new Store;
9119
9416
  try {
9120
9417
  const workflow = store.requireWorkflow(opts.workflow);
9121
9418
  const target = {
9122
9419
  type: "workflow",
9123
9420
  workflowId: workflow.id,
9421
+ timeoutMs: timeoutDuration(opts.timeout, "--timeout"),
9124
9422
  preflight: runtimePreflightFromOpts(opts)
9125
9423
  };
9126
9424
  const input = baseCreateInput(name, opts, target);
@@ -9138,10 +9436,10 @@ var events = program.command("events").description("handle Hasna event envelopes
9138
9436
  var machines = program.command("machines").description("inspect OpenMachines topology for loop assignment");
9139
9437
  var goal = program.command("goal").description("inspect goal runs");
9140
9438
  function addRouteEventOptions(command) {
9141
- return command.option("--event-file <file>", "read event envelope JSON from a file instead of stdin/HASNA_EVENT_JSON").option("--event-json <json>", "read event envelope JSON from this string instead of stdin/HASNA_EVENT_JSON").option("--todos-project <path>", "todos storage project path for generated task commands", defaultLoopsProject()).option("--template <id>", "todos-task route workflow template: todos-task-worker-verifier or task-lifecycle", TODOS_TASK_WORKER_VERIFIER_TEMPLATE_ID).option("--provider <provider>", "agent provider", "codewith").option("--auth-profile <profile>", "provider-native auth profile; currently supported for codewith").option("--auth-profile-pool <profiles>", "comma-separated provider-native auth profile pool").option("--triage-auth-profile <profile>", "provider-native auth profile for triage step").option("--planner-auth-profile <profile>", "provider-native auth profile for planner step").option("--worker-auth-profile <profile>", "provider-native auth profile for worker step").option("--verifier-auth-profile <profile>", "provider-native auth profile for verifier step").option("--account <profile>", "OpenAccounts profile name").option("--account-pool <profiles>", "comma-separated OpenAccounts profile pool").option("--triage-account <profile>", "OpenAccounts profile for triage step").option("--planner-account <profile>", "OpenAccounts profile for planner step").option("--worker-account <profile>", "OpenAccounts profile for worker step").option("--verifier-account <profile>", "OpenAccounts profile for verifier step").option("--account-tool <tool>", "OpenAccounts tool id").option("--model <model>", "provider model").option("--variant <variant>", "provider-specific model variant or reasoning effort").option("--agent <agent>", "provider-specific agent").option("--add-dir <dir>", "additional writable directory for provider sandboxes; may be repeated or comma-separated", collectValues, []).option("--permission-mode <mode>", "provider permission mode: default, plan, auto, or bypass", "bypass").option("--sandbox <mode>", "provider sandbox").option("--manual-break-glass", "allow danger-full-access in generated worker/verifier workflow metadata; for explicit operator emergency use only").option("--project-path <path>", "fallback project/repo working directory").option("--project-group <name>", "optional project group for concurrency limits").option("--max-active <n>", "skip creating a workflow when this many active routed workflows already exist globally").option("--max-active-per-project <n>", "skip creating a workflow when this many active routed workflows already exist for the project").option("--max-active-per-project-group <n>", "skip creating a workflow when this many active routed workflows already exist for the project group").option("--worktree-mode <mode>", "worktree isolation mode: auto, required, off, or main", "auto").option("--worktree-root <path>", "base directory for OpenLoops-managed git worktrees").option("--worktree-branch-prefix <prefix>", "branch prefix for generated worktrees", "openloops").option("--name-prefix <prefix>", "workflow/loop name prefix").option("--preflight", "check generated workflow steps before storing the workflow loop");
9439
+ return command.option("--event-file <file>", "read event envelope JSON from a file instead of stdin/HASNA_EVENT_JSON").option("--event-json <json>", "read event envelope JSON from this string instead of stdin/HASNA_EVENT_JSON").option("--todos-project <path>", "todos storage project path for generated task commands", defaultLoopsProject()).option("--template <id>", "todos-task route workflow template: todos-task-worker-verifier or task-lifecycle", TODOS_TASK_WORKER_VERIFIER_TEMPLATE_ID).option("--provider <provider>", "agent provider", "codewith").option("--auth-profile <profile>", "provider-native auth profile; currently supported for codewith").option("--auth-profile-pool <profiles>", "comma-separated provider-native auth profile pool").option("--triage-auth-profile <profile>", "provider-native auth profile for triage step").option("--planner-auth-profile <profile>", "provider-native auth profile for planner step").option("--worker-auth-profile <profile>", "provider-native auth profile for worker step").option("--verifier-auth-profile <profile>", "provider-native auth profile for verifier step").option("--account <profile>", "OpenAccounts profile name").option("--account-pool <profiles>", "comma-separated OpenAccounts profile pool").option("--triage-account <profile>", "OpenAccounts profile for triage step").option("--planner-account <profile>", "OpenAccounts profile for planner step").option("--worker-account <profile>", "OpenAccounts profile for worker step").option("--verifier-account <profile>", "OpenAccounts profile for verifier step").option("--account-tool <tool>", "OpenAccounts tool id").option("--model <model>", "provider model").option("--variant <variant>", "provider-specific model variant or reasoning effort").option("--agent <agent>", "provider-specific agent").option("--add-dir <dir>", "additional writable directory for provider sandboxes; may be repeated or comma-separated", collectValues, []).option("--timeout <duration>", "agent step timeout; use none/unlimited for no timeout").option("--permission-mode <mode>", "provider permission mode: default, plan, auto, or bypass", "bypass").option("--sandbox <mode>", "provider sandbox").option("--manual-break-glass", "allow danger-full-access in generated worker/verifier workflow metadata; for explicit operator emergency use only").option("--project-path <path>", "fallback project/repo working directory").option("--project-group <name>", "optional project group for concurrency limits").option("--max-active <n>", "skip creating a workflow when this many active routed workflows already exist globally").option("--max-active-per-project <n>", "skip creating a workflow when this many active routed workflows already exist for the project").option("--max-active-per-project-group <n>", "skip creating a workflow when this many active routed workflows already exist for the project group").option("--worktree-mode <mode>", "worktree isolation mode: auto, required, off, or main", "auto").option("--worktree-root <path>", "base directory for OpenLoops-managed git worktrees").option("--worktree-branch-prefix <prefix>", "branch prefix for generated worktrees", "openloops").option("--name-prefix <prefix>", "workflow/loop name prefix").option("--preflight", "check generated workflow steps before storing the workflow loop");
9142
9440
  }
9143
9441
  function addTodosDrainOptions(command, opts = {}) {
9144
- let configured = command.option("--todos-project <path>", "todos storage project path", defaultLoopsProject()).option("--todos-project-id <id>", "filter todos ready output to one todos project id").option("--task-list <id-or-slug>", "filter ready tasks to one task-list id, slug, or name").option("--project-path-prefix <path>", "filter ready tasks to a project/repo path prefix").option("--tags <tags>", "require all comma-separated tags before routing").option("--tag <tags>", "alias for --tags").option("--limit <n>", "maximum filtered ready-task candidates to consider", "50").option("--scan-limit <n>", "maximum raw todos ready rows to fetch before filters; defaults to 500 when filters are used").option("--max-dispatch <n>", "maximum new workflow loops to create in this drain run", "1").option("--evidence-dir <path>", "write a JSON drain report to this directory").option("--compact", "print compact JSON to stdout while preserving the full evidence file").option("--template <id>", "todos-task route workflow template: todos-task-worker-verifier or task-lifecycle", TODOS_TASK_WORKER_VERIFIER_TEMPLATE_ID).option("--provider <provider>", "agent provider", "codewith").option("--auth-profile <profile>", "provider-native auth profile; currently supported for codewith").option("--auth-profile-pool <profiles>", "comma-separated provider-native auth profile pool").option("--triage-auth-profile <profile>", "provider-native auth profile for triage step").option("--planner-auth-profile <profile>", "provider-native auth profile for planner step").option("--worker-auth-profile <profile>", "provider-native auth profile for worker step").option("--verifier-auth-profile <profile>", "provider-native auth profile for verifier step").option("--account <profile>", "OpenAccounts profile name").option("--account-pool <profiles>", "comma-separated OpenAccounts profile pool").option("--triage-account <profile>", "OpenAccounts profile for triage step").option("--planner-account <profile>", "OpenAccounts profile for planner step").option("--worker-account <profile>", "OpenAccounts profile for worker step").option("--verifier-account <profile>", "OpenAccounts profile for verifier step").option("--account-tool <tool>", "OpenAccounts tool id").option("--model <model>", "provider model").option("--variant <variant>", "provider-specific model variant or reasoning effort").option("--agent <agent>", "provider-specific agent").option("--add-dir <dir>", "additional writable directory for provider sandboxes; may be repeated or comma-separated", collectValues, []).option("--permission-mode <mode>", "provider permission mode: default, plan, auto, or bypass", "bypass").option("--sandbox <mode>", "provider sandbox").option("--manual-break-glass", "allow danger-full-access in generated worker/verifier workflow metadata; for explicit operator emergency use only").option("--project-path <path>", "fallback project/repo working directory").option("--project-group <name>", "optional project group for concurrency limits").option("--max-active <n>", "skip creating a workflow when this many active routed workflows already exist globally").option("--max-active-per-project <n>", "skip creating a workflow when this many active routed workflows already exist for the project").option("--max-active-per-project-group <n>", "skip creating a workflow when this many active routed workflows already exist for the project group").option("--worktree-mode <mode>", "worktree isolation mode: auto, required, off, or main", "auto").option("--worktree-root <path>", "base directory for OpenLoops-managed git worktrees").option("--worktree-branch-prefix <prefix>", "branch prefix for generated task worktrees", "openloops").option("--name-prefix <prefix>", "workflow/loop name prefix", "event:todos-task").option("--preflight", opts.preflightDescription ?? "check generated workflow steps before storing workflow loops");
9442
+ let configured = command.option("--todos-project <path>", "todos storage project path", defaultLoopsProject()).option("--todos-project-id <id>", "filter todos ready output to one todos project id").option("--task-list <id-or-slug>", "filter ready tasks to one task-list id, slug, or name").option("--project-path-prefix <path>", "filter ready tasks to a project/repo path prefix").option("--tags <tags>", "require all comma-separated tags before routing").option("--tag <tags>", "alias for --tags").option("--limit <n>", "maximum filtered ready-task candidates to consider", "50").option("--scan-limit <n>", "maximum raw todos ready rows to fetch before filters; defaults to 500 when filters are used").option("--max-dispatch <n>", "maximum new workflow loops to create in this drain run", "1").option("--evidence-dir <path>", "write a JSON drain report to this directory").option("--compact", "print compact JSON to stdout while preserving the full evidence file").option("--template <id>", "todos-task route workflow template: todos-task-worker-verifier or task-lifecycle", TODOS_TASK_WORKER_VERIFIER_TEMPLATE_ID).option("--provider <provider>", "agent provider", "codewith").option("--auth-profile <profile>", "provider-native auth profile; currently supported for codewith").option("--auth-profile-pool <profiles>", "comma-separated provider-native auth profile pool").option("--triage-auth-profile <profile>", "provider-native auth profile for triage step").option("--planner-auth-profile <profile>", "provider-native auth profile for planner step").option("--worker-auth-profile <profile>", "provider-native auth profile for worker step").option("--verifier-auth-profile <profile>", "provider-native auth profile for verifier step").option("--account <profile>", "OpenAccounts profile name").option("--account-pool <profiles>", "comma-separated OpenAccounts profile pool").option("--triage-account <profile>", "OpenAccounts profile for triage step").option("--planner-account <profile>", "OpenAccounts profile for planner step").option("--worker-account <profile>", "OpenAccounts profile for worker step").option("--verifier-account <profile>", "OpenAccounts profile for verifier step").option("--account-tool <tool>", "OpenAccounts tool id").option("--model <model>", "provider model").option("--variant <variant>", "provider-specific model variant or reasoning effort").option("--agent <agent>", "provider-specific agent").option("--add-dir <dir>", "additional writable directory for provider sandboxes; may be repeated or comma-separated", collectValues, []).option("--timeout <duration>", "agent step timeout; use none/unlimited for no timeout").option("--permission-mode <mode>", "provider permission mode: default, plan, auto, or bypass", "bypass").option("--sandbox <mode>", "provider sandbox").option("--manual-break-glass", "allow danger-full-access in generated worker/verifier workflow metadata; for explicit operator emergency use only").option("--project-path <path>", "fallback project/repo working directory").option("--project-group <name>", "optional project group for concurrency limits").option("--max-active <n>", "skip creating a workflow when this many active routed workflows already exist globally").option("--max-active-per-project <n>", "skip creating a workflow when this many active routed workflows already exist for the project").option("--max-active-per-project-group <n>", "skip creating a workflow when this many active routed workflows already exist for the project group").option("--worktree-mode <mode>", "worktree isolation mode: auto, required, off, or main", "auto").option("--worktree-root <path>", "base directory for OpenLoops-managed git worktrees").option("--worktree-branch-prefix <prefix>", "branch prefix for generated task worktrees", "openloops").option("--name-prefix <prefix>", "workflow/loop name prefix", "event:todos-task").option("--preflight", opts.preflightDescription ?? "check generated workflow steps before storing workflow loops");
9145
9443
  if (opts.includeDryRun ?? true) {
9146
9444
  configured = configured.option("--dry-run", "preview selected tasks and generated workflow loops without storing anything");
9147
9445
  }
@@ -9194,6 +9492,7 @@ function routeDrainArgs(opts) {
9194
9492
  add("--agent", opts.agent);
9195
9493
  for (const dir of listFromRepeatedOpts(opts.addDir) ?? [])
9196
9494
  add("--add-dir", dir);
9495
+ add("--timeout", opts.timeout);
9197
9496
  add("--permission-mode", opts.permissionMode);
9198
9497
  add("--sandbox", opts.sandbox);
9199
9498
  addBool("--manual-break-glass", opts.manualBreakGlass);
@@ -9400,7 +9699,8 @@ addTemplateSourceOption(templates.command("show <id>").description("show a templ
9400
9699
  });
9401
9700
  addTemplateSourceOption(templates.command("render <id>").description("render a template as workflow JSON").option("--var <key=value>", "template variable; may be repeated", collectValues, [])).action((id, opts) => {
9402
9701
  const workflow = renderLoopTemplate(id, parseVars(opts.var), { source: templateSource(opts.source) });
9403
- print(workflow, JSON.stringify(workflow, null, 2));
9702
+ const value = publicWorkflowBody(workflow);
9703
+ print(value, JSON.stringify(value, null, 2));
9404
9704
  });
9405
9705
  addTemplateSourceOption(templates.command("create-workflow <id>").description("render and store a template as a workflow").option("--var <key=value>", "template variable; may be repeated", collectValues, [])).action((id, opts) => {
9406
9706
  const store = new Store;
@@ -9504,7 +9804,7 @@ addScheduleOptions(addTodosDrainOptions(routes.command("schedule <kind> <name>")
9504
9804
  }
9505
9805
  });
9506
9806
  var eventsHandle = events.command("handle").description("handle a Hasna event envelope");
9507
- eventsHandle.command("todos-task").description("create a one-shot worker/verifier workflow loop for a todos task event").option("--todos-project <path>", "todos storage project path for generated task commands", defaultLoopsProject()).option("--template <id>", "todos-task route workflow template: todos-task-worker-verifier or task-lifecycle", TODOS_TASK_WORKER_VERIFIER_TEMPLATE_ID).option("--provider <provider>", "agent provider", "codewith").option("--auth-profile <profile>", "provider-native auth profile; currently supported for codewith").option("--auth-profile-pool <profiles>", "comma-separated provider-native auth profile pool").option("--triage-auth-profile <profile>", "provider-native auth profile for triage step").option("--planner-auth-profile <profile>", "provider-native auth profile for planner step").option("--worker-auth-profile <profile>", "provider-native auth profile for worker step").option("--verifier-auth-profile <profile>", "provider-native auth profile for verifier step").option("--account <profile>", "OpenAccounts profile name").option("--account-pool <profiles>", "comma-separated OpenAccounts profile pool").option("--triage-account <profile>", "OpenAccounts profile for triage step").option("--planner-account <profile>", "OpenAccounts profile for planner step").option("--worker-account <profile>", "OpenAccounts profile for worker step").option("--verifier-account <profile>", "OpenAccounts profile for verifier step").option("--account-tool <tool>", "OpenAccounts tool id").option("--model <model>", "provider model").option("--variant <variant>", "provider-specific model variant or reasoning effort").option("--agent <agent>", "provider-specific agent").option("--add-dir <dir>", "additional writable directory for provider sandboxes; may be repeated or comma-separated", collectValues, []).option("--permission-mode <mode>", "provider permission mode: default, plan, auto, or bypass", "bypass").option("--sandbox <mode>", "provider sandbox").option("--manual-break-glass", "allow danger-full-access in generated worker/verifier workflow metadata; for explicit operator emergency use only").option("--project-path <path>", "fallback project/repo working directory").option("--project-group <name>", "optional project group for concurrency limits").option("--max-active <n>", "skip creating a workflow when this many active routed workflows already exist globally").option("--max-active-per-project <n>", "skip creating a workflow when this many active routed workflows already exist for the project").option("--max-active-per-project-group <n>", "skip creating a workflow when this many active routed workflows already exist for the project group").option("--worktree-mode <mode>", "worktree isolation mode: auto, required, off, or main", "auto").option("--worktree-root <path>", "base directory for OpenLoops-managed git worktrees").option("--worktree-branch-prefix <prefix>", "branch prefix for generated task worktrees", "openloops").option("--name-prefix <prefix>", "workflow/loop name prefix", "event:todos-task").option("--preflight", "check generated workflow steps before storing the workflow loop").option("--dry-run", "print the workflow and loop input without storing anything").action(async (opts) => {
9807
+ eventsHandle.command("todos-task").description("create a one-shot worker/verifier workflow loop for a todos task event").option("--todos-project <path>", "todos storage project path for generated task commands", defaultLoopsProject()).option("--template <id>", "todos-task route workflow template: todos-task-worker-verifier or task-lifecycle", TODOS_TASK_WORKER_VERIFIER_TEMPLATE_ID).option("--provider <provider>", "agent provider", "codewith").option("--auth-profile <profile>", "provider-native auth profile; currently supported for codewith").option("--auth-profile-pool <profiles>", "comma-separated provider-native auth profile pool").option("--triage-auth-profile <profile>", "provider-native auth profile for triage step").option("--planner-auth-profile <profile>", "provider-native auth profile for planner step").option("--worker-auth-profile <profile>", "provider-native auth profile for worker step").option("--verifier-auth-profile <profile>", "provider-native auth profile for verifier step").option("--account <profile>", "OpenAccounts profile name").option("--account-pool <profiles>", "comma-separated OpenAccounts profile pool").option("--triage-account <profile>", "OpenAccounts profile for triage step").option("--planner-account <profile>", "OpenAccounts profile for planner step").option("--worker-account <profile>", "OpenAccounts profile for worker step").option("--verifier-account <profile>", "OpenAccounts profile for verifier step").option("--account-tool <tool>", "OpenAccounts tool id").option("--model <model>", "provider model").option("--variant <variant>", "provider-specific model variant or reasoning effort").option("--agent <agent>", "provider-specific agent").option("--add-dir <dir>", "additional writable directory for provider sandboxes; may be repeated or comma-separated", collectValues, []).option("--timeout <duration>", "agent step timeout; use none/unlimited for no timeout").option("--permission-mode <mode>", "provider permission mode: default, plan, auto, or bypass", "bypass").option("--sandbox <mode>", "provider sandbox").option("--manual-break-glass", "allow danger-full-access in generated worker/verifier workflow metadata; for explicit operator emergency use only").option("--project-path <path>", "fallback project/repo working directory").option("--project-group <name>", "optional project group for concurrency limits").option("--max-active <n>", "skip creating a workflow when this many active routed workflows already exist globally").option("--max-active-per-project <n>", "skip creating a workflow when this many active routed workflows already exist for the project").option("--max-active-per-project-group <n>", "skip creating a workflow when this many active routed workflows already exist for the project group").option("--worktree-mode <mode>", "worktree isolation mode: auto, required, off, or main", "auto").option("--worktree-root <path>", "base directory for OpenLoops-managed git worktrees").option("--worktree-branch-prefix <prefix>", "branch prefix for generated task worktrees", "openloops").option("--name-prefix <prefix>", "workflow/loop name prefix", "event:todos-task").option("--preflight", "check generated workflow steps before storing the workflow loop").option("--dry-run", "print the workflow and loop input without storing anything").action(async (opts) => {
9508
9808
  const event = await readEventEnvelopeFromStdin();
9509
9809
  const result = routeTodosTaskEvent(event, opts);
9510
9810
  print(result.value, result.human);
@@ -9513,7 +9813,7 @@ var eventsDrain = events.command("drain").description("drain durable source queu
9513
9813
  addTodosDrainOptions(eventsDrain.command("todos-task").description("drain ready todos tasks into bounded worker/verifier workflow loops")).action((opts) => {
9514
9814
  drainTodosTaskRoutes(opts);
9515
9815
  });
9516
- eventsHandle.command("generic").description("create a one-shot worker/verifier workflow loop for any Hasna event").option("--provider <provider>", "agent provider", "codewith").option("--auth-profile <profile>", "provider-native auth profile; currently supported for codewith").option("--auth-profile-pool <profiles>", "comma-separated provider-native auth profile pool").option("--worker-auth-profile <profile>", "provider-native auth profile for worker step").option("--verifier-auth-profile <profile>", "provider-native auth profile for verifier step").option("--account <profile>", "OpenAccounts profile name").option("--account-pool <profiles>", "comma-separated OpenAccounts profile pool").option("--worker-account <profile>", "OpenAccounts profile for worker step").option("--verifier-account <profile>", "OpenAccounts profile for verifier step").option("--account-tool <tool>", "OpenAccounts tool id").option("--model <model>", "provider model").option("--variant <variant>", "provider-specific model variant or reasoning effort").option("--agent <agent>", "provider-specific agent").option("--add-dir <dir>", "additional writable directory for provider sandboxes; may be repeated or comma-separated", collectValues, []).option("--permission-mode <mode>", "provider permission mode: default, plan, auto, or bypass", "bypass").option("--sandbox <mode>", "provider sandbox").option("--manual-break-glass", "allow danger-full-access in generated worker/verifier workflow metadata; for explicit operator emergency use only").option("--project-path <path>", "fallback project/repo working directory").option("--project-group <name>", "optional project group for concurrency limits").option("--max-active <n>", "skip creating a workflow when this many active routed workflows already exist globally").option("--max-active-per-project <n>", "skip creating a workflow when this many active routed workflows already exist for the project").option("--max-active-per-project-group <n>", "skip creating a workflow when this many active routed workflows already exist for the project group").option("--worktree-mode <mode>", "worktree isolation mode: auto, required, off, or main", "auto").option("--worktree-root <path>", "base directory for OpenLoops-managed git worktrees").option("--worktree-branch-prefix <prefix>", "branch prefix for generated event worktrees", "openloops").option("--name-prefix <prefix>", "workflow/loop name prefix", "event:generic").option("--preflight", "check generated workflow steps before storing the workflow loop").option("--dry-run", "print the workflow and loop input without storing anything").action(async (opts) => {
9816
+ eventsHandle.command("generic").description("create a one-shot worker/verifier workflow loop for any Hasna event").option("--provider <provider>", "agent provider", "codewith").option("--auth-profile <profile>", "provider-native auth profile; currently supported for codewith").option("--auth-profile-pool <profiles>", "comma-separated provider-native auth profile pool").option("--worker-auth-profile <profile>", "provider-native auth profile for worker step").option("--verifier-auth-profile <profile>", "provider-native auth profile for verifier step").option("--account <profile>", "OpenAccounts profile name").option("--account-pool <profiles>", "comma-separated OpenAccounts profile pool").option("--worker-account <profile>", "OpenAccounts profile for worker step").option("--verifier-account <profile>", "OpenAccounts profile for verifier step").option("--account-tool <tool>", "OpenAccounts tool id").option("--model <model>", "provider model").option("--variant <variant>", "provider-specific model variant or reasoning effort").option("--agent <agent>", "provider-specific agent").option("--add-dir <dir>", "additional writable directory for provider sandboxes; may be repeated or comma-separated", collectValues, []).option("--timeout <duration>", "agent step timeout; use none/unlimited for no timeout").option("--permission-mode <mode>", "provider permission mode: default, plan, auto, or bypass", "bypass").option("--sandbox <mode>", "provider sandbox").option("--manual-break-glass", "allow danger-full-access in generated worker/verifier workflow metadata; for explicit operator emergency use only").option("--project-path <path>", "fallback project/repo working directory").option("--project-group <name>", "optional project group for concurrency limits").option("--max-active <n>", "skip creating a workflow when this many active routed workflows already exist globally").option("--max-active-per-project <n>", "skip creating a workflow when this many active routed workflows already exist for the project").option("--max-active-per-project-group <n>", "skip creating a workflow when this many active routed workflows already exist for the project group").option("--worktree-mode <mode>", "worktree isolation mode: auto, required, off, or main", "auto").option("--worktree-root <path>", "base directory for OpenLoops-managed git worktrees").option("--worktree-branch-prefix <prefix>", "branch prefix for generated event worktrees", "openloops").option("--name-prefix <prefix>", "workflow/loop name prefix", "event:generic").option("--preflight", "check generated workflow steps before storing the workflow loop").option("--dry-run", "print the workflow and loop input without storing anything").action(async (opts) => {
9517
9817
  const event = await readEventEnvelopeFromStdin();
9518
9818
  const result = routeGenericEvent(event, opts);
9519
9819
  print(result.value, result.human);
@@ -9577,7 +9877,7 @@ machines.command("show <id>").description("resolve a machine assignment").action
9577
9877
  print(resolveLoopMachine(id));
9578
9878
  });
9579
9879
  workflows.command("validate <file>").description("validate a workflow JSON file without storing or running it").option("--name <name>", "override workflow name from the file").option("--preflight", "also check account env and target executables").action((file, opts) => {
9580
- const body = workflowBodyFromJson(JSON.parse(readFileSync3(file, "utf8")), opts.name);
9880
+ const body = workflowBodyFromFile(file, opts.name, { file, type: "workflow" });
9581
9881
  const workflow = workflowSpecForPreflight(body);
9582
9882
  const preflight = opts.preflight ? preflightWorkflow(workflow) : undefined;
9583
9883
  print({ valid: true, workflow: publicWorkflow(workflow), preflight }, `valid workflow ${workflow.name} steps=${workflow.steps.length}`);
@@ -9585,7 +9885,7 @@ workflows.command("validate <file>").description("validate a workflow JSON file
9585
9885
  workflows.command("create <file>").description("validate and store a workflow JSON file").option("--name <name>", "override workflow name from the file").option("--preflight", "also check account env and target executables before storing").action((file, opts) => {
9586
9886
  const store = new Store;
9587
9887
  try {
9588
- const body = workflowBodyFromJson(JSON.parse(readFileSync3(file, "utf8")), opts.name);
9888
+ const body = workflowBodyFromFile(file, opts.name, { file, type: "workflow" });
9589
9889
  const preflight = opts.preflight ? preflightStoredWorkflow(workflowSpecForPreflight(body, "creation-preflight"), { name: body.name, type: "workflow" }, {}) : undefined;
9590
9890
  const workflow = store.createWorkflow(body);
9591
9891
  if (preflight !== undefined)
@@ -9729,6 +10029,75 @@ workflows.command("recover <runId>").description("reset interrupted running work
9729
10029
  store.close();
9730
10030
  }
9731
10031
  });
10032
+ workflows.command("migrate-agent-timeouts").description("append-only migrate active workflow loops to a new agent timeout policy").option("--loop <idOrName>", "migrate only one loop instead of all active workflow loops").option("--timeout <duration>", "agent timeout policy; use none/unlimited for no timeout", "none").option("--apply", "create new workflow specs and retarget eligible loops").option("--archive-old", "archive old workflow specs after retargeting when no active loops still reference them").action((opts) => {
10033
+ const store = new Store;
10034
+ try {
10035
+ const timeoutMs = timeoutDuration(opts.timeout, "--timeout") ?? null;
10036
+ const candidateLoops = opts.loop ? [store.requireUniqueLoop(opts.loop)] : store.listLoops({ status: "active", limit: 1e4 }).filter((loop) => loop.target.type === "workflow");
10037
+ const rows = [];
10038
+ for (const loop of candidateLoops) {
10039
+ if (loop.archivedAt) {
10040
+ rows.push({ loop: publicLoop(loop), status: "skipped", reason: "loop is archived" });
10041
+ continue;
10042
+ }
10043
+ if (loop.target.type !== "workflow") {
10044
+ rows.push({ loop: publicLoop(loop), status: "skipped", reason: "loop is not a workflow loop" });
10045
+ continue;
10046
+ }
10047
+ const workflow = store.requireWorkflow(loop.target.workflowId);
10048
+ const nextWorkflowName = workflowTimeoutMigrationName(workflow, timeoutMs);
10049
+ const migration = workflowWithAgentTimeouts(workflow, timeoutMs, { name: nextWorkflowName });
10050
+ if (migration.agentStepIds.length === 0) {
10051
+ rows.push({ loop: publicLoop(loop), workflow: publicWorkflow(workflow), status: "skipped", reason: "workflow has no agent steps" });
10052
+ continue;
10053
+ }
10054
+ if (!migration.changed) {
10055
+ rows.push({ loop: publicLoop(loop), workflow: publicWorkflow(workflow), status: "skipped", reason: "agent timeout policy already matches" });
10056
+ continue;
10057
+ }
10058
+ if (store.hasRunningRun(loop.id)) {
10059
+ rows.push({ loop: publicLoop(loop), workflow: publicWorkflow(workflow), status: "blocked", reason: "loop has a running run; retry after it finishes" });
10060
+ continue;
10061
+ }
10062
+ if (!opts.apply) {
10063
+ rows.push({
10064
+ loop: publicLoop(loop),
10065
+ workflow: publicWorkflow(workflow),
10066
+ status: "would_migrate",
10067
+ agentStepIds: migration.agentStepIds,
10068
+ nextWorkflowName,
10069
+ timeoutMs
10070
+ });
10071
+ continue;
10072
+ }
10073
+ const migrated = store.createAndRetargetWorkflowLoop(loop.id, migration.body, {
10074
+ workflowTimeoutMs: timeoutMs,
10075
+ archiveOld: Boolean(opts.archiveOld)
10076
+ });
10077
+ rows.push({
10078
+ loop: publicLoop(migrated.loop),
10079
+ previousWorkflow: publicWorkflow(migrated.previousWorkflow),
10080
+ workflow: publicWorkflow(migrated.workflow),
10081
+ archivedOld: migrated.archivedOld ? publicWorkflow(migrated.archivedOld) : undefined,
10082
+ status: "migrated",
10083
+ agentStepIds: migration.agentStepIds,
10084
+ timeoutMs
10085
+ });
10086
+ }
10087
+ const summary = {
10088
+ apply: Boolean(opts.apply),
10089
+ timeoutMs,
10090
+ total: rows.length,
10091
+ migrated: rows.filter((row) => row.status === "migrated").length,
10092
+ wouldMigrate: rows.filter((row) => row.status === "would_migrate").length,
10093
+ blocked: rows.filter((row) => row.status === "blocked").length,
10094
+ skipped: rows.filter((row) => row.status === "skipped").length
10095
+ };
10096
+ print({ summary, rows }, opts.apply ? `migrated=${summary.migrated} blocked=${summary.blocked} skipped=${summary.skipped}` : `would_migrate=${summary.wouldMigrate} blocked=${summary.blocked} skipped=${summary.skipped}`);
10097
+ } finally {
10098
+ store.close();
10099
+ }
10100
+ });
9732
10101
  workflows.command("archive <idOrName>").action((idOrName) => {
9733
10102
  const store = new Store;
9734
10103
  try {
@@ -10394,7 +10763,7 @@ daemon.command("logs").option("-n, --lines <n>", "lines", "80").action((opts) =>
10394
10763
  console.log("");
10395
10764
  return;
10396
10765
  }
10397
- const lines = readFileSync3(path, "utf8").trimEnd().split(`
10766
+ const lines = readFileSync4(path, "utf8").trimEnd().split(`
10398
10767
  `);
10399
10768
  console.log(lines.slice(-Number(opts.lines)).join(`
10400
10769
  `));