@hasna/loops 0.3.58 → 0.3.60

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/sdk/index.js CHANGED
@@ -356,6 +356,20 @@ function optionalStringArray(value, label) {
356
356
  }).filter(Boolean);
357
357
  return values.length ? values : undefined;
358
358
  }
359
+ var UNSAFE_CODEWITH_DURABLE_EXTRA_ARGS = new Set([
360
+ "e",
361
+ "exec",
362
+ "agent",
363
+ "start",
364
+ "--ephemeral",
365
+ "--ignore-rules",
366
+ "--skip-git-repo-check",
367
+ "--json",
368
+ "--output-last-message",
369
+ "-o",
370
+ "--output-schema",
371
+ "--dangerously-bypass-approvals-and-sandbox"
372
+ ]);
359
373
  function optionalAccountRef(value, label) {
360
374
  if (value === undefined)
361
375
  return;
@@ -470,6 +484,15 @@ function validateTarget(value, label, opts) {
470
484
  throw new Error(`${label}.variant is not supported for provider cursor`);
471
485
  if (value.provider === "codex" && value.agent !== undefined)
472
486
  throw new Error(`${label}.agent is not supported for provider codex`);
487
+ if (value.provider === "codewith" && value.agent !== undefined) {
488
+ throw new Error(`${label}.agent is not supported for provider codewith durable background-agent execution`);
489
+ }
490
+ const extraArgs = optionalStringArray(value.extraArgs, `${label}.extraArgs`);
491
+ if (value.provider === "codewith") {
492
+ const unsafe = extraArgs?.find((arg) => UNSAFE_CODEWITH_DURABLE_EXTRA_ARGS.has(arg));
493
+ if (unsafe)
494
+ throw new Error(`${label}.extraArgs cannot include ${unsafe}; codewith agent steps use durable agent start, not exec/ephemeral flags`);
495
+ }
473
496
  optionalStringArray(value.addDirs, `${label}.addDirs`);
474
497
  if (Array.isArray(value.addDirs) && value.addDirs.length > 0 && !["codewith", "codex"].includes(value.provider)) {
475
498
  throw new Error(`${label}.addDirs is currently supported only for provider codewith or codex`);
@@ -545,7 +568,9 @@ function validateTarget(value, label, opts) {
545
568
  if (value.routing.eventSource !== undefined)
546
569
  assertString(value.routing.eventSource, `${label}.routing.eventSource`);
547
570
  }
548
- const target = { ...value };
571
+ const target = { ...value, extraArgs };
572
+ if (!extraArgs)
573
+ delete target.extraArgs;
549
574
  delete target.promptFile;
550
575
  delete target.promptSource;
551
576
  return { ...target, ...promptFields };
@@ -1541,6 +1566,70 @@ class Store {
1541
1566
  throw error;
1542
1567
  }
1543
1568
  }
1569
+ cloneWorkflowWithoutGoalAndRetargetLoop(idOrName, opts) {
1570
+ const updated = (opts.now ?? new Date).toISOString();
1571
+ this.db.exec("BEGIN IMMEDIATE");
1572
+ try {
1573
+ const current = this.requireUniqueLoop(idOrName);
1574
+ if (current.target.type !== "workflow")
1575
+ throw new Error(`loop is not a workflow loop: ${idOrName}`);
1576
+ if (this.hasRunningRun(current.id))
1577
+ throw new Error(`refusing to retarget running loop: ${current.id}`);
1578
+ if (!current.goal)
1579
+ throw new Error(`workflow loop ${current.name} has no loop-level goal wrapper`);
1580
+ const previousWorkflow = this.requireWorkflow(current.target.workflowId);
1581
+ if (!previousWorkflow.goal)
1582
+ throw new Error(`workflow ${previousWorkflow.name} has no top-level goal wrapper`);
1583
+ const workflow = {
1584
+ id: genId(),
1585
+ name: opts.workflowName,
1586
+ description: previousWorkflow.description,
1587
+ version: previousWorkflow.version,
1588
+ status: "active",
1589
+ steps: previousWorkflow.steps,
1590
+ createdAt: updated,
1591
+ updatedAt: updated
1592
+ };
1593
+ this.db.query(`INSERT INTO workflow_specs (id, name, description, version, status, goal_json, steps_json, created_at, updated_at)
1594
+ VALUES ($id, $name, $description, $version, $status, NULL, $steps, $created, $updated)`).run({
1595
+ $id: workflow.id,
1596
+ $name: workflow.name,
1597
+ $description: workflow.description ?? null,
1598
+ $version: workflow.version,
1599
+ $status: workflow.status,
1600
+ $steps: JSON.stringify(workflow.steps),
1601
+ $created: workflow.createdAt,
1602
+ $updated: workflow.updatedAt
1603
+ });
1604
+ const target = { ...current.target, workflowId: workflow.id };
1605
+ if (opts.workflowTimeoutMs !== undefined)
1606
+ target.timeoutMs = opts.workflowTimeoutMs;
1607
+ const res = this.db.query(`UPDATE loops SET target_json=$target, updated_at=$updated
1608
+ WHERE id=$id
1609
+ AND ($daemonLeaseId IS NULL OR EXISTS (
1610
+ SELECT 1 FROM daemon_lease WHERE id=$daemonLeaseId AND expires_at > $now
1611
+ ))`).run({
1612
+ $id: current.id,
1613
+ $target: JSON.stringify(target),
1614
+ $updated: updated,
1615
+ $daemonLeaseId: opts.daemonLeaseId ?? null,
1616
+ $now: updated
1617
+ });
1618
+ if (res.changes !== 1)
1619
+ throw new Error("daemon lease lost");
1620
+ const archivedOld = opts.archiveOld ? this.archiveWorkflowIfUnreferenced(previousWorkflow.id, updated) : undefined;
1621
+ this.db.exec("COMMIT");
1622
+ const loop = this.getLoop(current.id);
1623
+ if (!loop)
1624
+ throw new Error(`loop not found after retarget: ${current.id}`);
1625
+ return { loop, workflow, previousWorkflow, archivedOld };
1626
+ } catch (error) {
1627
+ try {
1628
+ this.db.exec("ROLLBACK");
1629
+ } catch {}
1630
+ throw error;
1631
+ }
1632
+ }
1544
1633
  renameLoop(id, name, opts = {}) {
1545
1634
  const current = this.getLoop(id);
1546
1635
  if (!current)
@@ -3414,6 +3503,9 @@ function appendBounded(current, chunk, maxBytes) {
3414
3503
  return `[truncated ${overflow} bytes]
3415
3504
  ${next.slice(-maxBytes)}`;
3416
3505
  }
3506
+ function appendBoundedText(current, chunk, maxBytes) {
3507
+ return appendBounded(current, Buffer.from(chunk, "utf8"), maxBytes);
3508
+ }
3417
3509
  function killProcessGroup(pid) {
3418
3510
  try {
3419
3511
  process.kill(-pid, "SIGTERM");
@@ -3461,6 +3553,21 @@ function metadataEnv(metadata) {
3461
3553
  env.LOOPS_GOAL_NODE_KEY = metadata.goalNodeKey;
3462
3554
  return env;
3463
3555
  }
3556
+ function codewithAgentIdempotencyKey(metadata) {
3557
+ const parts = [
3558
+ "openloops",
3559
+ metadata.workflowRunId ? `workflow-run:${metadata.workflowRunId}` : undefined,
3560
+ metadata.workflowStepId ? `step:${metadata.workflowStepId}` : undefined,
3561
+ metadata.runId ? `loop-run:${metadata.runId}` : undefined,
3562
+ metadata.loopId ? `loop:${metadata.loopId}` : undefined,
3563
+ metadata.scheduledFor ? `scheduled:${metadata.scheduledFor}` : undefined,
3564
+ metadata.goalId ? `goal:${metadata.goalId}` : undefined,
3565
+ metadata.goalNodeKey ? `node:${metadata.goalNodeKey}` : undefined
3566
+ ].filter(Boolean);
3567
+ if (parts.length > 1)
3568
+ return parts.join(":");
3569
+ return `openloops:adhoc:${randomBytes2(16).toString("hex")}`;
3570
+ }
3464
3571
  function allowlistEnv(allowlist) {
3465
3572
  const env = {};
3466
3573
  if (allowlist?.tools?.length)
@@ -3497,6 +3604,20 @@ function codewithLikeSandbox(target) {
3497
3604
  function configStringValue(value) {
3498
3605
  return JSON.stringify(value);
3499
3606
  }
3607
+ var UNSAFE_CODEWITH_DURABLE_EXTRA_ARGS2 = new Set([
3608
+ "e",
3609
+ "exec",
3610
+ "agent",
3611
+ "start",
3612
+ "--ephemeral",
3613
+ "--ignore-rules",
3614
+ "--skip-git-repo-check",
3615
+ "--json",
3616
+ "--output-last-message",
3617
+ "-o",
3618
+ "--output-schema",
3619
+ "--dangerously-bypass-approvals-and-sandbox"
3620
+ ]);
3500
3621
  function assertStringOption(value, label) {
3501
3622
  if (value !== undefined && typeof value !== "string")
3502
3623
  throw new Error(`${label} must be a string`);
@@ -3527,6 +3648,14 @@ function assertSupportedAgentOptions(target) {
3527
3648
  }
3528
3649
  if (target.provider === "codex" && target.agent !== undefined)
3529
3650
  throw new Error("codex.agent is not supported");
3651
+ if (target.provider === "codewith" && target.agent !== undefined) {
3652
+ throw new Error("codewith.agent is not supported by the durable background-agent adapter");
3653
+ }
3654
+ if (target.provider === "codewith") {
3655
+ const unsafe = target.extraArgs?.find((arg) => UNSAFE_CODEWITH_DURABLE_EXTRA_ARGS2.has(arg));
3656
+ if (unsafe)
3657
+ throw new Error(`codewith.extraArgs cannot include ${unsafe}; durable agent steps use codewith agent start, not exec/ephemeral flags`);
3658
+ }
3530
3659
  if (["codewith", "codex"].includes(target.provider)) {
3531
3660
  if (target.permissionMode && !["default", "bypass"].includes(target.permissionMode)) {
3532
3661
  throw new Error(`${target.provider}.permissionMode supports only default or bypass`);
@@ -3610,18 +3739,18 @@ function agentArgs(target) {
3610
3739
  args.push(...target.authProfile ? ["--auth-profile", target.authProfile] : []);
3611
3740
  if (target.variant)
3612
3741
  args.push("-c", `model_reasoning_effort=${configStringValue(target.variant)}`);
3613
- args.push("--ask-for-approval", "never", "exec", "--json", "--ephemeral", "--sandbox", codewithLikeSandbox(target), "--skip-git-repo-check");
3614
- if (isolation === "safe")
3615
- args.push("--ignore-rules");
3742
+ args.push("--ask-for-approval", "never", "--sandbox", codewithLikeSandbox(target));
3616
3743
  if (target.cwd)
3617
3744
  args.push("--cd", target.cwd);
3618
3745
  for (const dir of target.addDirs ?? [])
3619
3746
  args.push("--add-dir", dir);
3620
3747
  if (target.model)
3621
3748
  args.push("--model", target.model);
3622
- if (target.agent)
3623
- args.push("--agent", target.agent);
3624
3749
  args.push(...target.extraArgs ?? []);
3750
+ args.push("agent", "start");
3751
+ if (target.cwd)
3752
+ args.push("--cwd", target.cwd);
3753
+ args.push(target.prompt);
3625
3754
  return args;
3626
3755
  case "codex":
3627
3756
  if (target.variant)
@@ -3697,8 +3826,9 @@ function commandSpec(target) {
3697
3826
  accountTool: agentTarget.account?.tool ?? accountToolForProvider(agentTarget.provider),
3698
3827
  nativeAuthProfile: agentTarget.authProfile ? { provider: agentTarget.provider, profile: agentTarget.authProfile } : undefined,
3699
3828
  preflightAnyOf: agentTarget.provider === "cursor" ? ["agent"] : undefined,
3700
- stdin: agentTarget.prompt,
3701
- allowlist: agentTarget.allowlist
3829
+ stdin: agentTarget.provider === "codewith" ? undefined : agentTarget.prompt,
3830
+ allowlist: agentTarget.allowlist,
3831
+ codewithDurableAgent: agentTarget.provider === "codewith" ? { target: agentTarget } : undefined
3702
3832
  };
3703
3833
  }
3704
3834
  function executionEnv(spec, metadata, opts) {
@@ -3818,6 +3948,238 @@ function preflightNativeAuthProfile(spec, env) {
3818
3948
  throw new Error(`codewith auth profile not found: ${spec.nativeAuthProfile.profile}`);
3819
3949
  }
3820
3950
  }
3951
+ function codewithAgentStartArgs(target, idempotencyKey) {
3952
+ const args = agentArgs(target);
3953
+ const startIndex = args.findIndex((arg, index) => arg === "start" && args[index - 1] === "agent");
3954
+ if (startIndex === -1)
3955
+ throw new Error("internal error: codewith durable agent args missing agent start");
3956
+ args.splice(startIndex + 1, 0, "--idempotency-key", idempotencyKey);
3957
+ return args;
3958
+ }
3959
+ function codewithAgentControlArgs(target, command, agentId) {
3960
+ return [
3961
+ ...target.authProfile ? ["--auth-profile", target.authProfile] : [],
3962
+ "agent",
3963
+ command,
3964
+ ...command === "logs" ? ["--limit", "20"] : [],
3965
+ agentId
3966
+ ];
3967
+ }
3968
+ function parseJsonOutput(stdout, label) {
3969
+ try {
3970
+ const value = JSON.parse(stdout || "{}");
3971
+ if (!value || typeof value !== "object" || Array.isArray(value))
3972
+ throw new Error("not an object");
3973
+ return value;
3974
+ } catch (error) {
3975
+ throw new Error(`${label} did not return JSON${error instanceof Error ? `: ${error.message}` : ""}`);
3976
+ }
3977
+ }
3978
+ function recordField(value, key) {
3979
+ if (!value || typeof value !== "object" || Array.isArray(value))
3980
+ return;
3981
+ const field = value[key];
3982
+ return field && typeof field === "object" && !Array.isArray(field) ? field : undefined;
3983
+ }
3984
+ function stringField(value, key) {
3985
+ const field = value?.[key];
3986
+ return typeof field === "string" ? field : undefined;
3987
+ }
3988
+ function numberField(value, key) {
3989
+ const field = value?.[key];
3990
+ return typeof field === "number" && Number.isFinite(field) ? field : undefined;
3991
+ }
3992
+ function codewithAgentStatus(readJson) {
3993
+ return stringField(recordField(readJson, "agent"), "status") ?? stringField(recordField(readJson, "statusSnapshot"), "status");
3994
+ }
3995
+ function codewithAgentEvidence(startJson, readJson, logsJson) {
3996
+ const agent = recordField(readJson, "agent") ?? recordField(startJson, "agent");
3997
+ const statusSnapshot = recordField(readJson, "statusSnapshot");
3998
+ const events = Array.isArray(logsJson?.data) ? logsJson.data.filter((event) => Boolean(event) && typeof event === "object" && !Array.isArray(event)).map((event) => ({
3999
+ seq: numberField(event, "seq"),
4000
+ eventType: stringField(event, "eventType"),
4001
+ createdAt: numberField(event, "createdAt")
4002
+ })) : undefined;
4003
+ return JSON.stringify({
4004
+ codewithAgent: {
4005
+ agentId: stringField(agent, "agentId"),
4006
+ status: stringField(agent, "status"),
4007
+ desiredState: stringField(agent, "desiredState"),
4008
+ statusReason: stringField(agent, "statusReason"),
4009
+ threadId: stringField(agent, "threadId"),
4010
+ rolloutPath: stringField(agent, "rolloutPath"),
4011
+ pid: numberField(agent, "pid"),
4012
+ exitCode: numberField(agent, "exitCode"),
4013
+ created: typeof startJson.created === "boolean" ? startJson.created : undefined
4014
+ },
4015
+ statusSnapshot: statusSnapshot ? {
4016
+ seq: numberField(statusSnapshot, "seq"),
4017
+ status: stringField(statusSnapshot, "status"),
4018
+ summary: stringField(statusSnapshot, "summary"),
4019
+ pendingInteractionCount: numberField(statusSnapshot, "pendingInteractionCount"),
4020
+ lastEventSeq: numberField(statusSnapshot, "lastEventSeq")
4021
+ } : undefined,
4022
+ events
4023
+ }, null, 2);
4024
+ }
4025
+ function sleep(ms) {
4026
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
4027
+ }
4028
+ async function executeCodewithDurableAgent(spec, metadata, opts, env, startedAt) {
4029
+ const target = spec.codewithDurableAgent?.target;
4030
+ if (!target)
4031
+ throw new Error("internal error: missing codewith durable target");
4032
+ const maxOutputBytes = opts.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
4033
+ const idempotencyKey = codewithAgentIdempotencyKey(metadata);
4034
+ const start = spawnSync2(spec.command, codewithAgentStartArgs(target, idempotencyKey), {
4035
+ cwd: spec.cwd,
4036
+ env,
4037
+ encoding: "utf8",
4038
+ stdio: ["ignore", "pipe", "pipe"],
4039
+ timeout: 30000
4040
+ });
4041
+ let stderr = start.stderr || "";
4042
+ if (start.error || (start.status ?? 1) !== 0) {
4043
+ const finishedAt = nowIso();
4044
+ return {
4045
+ status: "failed",
4046
+ exitCode: start.status ?? undefined,
4047
+ stdout: start.stdout || "",
4048
+ stderr,
4049
+ error: start.error?.message ?? `codewith agent start exited with code ${start.status ?? "unknown"}`,
4050
+ startedAt,
4051
+ finishedAt,
4052
+ durationMs: new Date(finishedAt).getTime() - new Date(startedAt).getTime()
4053
+ };
4054
+ }
4055
+ let startJson;
4056
+ try {
4057
+ startJson = parseJsonOutput(start.stdout || "{}", "codewith agent start");
4058
+ } catch (error) {
4059
+ const finishedAt = nowIso();
4060
+ return {
4061
+ status: "failed",
4062
+ stdout: start.stdout || "",
4063
+ stderr,
4064
+ error: error instanceof Error ? error.message : String(error),
4065
+ startedAt,
4066
+ finishedAt,
4067
+ durationMs: new Date(finishedAt).getTime() - new Date(startedAt).getTime()
4068
+ };
4069
+ }
4070
+ const agentId = stringField(recordField(startJson, "agent"), "agentId");
4071
+ if (!agentId) {
4072
+ const finishedAt = nowIso();
4073
+ return {
4074
+ status: "failed",
4075
+ stdout: start.stdout || "",
4076
+ stderr,
4077
+ error: "codewith agent start did not return agent.agentId",
4078
+ startedAt,
4079
+ finishedAt,
4080
+ durationMs: new Date(finishedAt).getTime() - new Date(startedAt).getTime()
4081
+ };
4082
+ }
4083
+ const pollMs = Math.max(100, Number(opts.env?.LOOPS_CODEWITH_AGENT_POLL_MS ?? process.env.LOOPS_CODEWITH_AGENT_POLL_MS ?? 2000) || 2000);
4084
+ let lastReadJson = startJson;
4085
+ let lastLogsJson;
4086
+ let stdout = "";
4087
+ while (true) {
4088
+ if (opts.signal?.aborted) {
4089
+ spawnSync2(spec.command, codewithAgentControlArgs(target, "stop", agentId), { cwd: spec.cwd, env, stdio: "ignore", timeout: 15000 });
4090
+ const finishedAt = nowIso();
4091
+ return {
4092
+ status: "failed",
4093
+ stdout: appendBoundedText(stdout, codewithAgentEvidence(startJson, lastReadJson, lastLogsJson), maxOutputBytes),
4094
+ stderr,
4095
+ error: "cancelled",
4096
+ startedAt,
4097
+ finishedAt,
4098
+ durationMs: new Date(finishedAt).getTime() - new Date(startedAt).getTime()
4099
+ };
4100
+ }
4101
+ const read = spawnSync2(spec.command, codewithAgentControlArgs(target, "read", agentId), {
4102
+ cwd: spec.cwd,
4103
+ env,
4104
+ encoding: "utf8",
4105
+ stdio: ["ignore", "pipe", "pipe"],
4106
+ timeout: 30000
4107
+ });
4108
+ stderr = appendBoundedText(stderr, read.stderr || "", maxOutputBytes);
4109
+ if (read.error || (read.status ?? 1) !== 0) {
4110
+ const finishedAt = nowIso();
4111
+ return {
4112
+ status: "failed",
4113
+ exitCode: read.status ?? undefined,
4114
+ stdout: appendBoundedText(stdout, codewithAgentEvidence(startJson, lastReadJson, lastLogsJson), maxOutputBytes),
4115
+ stderr,
4116
+ error: read.error?.message ?? `codewith agent read exited with code ${read.status ?? "unknown"}`,
4117
+ startedAt,
4118
+ finishedAt,
4119
+ durationMs: new Date(finishedAt).getTime() - new Date(startedAt).getTime()
4120
+ };
4121
+ }
4122
+ try {
4123
+ lastReadJson = parseJsonOutput(read.stdout || "{}", "codewith agent read");
4124
+ } catch (error) {
4125
+ const finishedAt = nowIso();
4126
+ return {
4127
+ status: "failed",
4128
+ stdout: appendBoundedText(stdout, read.stdout || "", maxOutputBytes),
4129
+ stderr,
4130
+ error: error instanceof Error ? error.message : String(error),
4131
+ startedAt,
4132
+ finishedAt,
4133
+ durationMs: new Date(finishedAt).getTime() - new Date(startedAt).getTime()
4134
+ };
4135
+ }
4136
+ const status = codewithAgentStatus(lastReadJson);
4137
+ if (status === "completed" || status === "failed" || status === "cancelled") {
4138
+ const logs = spawnSync2(spec.command, codewithAgentControlArgs(target, "logs", agentId), {
4139
+ cwd: spec.cwd,
4140
+ env,
4141
+ encoding: "utf8",
4142
+ stdio: ["ignore", "pipe", "pipe"],
4143
+ timeout: 30000
4144
+ });
4145
+ if (!logs.error && (logs.status ?? 1) === 0) {
4146
+ try {
4147
+ lastLogsJson = parseJsonOutput(logs.stdout || "{}", "codewith agent logs");
4148
+ } catch {
4149
+ lastLogsJson = undefined;
4150
+ }
4151
+ } else {
4152
+ stderr = appendBoundedText(stderr, logs.stderr || logs.error?.message || "", maxOutputBytes);
4153
+ }
4154
+ const finishedAt = nowIso();
4155
+ const resultStatus = status === "completed" ? "succeeded" : "failed";
4156
+ return {
4157
+ status: resultStatus,
4158
+ exitCode: resultStatus === "succeeded" ? 0 : 1,
4159
+ stdout: appendBoundedText(stdout, codewithAgentEvidence(startJson, lastReadJson, lastLogsJson), maxOutputBytes),
4160
+ stderr,
4161
+ error: resultStatus === "succeeded" ? undefined : stringField(recordField(lastReadJson, "agent"), "statusReason") ?? `codewith agent ${status}`,
4162
+ startedAt,
4163
+ finishedAt,
4164
+ durationMs: new Date(finishedAt).getTime() - new Date(startedAt).getTime()
4165
+ };
4166
+ }
4167
+ if (typeof spec.timeoutMs === "number" && new Date(nowIso()).getTime() - new Date(startedAt).getTime() >= spec.timeoutMs) {
4168
+ spawnSync2(spec.command, codewithAgentControlArgs(target, "stop", agentId), { cwd: spec.cwd, env, stdio: "ignore", timeout: 15000 });
4169
+ const finishedAt = nowIso();
4170
+ return {
4171
+ status: "timed_out",
4172
+ stdout: appendBoundedText(stdout, codewithAgentEvidence(startJson, lastReadJson, lastLogsJson), maxOutputBytes),
4173
+ stderr,
4174
+ error: `timed out after ${spec.timeoutMs}ms`,
4175
+ startedAt,
4176
+ finishedAt,
4177
+ durationMs: new Date(finishedAt).getTime() - new Date(startedAt).getTime()
4178
+ };
4179
+ }
4180
+ await sleep(pollMs);
4181
+ }
4182
+ }
3821
4183
  function preflightRemoteSpec(spec, machine, metadata, opts) {
3822
4184
  const plan = (opts.machineCommandResolver ?? resolveMachineCommand)(machine.id, "bash -s");
3823
4185
  const result = spawnSync2(plan.command, plan.args, {
@@ -3989,6 +4351,19 @@ function preflightTarget(target, metadata = {}, opts = {}) {
3989
4351
  async function executeTarget(target, metadata = {}, opts = {}) {
3990
4352
  const spec = commandSpec(target);
3991
4353
  const machine = resolvedMachine(opts);
4354
+ if (machine && !machine.local && spec.codewithDurableAgent) {
4355
+ const startedAt2 = nowIso();
4356
+ const finishedAt2 = nowIso();
4357
+ return {
4358
+ status: "failed",
4359
+ stdout: "",
4360
+ stderr: "",
4361
+ error: "remote Codewith durable background-agent steps require remote status polling support; run this Codewith step locally or add remote durable readback before dispatch",
4362
+ startedAt: startedAt2,
4363
+ finishedAt: finishedAt2,
4364
+ durationMs: new Date(finishedAt2).getTime() - new Date(startedAt2).getTime()
4365
+ };
4366
+ }
3992
4367
  if (machine && !machine.local)
3993
4368
  return executeRemoteSpec(spec, machine, metadata, opts);
3994
4369
  const maxOutputBytes = opts.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
@@ -4035,6 +4410,9 @@ async function executeTarget(target, metadata = {}, opts = {}) {
4035
4410
  durationMs: 0
4036
4411
  };
4037
4412
  }
4413
+ if (spec.codewithDurableAgent) {
4414
+ return executeCodewithDurableAgent(spec, metadata, opts, env, startedAt);
4415
+ }
4038
4416
  const child = spawn(spec.command, spec.args, {
4039
4417
  cwd: spec.cwd,
4040
4418
  env,
package/docs/USAGE.md CHANGED
@@ -6,7 +6,7 @@ It supports deterministic command loops, JSON-defined workflows, and guarded CLI
6
6
 
7
7
  - `claude`
8
8
  - `agent` (Cursor Agent CLI)
9
- - `codewith exec`
9
+ - `codewith agent start`
10
10
  - `aicopilot run`
11
11
  - `opencode run`
12
12
  - `codex exec`
@@ -893,13 +893,13 @@ On Linux this writes a user systemd service. On macOS it writes a LaunchAgent pl
893
893
  The adapters intentionally use provider command surfaces instead of pretending every agent has one SDK:
894
894
 
895
895
  - Claude uses `claude -p --output-format json` and safe-mode/local setting sources by default.
896
- - Codewith uses `codewith --ask-for-approval never exec --json --ephemeral --skip-git-repo-check`, with `--add-dir` for explicit extra writable directories.
896
+ - Codewith uses durable `codewith --ask-for-approval never agent start` background-agent runs by default, then polls `codewith agent read` until the run is terminal and records compact status/event evidence. Remote Codewith agent steps fail closed until remote durable readback is implemented, so workflows do not advance immediately after enqueue. OpenLoops rejects Codewith `extraArgs` that try to force `exec`, `--ephemeral`, or other non-durable exec-only flags for task-lifecycle, planner, worker, verifier, reviewer, release, or other long-running agentic work.
897
897
  - AI Copilot and OpenCode use `run --format json --pure`. OpenCode requires an explicit provider/model id because ambient OpenCode config is machine-specific.
898
898
  - Cursor is CLI-first for now via the standalone `agent -p` binary. OpenLoops no longer falls back to `cursor agent`; install the standalone Cursor Agent CLI so preflight and scheduled runs use the same executable.
899
899
  - Codex uses `codex --ask-for-approval never exec --json --ephemeral --skip-git-repo-check`, with `--add-dir` for explicit extra writable directories where supported.
900
- - Agent prompts are sent through child stdin instead of argv so prompt bodies do not appear in process listings.
900
+ - Agent prompts are sent through child stdin instead of argv where the provider supports stdin. Codewith durable background agents currently accept prompts as native `agent start` arguments, so OpenLoops stores only bounded status/event evidence and omits raw Codewith event payloads from workflow stdout.
901
901
  - When `--account` or a step `account` is set, OpenLoops resolves `accounts env <profile> --tool <tool>` before spawning the target, strips inherited tool home/API-key variables, and applies the selected profile only to that process. Missing account profiles fail before the provider binary receives the prompt.
902
- - `--auth-profile` and step `authProfile` are provider-native auth selectors. They currently apply to Codewith and are passed to Codewith as `--auth-profile <name>` before `exec`; they do not call OpenAccounts.
902
+ - `--auth-profile` and step `authProfile` are provider-native auth selectors. They currently apply to Codewith and are passed to Codewith as `--auth-profile <name>` before `agent start/read/logs`; they do not call OpenAccounts.
903
903
  - `--sandbox` maps to provider-native sandbox flags. Codewith/Codex accept `read-only`, `workspace-write`, or `danger-full-access`; Cursor accepts `enabled` or `disabled`.
904
904
  - `--permission-mode` maps `plan`, `auto`, and `bypass` where the provider supports it. Claude uses native permission modes, Cursor maps bypass to `--force`, and OpenCode/AICopilot map bypass to `--dangerously-skip-permissions`.
905
905
  - `--variant` is provider-specific reasoning/model effort. Claude maps it to `--effort`, Codewith/Codex map it to `model_reasoning_effort`, and OpenCode/AICopilot pass `--variant`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/loops",
3
- "version": "0.3.58",
3
+ "version": "0.3.60",
4
4
  "description": "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",