@hasna/loops 0.4.8 → 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.8",
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 };
@@ -11078,7 +10790,7 @@ function eventMetadata(event) {
11078
10790
  return metadata;
11079
10791
  return {};
11080
10792
  }
11081
- function stringField2(value) {
10793
+ function stringField(value) {
11082
10794
  return typeof value === "string" && value.trim() ? value.trim() : undefined;
11083
10795
  }
11084
10796
  function slugSegment2(value, fallback = "event") {
@@ -11093,14 +10805,14 @@ function stableHash(parts) {
11093
10805
  }
11094
10806
  function taskEventField(data, keys) {
11095
10807
  for (const key of keys) {
11096
- const direct = stringField2(data[key]);
10808
+ const direct = stringField(data[key]);
11097
10809
  if (direct)
11098
10810
  return direct;
11099
10811
  }
11100
10812
  const task = data.task;
11101
10813
  if (task && typeof task === "object" && !Array.isArray(task)) {
11102
10814
  for (const key of keys) {
11103
- const direct = stringField2(task[key]);
10815
+ const direct = stringField(task[key]);
11104
10816
  if (direct)
11105
10817
  return direct;
11106
10818
  }
@@ -11108,7 +10820,7 @@ function taskEventField(data, keys) {
11108
10820
  const payload = data.payload;
11109
10821
  if (payload && typeof payload === "object" && !Array.isArray(payload)) {
11110
10822
  for (const key of keys) {
11111
- const direct = stringField2(payload[key]);
10823
+ const direct = stringField(payload[key]);
11112
10824
  if (direct)
11113
10825
  return direct;
11114
10826
  }
@@ -11612,6 +11324,7 @@ function permissionModeFromOpts(opts, provider) {
11612
11324
  return mode;
11613
11325
  }
11614
11326
  // src/lib/route/pr-review.ts
11327
+ import { spawnSync as spawnSync7 } from "child_process";
11615
11328
  var PR_AUTHOR_FIELDS = [
11616
11329
  "github_author",
11617
11330
  "githubAuthor",
@@ -11726,6 +11439,21 @@ function routeTextFieldValues(record, field) {
11726
11439
  }
11727
11440
  return values;
11728
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
+ }
11729
11457
  function authorFromPrText(text) {
11730
11458
  const patterns = [
11731
11459
  /\bauthor\s+(?:is\s+also|is|=|:)\s+@?([A-Za-z0-9](?:[A-Za-z0-9-]{0,38}))/i,
@@ -11752,7 +11480,7 @@ function reviewersFromPrText(text) {
11752
11480
  }
11753
11481
  return githubLogins(reviewers);
11754
11482
  }
11755
- function prReviewRoutingDecision(data, metadata, opts) {
11483
+ function prReviewRoutingDecision(data, metadata, opts, resolveAuthor = ghAuthorResolver) {
11756
11484
  const records = [...taskEventRecords(data, metadata), ...automationRecords(data, metadata)];
11757
11485
  const text = routeEvidenceText(records);
11758
11486
  const signals = [];
@@ -11772,7 +11500,6 @@ function prReviewRoutingDecision(data, metadata, opts) {
11772
11500
  const required = hasPrReference && (reviewRequiredByField || reviewRequiredByText || mergeBlockedByText || approvalIntent);
11773
11501
  if (!required)
11774
11502
  return { required: false, allowed: true, reviewers: [], signals };
11775
- const author = githubLogin(firstRouteField(records, PR_AUTHOR_FIELDS)) ?? authorFromPrText(text);
11776
11503
  const reviewers = githubLogins([
11777
11504
  opts.githubReviewer,
11778
11505
  ...splitList(opts.githubReviewerPool) ?? [],
@@ -11780,6 +11507,17 @@ function prReviewRoutingDecision(data, metadata, opts) {
11780
11507
  ...PR_REVIEWER_POOL_FIELDS.flatMap((field) => routeFieldValues(records, field)),
11781
11508
  ...reviewersFromPrText(text)
11782
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
+ }
11783
11521
  const selectedReviewer = author ? reviewers.find((reviewer) => reviewer.toLowerCase() !== author.toLowerCase()) : undefined;
11784
11522
  if (!author) {
11785
11523
  return {
@@ -11821,7 +11559,7 @@ function prReviewRoutingDecision(data, metadata, opts) {
11821
11559
  }
11822
11560
  // src/lib/route/throttle.ts
11823
11561
  import { realpathSync as realpathSync2 } from "fs";
11824
- import { spawnSync as spawnSync7 } from "child_process";
11562
+ import { spawnSync as spawnSync8 } from "child_process";
11825
11563
  import { resolve as resolve6 } from "path";
11826
11564
  function routeThrottleLimitsFromOpts(opts) {
11827
11565
  return {
@@ -11843,7 +11581,7 @@ function normalizeRoutePath(value) {
11843
11581
  } catch {
11844
11582
  return canonical;
11845
11583
  }
11846
- 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" });
11847
11585
  if (gitRoot.status === 0 && gitRoot.stdout.trim()) {
11848
11586
  try {
11849
11587
  return realpathSync2(gitRoot.stdout.trim());
@@ -11893,7 +11631,7 @@ function routeThrottleDryRunPreview(args) {
11893
11631
  };
11894
11632
  }
11895
11633
  function isExistingGitProjectPath(path) {
11896
- 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" });
11897
11635
  return result.status === 0;
11898
11636
  }
11899
11637
  function validateRequiredRouteWorktreeProjectPath(opts, projectPath) {
@@ -11985,11 +11723,11 @@ async function readEventEnvelopeInput(opts = {}) {
11985
11723
  const event = JSON.parse(raw);
11986
11724
  if (!event || typeof event !== "object" || Array.isArray(event))
11987
11725
  throw new ValidationError("event JSON must be an object");
11988
- if (!stringField2(event.id))
11726
+ if (!stringField(event.id))
11989
11727
  throw new ValidationError("event.id is required");
11990
- if (!stringField2(event.type))
11728
+ if (!stringField(event.type))
11991
11729
  throw new ValidationError("event.type is required");
11992
- if (!stringField2(event.source))
11730
+ if (!stringField(event.source))
11993
11731
  throw new ValidationError("event.source is required");
11994
11732
  return event;
11995
11733
  }
@@ -12360,8 +12098,8 @@ function routeGenericEvent(event, opts) {
12360
12098
  eventId: event.id,
12361
12099
  eventType: event.type,
12362
12100
  eventSource: event.source,
12363
- eventSubject: stringField2(event.subject),
12364
- eventMessage: stringField2(event.message),
12101
+ eventSubject: stringField(event.subject),
12102
+ eventMessage: stringField(event.message),
12365
12103
  eventJson: JSON.stringify(event),
12366
12104
  projectPath,
12367
12105
  routeProjectPath,
@@ -12402,9 +12140,9 @@ function routeGenericEvent(event, opts) {
12402
12140
  },
12403
12141
  subjectRef: {
12404
12142
  kind: "event",
12405
- id: stringField2(event.subject) ?? event.id,
12143
+ id: stringField(event.subject) ?? event.id,
12406
12144
  path: routeProjectPath,
12407
- raw: { message: stringField2(event.message) }
12145
+ raw: { message: stringField(event.message) }
12408
12146
  },
12409
12147
  intent: "route",
12410
12148
  scope: {
@@ -12433,7 +12171,7 @@ function routeGenericEvent(event, opts) {
12433
12171
  invocationInput,
12434
12172
  routeProjectPath,
12435
12173
  projectGroup,
12436
- subjectRef: stringField2(event.subject) ?? event.id,
12174
+ subjectRef: stringField(event.subject) ?? event.id,
12437
12175
  loopName,
12438
12176
  loopDescription: `Run ${workflowBody.name} once for event ${event.id}; idempotency=${idempotencyKey}`,
12439
12177
  throttleLimits,
@@ -12455,14 +12193,14 @@ import { resolve as resolve8 } from "path";
12455
12193
 
12456
12194
  // src/lib/route/todos-cli.ts
12457
12195
  import { closeSync, mkdtempSync as mkdtempSync2, openSync as openSync2, readFileSync as readFileSync9, rmSync as rmSync6 } from "fs";
12458
- import { spawnSync as spawnSync8 } from "child_process";
12196
+ import { spawnSync as spawnSync9 } from "child_process";
12459
12197
  import { join as join10 } from "path";
12460
12198
  import { homedir as homedir5, tmpdir as tmpdir2 } from "os";
12461
12199
  function defaultLoopsProject() {
12462
12200
  return process.env.LOOPS_TASK_PROJECT || process.env.LOOPS_DATA_DIR || `${process.env.HOME || homedir5()}/.hasna/loops`;
12463
12201
  }
12464
12202
  function runLocalCommand(command, args, opts = {}) {
12465
- const result = spawnSync8(command, args, {
12203
+ const result = spawnSync9(command, args, {
12466
12204
  input: opts.input,
12467
12205
  encoding: "utf8",
12468
12206
  timeout: opts.timeoutMs ?? 30000,
@@ -12483,7 +12221,7 @@ function runLocalCommandWithStdoutFile(command, args, opts = {}) {
12483
12221
  const stdoutFd = openSync2(stdoutPath, "w");
12484
12222
  let result;
12485
12223
  try {
12486
- result = spawnSync8(command, args, {
12224
+ result = spawnSync9(command, args, {
12487
12225
  input: opts.input,
12488
12226
  encoding: "utf8",
12489
12227
  timeout: opts.timeoutMs ?? 30000,
@@ -12528,7 +12266,7 @@ function todosMutationSummary(result) {
12528
12266
  // src/lib/route/drain.ts
12529
12267
  function taskField(task, keys) {
12530
12268
  for (const key of keys) {
12531
- const value = stringField2(task[key]);
12269
+ const value = stringField(task[key]);
12532
12270
  if (value)
12533
12271
  return value;
12534
12272
  }
@@ -12553,9 +12291,9 @@ function taskListValues(task) {
12553
12291
  const taskList = objectField2(task.task_list);
12554
12292
  return [
12555
12293
  taskField(task, ["task_list_id", "taskListId"]),
12556
- stringField2(task.task_list?.id),
12557
- stringField2(task.task_list?.slug),
12558
- stringField2(taskList?.name),
12294
+ stringField(task.task_list?.id),
12295
+ stringField(task.task_list?.slug),
12296
+ stringField(taskList?.name),
12559
12297
  taskField(task, ["task_list", "taskList"])
12560
12298
  ].filter((value) => Boolean(value));
12561
12299
  }
@@ -12629,12 +12367,12 @@ function compactDrainResult(result) {
12629
12367
  kind: result.kind,
12630
12368
  taskId: event?.subject,
12631
12369
  eventId: event?.id,
12632
- idempotencyKey: stringField2(value.idempotencyKey),
12633
- reason: stringField2(value.reason) ?? throttle?.reason,
12634
- loopId: stringField2(loop?.id),
12635
- loopName: stringField2(loop?.name),
12636
- workflowId: stringField2(workflow?.id),
12637
- 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),
12638
12376
  providerRouting,
12639
12377
  requeue,
12640
12378
  queuedAtSource: value.queuedAtSource,
@@ -12668,7 +12406,7 @@ function loadTodoProjectPathsFromRegistry(opts) {
12668
12406
  const includeFilters = listFromRepeatedOpts(opts.todosProjectInclude)?.map((entry) => normalizeRoutePath(entry) ?? resolve8(entry)) ?? [];
12669
12407
  const includesPrefix = normalizeRoutePath(opts.projectPathPrefix) ?? (opts.projectPathPrefix ? resolve8(opts.projectPathPrefix) : undefined);
12670
12408
  const fromProjects = projectsPayload.map((entry) => objectField2(entry)).filter((project) => Boolean(project)).map((project) => {
12671
- 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);
12672
12410
  if (!path)
12673
12411
  return;
12674
12412
  return { path };
@@ -14063,7 +13801,7 @@ routes.command("show <id>").description("show one admission work item").action(r
14063
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) => {
14064
13802
  const store = new Store;
14065
13803
  try {
14066
- const reason = stringField2(opts.reason);
13804
+ const reason = stringField(opts.reason);
14067
13805
  if (!reason)
14068
13806
  throw new ValidationError("routes requeue requires --reason <text>");
14069
13807
  const item = store.requeueWorkflowWorkItem(id, { reason });