@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/cli/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)
@@ -3540,6 +3629,9 @@ function appendBounded(current, chunk, maxBytes) {
3540
3629
  return `[truncated ${overflow} bytes]
3541
3630
  ${next.slice(-maxBytes)}`;
3542
3631
  }
3632
+ function appendBoundedText(current, chunk, maxBytes) {
3633
+ return appendBounded(current, Buffer.from(chunk, "utf8"), maxBytes);
3634
+ }
3543
3635
  function killProcessGroup(pid) {
3544
3636
  try {
3545
3637
  process.kill(-pid, "SIGTERM");
@@ -3587,6 +3679,21 @@ function metadataEnv(metadata) {
3587
3679
  env.LOOPS_GOAL_NODE_KEY = metadata.goalNodeKey;
3588
3680
  return env;
3589
3681
  }
3682
+ function codewithAgentIdempotencyKey(metadata) {
3683
+ const parts = [
3684
+ "openloops",
3685
+ metadata.workflowRunId ? `workflow-run:${metadata.workflowRunId}` : undefined,
3686
+ metadata.workflowStepId ? `step:${metadata.workflowStepId}` : undefined,
3687
+ metadata.runId ? `loop-run:${metadata.runId}` : undefined,
3688
+ metadata.loopId ? `loop:${metadata.loopId}` : undefined,
3689
+ metadata.scheduledFor ? `scheduled:${metadata.scheduledFor}` : undefined,
3690
+ metadata.goalId ? `goal:${metadata.goalId}` : undefined,
3691
+ metadata.goalNodeKey ? `node:${metadata.goalNodeKey}` : undefined
3692
+ ].filter(Boolean);
3693
+ if (parts.length > 1)
3694
+ return parts.join(":");
3695
+ return `openloops:adhoc:${randomBytes2(16).toString("hex")}`;
3696
+ }
3590
3697
  function allowlistEnv(allowlist) {
3591
3698
  const env = {};
3592
3699
  if (allowlist?.tools?.length)
@@ -3623,6 +3730,20 @@ function codewithLikeSandbox(target) {
3623
3730
  function configStringValue(value) {
3624
3731
  return JSON.stringify(value);
3625
3732
  }
3733
+ var UNSAFE_CODEWITH_DURABLE_EXTRA_ARGS2 = new Set([
3734
+ "e",
3735
+ "exec",
3736
+ "agent",
3737
+ "start",
3738
+ "--ephemeral",
3739
+ "--ignore-rules",
3740
+ "--skip-git-repo-check",
3741
+ "--json",
3742
+ "--output-last-message",
3743
+ "-o",
3744
+ "--output-schema",
3745
+ "--dangerously-bypass-approvals-and-sandbox"
3746
+ ]);
3626
3747
  function assertStringOption(value, label) {
3627
3748
  if (value !== undefined && typeof value !== "string")
3628
3749
  throw new Error(`${label} must be a string`);
@@ -3653,6 +3774,14 @@ function assertSupportedAgentOptions(target) {
3653
3774
  }
3654
3775
  if (target.provider === "codex" && target.agent !== undefined)
3655
3776
  throw new Error("codex.agent is not supported");
3777
+ if (target.provider === "codewith" && target.agent !== undefined) {
3778
+ throw new Error("codewith.agent is not supported by the durable background-agent adapter");
3779
+ }
3780
+ if (target.provider === "codewith") {
3781
+ const unsafe = target.extraArgs?.find((arg) => UNSAFE_CODEWITH_DURABLE_EXTRA_ARGS2.has(arg));
3782
+ if (unsafe)
3783
+ throw new Error(`codewith.extraArgs cannot include ${unsafe}; durable agent steps use codewith agent start, not exec/ephemeral flags`);
3784
+ }
3656
3785
  if (["codewith", "codex"].includes(target.provider)) {
3657
3786
  if (target.permissionMode && !["default", "bypass"].includes(target.permissionMode)) {
3658
3787
  throw new Error(`${target.provider}.permissionMode supports only default or bypass`);
@@ -3736,18 +3865,18 @@ function agentArgs(target) {
3736
3865
  args.push(...target.authProfile ? ["--auth-profile", target.authProfile] : []);
3737
3866
  if (target.variant)
3738
3867
  args.push("-c", `model_reasoning_effort=${configStringValue(target.variant)}`);
3739
- args.push("--ask-for-approval", "never", "exec", "--json", "--ephemeral", "--sandbox", codewithLikeSandbox(target), "--skip-git-repo-check");
3740
- if (isolation === "safe")
3741
- args.push("--ignore-rules");
3868
+ args.push("--ask-for-approval", "never", "--sandbox", codewithLikeSandbox(target));
3742
3869
  if (target.cwd)
3743
3870
  args.push("--cd", target.cwd);
3744
3871
  for (const dir of target.addDirs ?? [])
3745
3872
  args.push("--add-dir", dir);
3746
3873
  if (target.model)
3747
3874
  args.push("--model", target.model);
3748
- if (target.agent)
3749
- args.push("--agent", target.agent);
3750
3875
  args.push(...target.extraArgs ?? []);
3876
+ args.push("agent", "start");
3877
+ if (target.cwd)
3878
+ args.push("--cwd", target.cwd);
3879
+ args.push(target.prompt);
3751
3880
  return args;
3752
3881
  case "codex":
3753
3882
  if (target.variant)
@@ -3823,8 +3952,9 @@ function commandSpec(target) {
3823
3952
  accountTool: agentTarget.account?.tool ?? accountToolForProvider(agentTarget.provider),
3824
3953
  nativeAuthProfile: agentTarget.authProfile ? { provider: agentTarget.provider, profile: agentTarget.authProfile } : undefined,
3825
3954
  preflightAnyOf: agentTarget.provider === "cursor" ? ["agent"] : undefined,
3826
- stdin: agentTarget.prompt,
3827
- allowlist: agentTarget.allowlist
3955
+ stdin: agentTarget.provider === "codewith" ? undefined : agentTarget.prompt,
3956
+ allowlist: agentTarget.allowlist,
3957
+ codewithDurableAgent: agentTarget.provider === "codewith" ? { target: agentTarget } : undefined
3828
3958
  };
3829
3959
  }
3830
3960
  function executionEnv(spec, metadata, opts) {
@@ -3944,6 +4074,238 @@ function preflightNativeAuthProfile(spec, env) {
3944
4074
  throw new Error(`codewith auth profile not found: ${spec.nativeAuthProfile.profile}`);
3945
4075
  }
3946
4076
  }
4077
+ function codewithAgentStartArgs(target, idempotencyKey) {
4078
+ const args = agentArgs(target);
4079
+ const startIndex = args.findIndex((arg, index) => arg === "start" && args[index - 1] === "agent");
4080
+ if (startIndex === -1)
4081
+ throw new Error("internal error: codewith durable agent args missing agent start");
4082
+ args.splice(startIndex + 1, 0, "--idempotency-key", idempotencyKey);
4083
+ return args;
4084
+ }
4085
+ function codewithAgentControlArgs(target, command, agentId) {
4086
+ return [
4087
+ ...target.authProfile ? ["--auth-profile", target.authProfile] : [],
4088
+ "agent",
4089
+ command,
4090
+ ...command === "logs" ? ["--limit", "20"] : [],
4091
+ agentId
4092
+ ];
4093
+ }
4094
+ function parseJsonOutput(stdout, label) {
4095
+ try {
4096
+ const value = JSON.parse(stdout || "{}");
4097
+ if (!value || typeof value !== "object" || Array.isArray(value))
4098
+ throw new Error("not an object");
4099
+ return value;
4100
+ } catch (error) {
4101
+ throw new Error(`${label} did not return JSON${error instanceof Error ? `: ${error.message}` : ""}`);
4102
+ }
4103
+ }
4104
+ function recordField(value, key) {
4105
+ if (!value || typeof value !== "object" || Array.isArray(value))
4106
+ return;
4107
+ const field = value[key];
4108
+ return field && typeof field === "object" && !Array.isArray(field) ? field : undefined;
4109
+ }
4110
+ function stringField(value, key) {
4111
+ const field = value?.[key];
4112
+ return typeof field === "string" ? field : undefined;
4113
+ }
4114
+ function numberField(value, key) {
4115
+ const field = value?.[key];
4116
+ return typeof field === "number" && Number.isFinite(field) ? field : undefined;
4117
+ }
4118
+ function codewithAgentStatus(readJson) {
4119
+ return stringField(recordField(readJson, "agent"), "status") ?? stringField(recordField(readJson, "statusSnapshot"), "status");
4120
+ }
4121
+ function codewithAgentEvidence(startJson, readJson, logsJson) {
4122
+ const agent = recordField(readJson, "agent") ?? recordField(startJson, "agent");
4123
+ const statusSnapshot = recordField(readJson, "statusSnapshot");
4124
+ const events = Array.isArray(logsJson?.data) ? logsJson.data.filter((event) => Boolean(event) && typeof event === "object" && !Array.isArray(event)).map((event) => ({
4125
+ seq: numberField(event, "seq"),
4126
+ eventType: stringField(event, "eventType"),
4127
+ createdAt: numberField(event, "createdAt")
4128
+ })) : undefined;
4129
+ return JSON.stringify({
4130
+ codewithAgent: {
4131
+ agentId: stringField(agent, "agentId"),
4132
+ status: stringField(agent, "status"),
4133
+ desiredState: stringField(agent, "desiredState"),
4134
+ statusReason: stringField(agent, "statusReason"),
4135
+ threadId: stringField(agent, "threadId"),
4136
+ rolloutPath: stringField(agent, "rolloutPath"),
4137
+ pid: numberField(agent, "pid"),
4138
+ exitCode: numberField(agent, "exitCode"),
4139
+ created: typeof startJson.created === "boolean" ? startJson.created : undefined
4140
+ },
4141
+ statusSnapshot: statusSnapshot ? {
4142
+ seq: numberField(statusSnapshot, "seq"),
4143
+ status: stringField(statusSnapshot, "status"),
4144
+ summary: stringField(statusSnapshot, "summary"),
4145
+ pendingInteractionCount: numberField(statusSnapshot, "pendingInteractionCount"),
4146
+ lastEventSeq: numberField(statusSnapshot, "lastEventSeq")
4147
+ } : undefined,
4148
+ events
4149
+ }, null, 2);
4150
+ }
4151
+ function sleep(ms) {
4152
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
4153
+ }
4154
+ async function executeCodewithDurableAgent(spec, metadata, opts, env, startedAt) {
4155
+ const target = spec.codewithDurableAgent?.target;
4156
+ if (!target)
4157
+ throw new Error("internal error: missing codewith durable target");
4158
+ const maxOutputBytes = opts.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
4159
+ const idempotencyKey = codewithAgentIdempotencyKey(metadata);
4160
+ const start = spawnSync2(spec.command, codewithAgentStartArgs(target, idempotencyKey), {
4161
+ cwd: spec.cwd,
4162
+ env,
4163
+ encoding: "utf8",
4164
+ stdio: ["ignore", "pipe", "pipe"],
4165
+ timeout: 30000
4166
+ });
4167
+ let stderr = start.stderr || "";
4168
+ if (start.error || (start.status ?? 1) !== 0) {
4169
+ const finishedAt = nowIso();
4170
+ return {
4171
+ status: "failed",
4172
+ exitCode: start.status ?? undefined,
4173
+ stdout: start.stdout || "",
4174
+ stderr,
4175
+ error: start.error?.message ?? `codewith agent start exited with code ${start.status ?? "unknown"}`,
4176
+ startedAt,
4177
+ finishedAt,
4178
+ durationMs: new Date(finishedAt).getTime() - new Date(startedAt).getTime()
4179
+ };
4180
+ }
4181
+ let startJson;
4182
+ try {
4183
+ startJson = parseJsonOutput(start.stdout || "{}", "codewith agent start");
4184
+ } catch (error) {
4185
+ const finishedAt = nowIso();
4186
+ return {
4187
+ status: "failed",
4188
+ stdout: start.stdout || "",
4189
+ stderr,
4190
+ error: error instanceof Error ? error.message : String(error),
4191
+ startedAt,
4192
+ finishedAt,
4193
+ durationMs: new Date(finishedAt).getTime() - new Date(startedAt).getTime()
4194
+ };
4195
+ }
4196
+ const agentId = stringField(recordField(startJson, "agent"), "agentId");
4197
+ if (!agentId) {
4198
+ const finishedAt = nowIso();
4199
+ return {
4200
+ status: "failed",
4201
+ stdout: start.stdout || "",
4202
+ stderr,
4203
+ error: "codewith agent start did not return agent.agentId",
4204
+ startedAt,
4205
+ finishedAt,
4206
+ durationMs: new Date(finishedAt).getTime() - new Date(startedAt).getTime()
4207
+ };
4208
+ }
4209
+ const pollMs = Math.max(100, Number(opts.env?.LOOPS_CODEWITH_AGENT_POLL_MS ?? process.env.LOOPS_CODEWITH_AGENT_POLL_MS ?? 2000) || 2000);
4210
+ let lastReadJson = startJson;
4211
+ let lastLogsJson;
4212
+ let stdout = "";
4213
+ while (true) {
4214
+ if (opts.signal?.aborted) {
4215
+ spawnSync2(spec.command, codewithAgentControlArgs(target, "stop", agentId), { cwd: spec.cwd, env, stdio: "ignore", timeout: 15000 });
4216
+ const finishedAt = nowIso();
4217
+ return {
4218
+ status: "failed",
4219
+ stdout: appendBoundedText(stdout, codewithAgentEvidence(startJson, lastReadJson, lastLogsJson), maxOutputBytes),
4220
+ stderr,
4221
+ error: "cancelled",
4222
+ startedAt,
4223
+ finishedAt,
4224
+ durationMs: new Date(finishedAt).getTime() - new Date(startedAt).getTime()
4225
+ };
4226
+ }
4227
+ const read = spawnSync2(spec.command, codewithAgentControlArgs(target, "read", agentId), {
4228
+ cwd: spec.cwd,
4229
+ env,
4230
+ encoding: "utf8",
4231
+ stdio: ["ignore", "pipe", "pipe"],
4232
+ timeout: 30000
4233
+ });
4234
+ stderr = appendBoundedText(stderr, read.stderr || "", maxOutputBytes);
4235
+ if (read.error || (read.status ?? 1) !== 0) {
4236
+ const finishedAt = nowIso();
4237
+ return {
4238
+ status: "failed",
4239
+ exitCode: read.status ?? undefined,
4240
+ stdout: appendBoundedText(stdout, codewithAgentEvidence(startJson, lastReadJson, lastLogsJson), maxOutputBytes),
4241
+ stderr,
4242
+ error: read.error?.message ?? `codewith agent read exited with code ${read.status ?? "unknown"}`,
4243
+ startedAt,
4244
+ finishedAt,
4245
+ durationMs: new Date(finishedAt).getTime() - new Date(startedAt).getTime()
4246
+ };
4247
+ }
4248
+ try {
4249
+ lastReadJson = parseJsonOutput(read.stdout || "{}", "codewith agent read");
4250
+ } catch (error) {
4251
+ const finishedAt = nowIso();
4252
+ return {
4253
+ status: "failed",
4254
+ stdout: appendBoundedText(stdout, read.stdout || "", maxOutputBytes),
4255
+ stderr,
4256
+ error: error instanceof Error ? error.message : String(error),
4257
+ startedAt,
4258
+ finishedAt,
4259
+ durationMs: new Date(finishedAt).getTime() - new Date(startedAt).getTime()
4260
+ };
4261
+ }
4262
+ const status = codewithAgentStatus(lastReadJson);
4263
+ if (status === "completed" || status === "failed" || status === "cancelled") {
4264
+ const logs = spawnSync2(spec.command, codewithAgentControlArgs(target, "logs", agentId), {
4265
+ cwd: spec.cwd,
4266
+ env,
4267
+ encoding: "utf8",
4268
+ stdio: ["ignore", "pipe", "pipe"],
4269
+ timeout: 30000
4270
+ });
4271
+ if (!logs.error && (logs.status ?? 1) === 0) {
4272
+ try {
4273
+ lastLogsJson = parseJsonOutput(logs.stdout || "{}", "codewith agent logs");
4274
+ } catch {
4275
+ lastLogsJson = undefined;
4276
+ }
4277
+ } else {
4278
+ stderr = appendBoundedText(stderr, logs.stderr || logs.error?.message || "", maxOutputBytes);
4279
+ }
4280
+ const finishedAt = nowIso();
4281
+ const resultStatus = status === "completed" ? "succeeded" : "failed";
4282
+ return {
4283
+ status: resultStatus,
4284
+ exitCode: resultStatus === "succeeded" ? 0 : 1,
4285
+ stdout: appendBoundedText(stdout, codewithAgentEvidence(startJson, lastReadJson, lastLogsJson), maxOutputBytes),
4286
+ stderr,
4287
+ error: resultStatus === "succeeded" ? undefined : stringField(recordField(lastReadJson, "agent"), "statusReason") ?? `codewith agent ${status}`,
4288
+ startedAt,
4289
+ finishedAt,
4290
+ durationMs: new Date(finishedAt).getTime() - new Date(startedAt).getTime()
4291
+ };
4292
+ }
4293
+ if (typeof spec.timeoutMs === "number" && new Date(nowIso()).getTime() - new Date(startedAt).getTime() >= spec.timeoutMs) {
4294
+ spawnSync2(spec.command, codewithAgentControlArgs(target, "stop", agentId), { cwd: spec.cwd, env, stdio: "ignore", timeout: 15000 });
4295
+ const finishedAt = nowIso();
4296
+ return {
4297
+ status: "timed_out",
4298
+ stdout: appendBoundedText(stdout, codewithAgentEvidence(startJson, lastReadJson, lastLogsJson), maxOutputBytes),
4299
+ stderr,
4300
+ error: `timed out after ${spec.timeoutMs}ms`,
4301
+ startedAt,
4302
+ finishedAt,
4303
+ durationMs: new Date(finishedAt).getTime() - new Date(startedAt).getTime()
4304
+ };
4305
+ }
4306
+ await sleep(pollMs);
4307
+ }
4308
+ }
3947
4309
  function preflightRemoteSpec(spec, machine, metadata, opts) {
3948
4310
  const plan = (opts.machineCommandResolver ?? resolveMachineCommand)(machine.id, "bash -s");
3949
4311
  const result = spawnSync2(plan.command, plan.args, {
@@ -4115,6 +4477,19 @@ function preflightTarget(target, metadata = {}, opts = {}) {
4115
4477
  async function executeTarget(target, metadata = {}, opts = {}) {
4116
4478
  const spec = commandSpec(target);
4117
4479
  const machine = resolvedMachine(opts);
4480
+ if (machine && !machine.local && spec.codewithDurableAgent) {
4481
+ const startedAt2 = nowIso();
4482
+ const finishedAt2 = nowIso();
4483
+ return {
4484
+ status: "failed",
4485
+ stdout: "",
4486
+ stderr: "",
4487
+ error: "remote Codewith durable background-agent steps require remote status polling support; run this Codewith step locally or add remote durable readback before dispatch",
4488
+ startedAt: startedAt2,
4489
+ finishedAt: finishedAt2,
4490
+ durationMs: new Date(finishedAt2).getTime() - new Date(startedAt2).getTime()
4491
+ };
4492
+ }
4118
4493
  if (machine && !machine.local)
4119
4494
  return executeRemoteSpec(spec, machine, metadata, opts);
4120
4495
  const maxOutputBytes = opts.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
@@ -4161,6 +4536,9 @@ async function executeTarget(target, metadata = {}, opts = {}) {
4161
4536
  durationMs: 0
4162
4537
  };
4163
4538
  }
4539
+ if (spec.codewithDurableAgent) {
4540
+ return executeCodewithDurableAgent(spec, metadata, opts, env, startedAt);
4541
+ }
4164
4542
  const child = spawn(spec.command, spec.args, {
4165
4543
  cwd: spec.cwd,
4166
4544
  env,
@@ -5372,7 +5750,7 @@ function realSleep(ms) {
5372
5750
  return new Promise((resolve2) => setTimeout(resolve2, ms));
5373
5751
  }
5374
5752
  async function runLoop(opts) {
5375
- const sleep = opts.sleep ?? realSleep;
5753
+ const sleep2 = opts.sleep ?? realSleep;
5376
5754
  const sliceMs = opts.sliceMs ?? 200;
5377
5755
  while (!opts.shouldStop()) {
5378
5756
  try {
@@ -5383,7 +5761,7 @@ async function runLoop(opts) {
5383
5761
  let waited = 0;
5384
5762
  while (waited < opts.intervalMs && !opts.shouldStop()) {
5385
5763
  const chunk = Math.min(sliceMs, opts.intervalMs - waited);
5386
- await sleep(chunk);
5764
+ await sleep2(chunk);
5387
5765
  waited += chunk;
5388
5766
  }
5389
5767
  }
@@ -5465,7 +5843,7 @@ async function stopDaemon(opts = {}) {
5465
5843
  } finally {
5466
5844
  store.close();
5467
5845
  }
5468
- const sleep = opts.sleep ?? realSleep;
5846
+ const sleep2 = opts.sleep ?? realSleep;
5469
5847
  try {
5470
5848
  process.kill(state.pid, "SIGTERM");
5471
5849
  } catch {
@@ -5474,7 +5852,7 @@ async function stopDaemon(opts = {}) {
5474
5852
  }
5475
5853
  const steps = Math.max(1, Math.ceil((opts.timeoutMs ?? 6000) / 100));
5476
5854
  for (let i = 0;i < steps; i++) {
5477
- await sleep(100);
5855
+ await sleep2(100);
5478
5856
  if (!isAlive(state.pid)) {
5479
5857
  removePid(path);
5480
5858
  return { wasRunning: true, stopped: true, forced: false, pid: state.pid };
@@ -5483,7 +5861,7 @@ async function stopDaemon(opts = {}) {
5483
5861
  try {
5484
5862
  process.kill(state.pid, "SIGKILL");
5485
5863
  } catch {}
5486
- await sleep(150);
5864
+ await sleep2(150);
5487
5865
  removePid(path);
5488
5866
  return { wasRunning: true, stopped: !isAlive(state.pid), forced: true, pid: state.pid };
5489
5867
  }
@@ -5652,10 +6030,10 @@ async function startDaemon(opts) {
5652
6030
  stdio: ["ignore", out, out]
5653
6031
  });
5654
6032
  child.unref();
5655
- const sleep = opts.sleep ?? realSleep;
6033
+ const sleep2 = opts.sleep ?? realSleep;
5656
6034
  const deadline = Math.max(1, Math.ceil((opts.waitMs ?? 4000) / 100));
5657
6035
  for (let i = 0;i < deadline; i++) {
5658
- await sleep(100);
6036
+ await sleep2(100);
5659
6037
  const current = isDaemonRunning();
5660
6038
  if (current.running)
5661
6039
  return { started: true, alreadyRunning: false, pid: current.pid };
@@ -6497,7 +6875,7 @@ function buildScriptInventoryReport(store, opts = {}) {
6497
6875
  // package.json
6498
6876
  var package_default = {
6499
6877
  name: "@hasna/loops",
6500
- version: "0.3.58",
6878
+ version: "0.3.60",
6501
6879
  description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
6502
6880
  type: "module",
6503
6881
  main: "dist/index.js",
@@ -8637,21 +9015,47 @@ function workflowTimeoutMigrationName(workflow, timeoutMs) {
8637
9015
  const suffix = randomUUID().replace(/-/g, "").slice(0, 8);
8638
9016
  return `${workflow.name}-${policy}-${suffix}`;
8639
9017
  }
8640
- function workflowWithoutGoalWrapper(workflow, opts = {}) {
8641
- return {
8642
- body: {
8643
- name: opts.name ?? workflow.name,
8644
- description: workflow.description,
8645
- version: workflow.version,
8646
- steps: workflow.steps
8647
- },
8648
- changed: workflow.goal !== undefined
8649
- };
8650
- }
8651
9018
  function workflowGoalWrapperMigrationName(workflow) {
8652
9019
  const suffix = randomUUID().replace(/-/g, "").slice(0, 8);
8653
9020
  return `${workflow.name}-no-workflow-goal-${suffix}`;
8654
9021
  }
9022
+ function publicMigrationGoalSummary(goal) {
9023
+ if (!goal)
9024
+ return;
9025
+ return {
9026
+ objective: redact(goal.objective),
9027
+ model: goal.model,
9028
+ tokenBudget: goal.tokenBudget,
9029
+ maxTurns: goal.maxTurns
9030
+ };
9031
+ }
9032
+ function publicMigrationWorkflowSummary(workflow) {
9033
+ return {
9034
+ id: workflow.id,
9035
+ name: workflow.name,
9036
+ version: workflow.version,
9037
+ status: workflow.status,
9038
+ stepCount: workflow.steps.length,
9039
+ hasGoal: Boolean(workflow.goal),
9040
+ goal: publicMigrationGoalSummary(workflow.goal)
9041
+ };
9042
+ }
9043
+ function publicMigrationLoopSummary(loop) {
9044
+ return {
9045
+ id: loop.id,
9046
+ name: loop.name,
9047
+ status: loop.status,
9048
+ archivedAt: loop.archivedAt,
9049
+ nextRunAt: loop.nextRunAt,
9050
+ hasGoal: Boolean(loop.goal),
9051
+ goal: publicMigrationGoalSummary(loop.goal),
9052
+ target: loop.target.type === "workflow" ? { type: "workflow", workflowId: loop.target.workflowId, timeoutMs: loop.target.timeoutMs } : { type: loop.target.type }
9053
+ };
9054
+ }
9055
+ function migrationErrorReason(error) {
9056
+ const message = error instanceof Error ? error.message : String(error);
9057
+ return redact(message, 240);
9058
+ }
8655
9059
  function printTextOutput(value) {
8656
9060
  for (const line of textOutputBlocks(value, { indent: " " }))
8657
9061
  console.log(line);
@@ -9078,7 +9482,7 @@ function eventMetadata(event) {
9078
9482
  return metadata;
9079
9483
  return {};
9080
9484
  }
9081
- function stringField(value) {
9485
+ function stringField2(value) {
9082
9486
  return typeof value === "string" && value.trim() ? value.trim() : undefined;
9083
9487
  }
9084
9488
  function slugSegment2(value, fallback = "event") {
@@ -9089,14 +9493,14 @@ function stableSuffix(value) {
9089
9493
  }
9090
9494
  function taskEventField(data, keys) {
9091
9495
  for (const key of keys) {
9092
- const direct = stringField(data[key]);
9496
+ const direct = stringField2(data[key]);
9093
9497
  if (direct)
9094
9498
  return direct;
9095
9499
  }
9096
9500
  const task = data.task;
9097
9501
  if (task && typeof task === "object" && !Array.isArray(task)) {
9098
9502
  for (const key of keys) {
9099
- const direct = stringField(task[key]);
9503
+ const direct = stringField2(task[key]);
9100
9504
  if (direct)
9101
9505
  return direct;
9102
9506
  }
@@ -9104,7 +9508,7 @@ function taskEventField(data, keys) {
9104
9508
  const payload = data.payload;
9105
9509
  if (payload && typeof payload === "object" && !Array.isArray(payload)) {
9106
9510
  for (const key of keys) {
9107
- const direct = stringField(payload[key]);
9511
+ const direct = stringField2(payload[key]);
9108
9512
  if (direct)
9109
9513
  return direct;
9110
9514
  }
@@ -9804,11 +10208,11 @@ async function readEventEnvelopeInput(opts = {}) {
9804
10208
  const event = JSON.parse(raw);
9805
10209
  if (!event || typeof event !== "object" || Array.isArray(event))
9806
10210
  throw new Error("event JSON must be an object");
9807
- if (!stringField(event.id))
10211
+ if (!stringField2(event.id))
9808
10212
  throw new Error("event.id is required");
9809
- if (!stringField(event.type))
10213
+ if (!stringField2(event.type))
9810
10214
  throw new Error("event.type is required");
9811
- if (!stringField(event.source))
10215
+ if (!stringField2(event.source))
9812
10216
  throw new Error("event.source is required");
9813
10217
  return event;
9814
10218
  }
@@ -10148,8 +10552,8 @@ function routeGenericEvent(event, opts) {
10148
10552
  eventId: event.id,
10149
10553
  eventType: event.type,
10150
10554
  eventSource: event.source,
10151
- eventSubject: stringField(event.subject),
10152
- eventMessage: stringField(event.message),
10555
+ eventSubject: stringField2(event.subject),
10556
+ eventMessage: stringField2(event.message),
10153
10557
  eventJson: JSON.stringify(event),
10154
10558
  projectPath,
10155
10559
  routeProjectPath,
@@ -10195,9 +10599,9 @@ function routeGenericEvent(event, opts) {
10195
10599
  },
10196
10600
  subjectRef: {
10197
10601
  kind: "event",
10198
- id: stringField(event.subject) ?? event.id,
10602
+ id: stringField2(event.subject) ?? event.id,
10199
10603
  path: routeProjectPath,
10200
- raw: { message: stringField(event.message) }
10604
+ raw: { message: stringField2(event.message) }
10201
10605
  },
10202
10606
  intent: "route",
10203
10607
  scope: {
@@ -10221,7 +10625,7 @@ function routeGenericEvent(event, opts) {
10221
10625
  invocationId: "<created-invocation-id>",
10222
10626
  sourceType: event.type,
10223
10627
  sourceRef: event.id,
10224
- subjectRef: stringField(event.subject) ?? event.id,
10628
+ subjectRef: stringField2(event.subject) ?? event.id,
10225
10629
  projectKey: routeProjectPath,
10226
10630
  projectGroup,
10227
10631
  priority: 0,
@@ -10365,14 +10769,14 @@ function routeGenericEvent(event, opts) {
10365
10769
  }
10366
10770
  function taskField(task, keys) {
10367
10771
  for (const key of keys) {
10368
- const value = stringField(task[key]);
10772
+ const value = stringField2(task[key]);
10369
10773
  if (value)
10370
10774
  return value;
10371
10775
  }
10372
10776
  return;
10373
10777
  }
10374
10778
  function taskListId(task) {
10375
- return taskField(task, ["task_list_id", "taskListId"]) ?? stringField(task.task_list?.id);
10779
+ return taskField(task, ["task_list_id", "taskListId"]) ?? stringField2(task.task_list?.id);
10376
10780
  }
10377
10781
  function taskProjectId(task) {
10378
10782
  return taskField(task, ["project_id", "projectId"]);
@@ -10436,12 +10840,12 @@ function compactDrainResult(result) {
10436
10840
  kind: result.kind,
10437
10841
  taskId: event?.subject,
10438
10842
  eventId: event?.id,
10439
- idempotencyKey: stringField(value.idempotencyKey),
10440
- reason: stringField(value.reason) ?? throttle?.reason,
10441
- loopId: stringField(loop?.id),
10442
- loopName: stringField(loop?.name),
10443
- workflowId: stringField(workflow?.id),
10444
- workflowName: stringField(workflow?.name),
10843
+ idempotencyKey: stringField2(value.idempotencyKey),
10844
+ reason: stringField2(value.reason) ?? throttle?.reason,
10845
+ loopId: stringField2(loop?.id),
10846
+ loopName: stringField2(loop?.name),
10847
+ workflowId: stringField2(workflow?.id),
10848
+ workflowName: stringField2(workflow?.name),
10445
10849
  providerRouting,
10446
10850
  requeue,
10447
10851
  queuedAtSource: value.queuedAtSource
@@ -10946,7 +11350,7 @@ routes.command("show <id>").description("show one admission work item").action((
10946
11350
  routes.command("requeue <id>").description("requeue a terminal admission work item for the next task/event delivery").option("--reason <text>", "operator reason recorded on the work item").action((id, opts) => {
10947
11351
  const store = new Store;
10948
11352
  try {
10949
- const reason = stringField(opts.reason);
11353
+ const reason = stringField2(opts.reason);
10950
11354
  if (!reason)
10951
11355
  throw new Error("routes requeue requires --reason <text>");
10952
11356
  const item = store.requeueWorkflowWorkItem(id, { reason });
@@ -11275,19 +11679,30 @@ workflows.command("migrate-agent-timeouts").description("append-only migrate act
11275
11679
  });
11276
11680
  continue;
11277
11681
  }
11278
- const migrated = store.createAndRetargetWorkflowLoop(loop.id, migration.body, {
11279
- workflowTimeoutMs: timeoutMs,
11280
- archiveOld: Boolean(opts.archiveOld)
11281
- });
11282
- rows.push({
11283
- loop: publicLoop(migrated.loop),
11284
- previousWorkflow: publicWorkflow(migrated.previousWorkflow),
11285
- workflow: publicWorkflow(migrated.workflow),
11286
- archivedOld: migrated.archivedOld ? publicWorkflow(migrated.archivedOld) : undefined,
11287
- status: "migrated",
11288
- agentStepIds: migration.agentStepIds,
11289
- timeoutMs
11290
- });
11682
+ try {
11683
+ const migrated = store.createAndRetargetWorkflowLoop(loop.id, migration.body, {
11684
+ workflowTimeoutMs: timeoutMs,
11685
+ archiveOld: Boolean(opts.archiveOld)
11686
+ });
11687
+ rows.push({
11688
+ loop: publicLoop(migrated.loop),
11689
+ previousWorkflow: publicWorkflow(migrated.previousWorkflow),
11690
+ workflow: publicWorkflow(migrated.workflow),
11691
+ archivedOld: migrated.archivedOld ? publicWorkflow(migrated.archivedOld) : undefined,
11692
+ status: "migrated",
11693
+ agentStepIds: migration.agentStepIds,
11694
+ timeoutMs
11695
+ });
11696
+ } catch (error) {
11697
+ rows.push({
11698
+ loop: publicLoop(loop),
11699
+ workflow: publicWorkflow(workflow),
11700
+ status: "blocked",
11701
+ reason: migrationErrorReason(error),
11702
+ agentStepIds: migration.agentStepIds,
11703
+ timeoutMs
11704
+ });
11705
+ }
11291
11706
  }
11292
11707
  const summary = {
11293
11708
  apply: Boolean(opts.apply),
@@ -11310,45 +11725,73 @@ workflows.command("migrate-goal-wrappers").description("append-only migrate acti
11310
11725
  const rows = [];
11311
11726
  for (const loop of candidateLoops) {
11312
11727
  if (loop.archivedAt) {
11313
- rows.push({ loop: publicLoop(loop), status: "skipped", reason: "loop is archived" });
11728
+ rows.push({ loop: publicMigrationLoopSummary(loop), status: "skipped", reason: "loop is archived" });
11314
11729
  continue;
11315
11730
  }
11316
11731
  if (loop.target.type !== "workflow") {
11317
- rows.push({ loop: publicLoop(loop), status: "skipped", reason: "loop is not a workflow loop" });
11732
+ rows.push({ loop: publicMigrationLoopSummary(loop), status: "skipped", reason: "loop is not a workflow loop" });
11318
11733
  continue;
11319
11734
  }
11320
11735
  const workflow = store.requireWorkflow(loop.target.workflowId);
11321
- const nextWorkflowName = workflowGoalWrapperMigrationName(workflow);
11322
- const migration = workflowWithoutGoalWrapper(workflow, { name: nextWorkflowName });
11323
- if (!migration.changed) {
11324
- rows.push({ loop: publicLoop(loop), workflow: publicWorkflow(workflow), status: "skipped", reason: "workflow has no top-level goal wrapper" });
11736
+ if (!loop.goal) {
11737
+ rows.push({
11738
+ loop: publicMigrationLoopSummary(loop),
11739
+ workflow: publicMigrationWorkflowSummary(workflow),
11740
+ status: "skipped",
11741
+ reason: "loop has no loop-level goal wrapper"
11742
+ });
11325
11743
  continue;
11326
11744
  }
11745
+ if (!workflow.goal) {
11746
+ rows.push({
11747
+ loop: publicMigrationLoopSummary(loop),
11748
+ workflow: publicMigrationWorkflowSummary(workflow),
11749
+ status: "skipped",
11750
+ reason: "workflow has no top-level goal wrapper"
11751
+ });
11752
+ continue;
11753
+ }
11754
+ const nextWorkflowName = workflowGoalWrapperMigrationName(workflow);
11327
11755
  if (store.hasRunningRun(loop.id)) {
11328
- rows.push({ loop: publicLoop(loop), workflow: publicWorkflow(workflow), status: "blocked", reason: "loop has a running run; retry after it finishes" });
11756
+ rows.push({
11757
+ loop: publicMigrationLoopSummary(loop),
11758
+ workflow: publicMigrationWorkflowSummary(workflow),
11759
+ status: "blocked",
11760
+ reason: "loop has a running run; retry after it finishes"
11761
+ });
11329
11762
  continue;
11330
11763
  }
11331
11764
  if (!opts.apply) {
11332
11765
  rows.push({
11333
- loop: publicLoop(loop),
11334
- workflow: publicWorkflow(workflow),
11766
+ loop: publicMigrationLoopSummary(loop),
11767
+ workflow: publicMigrationWorkflowSummary(workflow),
11335
11768
  status: "would_migrate",
11336
11769
  nextWorkflowName,
11337
- removedGoal: workflow.goal
11770
+ removedGoal: publicMigrationGoalSummary(workflow.goal)
11338
11771
  });
11339
11772
  continue;
11340
11773
  }
11341
- const migrated = store.createAndRetargetWorkflowLoop(loop.id, migration.body, {
11342
- workflowTimeoutMs: loop.target.timeoutMs,
11343
- archiveOld: Boolean(opts.archiveOld)
11344
- });
11345
- rows.push({
11346
- loop: publicLoop(migrated.loop),
11347
- previousWorkflow: publicWorkflow(migrated.previousWorkflow),
11348
- workflow: publicWorkflow(migrated.workflow),
11349
- archivedOld: migrated.archivedOld ? publicWorkflow(migrated.archivedOld) : undefined,
11350
- status: "migrated"
11351
- });
11774
+ try {
11775
+ const migrated = store.cloneWorkflowWithoutGoalAndRetargetLoop(loop.id, {
11776
+ workflowName: nextWorkflowName,
11777
+ workflowTimeoutMs: loop.target.timeoutMs,
11778
+ archiveOld: Boolean(opts.archiveOld)
11779
+ });
11780
+ rows.push({
11781
+ loop: publicMigrationLoopSummary(migrated.loop),
11782
+ previousWorkflow: publicMigrationWorkflowSummary(migrated.previousWorkflow),
11783
+ workflow: publicMigrationWorkflowSummary(migrated.workflow),
11784
+ archivedOld: migrated.archivedOld ? publicMigrationWorkflowSummary(migrated.archivedOld) : undefined,
11785
+ status: "migrated"
11786
+ });
11787
+ } catch (error) {
11788
+ rows.push({
11789
+ loop: publicMigrationLoopSummary(loop),
11790
+ workflow: publicMigrationWorkflowSummary(workflow),
11791
+ status: "blocked",
11792
+ reason: migrationErrorReason(error)
11793
+ });
11794
+ }
11352
11795
  }
11353
11796
  const summary = {
11354
11797
  apply: Boolean(opts.apply),