@hasna/loops 0.3.54 → 0.3.56

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
@@ -331,6 +331,15 @@ function optionalPositiveInteger(value, label) {
331
331
  throw new Error(`${label} must be a positive integer`);
332
332
  return value;
333
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
+ }
334
343
  function optionalBoolean(value, label) {
335
344
  if (value === undefined)
336
345
  return;
@@ -419,7 +428,7 @@ function validateTarget(value, label, opts) {
419
428
  assertObject(value, label);
420
429
  if (value.type === "command") {
421
430
  assertString(value.command, `${label}.command`);
422
- optionalPositiveInteger(value.timeoutMs, `${label}.timeoutMs`);
431
+ optionalTimeoutMs(value.timeoutMs, `${label}.timeoutMs`);
423
432
  optionalPositiveInteger(value.idleTimeoutMs, `${label}.idleTimeoutMs`);
424
433
  if (value.shell !== true && /\s/.test(value.command.trim())) {
425
434
  throw new Error(`${label}.command must be an executable without spaces when shell is false; put flags in args or set shell true`);
@@ -438,7 +447,7 @@ function validateTarget(value, label, opts) {
438
447
  const providers = ["claude", "cursor", "codewith", "aicopilot", "opencode", "codex"];
439
448
  if (!providers.includes(value.provider))
440
449
  throw new Error(`${label}.provider must be one of ${providers.join(", ")}`);
441
- optionalPositiveInteger(value.timeoutMs, `${label}.timeoutMs`);
450
+ optionalTimeoutMs(value.timeoutMs, `${label}.timeoutMs`);
442
451
  optionalPositiveInteger(value.idleTimeoutMs, `${label}.idleTimeoutMs`);
443
452
  if (value.authProfile !== undefined) {
444
453
  assertString(value.authProfile, `${label}.authProfile`);
@@ -561,7 +570,7 @@ function normalizeCreateWorkflowInput(input, opts = {}) {
561
570
  target: validateTarget(step.target, `workflow.steps[${index}].target`, opts),
562
571
  dependsOn: optionalStringArray(step.dependsOn, `workflow.steps[${index}].dependsOn`) ?? [],
563
572
  continueOnFailure: optionalBoolean(step.continueOnFailure, `workflow.steps[${index}].continueOnFailure`) ?? false,
564
- timeoutMs: optionalPositiveInteger(step.timeoutMs, `workflow.steps[${index}].timeoutMs`),
573
+ timeoutMs: optionalTimeoutMs(step.timeoutMs, `workflow.steps[${index}].timeoutMs`),
565
574
  account: optionalAccountRef(step.account, `workflow.steps[${index}].account`)
566
575
  };
567
576
  });
@@ -1307,6 +1316,17 @@ class Store {
1307
1316
  const row = this.db.query("SELECT * FROM loops WHERE name = ? ORDER BY created_at DESC LIMIT 1").get(name);
1308
1317
  return row ? rowToLoop(row) : undefined;
1309
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
+ }
1310
1330
  requireLoop(idOrName) {
1311
1331
  return this.getLoop(idOrName) ?? this.findLoopByName(idOrName) ?? (() => {
1312
1332
  throw new Error(`loop not found: ${idOrName}`);
@@ -1381,6 +1401,130 @@ class Store {
1381
1401
  throw new Error(`loop not found after update: ${id}`);
1382
1402
  return after;
1383
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
+ }
1384
1528
  renameLoop(id, name, opts = {}) {
1385
1529
  const current = this.getLoop(id);
1386
1530
  if (!current)
@@ -3606,7 +3750,7 @@ function commandSpec(target) {
3606
3750
  cwd: commandTarget.cwd,
3607
3751
  shell: commandTarget.shell,
3608
3752
  env: commandTarget.env,
3609
- timeoutMs: commandTarget.timeoutMs ?? DEFAULT_TIMEOUT_MS,
3753
+ timeoutMs: commandTarget.timeoutMs === undefined ? DEFAULT_TIMEOUT_MS : commandTarget.timeoutMs,
3610
3754
  idleTimeoutMs: commandTarget.idleTimeoutMs,
3611
3755
  account: commandTarget.account,
3612
3756
  accountTool: commandTarget.account?.tool
@@ -3617,7 +3761,7 @@ function commandSpec(target) {
3617
3761
  command: providerCommand(agentTarget.provider),
3618
3762
  args: agentArgs(agentTarget),
3619
3763
  cwd: agentTarget.cwd,
3620
- timeoutMs: agentTarget.timeoutMs ?? DEFAULT_TIMEOUT_MS,
3764
+ timeoutMs: agentTarget.timeoutMs ?? null,
3621
3765
  idleTimeoutMs: agentTarget.idleTimeoutMs,
3622
3766
  account: agentTarget.account,
3623
3767
  accountTool: agentTarget.account?.tool ?? accountToolForProvider(agentTarget.provider),
@@ -3805,12 +3949,12 @@ async function executeRemoteSpec(spec, machine, metadata, opts) {
3805
3949
  if (opts.signal?.aborted)
3806
3950
  abortHandler();
3807
3951
  opts.signal?.addEventListener("abort", abortHandler, { once: true });
3808
- const timer = setTimeout(() => {
3952
+ const timer = typeof spec.timeoutMs === "number" ? setTimeout(() => {
3809
3953
  timedOut = true;
3810
3954
  if (child.pid)
3811
3955
  killProcessGroup(child.pid);
3812
- }, spec.timeoutMs);
3813
- timer.unref();
3956
+ }, spec.timeoutMs) : undefined;
3957
+ timer?.unref();
3814
3958
  let idleTimer;
3815
3959
  const resetIdleTimer = () => {
3816
3960
  if (!spec.idleTimeoutMs)
@@ -3842,7 +3986,8 @@ async function executeRemoteSpec(spec, machine, metadata, opts) {
3842
3986
  } catch (err) {
3843
3987
  error = err instanceof Error ? err.message : String(err);
3844
3988
  } finally {
3845
- clearTimeout(timer);
3989
+ if (timer)
3990
+ clearTimeout(timer);
3846
3991
  if (idleTimer)
3847
3992
  clearTimeout(idleTimer);
3848
3993
  opts.signal?.removeEventListener("abort", abortHandler);
@@ -3984,12 +4129,12 @@ async function executeTarget(target, metadata = {}, opts = {}) {
3984
4129
  if (opts.signal?.aborted)
3985
4130
  abortHandler();
3986
4131
  opts.signal?.addEventListener("abort", abortHandler, { once: true });
3987
- const timer = setTimeout(() => {
4132
+ const timer = typeof spec.timeoutMs === "number" ? setTimeout(() => {
3988
4133
  timedOut = true;
3989
4134
  if (child.pid)
3990
4135
  killProcessGroup(child.pid);
3991
- }, spec.timeoutMs);
3992
- timer.unref();
4136
+ }, spec.timeoutMs) : undefined;
4137
+ timer?.unref();
3993
4138
  let idleTimer;
3994
4139
  const resetIdleTimer = () => {
3995
4140
  if (!spec.idleTimeoutMs)
@@ -4021,7 +4166,8 @@ async function executeTarget(target, metadata = {}, opts = {}) {
4021
4166
  } catch (err) {
4022
4167
  error = err instanceof Error ? err.message : String(err);
4023
4168
  } finally {
4024
- clearTimeout(timer);
4169
+ if (timer)
4170
+ clearTimeout(timer);
4025
4171
  if (idleTimer)
4026
4172
  clearTimeout(idleTimer);
4027
4173
  opts.signal?.removeEventListener("abort", abortHandler);
@@ -4446,7 +4592,7 @@ ${result.stderr}`);
4446
4592
  // src/lib/workflow-runner.ts
4447
4593
  function targetWithStepAccount(step) {
4448
4594
  const account = step.account ?? step.target.account;
4449
- const timeoutMs = step.timeoutMs ?? step.target.timeoutMs;
4595
+ const timeoutMs = step.timeoutMs !== undefined ? step.timeoutMs : step.target.timeoutMs;
4450
4596
  if (!account && timeoutMs === step.target.timeoutMs)
4451
4597
  return step.target;
4452
4598
  return { ...step.target, account, timeoutMs };
@@ -4742,7 +4888,8 @@ async function executeLoopTarget(store, loop, run, opts = {}) {
4742
4888
  })
4743
4889
  });
4744
4890
  }
4745
- 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;
4746
4893
  let workflowTimedOut = false;
4747
4894
  const externalAbort = () => controller?.abort();
4748
4895
  if (controller && opts.signal?.aborted)
@@ -4752,13 +4899,13 @@ async function executeLoopTarget(store, loop, run, opts = {}) {
4752
4899
  const timer = controller ? setTimeout(() => {
4753
4900
  workflowTimedOut = true;
4754
4901
  controller.abort();
4755
- }, loop.target.timeoutMs) : undefined;
4902
+ }, workflowTimeoutMs) : undefined;
4756
4903
  timer?.unref();
4757
4904
  try {
4758
4905
  return await executeWorkflow(store, workflow, {
4759
4906
  ...opts,
4760
4907
  signal: controller?.signal ?? opts.signal,
4761
- 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,
4762
4909
  loop,
4763
4910
  loopRun: run,
4764
4911
  scheduledFor: run.scheduledFor,
@@ -5534,7 +5681,11 @@ function runDoctor(store) {
5534
5681
  if (loop.target.type === "workflow") {
5535
5682
  const workflow = store.requireWorkflow(loop.target.workflowId);
5536
5683
  for (const step of workflowExecutionOrder(workflow)) {
5537
- 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 });
5538
5689
  }
5539
5690
  } else {
5540
5691
  preflightTarget(loop.target, { loopId: loop.id, loopName: loop.name }, { machine: loop.machine });
@@ -6014,7 +6165,7 @@ function buildScriptInventoryReport(store, opts = {}) {
6014
6165
  // package.json
6015
6166
  var package_default = {
6016
6167
  name: "@hasna/loops",
6017
- version: "0.3.54",
6168
+ version: "0.3.56",
6018
6169
  description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
6019
6170
  type: "module",
6020
6171
  main: "dist/index.js",
@@ -6144,7 +6295,8 @@ var TEMPLATE_SUMMARIES = [
6144
6295
  { name: "manualBreakGlass", default: "false", description: "Allow explicit danger-full-access in a generated workflow. Intended for manual emergency use only." },
6145
6296
  { name: "worktreeMode", default: "auto", description: "Worktree isolation mode: auto, required, off, or main." },
6146
6297
  { name: "worktreeRoot", default: "~/.hasna/loops/worktrees", description: "Base directory for OpenLoops-managed git worktrees." },
6147
- { 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." }
6148
6300
  ]
6149
6301
  },
6150
6302
  {
@@ -6174,7 +6326,8 @@ var TEMPLATE_SUMMARIES = [
6174
6326
  { name: "manualBreakGlass", default: "false", description: "Allow explicit danger-full-access in a generated workflow. Intended for manual emergency use only." },
6175
6327
  { name: "worktreeMode", default: "auto", description: "Worktree isolation mode: auto, required, off, or main." },
6176
6328
  { name: "worktreeRoot", default: "~/.hasna/loops/worktrees", description: "Base directory for OpenLoops-managed git worktrees." },
6177
- { 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." }
6178
6331
  ]
6179
6332
  },
6180
6333
  {
@@ -6202,7 +6355,7 @@ var TEMPLATE_SUMMARIES = [
6202
6355
  { name: "worktreeMode", default: "auto", description: "Worktree isolation mode: auto, required, off, or main." },
6203
6356
  { name: "worktreeRoot", default: "~/.hasna/loops/worktrees", description: "Base directory for OpenLoops-managed git worktrees." },
6204
6357
  { name: "worktreeBranchPrefix", default: "openloops", description: "Branch prefix for generated bounded-agent worktree branches." },
6205
- { 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." }
6206
6359
  ]
6207
6360
  },
6208
6361
  {
@@ -6221,7 +6374,8 @@ var TEMPLATE_SUMMARIES = [
6221
6374
  { name: "accountPool", description: "Comma-separated OpenAccounts profiles for non-Codewith providers." },
6222
6375
  { name: "provider", default: "codewith", description: "Agent provider." },
6223
6376
  { name: "sandbox", default: "workspace-write", description: "Provider sandbox mode." },
6224
- { 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." }
6225
6379
  ]
6226
6380
  },
6227
6381
  {
@@ -6326,6 +6480,30 @@ function taskLabel(input) {
6326
6480
  const head = input.taskTitle?.trim() || input.taskId;
6327
6481
  return head.length > 160 ? `${head.slice(0, 157)}...` : head;
6328
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
+ }
6329
6507
  function stableIndex(seed, size) {
6330
6508
  let hash = 2166136261;
6331
6509
  for (let i = 0;i < seed.length; i += 1) {
@@ -6404,6 +6582,21 @@ function gitRootFor(path) {
6404
6582
  return;
6405
6583
  }
6406
6584
  }
6585
+ function gitCommonDirFor(path) {
6586
+ if (!existsSync4(path))
6587
+ return;
6588
+ try {
6589
+ const raw = execFileSync("git", ["-C", path, "rev-parse", "--git-common-dir"], {
6590
+ encoding: "utf8",
6591
+ stdio: ["ignore", "pipe", "ignore"]
6592
+ }).trim();
6593
+ if (!raw)
6594
+ return;
6595
+ return isAbsolute2(raw) ? raw : resolve2(path, raw);
6596
+ } catch {
6597
+ return;
6598
+ }
6599
+ }
6407
6600
  function prepareWorktreeCommand(plan) {
6408
6601
  const repo = shellQuote2(plan.repoRoot);
6409
6602
  const path = shellQuote2(plan.path);
@@ -6520,6 +6713,7 @@ function worktreePlan(input, seed) {
6520
6713
  root,
6521
6714
  path: worktreePath,
6522
6715
  branch,
6716
+ gitMetadataDir: gitCommonDirFor(repoRoot),
6523
6717
  prepareStep
6524
6718
  };
6525
6719
  }
@@ -6567,6 +6761,10 @@ function agentTarget(input, prompt, role, seed, plan) {
6567
6761
  assertNativeAuthProfileSupport(input, provider);
6568
6762
  const sandbox = input.sandbox ?? (provider === "codewith" || provider === "codex" ? "workspace-write" : provider === "cursor" ? "enabled" : undefined);
6569
6763
  failClosedSandbox(input, provider, sandbox);
6764
+ const addDirs = [...input.addDirs ?? []];
6765
+ if (plan.enabled && plan.gitMetadataDir && (provider === "codewith" || provider === "codex") && sandbox === "workspace-write") {
6766
+ addDirs.push(plan.gitMetadataDir);
6767
+ }
6570
6768
  return {
6571
6769
  type: "agent",
6572
6770
  provider,
@@ -6575,7 +6773,7 @@ function agentTarget(input, prompt, role, seed, plan) {
6575
6773
  model: input.model,
6576
6774
  variant: input.variant,
6577
6775
  agent: input.agent,
6578
- addDirs: input.addDirs,
6776
+ addDirs: addDirs.length ? [...new Set(addDirs)] : undefined,
6579
6777
  authProfile: provider === "codewith" ? authProfileForRole(input, role, seed) : undefined,
6580
6778
  configIsolation: "safe",
6581
6779
  permissionMode: input.permissionMode ?? "bypass",
@@ -6597,7 +6795,7 @@ function agentTarget(input, prompt, role, seed, plan) {
6597
6795
  ...input.projectGroup ? { projectGroup: input.projectGroup } : {}
6598
6796
  },
6599
6797
  account: accountForRole(input, role, seed),
6600
- timeoutMs: 45 * 60000
6798
+ timeoutMs: agentTimeoutMs(input)
6601
6799
  };
6602
6800
  }
6603
6801
  function workflowStepsWithWorktree(plan, steps) {
@@ -7040,18 +7238,15 @@ function renderTodosTaskWorkerVerifierWorkflow(input) {
7040
7238
  name: "Worker",
7041
7239
  description: "Implement the todos task and record evidence.",
7042
7240
  target: agentTarget(input, workerPrompt, "worker", input.taskId, plan),
7043
- timeoutMs: 45 * 60000
7241
+ timeoutMs: agentTimeoutMs(input)
7044
7242
  },
7045
7243
  {
7046
7244
  id: "verifier",
7047
7245
  name: "Verifier",
7048
7246
  description: "Adversarially verify worker output and update todos.",
7049
7247
  dependsOn: ["worker"],
7050
- target: {
7051
- ...agentTarget(input, verifierPrompt, "verifier", input.taskId, plan),
7052
- idleTimeoutMs: 10 * 60000
7053
- },
7054
- timeoutMs: 30 * 60000
7248
+ target: agentTarget(input, verifierPrompt, "verifier", input.taskId, plan),
7249
+ timeoutMs: agentTimeoutMs(input)
7055
7250
  }
7056
7251
  ])
7057
7252
  };
@@ -7212,7 +7407,7 @@ function renderTaskLifecycleWorkflow(input) {
7212
7407
  name: "Triage",
7213
7408
  description: "Check task eligibility, duplicates, dependencies, and automation gates.",
7214
7409
  target: agentTarget(input, triagePrompt, "triage", input.taskId, plan),
7215
- timeoutMs: 20 * 60000
7410
+ timeoutMs: agentTimeoutMs(input)
7216
7411
  },
7217
7412
  {
7218
7413
  id: "triage-gate",
@@ -7234,7 +7429,7 @@ function renderTaskLifecycleWorkflow(input) {
7234
7429
  description: "Create a concise implementation plan and split unsafe scope before work starts.",
7235
7430
  dependsOn: ["triage-gate"],
7236
7431
  target: agentTarget(input, plannerPrompt, "planner", input.taskId, plan),
7237
- timeoutMs: 25 * 60000
7432
+ timeoutMs: agentTimeoutMs(input)
7238
7433
  },
7239
7434
  {
7240
7435
  id: "planner-gate",
@@ -7256,18 +7451,15 @@ function renderTaskLifecycleWorkflow(input) {
7256
7451
  description: "Implement the todos task according to triage and planner evidence.",
7257
7452
  dependsOn: ["planner-gate"],
7258
7453
  target: agentTarget(input, workerPrompt, "worker", input.taskId, plan),
7259
- timeoutMs: 45 * 60000
7454
+ timeoutMs: agentTimeoutMs(input)
7260
7455
  },
7261
7456
  {
7262
7457
  id: "verifier",
7263
7458
  name: "Verifier",
7264
7459
  description: "Adversarially verify worker output and update todos.",
7265
7460
  dependsOn: ["worker"],
7266
- target: {
7267
- ...agentTarget(input, verifierPrompt, "verifier", input.taskId, plan),
7268
- idleTimeoutMs: 10 * 60000
7269
- },
7270
- timeoutMs: 30 * 60000
7461
+ target: agentTarget(input, verifierPrompt, "verifier", input.taskId, plan),
7462
+ timeoutMs: agentTimeoutMs(input)
7271
7463
  }
7272
7464
  ])
7273
7465
  };
@@ -7337,18 +7529,15 @@ function renderEventWorkerVerifierWorkflow(input) {
7337
7529
  name: "Worker",
7338
7530
  description: "Handle the Hasna event and record evidence.",
7339
7531
  target: agentTarget(input, workerPrompt, "worker", seed, plan),
7340
- timeoutMs: 45 * 60000
7532
+ timeoutMs: agentTimeoutMs(input)
7341
7533
  },
7342
7534
  {
7343
7535
  id: "verifier",
7344
7536
  name: "Verifier",
7345
7537
  description: "Adversarially verify event handling.",
7346
7538
  dependsOn: ["worker"],
7347
- target: {
7348
- ...agentTarget(input, verifierPrompt, "verifier", seed, plan),
7349
- idleTimeoutMs: 10 * 60000
7350
- },
7351
- timeoutMs: 30 * 60000
7539
+ target: agentTarget(input, verifierPrompt, "verifier", seed, plan),
7540
+ timeoutMs: agentTimeoutMs(input)
7352
7541
  }
7353
7542
  ])
7354
7543
  };
@@ -7360,7 +7549,7 @@ function renderBoundedAgentWorkerVerifierWorkflow(input) {
7360
7549
  throw new Error("projectPath is required");
7361
7550
  const seed = `${input.projectPath}:${input.objective}`;
7362
7551
  const plan = worktreePlan(input, seed);
7363
- const timeoutMs = input.timeoutMs && Number.isFinite(input.timeoutMs) ? input.timeoutMs : 45 * 60000;
7552
+ const timeoutMs = agentTimeoutMs(input);
7364
7553
  const workerPrompt = [
7365
7554
  `/goal ${input.objective}`,
7366
7555
  "",
@@ -7398,11 +7587,8 @@ function renderBoundedAgentWorkerVerifierWorkflow(input) {
7398
7587
  name: "Verifier",
7399
7588
  description: "Adversarially verify the bounded objective result.",
7400
7589
  dependsOn: ["worker"],
7401
- target: {
7402
- ...agentTarget(input, verifierPrompt, "verifier", seed, plan),
7403
- idleTimeoutMs: 10 * 60000
7404
- },
7405
- timeoutMs: Math.min(timeoutMs, 30 * 60000)
7590
+ target: agentTarget(input, verifierPrompt, "verifier", seed, plan),
7591
+ timeoutMs
7406
7592
  }
7407
7593
  ])
7408
7594
  };
@@ -7431,7 +7617,7 @@ function renderLifecycleBoundedTemplate(id, values) {
7431
7617
  worktreeMode: values.worktreeMode ?? (id === REPORT_ONLY_TEMPLATE_ID ? "main" : "required"),
7432
7618
  worktreeRoot: values.worktreeRoot,
7433
7619
  worktreeBranchPrefix: values.worktreeBranchPrefix,
7434
- timeoutMs: values.timeoutMs ? Number(values.timeoutMs) : undefined
7620
+ timeoutMs: parseTemplateTimeoutMs(values.timeoutMs)
7435
7621
  };
7436
7622
  if (id === TASK_LIFECYCLE_TEMPLATE_ID) {
7437
7623
  const taskId = values.taskId ?? "";
@@ -7468,6 +7654,7 @@ function renderLifecycleBoundedTemplate(id, values) {
7468
7654
  worktreeMode: values.worktreeMode ?? "required",
7469
7655
  worktreeRoot: values.worktreeRoot,
7470
7656
  worktreeBranchPrefix: values.worktreeBranchPrefix,
7657
+ timeoutMs: parseTemplateTimeoutMs(values.timeoutMs),
7471
7658
  eventId: values.eventId,
7472
7659
  eventType: values.eventType
7473
7660
  });
@@ -7534,6 +7721,8 @@ function renderDeterministicCheckCreateTaskWorkflow(values) {
7534
7721
  if (!checkCommand.trim())
7535
7722
  throw new Error("checkCommand is required");
7536
7723
  const seed = `${projectPath}:${checkCommand}`;
7724
+ const timeoutMs = parseDeterministicTimeoutMs(values.timeoutMs, 5 * 60000);
7725
+ const idleTimeoutMs = parseDeterministicTimeoutMs(values.idleTimeoutMs, 60000, "idleTimeoutMs");
7537
7726
  return {
7538
7727
  name: values.name ?? `deterministic-check-${stableIndex(seed, 4294967295).toString(16).padStart(8, "0")}`,
7539
7728
  description: values.description ?? "Deterministic check that writes compact evidence and upserts one deduped todos task when the expectation is not met.",
@@ -7548,10 +7737,10 @@ function renderDeterministicCheckCreateTaskWorkflow(values) {
7548
7737
  command: "bash",
7549
7738
  args: ["-lc", checkCommand],
7550
7739
  cwd: projectPath,
7551
- timeoutMs: values.timeoutMs ? Number(values.timeoutMs) : 5 * 60000,
7552
- idleTimeoutMs: values.idleTimeoutMs ? Number(values.idleTimeoutMs) : 60000
7740
+ timeoutMs,
7741
+ idleTimeoutMs
7553
7742
  },
7554
- timeoutMs: values.timeoutMs ? Number(values.timeoutMs) : 5 * 60000
7743
+ timeoutMs
7555
7744
  }
7556
7745
  ]
7557
7746
  };
@@ -7589,6 +7778,7 @@ function renderBuiltinLoopTemplate(id, values) {
7589
7778
  worktreeMode: values.worktreeMode,
7590
7779
  worktreeRoot: values.worktreeRoot,
7591
7780
  worktreeBranchPrefix: values.worktreeBranchPrefix,
7781
+ timeoutMs: parseTemplateTimeoutMs(values.timeoutMs),
7592
7782
  eventId: values.eventId,
7593
7783
  eventType: values.eventType
7594
7784
  });
@@ -7620,7 +7810,8 @@ function renderBuiltinLoopTemplate(id, values) {
7620
7810
  manualBreakGlass: booleanVar(values.manualBreakGlass),
7621
7811
  worktreeMode: values.worktreeMode,
7622
7812
  worktreeRoot: values.worktreeRoot,
7623
- worktreeBranchPrefix: values.worktreeBranchPrefix
7813
+ worktreeBranchPrefix: values.worktreeBranchPrefix,
7814
+ timeoutMs: parseTemplateTimeoutMs(values.timeoutMs)
7624
7815
  });
7625
7816
  }
7626
7817
  if (id === BOUNDED_AGENT_WORKER_VERIFIER_TEMPLATE_ID) {
@@ -7648,7 +7839,7 @@ function renderBuiltinLoopTemplate(id, values) {
7648
7839
  worktreeMode: values.worktreeMode,
7649
7840
  worktreeRoot: values.worktreeRoot,
7650
7841
  worktreeBranchPrefix: values.worktreeBranchPrefix,
7651
- timeoutMs: values.timeoutMs ? Number(values.timeoutMs) : undefined
7842
+ timeoutMs: parseTemplateTimeoutMs(values.timeoutMs)
7652
7843
  });
7653
7844
  }
7654
7845
  throw new Error(`unknown template: ${id}`);
@@ -7793,6 +7984,43 @@ function publicWorkflowBody(body) {
7793
7984
  const { id: _id, status: _status, createdAt: _createdAt, updatedAt: _updatedAt, ...bodyOnly } = value;
7794
7985
  return bodyOnly;
7795
7986
  }
7987
+ function workflowWithAgentTimeouts(workflow, timeoutMs, opts = {}) {
7988
+ let changed = false;
7989
+ const agentStepIds = [];
7990
+ const steps = workflow.steps.map((step) => {
7991
+ if (step.target.type !== "agent")
7992
+ return step;
7993
+ agentStepIds.push(step.id);
7994
+ const target = { ...step.target, timeoutMs };
7995
+ if (timeoutMs === null && target.idleTimeoutMs !== undefined) {
7996
+ delete target.idleTimeoutMs;
7997
+ changed = true;
7998
+ }
7999
+ if (step.timeoutMs !== timeoutMs || step.target.timeoutMs !== timeoutMs)
8000
+ changed = true;
8001
+ return {
8002
+ ...step,
8003
+ target,
8004
+ timeoutMs
8005
+ };
8006
+ });
8007
+ return {
8008
+ body: {
8009
+ name: opts.name ?? workflow.name,
8010
+ description: workflow.description,
8011
+ version: workflow.version,
8012
+ goal: workflow.goal,
8013
+ steps
8014
+ },
8015
+ changed,
8016
+ agentStepIds
8017
+ };
8018
+ }
8019
+ function workflowTimeoutMigrationName(workflow, timeoutMs) {
8020
+ const policy = timeoutMs === null ? "agent-timeout-unlimited" : `agent-timeout-${timeoutMs}ms`;
8021
+ const suffix = randomUUID().replace(/-/g, "").slice(0, 8);
8022
+ return `${workflow.name}-${policy}-${suffix}`;
8023
+ }
7796
8024
  function printTextOutput(value) {
7797
8025
  for (const line of textOutputBlocks(value, { indent: " " }))
7798
8026
  console.log(line);
@@ -7833,6 +8061,17 @@ function positiveDuration(raw, label) {
7833
8061
  throw new Error(`${label} must be greater than zero`);
7834
8062
  return value;
7835
8063
  }
8064
+ function timeoutDuration(raw, label) {
8065
+ if (raw === undefined)
8066
+ return;
8067
+ const normalized = raw.trim().toLowerCase();
8068
+ if (["none", "unlimited", "null", "never", "off", "false"].includes(normalized))
8069
+ return null;
8070
+ const value = parseDuration(raw);
8071
+ if (!Number.isFinite(value) || value <= 0)
8072
+ throw new Error(`${label} must be a duration greater than zero, or none/unlimited`);
8073
+ return value;
8074
+ }
7836
8075
  function parsePolicy(opts) {
7837
8076
  const catchUp = opts.catchUp ?? "latest";
7838
8077
  if (!["none", "latest", "all"].includes(catchUp))
@@ -8563,6 +8802,7 @@ function routeTodosTaskEvent(event, opts) {
8563
8802
  variant: opts.variant,
8564
8803
  agent: opts.agent,
8565
8804
  addDirs: listFromRepeatedOpts(opts.addDir),
8805
+ timeoutMs: timeoutDuration(opts.timeout, "--timeout"),
8566
8806
  permissionMode,
8567
8807
  sandbox,
8568
8808
  manualBreakGlass: Boolean(opts.manualBreakGlass),
@@ -8785,6 +9025,7 @@ function routeGenericEvent(event, opts) {
8785
9025
  variant: opts.variant,
8786
9026
  agent: opts.agent,
8787
9027
  addDirs: listFromRepeatedOpts(opts.addDir),
9028
+ timeoutMs: timeoutDuration(opts.timeout, "--timeout"),
8788
9029
  permissionMode,
8789
9030
  sandbox,
8790
9031
  manualBreakGlass: Boolean(opts.manualBreakGlass),
@@ -9133,7 +9374,7 @@ function permissionModeFromOpts(opts, provider) {
9133
9374
  return mode;
9134
9375
  }
9135
9376
  var create = program.command("create").description("create loops");
9136
- 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) => {
9377
+ 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) => {
9137
9378
  const store = new Store;
9138
9379
  try {
9139
9380
  const target = {
@@ -9141,7 +9382,7 @@ addGoalOptions(addAccountOptions(addMachineOptions(addScheduleOptions(create.com
9141
9382
  command: opts.cmd,
9142
9383
  cwd: opts.cwd,
9143
9384
  shell: opts.shell,
9144
- timeoutMs: opts.timeout ? parseDuration(opts.timeout) : undefined,
9385
+ timeoutMs: timeoutDuration(opts.timeout, "--timeout"),
9145
9386
  account: accountFromOpts(opts),
9146
9387
  preflight: runtimePreflightFromOpts(opts)
9147
9388
  };
@@ -9153,7 +9394,7 @@ addGoalOptions(addAccountOptions(addMachineOptions(addScheduleOptions(create.com
9153
9394
  store.close();
9154
9395
  }
9155
9396
  });
9156
- 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").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) => {
9397
+ 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) => {
9157
9398
  const provider = opts.provider;
9158
9399
  if (!["claude", "cursor", "codewith", "aicopilot", "opencode", "codex"].includes(provider)) {
9159
9400
  throw new Error("unsupported provider");
@@ -9174,7 +9415,7 @@ addGoalOptions(addAccountOptions(addMachineOptions(addScheduleOptions(create.com
9174
9415
  agent: opts.agent,
9175
9416
  authProfile: providerAuthProfileFromOpts(opts, provider),
9176
9417
  addDirs: listFromRepeatedOpts(opts.addDir),
9177
- timeoutMs: opts.timeout ? parseDuration(opts.timeout) : undefined,
9418
+ timeoutMs: timeoutDuration(opts.timeout, "--timeout"),
9178
9419
  configIsolation: opts.configIsolation,
9179
9420
  permissionMode: permissionModeFromOpts(opts, provider),
9180
9421
  sandbox: sandboxFromOpts(opts, provider),
@@ -9190,13 +9431,14 @@ addGoalOptions(addAccountOptions(addMachineOptions(addScheduleOptions(create.com
9190
9431
  store.close();
9191
9432
  }
9192
9433
  });
9193
- 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) => {
9434
+ 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) => {
9194
9435
  const store = new Store;
9195
9436
  try {
9196
9437
  const workflow = store.requireWorkflow(opts.workflow);
9197
9438
  const target = {
9198
9439
  type: "workflow",
9199
9440
  workflowId: workflow.id,
9441
+ timeoutMs: timeoutDuration(opts.timeout, "--timeout"),
9200
9442
  preflight: runtimePreflightFromOpts(opts)
9201
9443
  };
9202
9444
  const input = baseCreateInput(name, opts, target);
@@ -9214,10 +9456,10 @@ var events = program.command("events").description("handle Hasna event envelopes
9214
9456
  var machines = program.command("machines").description("inspect OpenMachines topology for loop assignment");
9215
9457
  var goal = program.command("goal").description("inspect goal runs");
9216
9458
  function addRouteEventOptions(command) {
9217
- 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");
9459
+ 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");
9218
9460
  }
9219
9461
  function addTodosDrainOptions(command, opts = {}) {
9220
- 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");
9462
+ 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");
9221
9463
  if (opts.includeDryRun ?? true) {
9222
9464
  configured = configured.option("--dry-run", "preview selected tasks and generated workflow loops without storing anything");
9223
9465
  }
@@ -9270,6 +9512,7 @@ function routeDrainArgs(opts) {
9270
9512
  add("--agent", opts.agent);
9271
9513
  for (const dir of listFromRepeatedOpts(opts.addDir) ?? [])
9272
9514
  add("--add-dir", dir);
9515
+ add("--timeout", opts.timeout);
9273
9516
  add("--permission-mode", opts.permissionMode);
9274
9517
  add("--sandbox", opts.sandbox);
9275
9518
  addBool("--manual-break-glass", opts.manualBreakGlass);
@@ -9581,7 +9824,7 @@ addScheduleOptions(addTodosDrainOptions(routes.command("schedule <kind> <name>")
9581
9824
  }
9582
9825
  });
9583
9826
  var eventsHandle = events.command("handle").description("handle a Hasna event envelope");
9584
- 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) => {
9827
+ 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) => {
9585
9828
  const event = await readEventEnvelopeFromStdin();
9586
9829
  const result = routeTodosTaskEvent(event, opts);
9587
9830
  print(result.value, result.human);
@@ -9590,7 +9833,7 @@ var eventsDrain = events.command("drain").description("drain durable source queu
9590
9833
  addTodosDrainOptions(eventsDrain.command("todos-task").description("drain ready todos tasks into bounded worker/verifier workflow loops")).action((opts) => {
9591
9834
  drainTodosTaskRoutes(opts);
9592
9835
  });
9593
- 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) => {
9836
+ 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) => {
9594
9837
  const event = await readEventEnvelopeFromStdin();
9595
9838
  const result = routeGenericEvent(event, opts);
9596
9839
  print(result.value, result.human);
@@ -9806,6 +10049,75 @@ workflows.command("recover <runId>").description("reset interrupted running work
9806
10049
  store.close();
9807
10050
  }
9808
10051
  });
10052
+ 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) => {
10053
+ const store = new Store;
10054
+ try {
10055
+ const timeoutMs = timeoutDuration(opts.timeout, "--timeout") ?? null;
10056
+ const candidateLoops = opts.loop ? [store.requireUniqueLoop(opts.loop)] : store.listLoops({ status: "active", limit: 1e4 }).filter((loop) => loop.target.type === "workflow");
10057
+ const rows = [];
10058
+ for (const loop of candidateLoops) {
10059
+ if (loop.archivedAt) {
10060
+ rows.push({ loop: publicLoop(loop), status: "skipped", reason: "loop is archived" });
10061
+ continue;
10062
+ }
10063
+ if (loop.target.type !== "workflow") {
10064
+ rows.push({ loop: publicLoop(loop), status: "skipped", reason: "loop is not a workflow loop" });
10065
+ continue;
10066
+ }
10067
+ const workflow = store.requireWorkflow(loop.target.workflowId);
10068
+ const nextWorkflowName = workflowTimeoutMigrationName(workflow, timeoutMs);
10069
+ const migration = workflowWithAgentTimeouts(workflow, timeoutMs, { name: nextWorkflowName });
10070
+ if (migration.agentStepIds.length === 0) {
10071
+ rows.push({ loop: publicLoop(loop), workflow: publicWorkflow(workflow), status: "skipped", reason: "workflow has no agent steps" });
10072
+ continue;
10073
+ }
10074
+ if (!migration.changed) {
10075
+ rows.push({ loop: publicLoop(loop), workflow: publicWorkflow(workflow), status: "skipped", reason: "agent timeout policy already matches" });
10076
+ continue;
10077
+ }
10078
+ if (store.hasRunningRun(loop.id)) {
10079
+ rows.push({ loop: publicLoop(loop), workflow: publicWorkflow(workflow), status: "blocked", reason: "loop has a running run; retry after it finishes" });
10080
+ continue;
10081
+ }
10082
+ if (!opts.apply) {
10083
+ rows.push({
10084
+ loop: publicLoop(loop),
10085
+ workflow: publicWorkflow(workflow),
10086
+ status: "would_migrate",
10087
+ agentStepIds: migration.agentStepIds,
10088
+ nextWorkflowName,
10089
+ timeoutMs
10090
+ });
10091
+ continue;
10092
+ }
10093
+ const migrated = store.createAndRetargetWorkflowLoop(loop.id, migration.body, {
10094
+ workflowTimeoutMs: timeoutMs,
10095
+ archiveOld: Boolean(opts.archiveOld)
10096
+ });
10097
+ rows.push({
10098
+ loop: publicLoop(migrated.loop),
10099
+ previousWorkflow: publicWorkflow(migrated.previousWorkflow),
10100
+ workflow: publicWorkflow(migrated.workflow),
10101
+ archivedOld: migrated.archivedOld ? publicWorkflow(migrated.archivedOld) : undefined,
10102
+ status: "migrated",
10103
+ agentStepIds: migration.agentStepIds,
10104
+ timeoutMs
10105
+ });
10106
+ }
10107
+ const summary = {
10108
+ apply: Boolean(opts.apply),
10109
+ timeoutMs,
10110
+ total: rows.length,
10111
+ migrated: rows.filter((row) => row.status === "migrated").length,
10112
+ wouldMigrate: rows.filter((row) => row.status === "would_migrate").length,
10113
+ blocked: rows.filter((row) => row.status === "blocked").length,
10114
+ skipped: rows.filter((row) => row.status === "skipped").length
10115
+ };
10116
+ print({ summary, rows }, opts.apply ? `migrated=${summary.migrated} blocked=${summary.blocked} skipped=${summary.skipped}` : `would_migrate=${summary.wouldMigrate} blocked=${summary.blocked} skipped=${summary.skipped}`);
10117
+ } finally {
10118
+ store.close();
10119
+ }
10120
+ });
9809
10121
  workflows.command("archive <idOrName>").action((idOrName) => {
9810
10122
  const store = new Store;
9811
10123
  try {