@hasna/loops 0.4.7 → 0.4.9

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
@@ -581,7 +581,7 @@ import { isAbsolute, resolve } from "path";
581
581
 
582
582
  // src/lib/agent-adapter.ts
583
583
  import { spawn } from "child_process";
584
- var UNSAFE_CODEWITH_DURABLE_EXTRA_ARGS = new Set([
584
+ var UNSAFE_CODEWITH_EXEC_EXTRA_ARGS = new Set([
585
585
  "e",
586
586
  "exec",
587
587
  "agent",
@@ -630,12 +630,12 @@ function validateAgentOptions(target, label, capabilities) {
630
630
  throw new Error(`${label}.agent is not supported for provider codex`);
631
631
  }
632
632
  if (provider === "codewith" && target.agent !== undefined) {
633
- throw new Error(`${label}.agent is not supported for provider codewith durable background-agent execution`);
633
+ throw new Error(`${label}.agent is not supported for provider codewith`);
634
634
  }
635
635
  if (provider === "codewith") {
636
- const unsafe = target.extraArgs?.find((arg) => UNSAFE_CODEWITH_DURABLE_EXTRA_ARGS.has(arg));
636
+ const unsafe = target.extraArgs?.find((arg) => UNSAFE_CODEWITH_EXEC_EXTRA_ARGS.has(arg));
637
637
  if (unsafe) {
638
- throw new Error(`${label}.extraArgs cannot include ${unsafe}; codewith agent steps use durable agent start, not exec/ephemeral flags`);
638
+ throw new Error(`${label}.extraArgs cannot include ${unsafe}; codewith exec launch flags are managed by the adapter`);
639
639
  }
640
640
  }
641
641
  if (target.addDirs?.length && !["codewith", "codex"].includes(provider)) {
@@ -718,7 +718,10 @@ function buildAgentInvocation(target) {
718
718
  args.push(...target.authProfile ? ["--auth-profile", target.authProfile] : []);
719
719
  if (target.variant)
720
720
  args.push("-c", `model_reasoning_effort=${configStringValue(target.variant)}`);
721
- args.push("--ask-for-approval", "never", "--sandbox", codewithLikeSandbox(target));
721
+ const sandbox = codewithLikeSandbox(target);
722
+ if (sandbox === "workspace-write")
723
+ args.push("-c", "sandbox_workspace_write.network_access=true");
724
+ args.push("--ask-for-approval", "never", "exec", "--json", "--ephemeral", "--sandbox", sandbox, "--skip-git-repo-check");
722
725
  if (target.cwd)
723
726
  args.push("--cd", target.cwd);
724
727
  for (const dir of target.addDirs ?? [])
@@ -726,11 +729,7 @@ function buildAgentInvocation(target) {
726
729
  if (target.model)
727
730
  args.push("--model", target.model);
728
731
  args.push(...target.extraArgs ?? []);
729
- args.push("agent", "start", "--json");
730
- if (target.cwd)
731
- args.push("--cwd", target.cwd);
732
- args.push(target.prompt);
733
- return { command: "codewith", args };
732
+ return { command: "codewith", args, stdin: target.prompt };
734
733
  }
735
734
  case "codex": {
736
735
  if (target.variant)
@@ -783,7 +782,7 @@ function adapterFor(provider, capabilities) {
783
782
  var PROVIDER_ADAPTERS = {
784
783
  claude: adapterFor("claude", { sandbox: [], durable: false, remote: true, promptChannel: "stdin" }),
785
784
  cursor: adapterFor("cursor", { sandbox: CURSOR_SANDBOXES, durable: false, remote: true, promptChannel: "stdin" }),
786
- codewith: adapterFor("codewith", { sandbox: CODEX_LIKE_SANDBOXES, durable: true, remote: false, promptChannel: "argv" }),
785
+ codewith: adapterFor("codewith", { sandbox: CODEX_LIKE_SANDBOXES, durable: false, remote: true, promptChannel: "stdin" }),
787
786
  codex: adapterFor("codex", { sandbox: CODEX_LIKE_SANDBOXES, durable: false, remote: true, promptChannel: "stdin" }),
788
787
  aicopilot: adapterFor("aicopilot", { sandbox: [], durable: false, remote: true, promptChannel: "stdin" }),
789
788
  opencode: adapterFor("opencode", { sandbox: [], durable: false, remote: true, promptChannel: "stdin" })
@@ -4219,7 +4218,7 @@ class Store {
4219
4218
  // package.json
4220
4219
  var package_default = {
4221
4220
  name: "@hasna/loops",
4222
- version: "0.4.7",
4221
+ version: "0.4.9",
4223
4222
  description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
4224
4223
  type: "module",
4225
4224
  main: "dist/index.js",
@@ -4902,11 +4901,6 @@ var TRANSPORT_ENV_KEYS = new Set([
4902
4901
  "USER",
4903
4902
  "XDG_RUNTIME_DIR"
4904
4903
  ]);
4905
- function boundedText(text, maxBytes) {
4906
- const buffer = new BoundedOutputBuffer(maxBytes);
4907
- buffer.append(text);
4908
- return buffer.value();
4909
- }
4910
4904
  function buildResult(status, startedAt, fields = {}) {
4911
4905
  const finishedAt = fields.finishedAt ?? nowIso();
4912
4906
  return {
@@ -4970,21 +4964,6 @@ function metadataEnv(metadata) {
4970
4964
  env.LOOPS_GOAL_NODE_KEY = metadata.goalNodeKey;
4971
4965
  return env;
4972
4966
  }
4973
- function codewithAgentIdempotencyKey(metadata) {
4974
- const parts = [
4975
- "openloops",
4976
- metadata.workflowRunId ? `workflow-run:${metadata.workflowRunId}` : undefined,
4977
- metadata.workflowStepId ? `step:${metadata.workflowStepId}` : undefined,
4978
- metadata.runId ? `loop-run:${metadata.runId}` : undefined,
4979
- metadata.loopId ? `loop:${metadata.loopId}` : undefined,
4980
- metadata.scheduledFor ? `scheduled:${metadata.scheduledFor}` : undefined,
4981
- metadata.goalId ? `goal:${metadata.goalId}` : undefined,
4982
- metadata.goalNodeKey ? `node:${metadata.goalNodeKey}` : undefined
4983
- ].filter(Boolean);
4984
- if (parts.length > 1)
4985
- return parts.join(":");
4986
- return `openloops:adhoc:${randomBytes2(16).toString("hex")}`;
4987
- }
4988
4967
  function allowlistEnv(allowlist) {
4989
4968
  const env = {};
4990
4969
  if (allowlist?.tools?.length)
@@ -5039,8 +5018,7 @@ function commandSpec(target, opts) {
5039
5018
  preflightAnyOf: invocation.preflightAnyOf,
5040
5019
  stdin: invocation.stdin,
5041
5020
  allowlist: agentTarget.allowlist,
5042
- worktree: agentTarget.worktree,
5043
- codewithDurableAgent: adapter.capabilities.durable ? { target: agentTarget } : undefined
5021
+ worktree: agentTarget.worktree
5044
5022
  };
5045
5023
  }
5046
5024
  function composeExecutionEnv(spec, metadata, opts, accountEnv) {
@@ -5250,266 +5228,6 @@ async function preflightNativeAuthProfile(spec, env) {
5250
5228
  const result = await spawnCapture(spec.command, ["profile", "list"], { env, timeoutMs: 15000 });
5251
5229
  assertCodewithProfileListed(spec.nativeAuthProfile.profile, result);
5252
5230
  }
5253
- function codewithAgentStartArgs(target, idempotencyKey) {
5254
- const args = providerAdapter(target.provider).buildInvocation(target).args;
5255
- const startIndex = args.findIndex((arg, index) => arg === "start" && args[index - 1] === "agent");
5256
- if (startIndex === -1)
5257
- throw new Error("internal error: codewith durable agent args missing agent start");
5258
- args.splice(startIndex + 1, 0, "--idempotency-key", idempotencyKey);
5259
- return args;
5260
- }
5261
- function codewithAgentControlArgs(target, command, agentId) {
5262
- return [
5263
- ...target.authProfile ? ["--auth-profile", target.authProfile] : [],
5264
- "agent",
5265
- command,
5266
- ...command === "logs" ? ["--limit", "20"] : [],
5267
- "--json",
5268
- agentId
5269
- ];
5270
- }
5271
- function extractFirstJsonObject(text) {
5272
- let depth = 0;
5273
- let start = -1;
5274
- let inString = false;
5275
- let escaped = false;
5276
- for (let i = 0;i < text.length; i += 1) {
5277
- const ch = text[i];
5278
- if (inString) {
5279
- if (escaped)
5280
- escaped = false;
5281
- else if (ch === "\\")
5282
- escaped = true;
5283
- else if (ch === '"')
5284
- inString = false;
5285
- continue;
5286
- }
5287
- if (ch === '"') {
5288
- inString = true;
5289
- } else if (ch === "{") {
5290
- if (depth === 0)
5291
- start = i;
5292
- depth += 1;
5293
- } else if (ch === "}") {
5294
- if (depth > 0) {
5295
- depth -= 1;
5296
- if (depth === 0 && start !== -1)
5297
- return text.slice(start, i + 1);
5298
- }
5299
- }
5300
- }
5301
- return;
5302
- }
5303
- function parseJsonOutput(stdout, label) {
5304
- const raw = stdout || "{}";
5305
- const candidates = [raw.trim()];
5306
- const scanned = extractFirstJsonObject(raw);
5307
- if (scanned && scanned !== raw.trim())
5308
- candidates.push(scanned);
5309
- let lastError;
5310
- for (const candidate of candidates) {
5311
- try {
5312
- const value = JSON.parse(candidate);
5313
- if (!value || typeof value !== "object" || Array.isArray(value))
5314
- throw new Error("not an object");
5315
- return value;
5316
- } catch (error) {
5317
- lastError = error;
5318
- }
5319
- }
5320
- throw new Error(`${label} did not return JSON${lastError instanceof Error ? `: ${lastError.message}` : ""}`);
5321
- }
5322
- function recordField(value, key) {
5323
- if (!value || typeof value !== "object" || Array.isArray(value))
5324
- return;
5325
- const field = value[key];
5326
- return field && typeof field === "object" && !Array.isArray(field) ? field : undefined;
5327
- }
5328
- function stringField(value, key) {
5329
- const field = value?.[key];
5330
- return typeof field === "string" ? field : undefined;
5331
- }
5332
- function numberField(value, key) {
5333
- const field = value?.[key];
5334
- return typeof field === "number" && Number.isFinite(field) ? field : undefined;
5335
- }
5336
- function codewithAgentStatus(readJson) {
5337
- return stringField(recordField(readJson, "agent"), "status") ?? stringField(recordField(readJson, "statusSnapshot"), "status");
5338
- }
5339
- function codewithAgentLastEventSeq(readJson) {
5340
- const agentSeq = numberField(recordField(readJson, "agent"), "lastEventSeq");
5341
- const snapshotSeq = numberField(recordField(readJson, "statusSnapshot"), "lastEventSeq");
5342
- if (agentSeq !== undefined && snapshotSeq !== undefined)
5343
- return Math.max(agentSeq, snapshotSeq);
5344
- return agentSeq ?? snapshotSeq;
5345
- }
5346
- function codewithAgentEvidence(startJson, readJson, logsJson) {
5347
- const agent = recordField(readJson, "agent") ?? recordField(startJson, "agent");
5348
- const statusSnapshot = recordField(readJson, "statusSnapshot");
5349
- const events = Array.isArray(logsJson?.data) ? logsJson.data.filter((event) => Boolean(event) && typeof event === "object" && !Array.isArray(event)).map((event) => ({
5350
- seq: numberField(event, "seq"),
5351
- eventType: stringField(event, "eventType"),
5352
- createdAt: numberField(event, "createdAt")
5353
- })) : undefined;
5354
- return JSON.stringify({
5355
- codewithAgent: {
5356
- agentId: stringField(agent, "agentId"),
5357
- status: stringField(agent, "status"),
5358
- desiredState: stringField(agent, "desiredState"),
5359
- statusReason: stringField(agent, "statusReason"),
5360
- threadId: stringField(agent, "threadId"),
5361
- rolloutPath: stringField(agent, "rolloutPath"),
5362
- pid: numberField(agent, "pid"),
5363
- exitCode: numberField(agent, "exitCode"),
5364
- created: typeof startJson.created === "boolean" ? startJson.created : undefined
5365
- },
5366
- statusSnapshot: statusSnapshot ? {
5367
- seq: numberField(statusSnapshot, "seq"),
5368
- status: stringField(statusSnapshot, "status"),
5369
- summary: stringField(statusSnapshot, "summary"),
5370
- pendingInteractionCount: numberField(statusSnapshot, "pendingInteractionCount"),
5371
- lastEventSeq: numberField(statusSnapshot, "lastEventSeq")
5372
- } : undefined,
5373
- events
5374
- }, null, 2);
5375
- }
5376
- function codewithAgentProgress(readJson) {
5377
- const agent = recordField(readJson, "agent");
5378
- const statusSnapshot = recordField(readJson, "statusSnapshot");
5379
- return {
5380
- provider: "codewith",
5381
- agentId: stringField(agent, "agentId"),
5382
- status: stringField(agent, "status") ?? stringField(statusSnapshot, "status"),
5383
- summary: stringField(statusSnapshot, "summary"),
5384
- statusReason: stringField(agent, "statusReason"),
5385
- threadId: stringField(agent, "threadId"),
5386
- rolloutPath: stringField(agent, "rolloutPath"),
5387
- pid: numberField(agent, "pid"),
5388
- lastEventSeq: codewithAgentLastEventSeq(readJson)
5389
- };
5390
- }
5391
- function sleep(ms) {
5392
- return new Promise((resolve3) => setTimeout(resolve3, ms));
5393
- }
5394
- async function executeCodewithDurableAgent(spec, metadata, opts, env, startedAt) {
5395
- const target = spec.codewithDurableAgent?.target;
5396
- if (!target)
5397
- throw new Error("internal error: missing codewith durable target");
5398
- const maxOutputBytes = opts.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
5399
- const idempotencyKey = codewithAgentIdempotencyKey(metadata);
5400
- const start = await spawnCapture(spec.command, codewithAgentStartArgs(target, idempotencyKey), {
5401
- cwd: spec.cwd,
5402
- env,
5403
- timeoutMs: 30000,
5404
- maxOutputBytes
5405
- });
5406
- const stderr = new BoundedOutputBuffer(maxOutputBytes);
5407
- stderr.append(start.stderr);
5408
- if (start.error || (start.status ?? 1) !== 0) {
5409
- return failureResult(startedAt, start.error ?? `codewith agent start exited with code ${start.status ?? "unknown"}`, {
5410
- exitCode: start.status ?? undefined,
5411
- stdout: start.stdout,
5412
- stderr: stderr.value()
5413
- });
5414
- }
5415
- let startJson;
5416
- try {
5417
- startJson = parseJsonOutput(start.stdout || "{}", "codewith agent start");
5418
- } catch (error) {
5419
- return failureResult(startedAt, error instanceof Error ? error.message : String(error), { stdout: start.stdout, stderr: stderr.value() });
5420
- }
5421
- const agentId = stringField(recordField(startJson, "agent"), "agentId");
5422
- if (!agentId) {
5423
- return failureResult(startedAt, "codewith agent start did not return agent.agentId", { stdout: start.stdout, stderr: stderr.value() });
5424
- }
5425
- opts.onAgentProgress?.(codewithAgentProgress(startJson));
5426
- const stopAgent = async () => {
5427
- await spawnCapture(spec.command, codewithAgentControlArgs(target, "stop", agentId), {
5428
- cwd: spec.cwd,
5429
- env,
5430
- timeoutMs: 15000,
5431
- maxOutputBytes
5432
- });
5433
- };
5434
- const pollMs = Math.max(100, Number(opts.env?.LOOPS_CODEWITH_AGENT_POLL_MS ?? process.env.LOOPS_CODEWITH_AGENT_POLL_MS ?? 2000) || 2000);
5435
- let lastReadJson = startJson;
5436
- let lastLogsJson;
5437
- let lastFingerprint;
5438
- let lastProgressAt = Date.now();
5439
- const evidence = () => boundedText(codewithAgentEvidence(startJson, lastReadJson, lastLogsJson), maxOutputBytes);
5440
- while (true) {
5441
- if (opts.signal?.aborted) {
5442
- await stopAgent();
5443
- return failureResult(startedAt, "cancelled", { stdout: evidence(), stderr: stderr.value() });
5444
- }
5445
- const read = await spawnCapture(spec.command, codewithAgentControlArgs(target, "read", agentId), {
5446
- cwd: spec.cwd,
5447
- env,
5448
- timeoutMs: 30000,
5449
- maxOutputBytes
5450
- });
5451
- stderr.append(read.stderr);
5452
- if (read.error || (read.status ?? 1) !== 0) {
5453
- return failureResult(startedAt, read.error ?? `codewith agent read exited with code ${read.status ?? "unknown"}`, {
5454
- exitCode: read.status ?? undefined,
5455
- stdout: evidence(),
5456
- stderr: stderr.value()
5457
- });
5458
- }
5459
- try {
5460
- lastReadJson = parseJsonOutput(read.stdout || "{}", "codewith agent read");
5461
- } catch (error) {
5462
- return failureResult(startedAt, error instanceof Error ? error.message : String(error), {
5463
- stdout: boundedText(read.stdout, maxOutputBytes),
5464
- stderr: stderr.value()
5465
- });
5466
- }
5467
- const status = codewithAgentStatus(lastReadJson);
5468
- const fingerprint = JSON.stringify({
5469
- status,
5470
- agentLastEventSeq: numberField(recordField(lastReadJson, "agent"), "lastEventSeq"),
5471
- snapshot: recordField(lastReadJson, "statusSnapshot") ?? null
5472
- });
5473
- if (fingerprint !== lastFingerprint) {
5474
- lastFingerprint = fingerprint;
5475
- lastProgressAt = Date.now();
5476
- opts.onAgentProgress?.(codewithAgentProgress(lastReadJson));
5477
- }
5478
- if (status === "completed" || status === "failed" || status === "cancelled") {
5479
- const logs = await spawnCapture(spec.command, codewithAgentControlArgs(target, "logs", agentId), {
5480
- cwd: spec.cwd,
5481
- env,
5482
- timeoutMs: 30000,
5483
- maxOutputBytes
5484
- });
5485
- if (!logs.error && (logs.status ?? 1) === 0) {
5486
- try {
5487
- lastLogsJson = parseJsonOutput(logs.stdout || "{}", "codewith agent logs");
5488
- } catch {
5489
- lastLogsJson = undefined;
5490
- }
5491
- } else {
5492
- stderr.append(logs.stderr || logs.error || "");
5493
- }
5494
- if (status === "completed") {
5495
- return successResult(startedAt, { exitCode: 0, stdout: evidence(), stderr: stderr.value() });
5496
- }
5497
- return failureResult(startedAt, stringField(recordField(lastReadJson, "agent"), "statusReason") ?? `codewith agent ${status}`, { exitCode: 1, stdout: evidence(), stderr: stderr.value() });
5498
- }
5499
- if (typeof spec.timeoutMs === "number" && new Date(nowIso()).getTime() - new Date(startedAt).getTime() >= spec.timeoutMs) {
5500
- await stopAgent();
5501
- return timeoutResult(startedAt, `timed out after ${spec.timeoutMs}ms`, { stdout: evidence(), stderr: stderr.value() });
5502
- }
5503
- if (typeof spec.idleTimeoutMs === "number" && Date.now() - lastProgressAt >= spec.idleTimeoutMs) {
5504
- await stopAgent();
5505
- return timeoutResult(startedAt, `idle timed out after ${spec.idleTimeoutMs}ms without agent progress`, {
5506
- stdout: evidence(),
5507
- stderr: stderr.value()
5508
- });
5509
- }
5510
- await sleep(pollMs);
5511
- }
5512
- }
5513
5231
  function resolvedDirEquals(left, right) {
5514
5232
  try {
5515
5233
  return realpathSync(left) === realpathSync(right);
@@ -5743,9 +5461,6 @@ function preflightTarget(target, metadata = {}, opts = {}) {
5743
5461
  async function executeTarget(target, metadata = {}, opts = {}) {
5744
5462
  let spec = commandSpec(target, opts);
5745
5463
  const machine = resolvedMachine(opts);
5746
- if (machine && !machine.local && spec.codewithDurableAgent) {
5747
- return failureResult(nowIso(), "remote Codewith durable background-agent steps require remote status polling support; run this Codewith step locally or add remote durable readback before dispatch");
5748
- }
5749
5464
  if (machine && !machine.local) {
5750
5465
  const remoteFallbackSpec = spec.worktree?.enabled && spec.worktree.mode === "auto" ? worktreeFallbackSpec(target, opts, spec.worktree.originalCwd) : undefined;
5751
5466
  return executeRemoteSpec(spec, machine, metadata, opts, remoteFallbackSpec);
@@ -5776,9 +5491,6 @@ async function executeTarget(target, metadata = {}, opts = {}) {
5776
5491
  if (worktreeEntry?.fallbackCwd) {
5777
5492
  spec = worktreeFallbackSpec(target, opts, worktreeEntry.fallbackCwd) ?? spec;
5778
5493
  }
5779
- if (spec.codewithDurableAgent) {
5780
- return executeCodewithDurableAgent(spec, metadata, opts, env, startedAt);
5781
- }
5782
5494
  const child = spawn2(spec.command, spec.args, {
5783
5495
  cwd: spec.cwd,
5784
5496
  env,
@@ -7676,7 +7388,7 @@ function realSleep(ms) {
7676
7388
  return new Promise((resolve3) => setTimeout(resolve3, ms));
7677
7389
  }
7678
7390
  async function runLoop(opts) {
7679
- const sleep2 = opts.sleep ?? realSleep;
7391
+ const sleep = opts.sleep ?? realSleep;
7680
7392
  const sliceMs = opts.sliceMs ?? 200;
7681
7393
  while (!opts.shouldStop()) {
7682
7394
  try {
@@ -7687,7 +7399,7 @@ async function runLoop(opts) {
7687
7399
  let waited = 0;
7688
7400
  while (waited < opts.intervalMs && !opts.shouldStop()) {
7689
7401
  const chunk = Math.min(sliceMs, opts.intervalMs - waited);
7690
- await sleep2(chunk);
7402
+ await sleep(chunk);
7691
7403
  waited += chunk;
7692
7404
  }
7693
7405
  }
@@ -7779,7 +7491,7 @@ function processExists(pid) {
7779
7491
  }
7780
7492
  }
7781
7493
  async function reapProcessGroups(entries, opts = {}) {
7782
- const sleep2 = opts.sleep ?? realSleep;
7494
+ const sleep = opts.sleep ?? realSleep;
7783
7495
  const graceMs = Math.max(100, opts.graceMs ?? 2000);
7784
7496
  const ownPgid = ownProcessGroupId();
7785
7497
  const targets = new Map;
@@ -7808,7 +7520,7 @@ async function reapProcessGroups(entries, opts = {}) {
7808
7520
  const steps = Math.max(1, Math.ceil(graceMs / 100));
7809
7521
  let remaining = live;
7810
7522
  for (let i = 0;i < steps && remaining.length > 0; i++) {
7811
- await sleep2(100);
7523
+ await sleep(100);
7812
7524
  remaining = remaining.filter((target) => signalReapTarget(target, 0));
7813
7525
  }
7814
7526
  for (const target of remaining) {
@@ -7850,7 +7562,7 @@ async function stopDaemon(opts = {}) {
7850
7562
  }
7851
7563
  if (!state.running || !state.pid)
7852
7564
  return { wasRunning: false, stopped: false, forced: false };
7853
- const sleep2 = opts.sleep ?? realSleep;
7565
+ const sleep = opts.sleep ?? realSleep;
7854
7566
  const store = new Store(join5(dirname4(path), "loops.db"));
7855
7567
  try {
7856
7568
  const lease = store.getDaemonLease();
@@ -7863,7 +7575,7 @@ async function stopDaemon(opts = {}) {
7863
7575
  }
7864
7576
  const steps = Math.max(1, Math.ceil((opts.timeoutMs ?? 6000) / 100));
7865
7577
  for (let i = 0;i < steps; i++) {
7866
- await sleep2(100);
7578
+ await sleep(100);
7867
7579
  if (!isAlive(state.pid, record?.startedAt)) {
7868
7580
  removePid(path);
7869
7581
  return { wasRunning: true, stopped: true, forced: false, pid: state.pid };
@@ -7872,13 +7584,13 @@ async function stopDaemon(opts = {}) {
7872
7584
  try {
7873
7585
  process.kill(state.pid, "SIGKILL");
7874
7586
  } catch {}
7875
- await sleep2(150);
7587
+ await sleep(150);
7876
7588
  removePid(path);
7877
7589
  let reapedPgids = [];
7878
7590
  if (leaseVerified && lease) {
7879
7591
  const ownedRuns = store.listRuns({ status: "running", limit: 1000 }).filter((run) => run.claimedBy !== undefined && run.claimedBy.endsWith(`:${lease.id}`));
7880
7592
  reapedPgids = await reapProcessGroups(ownedRuns.map(toReapableProcess), {
7881
- sleep: sleep2,
7593
+ sleep,
7882
7594
  graceMs: opts.reapGraceMs
7883
7595
  });
7884
7596
  }
@@ -7953,7 +7665,7 @@ async function runDaemon(opts = {}) {
7953
7665
  const leaseTtlMs = opts.leaseTtlMs ?? Math.max(60000, intervalMs * 10);
7954
7666
  const concurrency = Math.max(1, opts.concurrency ?? concurrencyFromEnv() ?? 4);
7955
7667
  const log = opts.log ?? defaultDaemonLog;
7956
- const sleep2 = opts.sleep ?? realSleep;
7668
+ const sleep = opts.sleep ?? realSleep;
7957
7669
  const lease = store.acquireDaemonLease({
7958
7670
  id: leaseId,
7959
7671
  pid: process.pid,
@@ -8017,7 +7729,7 @@ async function runDaemon(opts = {}) {
8017
7729
  const entries = recovered.map(toReapableProcess).filter((entry) => entry.pid !== undefined || entry.pgid !== undefined);
8018
7730
  if (entries.length === 0)
8019
7731
  return;
8020
- const killed = await reapProcessGroups(entries, { sleep: sleep2, graceMs: opts.reapGraceMs, log });
7732
+ const killed = await reapProcessGroups(entries, { sleep, graceMs: opts.reapGraceMs, log });
8021
7733
  if (killed.length > 0)
8022
7734
  log(`reaped orphan process groups: ${killed.join(", ")}`);
8023
7735
  };
@@ -8092,7 +7804,7 @@ async function runDaemon(opts = {}) {
8092
7804
  }
8093
7805
  await runLoop({
8094
7806
  intervalMs,
8095
- sleep: sleep2,
7807
+ sleep,
8096
7808
  shouldStop: opts.shouldStop ?? (() => stopFlag),
8097
7809
  onTickError: (err) => log(`tick error: ${err instanceof Error ? err.message : String(err)}`),
8098
7810
  tickFn: async () => {
@@ -8142,10 +7854,10 @@ async function startDaemon(opts) {
8142
7854
  stdio: ["ignore", out, out]
8143
7855
  });
8144
7856
  child.unref();
8145
- const sleep2 = opts.sleep ?? realSleep;
7857
+ const sleep = opts.sleep ?? realSleep;
8146
7858
  const deadline = Math.max(1, Math.ceil((opts.waitMs ?? 4000) / 100));
8147
7859
  for (let i = 0;i < deadline; i++) {
8148
- await sleep2(100);
7860
+ await sleep(100);
8149
7861
  const current = isDaemonRunning();
8150
7862
  if (current.running)
8151
7863
  return { started: true, alreadyRunning: false, pid: current.pid };
@@ -10505,9 +10217,8 @@ function renderTaskLifecycleWorkflow(input) {
10505
10217
  `) : "";
10506
10218
  const shared = [
10507
10219
  worktreePrompt(plan),
10508
- ...todosExactCommandsFragment(todosProjectPath, input.taskId, [
10509
- todosEvidenceLine(todosProjectPath, input.taskId, "concise evidence, decision, or blocker")
10510
- ]),
10220
+ ...todosExactCommandsFragment(todosProjectPath, input.taskId, []),
10221
+ "Use concrete task-specific text in lifecycle comments. Do not copy placeholder text into lifecycle comments; triage and planner comments must start with the exact stage marker when advancing or blocking the workflow.",
10511
10222
  NO_TMUX_DISPATCH_FRAGMENT,
10512
10223
  "Preserve unrelated user changes and keep scope tied to the task acceptance criteria.",
10513
10224
  prReviewFollowUpFragment(input),
@@ -10518,6 +10229,8 @@ function renderTaskLifecycleWorkflow(input) {
10518
10229
  `);
10519
10230
  const gateMarker = (stage, state) => `openloops:${stage}=${state} task=${input.taskId}${input.eventId ? ` event=${input.eventId}` : ""}`;
10520
10231
  const blockTaskCommand = `todos --project ${todosProjectPath} update ${input.taskId} --status blocked`;
10232
+ const markerCommentCommand = (stage, state, evidencePlaceholder) => `todos --project ${todosProjectPath} comment ${input.taskId} "${gateMarker(stage, state)}
10233
+ <${evidencePlaceholder}>"`;
10521
10234
  const gateStopFragment = (stage, stops) => `The deterministic ${stage} gate will stop ${stops} unless the latest ${stage} marker is the exact go marker and the task has no blocked/completed/done/cancelled/failed/archived/no-auto/manual/approval-required state.`;
10522
10235
  const triagePrompt = [
10523
10236
  ...boundedStepHeaderFragment(`Triage todos task ${input.taskId} for safe automated execution.`, "triage", "lifecycle"),
@@ -10525,9 +10238,12 @@ function renderTaskLifecycleWorkflow(input) {
10525
10238
  "Decide whether the task is eligible for loop execution. Check status, dependencies, duplicate tasks, no-auto/manual/approval metadata, project path, acceptance criteria, and whether the requested work should be split before implementation.",
10526
10239
  "Do not implement repo changes in this step.",
10527
10240
  `If the task is eligible for automated planning, add a task comment whose first line is exactly: ${gateMarker("triage", "go")}`,
10528
- "Include the triage decision, duplicates/dependencies found, and any follow-up tasks created in that same comment.",
10241
+ `Use this copy-safe marker comment command for triage go: ${markerCommentCommand("triage", "go", "task-specific triage evidence")}`,
10242
+ "Do not run a separate generic evidence comment before the marker; include the triage decision, duplicates/dependencies found, and any follow-up tasks created in that same marker comment.",
10529
10243
  `If the task should not proceed automatically, run: ${blockTaskCommand}`,
10530
10244
  `Then add a task comment whose first line is exactly: ${gateMarker("triage", "blocked")}`,
10245
+ `Use this copy-safe marker comment command for triage blocked: ${markerCommentCommand("triage", "blocked", "task-specific triage evidence")}`,
10246
+ "Do not run a separate generic blocker comment before the marker; include the blocker evidence in that same marker comment.",
10531
10247
  gateStopFragment("triage", "later steps")
10532
10248
  ].join(`
10533
10249
  `);
@@ -10536,17 +10252,19 @@ function renderTaskLifecycleWorkflow(input) {
10536
10252
  shared,
10537
10253
  "Read the triage comment and current task details.",
10538
10254
  `If the task is ready for implementation, add a task comment whose first line is exactly: ${gateMarker("planner", "go")}`,
10539
- "In that same comment, include a concise implementation plan: files/areas to inspect, validation commands, risk checks, expected commit/PR behavior, and any cross-repo tasks that should be created separately.",
10255
+ `Use this copy-safe marker comment command for planner go: ${markerCommentCommand("planner", "go", "task-specific plan/evidence")}`,
10256
+ "Do not run a separate generic evidence comment before the marker; in that same marker comment, include a concise implementation plan: files/areas to inspect, validation commands, risk checks, expected commit/PR behavior, and any cross-repo tasks that should be created separately.",
10540
10257
  `Do not implement repo changes in this step. If the task is too broad or unsafe for automation, run: ${blockTaskCommand}`,
10541
10258
  `Then add a task comment whose first line is exactly: ${gateMarker("planner", "blocked")}`,
10542
- `Create smaller deduped tasks and record evidence. ${gateStopFragment("planner", "the worker")}`
10259
+ `Use this copy-safe marker comment command for planner blocked: ${markerCommentCommand("planner", "blocked", "task-specific plan/evidence")}`,
10260
+ `Do not run a separate generic blocker comment before the marker; create smaller deduped tasks and record blocker evidence in that same marker comment. ${gateStopFragment("planner", "the worker")}`
10543
10261
  ].join(`
10544
10262
  `);
10545
10263
  const workerPrompt = [
10546
10264
  ...boundedStepHeaderFragment(`Complete todos task ${input.taskId} according to the planner evidence.`, "worker", "lifecycle"),
10547
10265
  shared,
10548
10266
  todosStartLine(todosProjectPath, input.taskId),
10549
- "Read the triage and planner comments first. Implement only the scoped task, run focused validation, and record changed files, commits, evidence, blockers, and residual risks.",
10267
+ "Read the triage and planner comments first. Implement only the scoped task, run focused validation, and record concrete worker evidence in todos: changed files, commits, validation results, blockers, and residual risks.",
10550
10268
  input.prHandoff ? `When only GitHub network access is blocked after a successful commit/validation, record the handoff artifact at ${handoffArtifactPath} instead of repeatedly retrying push/PR creation.` : undefined,
10551
10269
  WORKER_LEAVES_COMPLETION_FRAGMENT
10552
10270
  ].filter(Boolean).join(`
@@ -10554,7 +10272,7 @@ function renderTaskLifecycleWorkflow(input) {
10554
10272
  const verifierPrompt = [
10555
10273
  ...boundedStepHeaderFragment(`Verify todos task ${input.taskId} after the full lifecycle worker step.`, "verifier", "lifecycle"),
10556
10274
  shared,
10557
- todosVerificationLine(todosProjectPath, input.taskId),
10275
+ "Before completion, record concrete verification evidence in todos with changed files, validation results, findings, and the task decision.",
10558
10276
  todosDoneLine(todosProjectPath, input.taskId),
10559
10277
  adversarialReviewFragment("triage, plan, worker evidence, repo state, commits, tests, and acceptance criteria", TASK_REVIEW_FOCUS),
10560
10278
  verifierRuntimeGuidance(input),
@@ -11072,7 +10790,7 @@ function eventMetadata(event) {
11072
10790
  return metadata;
11073
10791
  return {};
11074
10792
  }
11075
- function stringField2(value) {
10793
+ function stringField(value) {
11076
10794
  return typeof value === "string" && value.trim() ? value.trim() : undefined;
11077
10795
  }
11078
10796
  function slugSegment2(value, fallback = "event") {
@@ -11087,14 +10805,14 @@ function stableHash(parts) {
11087
10805
  }
11088
10806
  function taskEventField(data, keys) {
11089
10807
  for (const key of keys) {
11090
- const direct = stringField2(data[key]);
10808
+ const direct = stringField(data[key]);
11091
10809
  if (direct)
11092
10810
  return direct;
11093
10811
  }
11094
10812
  const task = data.task;
11095
10813
  if (task && typeof task === "object" && !Array.isArray(task)) {
11096
10814
  for (const key of keys) {
11097
- const direct = stringField2(task[key]);
10815
+ const direct = stringField(task[key]);
11098
10816
  if (direct)
11099
10817
  return direct;
11100
10818
  }
@@ -11102,7 +10820,7 @@ function taskEventField(data, keys) {
11102
10820
  const payload = data.payload;
11103
10821
  if (payload && typeof payload === "object" && !Array.isArray(payload)) {
11104
10822
  for (const key of keys) {
11105
- const direct = stringField2(payload[key]);
10823
+ const direct = stringField(payload[key]);
11106
10824
  if (direct)
11107
10825
  return direct;
11108
10826
  }
@@ -11606,6 +11324,7 @@ function permissionModeFromOpts(opts, provider) {
11606
11324
  return mode;
11607
11325
  }
11608
11326
  // src/lib/route/pr-review.ts
11327
+ import { spawnSync as spawnSync7 } from "child_process";
11609
11328
  var PR_AUTHOR_FIELDS = [
11610
11329
  "github_author",
11611
11330
  "githubAuthor",
@@ -11720,6 +11439,21 @@ function routeTextFieldValues(record, field) {
11720
11439
  }
11721
11440
  return values;
11722
11441
  }
11442
+ function prReferenceFrom(text) {
11443
+ const url = /github\.com\/([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)\/pull\/(\d+)/i.exec(text);
11444
+ if (url)
11445
+ return { owner: url[1], repo: url[2], number: Number(url[3]) };
11446
+ const short = /github-pr:([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)#(\d+)/i.exec(text);
11447
+ if (short)
11448
+ return { owner: short[1], repo: short[2], number: Number(short[3]) };
11449
+ return;
11450
+ }
11451
+ function ghAuthorResolver(ref) {
11452
+ const result = spawnSync7("gh", ["pr", "view", String(ref.number), "--repo", `${ref.owner}/${ref.repo}`, "--json", "author", "-q", ".author.login"], { encoding: "utf8", timeout: 20000 });
11453
+ if (result.error || result.status !== 0)
11454
+ return;
11455
+ return githubLogin((result.stdout ?? "").trim());
11456
+ }
11723
11457
  function authorFromPrText(text) {
11724
11458
  const patterns = [
11725
11459
  /\bauthor\s+(?:is\s+also|is|=|:)\s+@?([A-Za-z0-9](?:[A-Za-z0-9-]{0,38}))/i,
@@ -11746,7 +11480,7 @@ function reviewersFromPrText(text) {
11746
11480
  }
11747
11481
  return githubLogins(reviewers);
11748
11482
  }
11749
- function prReviewRoutingDecision(data, metadata, opts) {
11483
+ function prReviewRoutingDecision(data, metadata, opts, resolveAuthor = ghAuthorResolver) {
11750
11484
  const records = [...taskEventRecords(data, metadata), ...automationRecords(data, metadata)];
11751
11485
  const text = routeEvidenceText(records);
11752
11486
  const signals = [];
@@ -11766,7 +11500,6 @@ function prReviewRoutingDecision(data, metadata, opts) {
11766
11500
  const required = hasPrReference && (reviewRequiredByField || reviewRequiredByText || mergeBlockedByText || approvalIntent);
11767
11501
  if (!required)
11768
11502
  return { required: false, allowed: true, reviewers: [], signals };
11769
- const author = githubLogin(firstRouteField(records, PR_AUTHOR_FIELDS)) ?? authorFromPrText(text);
11770
11503
  const reviewers = githubLogins([
11771
11504
  opts.githubReviewer,
11772
11505
  ...splitList(opts.githubReviewerPool) ?? [],
@@ -11774,6 +11507,17 @@ function prReviewRoutingDecision(data, metadata, opts) {
11774
11507
  ...PR_REVIEWER_POOL_FIELDS.flatMap((field) => routeFieldValues(records, field)),
11775
11508
  ...reviewersFromPrText(text)
11776
11509
  ]);
11510
+ let author = githubLogin(firstRouteField(records, PR_AUTHOR_FIELDS)) ?? authorFromPrText(text);
11511
+ if (!author) {
11512
+ const ref = prReferenceFrom(text);
11513
+ if (ref) {
11514
+ const derived = githubLogin(resolveAuthor(ref));
11515
+ if (derived) {
11516
+ author = derived;
11517
+ signals.push("author-derived-gh");
11518
+ }
11519
+ }
11520
+ }
11777
11521
  const selectedReviewer = author ? reviewers.find((reviewer) => reviewer.toLowerCase() !== author.toLowerCase()) : undefined;
11778
11522
  if (!author) {
11779
11523
  return {
@@ -11815,7 +11559,7 @@ function prReviewRoutingDecision(data, metadata, opts) {
11815
11559
  }
11816
11560
  // src/lib/route/throttle.ts
11817
11561
  import { realpathSync as realpathSync2 } from "fs";
11818
- import { spawnSync as spawnSync7 } from "child_process";
11562
+ import { spawnSync as spawnSync8 } from "child_process";
11819
11563
  import { resolve as resolve6 } from "path";
11820
11564
  function routeThrottleLimitsFromOpts(opts) {
11821
11565
  return {
@@ -11837,7 +11581,7 @@ function normalizeRoutePath(value) {
11837
11581
  } catch {
11838
11582
  return canonical;
11839
11583
  }
11840
- const gitRoot = spawnSync7("git", ["-C", canonical, "rev-parse", "--show-toplevel"], { encoding: "utf8" });
11584
+ const gitRoot = spawnSync8("git", ["-C", canonical, "rev-parse", "--show-toplevel"], { encoding: "utf8" });
11841
11585
  if (gitRoot.status === 0 && gitRoot.stdout.trim()) {
11842
11586
  try {
11843
11587
  return realpathSync2(gitRoot.stdout.trim());
@@ -11887,7 +11631,7 @@ function routeThrottleDryRunPreview(args) {
11887
11631
  };
11888
11632
  }
11889
11633
  function isExistingGitProjectPath(path) {
11890
- const result = spawnSync7("git", ["-C", path, "rev-parse", "--is-inside-work-tree"], { encoding: "utf8" });
11634
+ const result = spawnSync8("git", ["-C", path, "rev-parse", "--is-inside-work-tree"], { encoding: "utf8" });
11891
11635
  return result.status === 0;
11892
11636
  }
11893
11637
  function validateRequiredRouteWorktreeProjectPath(opts, projectPath) {
@@ -11979,11 +11723,11 @@ async function readEventEnvelopeInput(opts = {}) {
11979
11723
  const event = JSON.parse(raw);
11980
11724
  if (!event || typeof event !== "object" || Array.isArray(event))
11981
11725
  throw new ValidationError("event JSON must be an object");
11982
- if (!stringField2(event.id))
11726
+ if (!stringField(event.id))
11983
11727
  throw new ValidationError("event.id is required");
11984
- if (!stringField2(event.type))
11728
+ if (!stringField(event.type))
11985
11729
  throw new ValidationError("event.type is required");
11986
- if (!stringField2(event.source))
11730
+ if (!stringField(event.source))
11987
11731
  throw new ValidationError("event.source is required");
11988
11732
  return event;
11989
11733
  }
@@ -12354,8 +12098,8 @@ function routeGenericEvent(event, opts) {
12354
12098
  eventId: event.id,
12355
12099
  eventType: event.type,
12356
12100
  eventSource: event.source,
12357
- eventSubject: stringField2(event.subject),
12358
- eventMessage: stringField2(event.message),
12101
+ eventSubject: stringField(event.subject),
12102
+ eventMessage: stringField(event.message),
12359
12103
  eventJson: JSON.stringify(event),
12360
12104
  projectPath,
12361
12105
  routeProjectPath,
@@ -12396,9 +12140,9 @@ function routeGenericEvent(event, opts) {
12396
12140
  },
12397
12141
  subjectRef: {
12398
12142
  kind: "event",
12399
- id: stringField2(event.subject) ?? event.id,
12143
+ id: stringField(event.subject) ?? event.id,
12400
12144
  path: routeProjectPath,
12401
- raw: { message: stringField2(event.message) }
12145
+ raw: { message: stringField(event.message) }
12402
12146
  },
12403
12147
  intent: "route",
12404
12148
  scope: {
@@ -12427,7 +12171,7 @@ function routeGenericEvent(event, opts) {
12427
12171
  invocationInput,
12428
12172
  routeProjectPath,
12429
12173
  projectGroup,
12430
- subjectRef: stringField2(event.subject) ?? event.id,
12174
+ subjectRef: stringField(event.subject) ?? event.id,
12431
12175
  loopName,
12432
12176
  loopDescription: `Run ${workflowBody.name} once for event ${event.id}; idempotency=${idempotencyKey}`,
12433
12177
  throttleLimits,
@@ -12449,14 +12193,14 @@ import { resolve as resolve8 } from "path";
12449
12193
 
12450
12194
  // src/lib/route/todos-cli.ts
12451
12195
  import { closeSync, mkdtempSync as mkdtempSync2, openSync as openSync2, readFileSync as readFileSync9, rmSync as rmSync6 } from "fs";
12452
- import { spawnSync as spawnSync8 } from "child_process";
12196
+ import { spawnSync as spawnSync9 } from "child_process";
12453
12197
  import { join as join10 } from "path";
12454
12198
  import { homedir as homedir5, tmpdir as tmpdir2 } from "os";
12455
12199
  function defaultLoopsProject() {
12456
12200
  return process.env.LOOPS_TASK_PROJECT || process.env.LOOPS_DATA_DIR || `${process.env.HOME || homedir5()}/.hasna/loops`;
12457
12201
  }
12458
12202
  function runLocalCommand(command, args, opts = {}) {
12459
- const result = spawnSync8(command, args, {
12203
+ const result = spawnSync9(command, args, {
12460
12204
  input: opts.input,
12461
12205
  encoding: "utf8",
12462
12206
  timeout: opts.timeoutMs ?? 30000,
@@ -12477,7 +12221,7 @@ function runLocalCommandWithStdoutFile(command, args, opts = {}) {
12477
12221
  const stdoutFd = openSync2(stdoutPath, "w");
12478
12222
  let result;
12479
12223
  try {
12480
- result = spawnSync8(command, args, {
12224
+ result = spawnSync9(command, args, {
12481
12225
  input: opts.input,
12482
12226
  encoding: "utf8",
12483
12227
  timeout: opts.timeoutMs ?? 30000,
@@ -12522,7 +12266,7 @@ function todosMutationSummary(result) {
12522
12266
  // src/lib/route/drain.ts
12523
12267
  function taskField(task, keys) {
12524
12268
  for (const key of keys) {
12525
- const value = stringField2(task[key]);
12269
+ const value = stringField(task[key]);
12526
12270
  if (value)
12527
12271
  return value;
12528
12272
  }
@@ -12547,9 +12291,9 @@ function taskListValues(task) {
12547
12291
  const taskList = objectField2(task.task_list);
12548
12292
  return [
12549
12293
  taskField(task, ["task_list_id", "taskListId"]),
12550
- stringField2(task.task_list?.id),
12551
- stringField2(task.task_list?.slug),
12552
- stringField2(taskList?.name),
12294
+ stringField(task.task_list?.id),
12295
+ stringField(task.task_list?.slug),
12296
+ stringField(taskList?.name),
12553
12297
  taskField(task, ["task_list", "taskList"])
12554
12298
  ].filter((value) => Boolean(value));
12555
12299
  }
@@ -12623,12 +12367,12 @@ function compactDrainResult(result) {
12623
12367
  kind: result.kind,
12624
12368
  taskId: event?.subject,
12625
12369
  eventId: event?.id,
12626
- idempotencyKey: stringField2(value.idempotencyKey),
12627
- reason: stringField2(value.reason) ?? throttle?.reason,
12628
- loopId: stringField2(loop?.id),
12629
- loopName: stringField2(loop?.name),
12630
- workflowId: stringField2(workflow?.id),
12631
- workflowName: stringField2(workflow?.name),
12370
+ idempotencyKey: stringField(value.idempotencyKey),
12371
+ reason: stringField(value.reason) ?? throttle?.reason,
12372
+ loopId: stringField(loop?.id),
12373
+ loopName: stringField(loop?.name),
12374
+ workflowId: stringField(workflow?.id),
12375
+ workflowName: stringField(workflow?.name),
12632
12376
  providerRouting,
12633
12377
  requeue,
12634
12378
  queuedAtSource: value.queuedAtSource,
@@ -12662,7 +12406,7 @@ function loadTodoProjectPathsFromRegistry(opts) {
12662
12406
  const includeFilters = listFromRepeatedOpts(opts.todosProjectInclude)?.map((entry) => normalizeRoutePath(entry) ?? resolve8(entry)) ?? [];
12663
12407
  const includesPrefix = normalizeRoutePath(opts.projectPathPrefix) ?? (opts.projectPathPrefix ? resolve8(opts.projectPathPrefix) : undefined);
12664
12408
  const fromProjects = projectsPayload.map((entry) => objectField2(entry)).filter((project) => Boolean(project)).map((project) => {
12665
- const path = stringField2(project.path) ?? stringField2(project.projectPath) ?? stringField2(project.project_path) ?? stringField2(project.dir) ?? stringField2(project.root) ?? stringField2(project.cwd);
12409
+ const path = stringField(project.path) ?? stringField(project.projectPath) ?? stringField(project.project_path) ?? stringField(project.dir) ?? stringField(project.root) ?? stringField(project.cwd);
12666
12410
  if (!path)
12667
12411
  return;
12668
12412
  return { path };
@@ -14057,7 +13801,7 @@ routes.command("show <id>").description("show one admission work item").action(r
14057
13801
  routes.command("requeue <id>").description("requeue a terminal admission work item for the next task/event delivery").option("--reason <text>", "operator reason recorded on the work item").action(runAction((id, opts) => {
14058
13802
  const store = new Store;
14059
13803
  try {
14060
- const reason = stringField2(opts.reason);
13804
+ const reason = stringField(opts.reason);
14061
13805
  if (!reason)
14062
13806
  throw new ValidationError("routes requeue requires --reason <text>");
14063
13807
  const item = store.requeueWorkflowWorkItem(id, { reason });