@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/mcp/index.js CHANGED
@@ -358,6 +358,20 @@ function optionalStringArray(value, label) {
358
358
  }).filter(Boolean);
359
359
  return values.length ? values : undefined;
360
360
  }
361
+ var UNSAFE_CODEWITH_DURABLE_EXTRA_ARGS = new Set([
362
+ "e",
363
+ "exec",
364
+ "agent",
365
+ "start",
366
+ "--ephemeral",
367
+ "--ignore-rules",
368
+ "--skip-git-repo-check",
369
+ "--json",
370
+ "--output-last-message",
371
+ "-o",
372
+ "--output-schema",
373
+ "--dangerously-bypass-approvals-and-sandbox"
374
+ ]);
361
375
  function optionalAccountRef(value, label) {
362
376
  if (value === undefined)
363
377
  return;
@@ -472,6 +486,15 @@ function validateTarget(value, label, opts) {
472
486
  throw new Error(`${label}.variant is not supported for provider cursor`);
473
487
  if (value.provider === "codex" && value.agent !== undefined)
474
488
  throw new Error(`${label}.agent is not supported for provider codex`);
489
+ if (value.provider === "codewith" && value.agent !== undefined) {
490
+ throw new Error(`${label}.agent is not supported for provider codewith durable background-agent execution`);
491
+ }
492
+ const extraArgs = optionalStringArray(value.extraArgs, `${label}.extraArgs`);
493
+ if (value.provider === "codewith") {
494
+ const unsafe = extraArgs?.find((arg) => UNSAFE_CODEWITH_DURABLE_EXTRA_ARGS.has(arg));
495
+ if (unsafe)
496
+ throw new Error(`${label}.extraArgs cannot include ${unsafe}; codewith agent steps use durable agent start, not exec/ephemeral flags`);
497
+ }
475
498
  optionalStringArray(value.addDirs, `${label}.addDirs`);
476
499
  if (Array.isArray(value.addDirs) && value.addDirs.length > 0 && !["codewith", "codex"].includes(value.provider)) {
477
500
  throw new Error(`${label}.addDirs is currently supported only for provider codewith or codex`);
@@ -547,7 +570,9 @@ function validateTarget(value, label, opts) {
547
570
  if (value.routing.eventSource !== undefined)
548
571
  assertString(value.routing.eventSource, `${label}.routing.eventSource`);
549
572
  }
550
- const target = { ...value };
573
+ const target = { ...value, extraArgs };
574
+ if (!extraArgs)
575
+ delete target.extraArgs;
551
576
  delete target.promptFile;
552
577
  delete target.promptSource;
553
578
  return { ...target, ...promptFields };
@@ -1543,6 +1568,70 @@ class Store {
1543
1568
  throw error;
1544
1569
  }
1545
1570
  }
1571
+ cloneWorkflowWithoutGoalAndRetargetLoop(idOrName, opts) {
1572
+ const updated = (opts.now ?? new Date).toISOString();
1573
+ this.db.exec("BEGIN IMMEDIATE");
1574
+ try {
1575
+ const current = this.requireUniqueLoop(idOrName);
1576
+ if (current.target.type !== "workflow")
1577
+ throw new Error(`loop is not a workflow loop: ${idOrName}`);
1578
+ if (this.hasRunningRun(current.id))
1579
+ throw new Error(`refusing to retarget running loop: ${current.id}`);
1580
+ if (!current.goal)
1581
+ throw new Error(`workflow loop ${current.name} has no loop-level goal wrapper`);
1582
+ const previousWorkflow = this.requireWorkflow(current.target.workflowId);
1583
+ if (!previousWorkflow.goal)
1584
+ throw new Error(`workflow ${previousWorkflow.name} has no top-level goal wrapper`);
1585
+ const workflow = {
1586
+ id: genId(),
1587
+ name: opts.workflowName,
1588
+ description: previousWorkflow.description,
1589
+ version: previousWorkflow.version,
1590
+ status: "active",
1591
+ steps: previousWorkflow.steps,
1592
+ createdAt: updated,
1593
+ updatedAt: updated
1594
+ };
1595
+ this.db.query(`INSERT INTO workflow_specs (id, name, description, version, status, goal_json, steps_json, created_at, updated_at)
1596
+ VALUES ($id, $name, $description, $version, $status, NULL, $steps, $created, $updated)`).run({
1597
+ $id: workflow.id,
1598
+ $name: workflow.name,
1599
+ $description: workflow.description ?? null,
1600
+ $version: workflow.version,
1601
+ $status: workflow.status,
1602
+ $steps: JSON.stringify(workflow.steps),
1603
+ $created: workflow.createdAt,
1604
+ $updated: workflow.updatedAt
1605
+ });
1606
+ const target = { ...current.target, workflowId: workflow.id };
1607
+ if (opts.workflowTimeoutMs !== undefined)
1608
+ target.timeoutMs = opts.workflowTimeoutMs;
1609
+ const res = this.db.query(`UPDATE loops SET target_json=$target, updated_at=$updated
1610
+ WHERE id=$id
1611
+ AND ($daemonLeaseId IS NULL OR EXISTS (
1612
+ SELECT 1 FROM daemon_lease WHERE id=$daemonLeaseId AND expires_at > $now
1613
+ ))`).run({
1614
+ $id: current.id,
1615
+ $target: JSON.stringify(target),
1616
+ $updated: updated,
1617
+ $daemonLeaseId: opts.daemonLeaseId ?? null,
1618
+ $now: updated
1619
+ });
1620
+ if (res.changes !== 1)
1621
+ throw new Error("daemon lease lost");
1622
+ const archivedOld = opts.archiveOld ? this.archiveWorkflowIfUnreferenced(previousWorkflow.id, updated) : undefined;
1623
+ this.db.exec("COMMIT");
1624
+ const loop = this.getLoop(current.id);
1625
+ if (!loop)
1626
+ throw new Error(`loop not found after retarget: ${current.id}`);
1627
+ return { loop, workflow, previousWorkflow, archivedOld };
1628
+ } catch (error) {
1629
+ try {
1630
+ this.db.exec("ROLLBACK");
1631
+ } catch {}
1632
+ throw error;
1633
+ }
1634
+ }
1546
1635
  renameLoop(id, name, opts = {}) {
1547
1636
  const current = this.getLoop(id);
1548
1637
  if (!current)
@@ -3551,6 +3640,9 @@ function appendBounded(current, chunk, maxBytes) {
3551
3640
  return `[truncated ${overflow} bytes]
3552
3641
  ${next.slice(-maxBytes)}`;
3553
3642
  }
3643
+ function appendBoundedText(current, chunk, maxBytes) {
3644
+ return appendBounded(current, Buffer.from(chunk, "utf8"), maxBytes);
3645
+ }
3554
3646
  function killProcessGroup(pid) {
3555
3647
  try {
3556
3648
  process.kill(-pid, "SIGTERM");
@@ -3598,6 +3690,21 @@ function metadataEnv(metadata) {
3598
3690
  env.LOOPS_GOAL_NODE_KEY = metadata.goalNodeKey;
3599
3691
  return env;
3600
3692
  }
3693
+ function codewithAgentIdempotencyKey(metadata) {
3694
+ const parts = [
3695
+ "openloops",
3696
+ metadata.workflowRunId ? `workflow-run:${metadata.workflowRunId}` : undefined,
3697
+ metadata.workflowStepId ? `step:${metadata.workflowStepId}` : undefined,
3698
+ metadata.runId ? `loop-run:${metadata.runId}` : undefined,
3699
+ metadata.loopId ? `loop:${metadata.loopId}` : undefined,
3700
+ metadata.scheduledFor ? `scheduled:${metadata.scheduledFor}` : undefined,
3701
+ metadata.goalId ? `goal:${metadata.goalId}` : undefined,
3702
+ metadata.goalNodeKey ? `node:${metadata.goalNodeKey}` : undefined
3703
+ ].filter(Boolean);
3704
+ if (parts.length > 1)
3705
+ return parts.join(":");
3706
+ return `openloops:adhoc:${randomBytes2(16).toString("hex")}`;
3707
+ }
3601
3708
  function allowlistEnv(allowlist) {
3602
3709
  const env = {};
3603
3710
  if (allowlist?.tools?.length)
@@ -3634,6 +3741,20 @@ function codewithLikeSandbox(target) {
3634
3741
  function configStringValue(value) {
3635
3742
  return JSON.stringify(value);
3636
3743
  }
3744
+ var UNSAFE_CODEWITH_DURABLE_EXTRA_ARGS2 = new Set([
3745
+ "e",
3746
+ "exec",
3747
+ "agent",
3748
+ "start",
3749
+ "--ephemeral",
3750
+ "--ignore-rules",
3751
+ "--skip-git-repo-check",
3752
+ "--json",
3753
+ "--output-last-message",
3754
+ "-o",
3755
+ "--output-schema",
3756
+ "--dangerously-bypass-approvals-and-sandbox"
3757
+ ]);
3637
3758
  function assertStringOption(value, label) {
3638
3759
  if (value !== undefined && typeof value !== "string")
3639
3760
  throw new Error(`${label} must be a string`);
@@ -3664,6 +3785,14 @@ function assertSupportedAgentOptions(target) {
3664
3785
  }
3665
3786
  if (target.provider === "codex" && target.agent !== undefined)
3666
3787
  throw new Error("codex.agent is not supported");
3788
+ if (target.provider === "codewith" && target.agent !== undefined) {
3789
+ throw new Error("codewith.agent is not supported by the durable background-agent adapter");
3790
+ }
3791
+ if (target.provider === "codewith") {
3792
+ const unsafe = target.extraArgs?.find((arg) => UNSAFE_CODEWITH_DURABLE_EXTRA_ARGS2.has(arg));
3793
+ if (unsafe)
3794
+ throw new Error(`codewith.extraArgs cannot include ${unsafe}; durable agent steps use codewith agent start, not exec/ephemeral flags`);
3795
+ }
3667
3796
  if (["codewith", "codex"].includes(target.provider)) {
3668
3797
  if (target.permissionMode && !["default", "bypass"].includes(target.permissionMode)) {
3669
3798
  throw new Error(`${target.provider}.permissionMode supports only default or bypass`);
@@ -3747,18 +3876,18 @@ function agentArgs(target) {
3747
3876
  args.push(...target.authProfile ? ["--auth-profile", target.authProfile] : []);
3748
3877
  if (target.variant)
3749
3878
  args.push("-c", `model_reasoning_effort=${configStringValue(target.variant)}`);
3750
- args.push("--ask-for-approval", "never", "exec", "--json", "--ephemeral", "--sandbox", codewithLikeSandbox(target), "--skip-git-repo-check");
3751
- if (isolation === "safe")
3752
- args.push("--ignore-rules");
3879
+ args.push("--ask-for-approval", "never", "--sandbox", codewithLikeSandbox(target));
3753
3880
  if (target.cwd)
3754
3881
  args.push("--cd", target.cwd);
3755
3882
  for (const dir of target.addDirs ?? [])
3756
3883
  args.push("--add-dir", dir);
3757
3884
  if (target.model)
3758
3885
  args.push("--model", target.model);
3759
- if (target.agent)
3760
- args.push("--agent", target.agent);
3761
3886
  args.push(...target.extraArgs ?? []);
3887
+ args.push("agent", "start");
3888
+ if (target.cwd)
3889
+ args.push("--cwd", target.cwd);
3890
+ args.push(target.prompt);
3762
3891
  return args;
3763
3892
  case "codex":
3764
3893
  if (target.variant)
@@ -3834,8 +3963,9 @@ function commandSpec(target) {
3834
3963
  accountTool: agentTarget.account?.tool ?? accountToolForProvider(agentTarget.provider),
3835
3964
  nativeAuthProfile: agentTarget.authProfile ? { provider: agentTarget.provider, profile: agentTarget.authProfile } : undefined,
3836
3965
  preflightAnyOf: agentTarget.provider === "cursor" ? ["agent"] : undefined,
3837
- stdin: agentTarget.prompt,
3838
- allowlist: agentTarget.allowlist
3966
+ stdin: agentTarget.provider === "codewith" ? undefined : agentTarget.prompt,
3967
+ allowlist: agentTarget.allowlist,
3968
+ codewithDurableAgent: agentTarget.provider === "codewith" ? { target: agentTarget } : undefined
3839
3969
  };
3840
3970
  }
3841
3971
  function executionEnv(spec, metadata, opts) {
@@ -3955,6 +4085,238 @@ function preflightNativeAuthProfile(spec, env) {
3955
4085
  throw new Error(`codewith auth profile not found: ${spec.nativeAuthProfile.profile}`);
3956
4086
  }
3957
4087
  }
4088
+ function codewithAgentStartArgs(target, idempotencyKey) {
4089
+ const args = agentArgs(target);
4090
+ const startIndex = args.findIndex((arg, index) => arg === "start" && args[index - 1] === "agent");
4091
+ if (startIndex === -1)
4092
+ throw new Error("internal error: codewith durable agent args missing agent start");
4093
+ args.splice(startIndex + 1, 0, "--idempotency-key", idempotencyKey);
4094
+ return args;
4095
+ }
4096
+ function codewithAgentControlArgs(target, command, agentId) {
4097
+ return [
4098
+ ...target.authProfile ? ["--auth-profile", target.authProfile] : [],
4099
+ "agent",
4100
+ command,
4101
+ ...command === "logs" ? ["--limit", "20"] : [],
4102
+ agentId
4103
+ ];
4104
+ }
4105
+ function parseJsonOutput(stdout, label) {
4106
+ try {
4107
+ const value = JSON.parse(stdout || "{}");
4108
+ if (!value || typeof value !== "object" || Array.isArray(value))
4109
+ throw new Error("not an object");
4110
+ return value;
4111
+ } catch (error) {
4112
+ throw new Error(`${label} did not return JSON${error instanceof Error ? `: ${error.message}` : ""}`);
4113
+ }
4114
+ }
4115
+ function recordField(value, key) {
4116
+ if (!value || typeof value !== "object" || Array.isArray(value))
4117
+ return;
4118
+ const field = value[key];
4119
+ return field && typeof field === "object" && !Array.isArray(field) ? field : undefined;
4120
+ }
4121
+ function stringField(value, key) {
4122
+ const field = value?.[key];
4123
+ return typeof field === "string" ? field : undefined;
4124
+ }
4125
+ function numberField(value, key) {
4126
+ const field = value?.[key];
4127
+ return typeof field === "number" && Number.isFinite(field) ? field : undefined;
4128
+ }
4129
+ function codewithAgentStatus(readJson) {
4130
+ return stringField(recordField(readJson, "agent"), "status") ?? stringField(recordField(readJson, "statusSnapshot"), "status");
4131
+ }
4132
+ function codewithAgentEvidence(startJson, readJson, logsJson) {
4133
+ const agent = recordField(readJson, "agent") ?? recordField(startJson, "agent");
4134
+ const statusSnapshot = recordField(readJson, "statusSnapshot");
4135
+ const events = Array.isArray(logsJson?.data) ? logsJson.data.filter((event) => Boolean(event) && typeof event === "object" && !Array.isArray(event)).map((event) => ({
4136
+ seq: numberField(event, "seq"),
4137
+ eventType: stringField(event, "eventType"),
4138
+ createdAt: numberField(event, "createdAt")
4139
+ })) : undefined;
4140
+ return JSON.stringify({
4141
+ codewithAgent: {
4142
+ agentId: stringField(agent, "agentId"),
4143
+ status: stringField(agent, "status"),
4144
+ desiredState: stringField(agent, "desiredState"),
4145
+ statusReason: stringField(agent, "statusReason"),
4146
+ threadId: stringField(agent, "threadId"),
4147
+ rolloutPath: stringField(agent, "rolloutPath"),
4148
+ pid: numberField(agent, "pid"),
4149
+ exitCode: numberField(agent, "exitCode"),
4150
+ created: typeof startJson.created === "boolean" ? startJson.created : undefined
4151
+ },
4152
+ statusSnapshot: statusSnapshot ? {
4153
+ seq: numberField(statusSnapshot, "seq"),
4154
+ status: stringField(statusSnapshot, "status"),
4155
+ summary: stringField(statusSnapshot, "summary"),
4156
+ pendingInteractionCount: numberField(statusSnapshot, "pendingInteractionCount"),
4157
+ lastEventSeq: numberField(statusSnapshot, "lastEventSeq")
4158
+ } : undefined,
4159
+ events
4160
+ }, null, 2);
4161
+ }
4162
+ function sleep(ms) {
4163
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
4164
+ }
4165
+ async function executeCodewithDurableAgent(spec, metadata, opts, env, startedAt) {
4166
+ const target = spec.codewithDurableAgent?.target;
4167
+ if (!target)
4168
+ throw new Error("internal error: missing codewith durable target");
4169
+ const maxOutputBytes = opts.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
4170
+ const idempotencyKey = codewithAgentIdempotencyKey(metadata);
4171
+ const start = spawnSync2(spec.command, codewithAgentStartArgs(target, idempotencyKey), {
4172
+ cwd: spec.cwd,
4173
+ env,
4174
+ encoding: "utf8",
4175
+ stdio: ["ignore", "pipe", "pipe"],
4176
+ timeout: 30000
4177
+ });
4178
+ let stderr = start.stderr || "";
4179
+ if (start.error || (start.status ?? 1) !== 0) {
4180
+ const finishedAt = nowIso();
4181
+ return {
4182
+ status: "failed",
4183
+ exitCode: start.status ?? undefined,
4184
+ stdout: start.stdout || "",
4185
+ stderr,
4186
+ error: start.error?.message ?? `codewith agent start exited with code ${start.status ?? "unknown"}`,
4187
+ startedAt,
4188
+ finishedAt,
4189
+ durationMs: new Date(finishedAt).getTime() - new Date(startedAt).getTime()
4190
+ };
4191
+ }
4192
+ let startJson;
4193
+ try {
4194
+ startJson = parseJsonOutput(start.stdout || "{}", "codewith agent start");
4195
+ } catch (error) {
4196
+ const finishedAt = nowIso();
4197
+ return {
4198
+ status: "failed",
4199
+ stdout: start.stdout || "",
4200
+ stderr,
4201
+ error: error instanceof Error ? error.message : String(error),
4202
+ startedAt,
4203
+ finishedAt,
4204
+ durationMs: new Date(finishedAt).getTime() - new Date(startedAt).getTime()
4205
+ };
4206
+ }
4207
+ const agentId = stringField(recordField(startJson, "agent"), "agentId");
4208
+ if (!agentId) {
4209
+ const finishedAt = nowIso();
4210
+ return {
4211
+ status: "failed",
4212
+ stdout: start.stdout || "",
4213
+ stderr,
4214
+ error: "codewith agent start did not return agent.agentId",
4215
+ startedAt,
4216
+ finishedAt,
4217
+ durationMs: new Date(finishedAt).getTime() - new Date(startedAt).getTime()
4218
+ };
4219
+ }
4220
+ const pollMs = Math.max(100, Number(opts.env?.LOOPS_CODEWITH_AGENT_POLL_MS ?? process.env.LOOPS_CODEWITH_AGENT_POLL_MS ?? 2000) || 2000);
4221
+ let lastReadJson = startJson;
4222
+ let lastLogsJson;
4223
+ let stdout = "";
4224
+ while (true) {
4225
+ if (opts.signal?.aborted) {
4226
+ spawnSync2(spec.command, codewithAgentControlArgs(target, "stop", agentId), { cwd: spec.cwd, env, stdio: "ignore", timeout: 15000 });
4227
+ const finishedAt = nowIso();
4228
+ return {
4229
+ status: "failed",
4230
+ stdout: appendBoundedText(stdout, codewithAgentEvidence(startJson, lastReadJson, lastLogsJson), maxOutputBytes),
4231
+ stderr,
4232
+ error: "cancelled",
4233
+ startedAt,
4234
+ finishedAt,
4235
+ durationMs: new Date(finishedAt).getTime() - new Date(startedAt).getTime()
4236
+ };
4237
+ }
4238
+ const read = spawnSync2(spec.command, codewithAgentControlArgs(target, "read", agentId), {
4239
+ cwd: spec.cwd,
4240
+ env,
4241
+ encoding: "utf8",
4242
+ stdio: ["ignore", "pipe", "pipe"],
4243
+ timeout: 30000
4244
+ });
4245
+ stderr = appendBoundedText(stderr, read.stderr || "", maxOutputBytes);
4246
+ if (read.error || (read.status ?? 1) !== 0) {
4247
+ const finishedAt = nowIso();
4248
+ return {
4249
+ status: "failed",
4250
+ exitCode: read.status ?? undefined,
4251
+ stdout: appendBoundedText(stdout, codewithAgentEvidence(startJson, lastReadJson, lastLogsJson), maxOutputBytes),
4252
+ stderr,
4253
+ error: read.error?.message ?? `codewith agent read exited with code ${read.status ?? "unknown"}`,
4254
+ startedAt,
4255
+ finishedAt,
4256
+ durationMs: new Date(finishedAt).getTime() - new Date(startedAt).getTime()
4257
+ };
4258
+ }
4259
+ try {
4260
+ lastReadJson = parseJsonOutput(read.stdout || "{}", "codewith agent read");
4261
+ } catch (error) {
4262
+ const finishedAt = nowIso();
4263
+ return {
4264
+ status: "failed",
4265
+ stdout: appendBoundedText(stdout, read.stdout || "", maxOutputBytes),
4266
+ stderr,
4267
+ error: error instanceof Error ? error.message : String(error),
4268
+ startedAt,
4269
+ finishedAt,
4270
+ durationMs: new Date(finishedAt).getTime() - new Date(startedAt).getTime()
4271
+ };
4272
+ }
4273
+ const status = codewithAgentStatus(lastReadJson);
4274
+ if (status === "completed" || status === "failed" || status === "cancelled") {
4275
+ const logs = spawnSync2(spec.command, codewithAgentControlArgs(target, "logs", agentId), {
4276
+ cwd: spec.cwd,
4277
+ env,
4278
+ encoding: "utf8",
4279
+ stdio: ["ignore", "pipe", "pipe"],
4280
+ timeout: 30000
4281
+ });
4282
+ if (!logs.error && (logs.status ?? 1) === 0) {
4283
+ try {
4284
+ lastLogsJson = parseJsonOutput(logs.stdout || "{}", "codewith agent logs");
4285
+ } catch {
4286
+ lastLogsJson = undefined;
4287
+ }
4288
+ } else {
4289
+ stderr = appendBoundedText(stderr, logs.stderr || logs.error?.message || "", maxOutputBytes);
4290
+ }
4291
+ const finishedAt = nowIso();
4292
+ const resultStatus = status === "completed" ? "succeeded" : "failed";
4293
+ return {
4294
+ status: resultStatus,
4295
+ exitCode: resultStatus === "succeeded" ? 0 : 1,
4296
+ stdout: appendBoundedText(stdout, codewithAgentEvidence(startJson, lastReadJson, lastLogsJson), maxOutputBytes),
4297
+ stderr,
4298
+ error: resultStatus === "succeeded" ? undefined : stringField(recordField(lastReadJson, "agent"), "statusReason") ?? `codewith agent ${status}`,
4299
+ startedAt,
4300
+ finishedAt,
4301
+ durationMs: new Date(finishedAt).getTime() - new Date(startedAt).getTime()
4302
+ };
4303
+ }
4304
+ if (typeof spec.timeoutMs === "number" && new Date(nowIso()).getTime() - new Date(startedAt).getTime() >= spec.timeoutMs) {
4305
+ spawnSync2(spec.command, codewithAgentControlArgs(target, "stop", agentId), { cwd: spec.cwd, env, stdio: "ignore", timeout: 15000 });
4306
+ const finishedAt = nowIso();
4307
+ return {
4308
+ status: "timed_out",
4309
+ stdout: appendBoundedText(stdout, codewithAgentEvidence(startJson, lastReadJson, lastLogsJson), maxOutputBytes),
4310
+ stderr,
4311
+ error: `timed out after ${spec.timeoutMs}ms`,
4312
+ startedAt,
4313
+ finishedAt,
4314
+ durationMs: new Date(finishedAt).getTime() - new Date(startedAt).getTime()
4315
+ };
4316
+ }
4317
+ await sleep(pollMs);
4318
+ }
4319
+ }
3958
4320
  function preflightRemoteSpec(spec, machine, metadata, opts) {
3959
4321
  const plan = (opts.machineCommandResolver ?? resolveMachineCommand)(machine.id, "bash -s");
3960
4322
  const result = spawnSync2(plan.command, plan.args, {
@@ -4126,6 +4488,19 @@ function preflightTarget(target, metadata = {}, opts = {}) {
4126
4488
  async function executeTarget(target, metadata = {}, opts = {}) {
4127
4489
  const spec = commandSpec(target);
4128
4490
  const machine = resolvedMachine(opts);
4491
+ if (machine && !machine.local && spec.codewithDurableAgent) {
4492
+ const startedAt2 = nowIso();
4493
+ const finishedAt2 = nowIso();
4494
+ return {
4495
+ status: "failed",
4496
+ stdout: "",
4497
+ stderr: "",
4498
+ error: "remote Codewith durable background-agent steps require remote status polling support; run this Codewith step locally or add remote durable readback before dispatch",
4499
+ startedAt: startedAt2,
4500
+ finishedAt: finishedAt2,
4501
+ durationMs: new Date(finishedAt2).getTime() - new Date(startedAt2).getTime()
4502
+ };
4503
+ }
4129
4504
  if (machine && !machine.local)
4130
4505
  return executeRemoteSpec(spec, machine, metadata, opts);
4131
4506
  const maxOutputBytes = opts.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
@@ -4172,6 +4547,9 @@ async function executeTarget(target, metadata = {}, opts = {}) {
4172
4547
  durationMs: 0
4173
4548
  };
4174
4549
  }
4550
+ if (spec.codewithDurableAgent) {
4551
+ return executeCodewithDurableAgent(spec, metadata, opts, env, startedAt);
4552
+ }
4175
4553
  const child = spawn(spec.command, spec.args, {
4176
4554
  cwd: spec.cwd,
4177
4555
  env,
@@ -4523,7 +4901,7 @@ function publicGoalRun(run) {
4523
4901
  // package.json
4524
4902
  var package_default = {
4525
4903
  name: "@hasna/loops",
4526
- version: "0.3.58",
4904
+ version: "0.3.60",
4527
4905
  description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
4528
4906
  type: "module",
4529
4907
  main: "dist/index.js",