@hasna/loops 0.4.8 → 0.4.10

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/index.js CHANGED
@@ -54,8 +54,12 @@ function nowIso() {
54
54
  import { mkdirSync } from "fs";
55
55
  import { homedir } from "os";
56
56
  import { join } from "path";
57
+ function homeDir() {
58
+ const home = process.env.HOME?.trim();
59
+ return home ? home : homedir();
60
+ }
57
61
  function dataDir() {
58
- return process.env.LOOPS_DATA_DIR || join(homedir(), ".hasna", "loops");
62
+ return process.env.LOOPS_DATA_DIR || join(homeDir(), ".hasna", "loops");
59
63
  }
60
64
  function ensureDataDir() {
61
65
  const dir = dataDir();
@@ -72,10 +76,10 @@ function daemonLogPath() {
72
76
  return join(dataDir(), "daemon.log");
73
77
  }
74
78
  function systemdServicePath() {
75
- return join(homedir(), ".config", "systemd", "user", "loops-daemon.service");
79
+ return join(homeDir(), ".config", "systemd", "user", "loops-daemon.service");
76
80
  }
77
81
  function launchdPlistPath() {
78
- return join(homedir(), "Library", "LaunchAgents", "com.hasna.loops.daemon.plist");
82
+ return join(homeDir(), "Library", "LaunchAgents", "com.hasna.loops.daemon.plist");
79
83
  }
80
84
 
81
85
  // src/lib/process-identity.ts
@@ -579,7 +583,7 @@ import { isAbsolute, resolve } from "path";
579
583
 
580
584
  // src/lib/agent-adapter.ts
581
585
  import { spawn } from "child_process";
582
- var UNSAFE_CODEWITH_DURABLE_EXTRA_ARGS = new Set([
586
+ var UNSAFE_CODEWITH_EXEC_EXTRA_ARGS = new Set([
583
587
  "e",
584
588
  "exec",
585
589
  "agent",
@@ -628,12 +632,12 @@ function validateAgentOptions(target, label, capabilities) {
628
632
  throw new Error(`${label}.agent is not supported for provider codex`);
629
633
  }
630
634
  if (provider === "codewith" && target.agent !== undefined) {
631
- throw new Error(`${label}.agent is not supported for provider codewith durable background-agent execution`);
635
+ throw new Error(`${label}.agent is not supported for provider codewith`);
632
636
  }
633
637
  if (provider === "codewith") {
634
- const unsafe = target.extraArgs?.find((arg) => UNSAFE_CODEWITH_DURABLE_EXTRA_ARGS.has(arg));
638
+ const unsafe = target.extraArgs?.find((arg) => UNSAFE_CODEWITH_EXEC_EXTRA_ARGS.has(arg));
635
639
  if (unsafe) {
636
- throw new Error(`${label}.extraArgs cannot include ${unsafe}; codewith agent steps use durable agent start, not exec/ephemeral flags`);
640
+ throw new Error(`${label}.extraArgs cannot include ${unsafe}; codewith exec launch flags are managed by the adapter`);
637
641
  }
638
642
  }
639
643
  if (target.addDirs?.length && !["codewith", "codex"].includes(provider)) {
@@ -716,7 +720,10 @@ function buildAgentInvocation(target) {
716
720
  args.push(...target.authProfile ? ["--auth-profile", target.authProfile] : []);
717
721
  if (target.variant)
718
722
  args.push("-c", `model_reasoning_effort=${configStringValue(target.variant)}`);
719
- args.push("--ask-for-approval", "never", "--sandbox", codewithLikeSandbox(target));
723
+ const sandbox = codewithLikeSandbox(target);
724
+ if (sandbox === "workspace-write")
725
+ args.push("-c", "sandbox_workspace_write.network_access=true");
726
+ args.push("--ask-for-approval", "never", "exec", "--json", "--ephemeral", "--sandbox", sandbox, "--skip-git-repo-check");
720
727
  if (target.cwd)
721
728
  args.push("--cd", target.cwd);
722
729
  for (const dir of target.addDirs ?? [])
@@ -724,11 +731,7 @@ function buildAgentInvocation(target) {
724
731
  if (target.model)
725
732
  args.push("--model", target.model);
726
733
  args.push(...target.extraArgs ?? []);
727
- args.push("agent", "start", "--json");
728
- if (target.cwd)
729
- args.push("--cwd", target.cwd);
730
- args.push(target.prompt);
731
- return { command: "codewith", args };
734
+ return { command: "codewith", args, stdin: target.prompt };
732
735
  }
733
736
  case "codex": {
734
737
  if (target.variant)
@@ -781,7 +784,7 @@ function adapterFor(provider, capabilities) {
781
784
  var PROVIDER_ADAPTERS = {
782
785
  claude: adapterFor("claude", { sandbox: [], durable: false, remote: true, promptChannel: "stdin" }),
783
786
  cursor: adapterFor("cursor", { sandbox: CURSOR_SANDBOXES, durable: false, remote: true, promptChannel: "stdin" }),
784
- codewith: adapterFor("codewith", { sandbox: CODEX_LIKE_SANDBOXES, durable: true, remote: false, promptChannel: "argv" }),
787
+ codewith: adapterFor("codewith", { sandbox: CODEX_LIKE_SANDBOXES, durable: false, remote: true, promptChannel: "stdin" }),
785
788
  codex: adapterFor("codex", { sandbox: CODEX_LIKE_SANDBOXES, durable: false, remote: true, promptChannel: "stdin" }),
786
789
  aicopilot: adapterFor("aicopilot", { sandbox: [], durable: false, remote: true, promptChannel: "stdin" }),
787
790
  opencode: adapterFor("opencode", { sandbox: [], durable: false, remote: true, promptChannel: "stdin" })
@@ -1473,6 +1476,21 @@ function workItemStatusForLoopRun(status, attempt, maxAttempts) {
1473
1476
  function scrubbedOrNull(value) {
1474
1477
  return value == null ? null : scrubSecrets(value);
1475
1478
  }
1479
+ var MAX_PERSISTED_RUN_OUTPUT_CHARS = 64 * 1024;
1480
+ function clampPersistedRunOutput(value) {
1481
+ if (value == null || value.length <= MAX_PERSISTED_RUN_OUTPUT_CHARS)
1482
+ return value;
1483
+ const half = Math.floor(MAX_PERSISTED_RUN_OUTPUT_CHARS / 2);
1484
+ const head = value.slice(0, half);
1485
+ const tail = value.slice(value.length - half);
1486
+ const omitted = value.length - head.length - tail.length;
1487
+ return `${head}
1488
+ \u2026[${omitted} chars truncated by loops run-output retention]\u2026
1489
+ ${tail}`;
1490
+ }
1491
+ function persistedRunOutput(value) {
1492
+ return clampPersistedRunOutput(scrubbedOrNull(value));
1493
+ }
1476
1494
  function chmodIfExists(path, mode) {
1477
1495
  try {
1478
1496
  if (existsSync(path))
@@ -3219,8 +3237,8 @@ class Store {
3219
3237
  ))`).run({
3220
3238
  $workflowRunId: workflowRunId,
3221
3239
  $stepId: stepId,
3222
- $stdout: progress.stdout === undefined ? null : scrubbedOrNull(progress.stdout),
3223
- $stderr: progress.stderr === undefined ? null : scrubbedOrNull(progress.stderr),
3240
+ $stdout: progress.stdout === undefined ? null : persistedRunOutput(progress.stdout),
3241
+ $stderr: progress.stderr === undefined ? null : persistedRunOutput(progress.stderr),
3224
3242
  $updated: now,
3225
3243
  $daemonLeaseId: opts.daemonLeaseId ?? null,
3226
3244
  $now: now
@@ -3281,8 +3299,8 @@ class Store {
3281
3299
  $finished: finishedAt,
3282
3300
  $exitCode: patch.exitCode ?? null,
3283
3301
  $durationMs: patch.durationMs ?? null,
3284
- $stdout: scrubbedOrNull(patch.stdout),
3285
- $stderr: scrubbedOrNull(patch.stderr),
3302
+ $stdout: persistedRunOutput(patch.stdout),
3303
+ $stderr: persistedRunOutput(patch.stderr),
3286
3304
  $error: error ?? null,
3287
3305
  $updated: finishedAt,
3288
3306
  $daemonLeaseId: opts.daemonLeaseId ?? null,
@@ -3660,8 +3678,8 @@ class Store {
3660
3678
  $pid: patch.pid ?? null,
3661
3679
  $exitCode: patch.exitCode ?? null,
3662
3680
  $durationMs: patch.durationMs ?? null,
3663
- $stdout: scrubbedOrNull(patch.stdout),
3664
- $stderr: scrubbedOrNull(patch.stderr),
3681
+ $stdout: persistedRunOutput(patch.stdout),
3682
+ $stderr: persistedRunOutput(patch.stderr),
3665
3683
  $error: error ?? null,
3666
3684
  $updated: finishedAt,
3667
3685
  $claimedBy: opts.claimedBy ?? null,
@@ -4058,8 +4076,8 @@ class Store {
4058
4076
  $processStartedAt: run.processStartedAt ?? null,
4059
4077
  $exitCode: run.exitCode ?? null,
4060
4078
  $durationMs: run.durationMs ?? null,
4061
- $stdout: scrubbedOrNull(run.stdout),
4062
- $stderr: scrubbedOrNull(run.stderr),
4079
+ $stdout: persistedRunOutput(run.stdout),
4080
+ $stderr: persistedRunOutput(run.stderr),
4063
4081
  $error: scrubbedOrNull(run.error),
4064
4082
  $goalRunId: run.goalRunId ?? null,
4065
4083
  $created: run.createdAt,
@@ -4217,7 +4235,7 @@ class Store {
4217
4235
  // package.json
4218
4236
  var package_default = {
4219
4237
  name: "@hasna/loops",
4220
- version: "0.4.8",
4238
+ version: "0.4.10",
4221
4239
  description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
4222
4240
  type: "module",
4223
4241
  main: "dist/index.js",
@@ -5009,11 +5027,6 @@ var TRANSPORT_ENV_KEYS = new Set([
5009
5027
  "USER",
5010
5028
  "XDG_RUNTIME_DIR"
5011
5029
  ]);
5012
- function boundedText(text, maxBytes) {
5013
- const buffer = new BoundedOutputBuffer(maxBytes);
5014
- buffer.append(text);
5015
- return buffer.value();
5016
- }
5017
5030
  function buildResult(status, startedAt, fields = {}) {
5018
5031
  const finishedAt = fields.finishedAt ?? nowIso();
5019
5032
  return {
@@ -5051,6 +5064,20 @@ function notifySpawn(pid, opts) {
5051
5064
  function shellQuote(value) {
5052
5065
  return `'${value.replace(/'/g, `'\\''`)}'`;
5053
5066
  }
5067
+ function codewithProfileCandidateFromLine(line) {
5068
+ const cols = line.trim().split(/\s+/);
5069
+ if (cols[0] === "*")
5070
+ return cols[1];
5071
+ return cols[0];
5072
+ }
5073
+ function codewithProfileForError(profile) {
5074
+ return /[\u0000-\u001F\u007F]/.test(profile) ? JSON.stringify(profile) ?? profile : profile;
5075
+ }
5076
+ function assertCodewithAuthProfileSupported(profile) {
5077
+ if (profile.includes("\x00")) {
5078
+ throw new Error(`codewith auth profile contains unsupported NUL byte: ${codewithProfileForError(profile)}`);
5079
+ }
5080
+ }
5054
5081
  function metadataEnv(metadata) {
5055
5082
  const env = {};
5056
5083
  if (metadata.loopId)
@@ -5077,21 +5104,6 @@ function metadataEnv(metadata) {
5077
5104
  env.LOOPS_GOAL_NODE_KEY = metadata.goalNodeKey;
5078
5105
  return env;
5079
5106
  }
5080
- function codewithAgentIdempotencyKey(metadata) {
5081
- const parts = [
5082
- "openloops",
5083
- metadata.workflowRunId ? `workflow-run:${metadata.workflowRunId}` : undefined,
5084
- metadata.workflowStepId ? `step:${metadata.workflowStepId}` : undefined,
5085
- metadata.runId ? `loop-run:${metadata.runId}` : undefined,
5086
- metadata.loopId ? `loop:${metadata.loopId}` : undefined,
5087
- metadata.scheduledFor ? `scheduled:${metadata.scheduledFor}` : undefined,
5088
- metadata.goalId ? `goal:${metadata.goalId}` : undefined,
5089
- metadata.goalNodeKey ? `node:${metadata.goalNodeKey}` : undefined
5090
- ].filter(Boolean);
5091
- if (parts.length > 1)
5092
- return parts.join(":");
5093
- return `openloops:adhoc:${randomBytes2(16).toString("hex")}`;
5094
- }
5095
5107
  function allowlistEnv(allowlist) {
5096
5108
  const env = {};
5097
5109
  if (allowlist?.tools?.length)
@@ -5132,6 +5144,9 @@ function commandSpec(target, opts) {
5132
5144
  };
5133
5145
  }
5134
5146
  const agentTarget = target;
5147
+ if (agentTarget.provider === "codewith" && agentTarget.authProfile) {
5148
+ assertCodewithAuthProfileSupported(agentTarget.authProfile);
5149
+ }
5135
5150
  const adapter = providerAdapter(agentTarget.provider);
5136
5151
  const invocation = adapter.buildInvocation(agentTarget);
5137
5152
  return {
@@ -5146,8 +5161,7 @@ function commandSpec(target, opts) {
5146
5161
  preflightAnyOf: invocation.preflightAnyOf,
5147
5162
  stdin: invocation.stdin,
5148
5163
  allowlist: agentTarget.allowlist,
5149
- worktree: agentTarget.worktree,
5150
- codewithDurableAgent: adapter.capabilities.durable ? { target: agentTarget } : undefined
5164
+ worktree: agentTarget.worktree
5151
5165
  };
5152
5166
  }
5153
5167
  function composeExecutionEnv(spec, metadata, opts, accountEnv) {
@@ -5305,7 +5319,8 @@ function remotePreflightScript(spec, metadata) {
5305
5319
  lines.push(`if ! ${spec.preflightAnyOf.map((command) => `command -v ${shellQuote(command)} >/dev/null 2>&1`).join(" && ! ")}; then`, ` echo 'none of required executables found: ${spec.preflightAnyOf.join(", ")}' >&2`, " exit 127", "fi");
5306
5320
  }
5307
5321
  if (spec.nativeAuthProfile?.provider === "codewith") {
5308
- lines.push(`__OPENLOOPS_CODEWITH_PROFILES="$(${shellQuote(spec.command)} profile list)" || {`, ` printf '%s\\n' ${shellQuote("codewith auth profile preflight failed")} >&2`, " exit 1", "}", `if ! printf '%s\\n' "$__OPENLOOPS_CODEWITH_PROFILES" | awk 'NR > 1 { print $1 }' | grep -Fx ${shellQuote(spec.nativeAuthProfile.profile)} >/dev/null; then`, ` printf '%s\\n' ${shellQuote(`codewith auth profile not found: ${spec.nativeAuthProfile.profile}`)} >&2`, " exit 1", "fi");
5322
+ const profileForError = codewithProfileForError(spec.nativeAuthProfile.profile);
5323
+ lines.push(`__OPENLOOPS_CODEWITH_PROFILE=${shellQuote(spec.nativeAuthProfile.profile)}`, "export __OPENLOOPS_CODEWITH_PROFILE", `__OPENLOOPS_CODEWITH_PROFILES="$(${shellQuote(spec.command)} profile list)" || {`, ` printf '%s\\n' ${shellQuote("codewith auth profile preflight failed")} >&2`, " exit 1", "}", `if ! printf '%s\\n' "$__OPENLOOPS_CODEWITH_PROFILES" | awk 'NR > 1 { candidate = ($1 == "*" ? $2 : $1); if (candidate == ENVIRON["__OPENLOOPS_CODEWITH_PROFILE"]) found = 1 } END { exit(found ? 0 : 1) }'; then`, ` printf '%s\\n' ${shellQuote(`codewith auth profile not found: ${profileForError}`)} >&2`, " exit 1", "fi");
5309
5324
  }
5310
5325
  return lines.join(`
5311
5326
  `);
@@ -5330,9 +5345,9 @@ function assertCodewithProfileListed(profile, result) {
5330
5345
  const detail = (result.stderr || result.stdout || `exit ${result.status ?? "unknown"}`).trim();
5331
5346
  throw new Error(`codewith auth profile preflight failed${detail ? `: ${detail}` : ""}`);
5332
5347
  }
5333
- const profiles = new Set((result.stdout || "").split(/\r?\n/).slice(1).map((line) => line.trim().split(/\s+/)[0]).filter(Boolean));
5348
+ const profiles = new Set((result.stdout || "").split(/\r?\n/).slice(1).map(codewithProfileCandidateFromLine).filter(Boolean));
5334
5349
  if (!profiles.has(profile)) {
5335
- throw new Error(`codewith auth profile not found: ${profile}`);
5350
+ throw new Error(`codewith auth profile not found: ${codewithProfileForError(profile)}`);
5336
5351
  }
5337
5352
  }
5338
5353
  function preflightNativeAuthProfileSync(spec, env) {
@@ -5357,266 +5372,6 @@ async function preflightNativeAuthProfile(spec, env) {
5357
5372
  const result = await spawnCapture(spec.command, ["profile", "list"], { env, timeoutMs: 15000 });
5358
5373
  assertCodewithProfileListed(spec.nativeAuthProfile.profile, result);
5359
5374
  }
5360
- function codewithAgentStartArgs(target, idempotencyKey) {
5361
- const args = providerAdapter(target.provider).buildInvocation(target).args;
5362
- const startIndex = args.findIndex((arg, index) => arg === "start" && args[index - 1] === "agent");
5363
- if (startIndex === -1)
5364
- throw new Error("internal error: codewith durable agent args missing agent start");
5365
- args.splice(startIndex + 1, 0, "--idempotency-key", idempotencyKey);
5366
- return args;
5367
- }
5368
- function codewithAgentControlArgs(target, command, agentId) {
5369
- return [
5370
- ...target.authProfile ? ["--auth-profile", target.authProfile] : [],
5371
- "agent",
5372
- command,
5373
- ...command === "logs" ? ["--limit", "20"] : [],
5374
- "--json",
5375
- agentId
5376
- ];
5377
- }
5378
- function extractFirstJsonObject(text) {
5379
- let depth = 0;
5380
- let start = -1;
5381
- let inString = false;
5382
- let escaped = false;
5383
- for (let i = 0;i < text.length; i += 1) {
5384
- const ch = text[i];
5385
- if (inString) {
5386
- if (escaped)
5387
- escaped = false;
5388
- else if (ch === "\\")
5389
- escaped = true;
5390
- else if (ch === '"')
5391
- inString = false;
5392
- continue;
5393
- }
5394
- if (ch === '"') {
5395
- inString = true;
5396
- } else if (ch === "{") {
5397
- if (depth === 0)
5398
- start = i;
5399
- depth += 1;
5400
- } else if (ch === "}") {
5401
- if (depth > 0) {
5402
- depth -= 1;
5403
- if (depth === 0 && start !== -1)
5404
- return text.slice(start, i + 1);
5405
- }
5406
- }
5407
- }
5408
- return;
5409
- }
5410
- function parseJsonOutput(stdout, label) {
5411
- const raw = stdout || "{}";
5412
- const candidates = [raw.trim()];
5413
- const scanned = extractFirstJsonObject(raw);
5414
- if (scanned && scanned !== raw.trim())
5415
- candidates.push(scanned);
5416
- let lastError;
5417
- for (const candidate of candidates) {
5418
- try {
5419
- const value = JSON.parse(candidate);
5420
- if (!value || typeof value !== "object" || Array.isArray(value))
5421
- throw new Error("not an object");
5422
- return value;
5423
- } catch (error) {
5424
- lastError = error;
5425
- }
5426
- }
5427
- throw new Error(`${label} did not return JSON${lastError instanceof Error ? `: ${lastError.message}` : ""}`);
5428
- }
5429
- function recordField(value, key) {
5430
- if (!value || typeof value !== "object" || Array.isArray(value))
5431
- return;
5432
- const field = value[key];
5433
- return field && typeof field === "object" && !Array.isArray(field) ? field : undefined;
5434
- }
5435
- function stringField(value, key) {
5436
- const field = value?.[key];
5437
- return typeof field === "string" ? field : undefined;
5438
- }
5439
- function numberField(value, key) {
5440
- const field = value?.[key];
5441
- return typeof field === "number" && Number.isFinite(field) ? field : undefined;
5442
- }
5443
- function codewithAgentStatus(readJson) {
5444
- return stringField(recordField(readJson, "agent"), "status") ?? stringField(recordField(readJson, "statusSnapshot"), "status");
5445
- }
5446
- function codewithAgentLastEventSeq(readJson) {
5447
- const agentSeq = numberField(recordField(readJson, "agent"), "lastEventSeq");
5448
- const snapshotSeq = numberField(recordField(readJson, "statusSnapshot"), "lastEventSeq");
5449
- if (agentSeq !== undefined && snapshotSeq !== undefined)
5450
- return Math.max(agentSeq, snapshotSeq);
5451
- return agentSeq ?? snapshotSeq;
5452
- }
5453
- function codewithAgentEvidence(startJson, readJson, logsJson) {
5454
- const agent = recordField(readJson, "agent") ?? recordField(startJson, "agent");
5455
- const statusSnapshot = recordField(readJson, "statusSnapshot");
5456
- const events = Array.isArray(logsJson?.data) ? logsJson.data.filter((event) => Boolean(event) && typeof event === "object" && !Array.isArray(event)).map((event) => ({
5457
- seq: numberField(event, "seq"),
5458
- eventType: stringField(event, "eventType"),
5459
- createdAt: numberField(event, "createdAt")
5460
- })) : undefined;
5461
- return JSON.stringify({
5462
- codewithAgent: {
5463
- agentId: stringField(agent, "agentId"),
5464
- status: stringField(agent, "status"),
5465
- desiredState: stringField(agent, "desiredState"),
5466
- statusReason: stringField(agent, "statusReason"),
5467
- threadId: stringField(agent, "threadId"),
5468
- rolloutPath: stringField(agent, "rolloutPath"),
5469
- pid: numberField(agent, "pid"),
5470
- exitCode: numberField(agent, "exitCode"),
5471
- created: typeof startJson.created === "boolean" ? startJson.created : undefined
5472
- },
5473
- statusSnapshot: statusSnapshot ? {
5474
- seq: numberField(statusSnapshot, "seq"),
5475
- status: stringField(statusSnapshot, "status"),
5476
- summary: stringField(statusSnapshot, "summary"),
5477
- pendingInteractionCount: numberField(statusSnapshot, "pendingInteractionCount"),
5478
- lastEventSeq: numberField(statusSnapshot, "lastEventSeq")
5479
- } : undefined,
5480
- events
5481
- }, null, 2);
5482
- }
5483
- function codewithAgentProgress(readJson) {
5484
- const agent = recordField(readJson, "agent");
5485
- const statusSnapshot = recordField(readJson, "statusSnapshot");
5486
- return {
5487
- provider: "codewith",
5488
- agentId: stringField(agent, "agentId"),
5489
- status: stringField(agent, "status") ?? stringField(statusSnapshot, "status"),
5490
- summary: stringField(statusSnapshot, "summary"),
5491
- statusReason: stringField(agent, "statusReason"),
5492
- threadId: stringField(agent, "threadId"),
5493
- rolloutPath: stringField(agent, "rolloutPath"),
5494
- pid: numberField(agent, "pid"),
5495
- lastEventSeq: codewithAgentLastEventSeq(readJson)
5496
- };
5497
- }
5498
- function sleep(ms) {
5499
- return new Promise((resolve3) => setTimeout(resolve3, ms));
5500
- }
5501
- async function executeCodewithDurableAgent(spec, metadata, opts, env, startedAt) {
5502
- const target = spec.codewithDurableAgent?.target;
5503
- if (!target)
5504
- throw new Error("internal error: missing codewith durable target");
5505
- const maxOutputBytes = opts.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
5506
- const idempotencyKey = codewithAgentIdempotencyKey(metadata);
5507
- const start = await spawnCapture(spec.command, codewithAgentStartArgs(target, idempotencyKey), {
5508
- cwd: spec.cwd,
5509
- env,
5510
- timeoutMs: 30000,
5511
- maxOutputBytes
5512
- });
5513
- const stderr = new BoundedOutputBuffer(maxOutputBytes);
5514
- stderr.append(start.stderr);
5515
- if (start.error || (start.status ?? 1) !== 0) {
5516
- return failureResult(startedAt, start.error ?? `codewith agent start exited with code ${start.status ?? "unknown"}`, {
5517
- exitCode: start.status ?? undefined,
5518
- stdout: start.stdout,
5519
- stderr: stderr.value()
5520
- });
5521
- }
5522
- let startJson;
5523
- try {
5524
- startJson = parseJsonOutput(start.stdout || "{}", "codewith agent start");
5525
- } catch (error) {
5526
- return failureResult(startedAt, error instanceof Error ? error.message : String(error), { stdout: start.stdout, stderr: stderr.value() });
5527
- }
5528
- const agentId = stringField(recordField(startJson, "agent"), "agentId");
5529
- if (!agentId) {
5530
- return failureResult(startedAt, "codewith agent start did not return agent.agentId", { stdout: start.stdout, stderr: stderr.value() });
5531
- }
5532
- opts.onAgentProgress?.(codewithAgentProgress(startJson));
5533
- const stopAgent = async () => {
5534
- await spawnCapture(spec.command, codewithAgentControlArgs(target, "stop", agentId), {
5535
- cwd: spec.cwd,
5536
- env,
5537
- timeoutMs: 15000,
5538
- maxOutputBytes
5539
- });
5540
- };
5541
- const pollMs = Math.max(100, Number(opts.env?.LOOPS_CODEWITH_AGENT_POLL_MS ?? process.env.LOOPS_CODEWITH_AGENT_POLL_MS ?? 2000) || 2000);
5542
- let lastReadJson = startJson;
5543
- let lastLogsJson;
5544
- let lastFingerprint;
5545
- let lastProgressAt = Date.now();
5546
- const evidence = () => boundedText(codewithAgentEvidence(startJson, lastReadJson, lastLogsJson), maxOutputBytes);
5547
- while (true) {
5548
- if (opts.signal?.aborted) {
5549
- await stopAgent();
5550
- return failureResult(startedAt, "cancelled", { stdout: evidence(), stderr: stderr.value() });
5551
- }
5552
- const read = await spawnCapture(spec.command, codewithAgentControlArgs(target, "read", agentId), {
5553
- cwd: spec.cwd,
5554
- env,
5555
- timeoutMs: 30000,
5556
- maxOutputBytes
5557
- });
5558
- stderr.append(read.stderr);
5559
- if (read.error || (read.status ?? 1) !== 0) {
5560
- return failureResult(startedAt, read.error ?? `codewith agent read exited with code ${read.status ?? "unknown"}`, {
5561
- exitCode: read.status ?? undefined,
5562
- stdout: evidence(),
5563
- stderr: stderr.value()
5564
- });
5565
- }
5566
- try {
5567
- lastReadJson = parseJsonOutput(read.stdout || "{}", "codewith agent read");
5568
- } catch (error) {
5569
- return failureResult(startedAt, error instanceof Error ? error.message : String(error), {
5570
- stdout: boundedText(read.stdout, maxOutputBytes),
5571
- stderr: stderr.value()
5572
- });
5573
- }
5574
- const status = codewithAgentStatus(lastReadJson);
5575
- const fingerprint = JSON.stringify({
5576
- status,
5577
- agentLastEventSeq: numberField(recordField(lastReadJson, "agent"), "lastEventSeq"),
5578
- snapshot: recordField(lastReadJson, "statusSnapshot") ?? null
5579
- });
5580
- if (fingerprint !== lastFingerprint) {
5581
- lastFingerprint = fingerprint;
5582
- lastProgressAt = Date.now();
5583
- opts.onAgentProgress?.(codewithAgentProgress(lastReadJson));
5584
- }
5585
- if (status === "completed" || status === "failed" || status === "cancelled") {
5586
- const logs = await spawnCapture(spec.command, codewithAgentControlArgs(target, "logs", agentId), {
5587
- cwd: spec.cwd,
5588
- env,
5589
- timeoutMs: 30000,
5590
- maxOutputBytes
5591
- });
5592
- if (!logs.error && (logs.status ?? 1) === 0) {
5593
- try {
5594
- lastLogsJson = parseJsonOutput(logs.stdout || "{}", "codewith agent logs");
5595
- } catch {
5596
- lastLogsJson = undefined;
5597
- }
5598
- } else {
5599
- stderr.append(logs.stderr || logs.error || "");
5600
- }
5601
- if (status === "completed") {
5602
- return successResult(startedAt, { exitCode: 0, stdout: evidence(), stderr: stderr.value() });
5603
- }
5604
- return failureResult(startedAt, stringField(recordField(lastReadJson, "agent"), "statusReason") ?? `codewith agent ${status}`, { exitCode: 1, stdout: evidence(), stderr: stderr.value() });
5605
- }
5606
- if (typeof spec.timeoutMs === "number" && new Date(nowIso()).getTime() - new Date(startedAt).getTime() >= spec.timeoutMs) {
5607
- await stopAgent();
5608
- return timeoutResult(startedAt, `timed out after ${spec.timeoutMs}ms`, { stdout: evidence(), stderr: stderr.value() });
5609
- }
5610
- if (typeof spec.idleTimeoutMs === "number" && Date.now() - lastProgressAt >= spec.idleTimeoutMs) {
5611
- await stopAgent();
5612
- return timeoutResult(startedAt, `idle timed out after ${spec.idleTimeoutMs}ms without agent progress`, {
5613
- stdout: evidence(),
5614
- stderr: stderr.value()
5615
- });
5616
- }
5617
- await sleep(pollMs);
5618
- }
5619
- }
5620
5375
  function resolvedDirEquals(left, right) {
5621
5376
  try {
5622
5377
  return realpathSync(left) === realpathSync(right);
@@ -5850,9 +5605,6 @@ function preflightTarget(target, metadata = {}, opts = {}) {
5850
5605
  async function executeTarget(target, metadata = {}, opts = {}) {
5851
5606
  let spec = commandSpec(target, opts);
5852
5607
  const machine = resolvedMachine(opts);
5853
- if (machine && !machine.local && spec.codewithDurableAgent) {
5854
- 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");
5855
- }
5856
5608
  if (machine && !machine.local) {
5857
5609
  const remoteFallbackSpec = spec.worktree?.enabled && spec.worktree.mode === "auto" ? worktreeFallbackSpec(target, opts, spec.worktree.originalCwd) : undefined;
5858
5610
  return executeRemoteSpec(spec, machine, metadata, opts, remoteFallbackSpec);
@@ -5883,9 +5635,6 @@ async function executeTarget(target, metadata = {}, opts = {}) {
5883
5635
  if (worktreeEntry?.fallbackCwd) {
5884
5636
  spec = worktreeFallbackSpec(target, opts, worktreeEntry.fallbackCwd) ?? spec;
5885
5637
  }
5886
- if (spec.codewithDurableAgent) {
5887
- return executeCodewithDurableAgent(spec, metadata, opts, env, startedAt);
5888
- }
5889
5638
  const child = spawn2(spec.command, spec.args, {
5890
5639
  cwd: spec.cwd,
5891
5640
  env,
@@ -24,7 +24,7 @@ export interface ProviderAdapter {
24
24
  validate(target: AgentTarget, label?: string): void;
25
25
  buildInvocation(target: AgentTarget): AgentInvocation;
26
26
  }
27
- export declare const UNSAFE_CODEWITH_DURABLE_EXTRA_ARGS: Set<string>;
27
+ export declare const UNSAFE_CODEWITH_EXEC_EXTRA_ARGS: Set<string>;
28
28
  export declare const PROVIDER_ADAPTERS: Record<AgentProvider, ProviderAdapter>;
29
29
  export declare const AGENT_PROVIDERS: AgentProvider[];
30
30
  export declare function providerAdapter(provider: AgentProvider): ProviderAdapter;
package/dist/lib/mode.js CHANGED
@@ -2,7 +2,7 @@
2
2
  // package.json
3
3
  var package_default = {
4
4
  name: "@hasna/loops",
5
- version: "0.4.8",
5
+ version: "0.4.10",
6
6
  description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
7
7
  type: "module",
8
8
  main: "dist/index.js",
@@ -8,4 +8,21 @@ export interface PrReviewRoutingDecision {
8
8
  selectedReviewer?: string;
9
9
  signals: string[];
10
10
  }
11
- export declare function prReviewRoutingDecision(data: Record<string, unknown>, metadata: Record<string, unknown>, opts: TodosTaskRouteOptions): PrReviewRoutingDecision;
11
+ /** A GitHub PR reference resolvable to `owner/repo#number`. */
12
+ export interface PrReference {
13
+ owner: string;
14
+ repo: string;
15
+ number: number;
16
+ }
17
+ /** Resolves a PR author login from a concrete `owner/repo#number` reference. */
18
+ export type PrAuthorResolver = (ref: PrReference) => string | undefined;
19
+ /** Live PR lifecycle state from a concrete `owner/repo#number` reference. */
20
+ export interface PrLiveState {
21
+ state?: string;
22
+ mergeStateStatus?: string;
23
+ }
24
+ /** Resolves live PR state; returns undefined when it cannot be determined. */
25
+ export type PrStateResolver = (ref: PrReference) => PrLiveState | undefined;
26
+ /** Extracts a concrete owner/repo/number PR reference from route evidence text. */
27
+ export declare function prReferenceFrom(text: string): PrReference | undefined;
28
+ export declare function prReviewRoutingDecision(data: Record<string, unknown>, metadata: Record<string, unknown>, opts: TodosTaskRouteOptions, resolveAuthor?: PrAuthorResolver, resolveState?: PrStateResolver): PrReviewRoutingDecision;