@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.
@@ -3,7 +3,7 @@
3
3
  // package.json
4
4
  var package_default = {
5
5
  name: "@hasna/loops",
6
- version: "0.4.8",
6
+ version: "0.4.9",
7
7
  description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
8
8
  type: "module",
9
9
  main: "dist/index.js",
@@ -365,7 +365,7 @@ function scrubSecretsDeep(value) {
365
365
  }
366
366
 
367
367
  // src/lib/agent-adapter.ts
368
- var UNSAFE_CODEWITH_DURABLE_EXTRA_ARGS = new Set([
368
+ var UNSAFE_CODEWITH_EXEC_EXTRA_ARGS = new Set([
369
369
  "e",
370
370
  "exec",
371
371
  "agent",
@@ -414,12 +414,12 @@ function validateAgentOptions(target, label, capabilities) {
414
414
  throw new Error(`${label}.agent is not supported for provider codex`);
415
415
  }
416
416
  if (provider === "codewith" && target.agent !== undefined) {
417
- throw new Error(`${label}.agent is not supported for provider codewith durable background-agent execution`);
417
+ throw new Error(`${label}.agent is not supported for provider codewith`);
418
418
  }
419
419
  if (provider === "codewith") {
420
- const unsafe = target.extraArgs?.find((arg) => UNSAFE_CODEWITH_DURABLE_EXTRA_ARGS.has(arg));
420
+ const unsafe = target.extraArgs?.find((arg) => UNSAFE_CODEWITH_EXEC_EXTRA_ARGS.has(arg));
421
421
  if (unsafe) {
422
- throw new Error(`${label}.extraArgs cannot include ${unsafe}; codewith agent steps use durable agent start, not exec/ephemeral flags`);
422
+ throw new Error(`${label}.extraArgs cannot include ${unsafe}; codewith exec launch flags are managed by the adapter`);
423
423
  }
424
424
  }
425
425
  if (target.addDirs?.length && !["codewith", "codex"].includes(provider)) {
@@ -502,7 +502,10 @@ function buildAgentInvocation(target) {
502
502
  args.push(...target.authProfile ? ["--auth-profile", target.authProfile] : []);
503
503
  if (target.variant)
504
504
  args.push("-c", `model_reasoning_effort=${configStringValue(target.variant)}`);
505
- args.push("--ask-for-approval", "never", "--sandbox", codewithLikeSandbox(target));
505
+ const sandbox = codewithLikeSandbox(target);
506
+ if (sandbox === "workspace-write")
507
+ args.push("-c", "sandbox_workspace_write.network_access=true");
508
+ args.push("--ask-for-approval", "never", "exec", "--json", "--ephemeral", "--sandbox", sandbox, "--skip-git-repo-check");
506
509
  if (target.cwd)
507
510
  args.push("--cd", target.cwd);
508
511
  for (const dir of target.addDirs ?? [])
@@ -510,11 +513,7 @@ function buildAgentInvocation(target) {
510
513
  if (target.model)
511
514
  args.push("--model", target.model);
512
515
  args.push(...target.extraArgs ?? []);
513
- args.push("agent", "start", "--json");
514
- if (target.cwd)
515
- args.push("--cwd", target.cwd);
516
- args.push(target.prompt);
517
- return { command: "codewith", args };
516
+ return { command: "codewith", args, stdin: target.prompt };
518
517
  }
519
518
  case "codex": {
520
519
  if (target.variant)
@@ -567,7 +566,7 @@ function adapterFor(provider, capabilities) {
567
566
  var PROVIDER_ADAPTERS = {
568
567
  claude: adapterFor("claude", { sandbox: [], durable: false, remote: true, promptChannel: "stdin" }),
569
568
  cursor: adapterFor("cursor", { sandbox: CURSOR_SANDBOXES, durable: false, remote: true, promptChannel: "stdin" }),
570
- codewith: adapterFor("codewith", { sandbox: CODEX_LIKE_SANDBOXES, durable: true, remote: false, promptChannel: "argv" }),
569
+ codewith: adapterFor("codewith", { sandbox: CODEX_LIKE_SANDBOXES, durable: false, remote: true, promptChannel: "stdin" }),
571
570
  codex: adapterFor("codex", { sandbox: CODEX_LIKE_SANDBOXES, durable: false, remote: true, promptChannel: "stdin" }),
572
571
  aicopilot: adapterFor("aicopilot", { sandbox: [], durable: false, remote: true, promptChannel: "stdin" }),
573
572
  opencode: adapterFor("opencode", { sandbox: [], durable: false, remote: true, promptChannel: "stdin" })
@@ -1041,11 +1040,6 @@ var TRANSPORT_ENV_KEYS = new Set([
1041
1040
  "USER",
1042
1041
  "XDG_RUNTIME_DIR"
1043
1042
  ]);
1044
- function boundedText(text, maxBytes) {
1045
- const buffer = new BoundedOutputBuffer(maxBytes);
1046
- buffer.append(text);
1047
- return buffer.value();
1048
- }
1049
1043
  function buildResult(status, startedAt, fields = {}) {
1050
1044
  const finishedAt = fields.finishedAt ?? nowIso();
1051
1045
  return {
@@ -1109,21 +1103,6 @@ function metadataEnv(metadata) {
1109
1103
  env.LOOPS_GOAL_NODE_KEY = metadata.goalNodeKey;
1110
1104
  return env;
1111
1105
  }
1112
- function codewithAgentIdempotencyKey(metadata) {
1113
- const parts = [
1114
- "openloops",
1115
- metadata.workflowRunId ? `workflow-run:${metadata.workflowRunId}` : undefined,
1116
- metadata.workflowStepId ? `step:${metadata.workflowStepId}` : undefined,
1117
- metadata.runId ? `loop-run:${metadata.runId}` : undefined,
1118
- metadata.loopId ? `loop:${metadata.loopId}` : undefined,
1119
- metadata.scheduledFor ? `scheduled:${metadata.scheduledFor}` : undefined,
1120
- metadata.goalId ? `goal:${metadata.goalId}` : undefined,
1121
- metadata.goalNodeKey ? `node:${metadata.goalNodeKey}` : undefined
1122
- ].filter(Boolean);
1123
- if (parts.length > 1)
1124
- return parts.join(":");
1125
- return `openloops:adhoc:${randomBytes2(16).toString("hex")}`;
1126
- }
1127
1106
  function allowlistEnv(allowlist) {
1128
1107
  const env = {};
1129
1108
  if (allowlist?.tools?.length)
@@ -1178,8 +1157,7 @@ function commandSpec(target, opts) {
1178
1157
  preflightAnyOf: invocation.preflightAnyOf,
1179
1158
  stdin: invocation.stdin,
1180
1159
  allowlist: agentTarget.allowlist,
1181
- worktree: agentTarget.worktree,
1182
- codewithDurableAgent: adapter.capabilities.durable ? { target: agentTarget } : undefined
1160
+ worktree: agentTarget.worktree
1183
1161
  };
1184
1162
  }
1185
1163
  function composeExecutionEnv(spec, metadata, opts, accountEnv) {
@@ -1389,266 +1367,6 @@ async function preflightNativeAuthProfile(spec, env) {
1389
1367
  const result = await spawnCapture(spec.command, ["profile", "list"], { env, timeoutMs: 15000 });
1390
1368
  assertCodewithProfileListed(spec.nativeAuthProfile.profile, result);
1391
1369
  }
1392
- function codewithAgentStartArgs(target, idempotencyKey) {
1393
- const args = providerAdapter(target.provider).buildInvocation(target).args;
1394
- const startIndex = args.findIndex((arg, index) => arg === "start" && args[index - 1] === "agent");
1395
- if (startIndex === -1)
1396
- throw new Error("internal error: codewith durable agent args missing agent start");
1397
- args.splice(startIndex + 1, 0, "--idempotency-key", idempotencyKey);
1398
- return args;
1399
- }
1400
- function codewithAgentControlArgs(target, command, agentId) {
1401
- return [
1402
- ...target.authProfile ? ["--auth-profile", target.authProfile] : [],
1403
- "agent",
1404
- command,
1405
- ...command === "logs" ? ["--limit", "20"] : [],
1406
- "--json",
1407
- agentId
1408
- ];
1409
- }
1410
- function extractFirstJsonObject(text) {
1411
- let depth = 0;
1412
- let start = -1;
1413
- let inString = false;
1414
- let escaped = false;
1415
- for (let i = 0;i < text.length; i += 1) {
1416
- const ch = text[i];
1417
- if (inString) {
1418
- if (escaped)
1419
- escaped = false;
1420
- else if (ch === "\\")
1421
- escaped = true;
1422
- else if (ch === '"')
1423
- inString = false;
1424
- continue;
1425
- }
1426
- if (ch === '"') {
1427
- inString = true;
1428
- } else if (ch === "{") {
1429
- if (depth === 0)
1430
- start = i;
1431
- depth += 1;
1432
- } else if (ch === "}") {
1433
- if (depth > 0) {
1434
- depth -= 1;
1435
- if (depth === 0 && start !== -1)
1436
- return text.slice(start, i + 1);
1437
- }
1438
- }
1439
- }
1440
- return;
1441
- }
1442
- function parseJsonOutput(stdout, label) {
1443
- const raw = stdout || "{}";
1444
- const candidates = [raw.trim()];
1445
- const scanned = extractFirstJsonObject(raw);
1446
- if (scanned && scanned !== raw.trim())
1447
- candidates.push(scanned);
1448
- let lastError;
1449
- for (const candidate of candidates) {
1450
- try {
1451
- const value = JSON.parse(candidate);
1452
- if (!value || typeof value !== "object" || Array.isArray(value))
1453
- throw new Error("not an object");
1454
- return value;
1455
- } catch (error) {
1456
- lastError = error;
1457
- }
1458
- }
1459
- throw new Error(`${label} did not return JSON${lastError instanceof Error ? `: ${lastError.message}` : ""}`);
1460
- }
1461
- function recordField(value, key) {
1462
- if (!value || typeof value !== "object" || Array.isArray(value))
1463
- return;
1464
- const field = value[key];
1465
- return field && typeof field === "object" && !Array.isArray(field) ? field : undefined;
1466
- }
1467
- function stringField(value, key) {
1468
- const field = value?.[key];
1469
- return typeof field === "string" ? field : undefined;
1470
- }
1471
- function numberField(value, key) {
1472
- const field = value?.[key];
1473
- return typeof field === "number" && Number.isFinite(field) ? field : undefined;
1474
- }
1475
- function codewithAgentStatus(readJson) {
1476
- return stringField(recordField(readJson, "agent"), "status") ?? stringField(recordField(readJson, "statusSnapshot"), "status");
1477
- }
1478
- function codewithAgentLastEventSeq(readJson) {
1479
- const agentSeq = numberField(recordField(readJson, "agent"), "lastEventSeq");
1480
- const snapshotSeq = numberField(recordField(readJson, "statusSnapshot"), "lastEventSeq");
1481
- if (agentSeq !== undefined && snapshotSeq !== undefined)
1482
- return Math.max(agentSeq, snapshotSeq);
1483
- return agentSeq ?? snapshotSeq;
1484
- }
1485
- function codewithAgentEvidence(startJson, readJson, logsJson) {
1486
- const agent = recordField(readJson, "agent") ?? recordField(startJson, "agent");
1487
- const statusSnapshot = recordField(readJson, "statusSnapshot");
1488
- const events = Array.isArray(logsJson?.data) ? logsJson.data.filter((event) => Boolean(event) && typeof event === "object" && !Array.isArray(event)).map((event) => ({
1489
- seq: numberField(event, "seq"),
1490
- eventType: stringField(event, "eventType"),
1491
- createdAt: numberField(event, "createdAt")
1492
- })) : undefined;
1493
- return JSON.stringify({
1494
- codewithAgent: {
1495
- agentId: stringField(agent, "agentId"),
1496
- status: stringField(agent, "status"),
1497
- desiredState: stringField(agent, "desiredState"),
1498
- statusReason: stringField(agent, "statusReason"),
1499
- threadId: stringField(agent, "threadId"),
1500
- rolloutPath: stringField(agent, "rolloutPath"),
1501
- pid: numberField(agent, "pid"),
1502
- exitCode: numberField(agent, "exitCode"),
1503
- created: typeof startJson.created === "boolean" ? startJson.created : undefined
1504
- },
1505
- statusSnapshot: statusSnapshot ? {
1506
- seq: numberField(statusSnapshot, "seq"),
1507
- status: stringField(statusSnapshot, "status"),
1508
- summary: stringField(statusSnapshot, "summary"),
1509
- pendingInteractionCount: numberField(statusSnapshot, "pendingInteractionCount"),
1510
- lastEventSeq: numberField(statusSnapshot, "lastEventSeq")
1511
- } : undefined,
1512
- events
1513
- }, null, 2);
1514
- }
1515
- function codewithAgentProgress(readJson) {
1516
- const agent = recordField(readJson, "agent");
1517
- const statusSnapshot = recordField(readJson, "statusSnapshot");
1518
- return {
1519
- provider: "codewith",
1520
- agentId: stringField(agent, "agentId"),
1521
- status: stringField(agent, "status") ?? stringField(statusSnapshot, "status"),
1522
- summary: stringField(statusSnapshot, "summary"),
1523
- statusReason: stringField(agent, "statusReason"),
1524
- threadId: stringField(agent, "threadId"),
1525
- rolloutPath: stringField(agent, "rolloutPath"),
1526
- pid: numberField(agent, "pid"),
1527
- lastEventSeq: codewithAgentLastEventSeq(readJson)
1528
- };
1529
- }
1530
- function sleep(ms) {
1531
- return new Promise((resolve2) => setTimeout(resolve2, ms));
1532
- }
1533
- async function executeCodewithDurableAgent(spec, metadata, opts, env, startedAt) {
1534
- const target = spec.codewithDurableAgent?.target;
1535
- if (!target)
1536
- throw new Error("internal error: missing codewith durable target");
1537
- const maxOutputBytes = opts.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
1538
- const idempotencyKey = codewithAgentIdempotencyKey(metadata);
1539
- const start = await spawnCapture(spec.command, codewithAgentStartArgs(target, idempotencyKey), {
1540
- cwd: spec.cwd,
1541
- env,
1542
- timeoutMs: 30000,
1543
- maxOutputBytes
1544
- });
1545
- const stderr = new BoundedOutputBuffer(maxOutputBytes);
1546
- stderr.append(start.stderr);
1547
- if (start.error || (start.status ?? 1) !== 0) {
1548
- return failureResult(startedAt, start.error ?? `codewith agent start exited with code ${start.status ?? "unknown"}`, {
1549
- exitCode: start.status ?? undefined,
1550
- stdout: start.stdout,
1551
- stderr: stderr.value()
1552
- });
1553
- }
1554
- let startJson;
1555
- try {
1556
- startJson = parseJsonOutput(start.stdout || "{}", "codewith agent start");
1557
- } catch (error) {
1558
- return failureResult(startedAt, error instanceof Error ? error.message : String(error), { stdout: start.stdout, stderr: stderr.value() });
1559
- }
1560
- const agentId = stringField(recordField(startJson, "agent"), "agentId");
1561
- if (!agentId) {
1562
- return failureResult(startedAt, "codewith agent start did not return agent.agentId", { stdout: start.stdout, stderr: stderr.value() });
1563
- }
1564
- opts.onAgentProgress?.(codewithAgentProgress(startJson));
1565
- const stopAgent = async () => {
1566
- await spawnCapture(spec.command, codewithAgentControlArgs(target, "stop", agentId), {
1567
- cwd: spec.cwd,
1568
- env,
1569
- timeoutMs: 15000,
1570
- maxOutputBytes
1571
- });
1572
- };
1573
- const pollMs = Math.max(100, Number(opts.env?.LOOPS_CODEWITH_AGENT_POLL_MS ?? process.env.LOOPS_CODEWITH_AGENT_POLL_MS ?? 2000) || 2000);
1574
- let lastReadJson = startJson;
1575
- let lastLogsJson;
1576
- let lastFingerprint;
1577
- let lastProgressAt = Date.now();
1578
- const evidence = () => boundedText(codewithAgentEvidence(startJson, lastReadJson, lastLogsJson), maxOutputBytes);
1579
- while (true) {
1580
- if (opts.signal?.aborted) {
1581
- await stopAgent();
1582
- return failureResult(startedAt, "cancelled", { stdout: evidence(), stderr: stderr.value() });
1583
- }
1584
- const read = await spawnCapture(spec.command, codewithAgentControlArgs(target, "read", agentId), {
1585
- cwd: spec.cwd,
1586
- env,
1587
- timeoutMs: 30000,
1588
- maxOutputBytes
1589
- });
1590
- stderr.append(read.stderr);
1591
- if (read.error || (read.status ?? 1) !== 0) {
1592
- return failureResult(startedAt, read.error ?? `codewith agent read exited with code ${read.status ?? "unknown"}`, {
1593
- exitCode: read.status ?? undefined,
1594
- stdout: evidence(),
1595
- stderr: stderr.value()
1596
- });
1597
- }
1598
- try {
1599
- lastReadJson = parseJsonOutput(read.stdout || "{}", "codewith agent read");
1600
- } catch (error) {
1601
- return failureResult(startedAt, error instanceof Error ? error.message : String(error), {
1602
- stdout: boundedText(read.stdout, maxOutputBytes),
1603
- stderr: stderr.value()
1604
- });
1605
- }
1606
- const status = codewithAgentStatus(lastReadJson);
1607
- const fingerprint = JSON.stringify({
1608
- status,
1609
- agentLastEventSeq: numberField(recordField(lastReadJson, "agent"), "lastEventSeq"),
1610
- snapshot: recordField(lastReadJson, "statusSnapshot") ?? null
1611
- });
1612
- if (fingerprint !== lastFingerprint) {
1613
- lastFingerprint = fingerprint;
1614
- lastProgressAt = Date.now();
1615
- opts.onAgentProgress?.(codewithAgentProgress(lastReadJson));
1616
- }
1617
- if (status === "completed" || status === "failed" || status === "cancelled") {
1618
- const logs = await spawnCapture(spec.command, codewithAgentControlArgs(target, "logs", agentId), {
1619
- cwd: spec.cwd,
1620
- env,
1621
- timeoutMs: 30000,
1622
- maxOutputBytes
1623
- });
1624
- if (!logs.error && (logs.status ?? 1) === 0) {
1625
- try {
1626
- lastLogsJson = parseJsonOutput(logs.stdout || "{}", "codewith agent logs");
1627
- } catch {
1628
- lastLogsJson = undefined;
1629
- }
1630
- } else {
1631
- stderr.append(logs.stderr || logs.error || "");
1632
- }
1633
- if (status === "completed") {
1634
- return successResult(startedAt, { exitCode: 0, stdout: evidence(), stderr: stderr.value() });
1635
- }
1636
- return failureResult(startedAt, stringField(recordField(lastReadJson, "agent"), "statusReason") ?? `codewith agent ${status}`, { exitCode: 1, stdout: evidence(), stderr: stderr.value() });
1637
- }
1638
- if (typeof spec.timeoutMs === "number" && new Date(nowIso()).getTime() - new Date(startedAt).getTime() >= spec.timeoutMs) {
1639
- await stopAgent();
1640
- return timeoutResult(startedAt, `timed out after ${spec.timeoutMs}ms`, { stdout: evidence(), stderr: stderr.value() });
1641
- }
1642
- if (typeof spec.idleTimeoutMs === "number" && Date.now() - lastProgressAt >= spec.idleTimeoutMs) {
1643
- await stopAgent();
1644
- return timeoutResult(startedAt, `idle timed out after ${spec.idleTimeoutMs}ms without agent progress`, {
1645
- stdout: evidence(),
1646
- stderr: stderr.value()
1647
- });
1648
- }
1649
- await sleep(pollMs);
1650
- }
1651
- }
1652
1370
  function resolvedDirEquals(left, right) {
1653
1371
  try {
1654
1372
  return realpathSync(left) === realpathSync(right);
@@ -1882,9 +1600,6 @@ function preflightTarget(target, metadata = {}, opts = {}) {
1882
1600
  async function executeTarget(target, metadata = {}, opts = {}) {
1883
1601
  let spec = commandSpec(target, opts);
1884
1602
  const machine = resolvedMachine(opts);
1885
- if (machine && !machine.local && spec.codewithDurableAgent) {
1886
- 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");
1887
- }
1888
1603
  if (machine && !machine.local) {
1889
1604
  const remoteFallbackSpec = spec.worktree?.enabled && spec.worktree.mode === "auto" ? worktreeFallbackSpec(target, opts, spec.worktree.originalCwd) : undefined;
1890
1605
  return executeRemoteSpec(spec, machine, metadata, opts, remoteFallbackSpec);
@@ -1915,9 +1630,6 @@ async function executeTarget(target, metadata = {}, opts = {}) {
1915
1630
  if (worktreeEntry?.fallbackCwd) {
1916
1631
  spec = worktreeFallbackSpec(target, opts, worktreeEntry.fallbackCwd) ?? spec;
1917
1632
  }
1918
- if (spec.codewithDurableAgent) {
1919
- return executeCodewithDurableAgent(spec, metadata, opts, env, startedAt);
1920
- }
1921
1633
  const child = spawn2(spec.command, spec.args, {
1922
1634
  cwd: spec.cwd,
1923
1635
  env,