@hasna/loops 0.3.59 → 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/README.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`
@@ -867,13 +867,13 @@ On Linux this writes a user systemd service. On macOS it writes a LaunchAgent pl
867
867
  The adapters intentionally use provider command surfaces instead of pretending every agent has one SDK:
868
868
 
869
869
  - Claude uses `claude -p --output-format json` and safe-mode/local setting sources by default.
870
- - Codewith uses `codewith --ask-for-approval never exec --json --ephemeral --skip-git-repo-check`, with `--add-dir` for explicit extra writable directories.
870
+ - 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.
871
871
  - AI Copilot and OpenCode use `run --format json --pure`. OpenCode requires an explicit provider/model id because ambient OpenCode config is machine-specific.
872
872
  - 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.
873
873
  - Codex uses `codex --ask-for-approval never exec --json --ephemeral --skip-git-repo-check`, with `--add-dir` for explicit extra writable directories where supported.
874
- - Agent prompts are sent through child stdin instead of argv so prompt bodies do not appear in process listings.
874
+ - 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.
875
875
  - 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.
876
- - `--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.
876
+ - `--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.
877
877
  - `--sandbox` maps to provider-native sandbox flags. Codewith/Codex accept `read-only`, `workspace-write`, or `danger-full-access`; Cursor accepts `enabled` or `disabled`.
878
878
  - `--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`.
879
879
  - `--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/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 };
@@ -3604,6 +3629,9 @@ function appendBounded(current, chunk, maxBytes) {
3604
3629
  return `[truncated ${overflow} bytes]
3605
3630
  ${next.slice(-maxBytes)}`;
3606
3631
  }
3632
+ function appendBoundedText(current, chunk, maxBytes) {
3633
+ return appendBounded(current, Buffer.from(chunk, "utf8"), maxBytes);
3634
+ }
3607
3635
  function killProcessGroup(pid) {
3608
3636
  try {
3609
3637
  process.kill(-pid, "SIGTERM");
@@ -3651,6 +3679,21 @@ function metadataEnv(metadata) {
3651
3679
  env.LOOPS_GOAL_NODE_KEY = metadata.goalNodeKey;
3652
3680
  return env;
3653
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
+ }
3654
3697
  function allowlistEnv(allowlist) {
3655
3698
  const env = {};
3656
3699
  if (allowlist?.tools?.length)
@@ -3687,6 +3730,20 @@ function codewithLikeSandbox(target) {
3687
3730
  function configStringValue(value) {
3688
3731
  return JSON.stringify(value);
3689
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
+ ]);
3690
3747
  function assertStringOption(value, label) {
3691
3748
  if (value !== undefined && typeof value !== "string")
3692
3749
  throw new Error(`${label} must be a string`);
@@ -3717,6 +3774,14 @@ function assertSupportedAgentOptions(target) {
3717
3774
  }
3718
3775
  if (target.provider === "codex" && target.agent !== undefined)
3719
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
+ }
3720
3785
  if (["codewith", "codex"].includes(target.provider)) {
3721
3786
  if (target.permissionMode && !["default", "bypass"].includes(target.permissionMode)) {
3722
3787
  throw new Error(`${target.provider}.permissionMode supports only default or bypass`);
@@ -3800,18 +3865,18 @@ function agentArgs(target) {
3800
3865
  args.push(...target.authProfile ? ["--auth-profile", target.authProfile] : []);
3801
3866
  if (target.variant)
3802
3867
  args.push("-c", `model_reasoning_effort=${configStringValue(target.variant)}`);
3803
- args.push("--ask-for-approval", "never", "exec", "--json", "--ephemeral", "--sandbox", codewithLikeSandbox(target), "--skip-git-repo-check");
3804
- if (isolation === "safe")
3805
- args.push("--ignore-rules");
3868
+ args.push("--ask-for-approval", "never", "--sandbox", codewithLikeSandbox(target));
3806
3869
  if (target.cwd)
3807
3870
  args.push("--cd", target.cwd);
3808
3871
  for (const dir of target.addDirs ?? [])
3809
3872
  args.push("--add-dir", dir);
3810
3873
  if (target.model)
3811
3874
  args.push("--model", target.model);
3812
- if (target.agent)
3813
- args.push("--agent", target.agent);
3814
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);
3815
3880
  return args;
3816
3881
  case "codex":
3817
3882
  if (target.variant)
@@ -3887,8 +3952,9 @@ function commandSpec(target) {
3887
3952
  accountTool: agentTarget.account?.tool ?? accountToolForProvider(agentTarget.provider),
3888
3953
  nativeAuthProfile: agentTarget.authProfile ? { provider: agentTarget.provider, profile: agentTarget.authProfile } : undefined,
3889
3954
  preflightAnyOf: agentTarget.provider === "cursor" ? ["agent"] : undefined,
3890
- stdin: agentTarget.prompt,
3891
- allowlist: agentTarget.allowlist
3955
+ stdin: agentTarget.provider === "codewith" ? undefined : agentTarget.prompt,
3956
+ allowlist: agentTarget.allowlist,
3957
+ codewithDurableAgent: agentTarget.provider === "codewith" ? { target: agentTarget } : undefined
3892
3958
  };
3893
3959
  }
3894
3960
  function executionEnv(spec, metadata, opts) {
@@ -4008,6 +4074,238 @@ function preflightNativeAuthProfile(spec, env) {
4008
4074
  throw new Error(`codewith auth profile not found: ${spec.nativeAuthProfile.profile}`);
4009
4075
  }
4010
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
+ }
4011
4309
  function preflightRemoteSpec(spec, machine, metadata, opts) {
4012
4310
  const plan = (opts.machineCommandResolver ?? resolveMachineCommand)(machine.id, "bash -s");
4013
4311
  const result = spawnSync2(plan.command, plan.args, {
@@ -4179,6 +4477,19 @@ function preflightTarget(target, metadata = {}, opts = {}) {
4179
4477
  async function executeTarget(target, metadata = {}, opts = {}) {
4180
4478
  const spec = commandSpec(target);
4181
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
+ }
4182
4493
  if (machine && !machine.local)
4183
4494
  return executeRemoteSpec(spec, machine, metadata, opts);
4184
4495
  const maxOutputBytes = opts.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
@@ -4225,6 +4536,9 @@ async function executeTarget(target, metadata = {}, opts = {}) {
4225
4536
  durationMs: 0
4226
4537
  };
4227
4538
  }
4539
+ if (spec.codewithDurableAgent) {
4540
+ return executeCodewithDurableAgent(spec, metadata, opts, env, startedAt);
4541
+ }
4228
4542
  const child = spawn(spec.command, spec.args, {
4229
4543
  cwd: spec.cwd,
4230
4544
  env,
@@ -5436,7 +5750,7 @@ function realSleep(ms) {
5436
5750
  return new Promise((resolve2) => setTimeout(resolve2, ms));
5437
5751
  }
5438
5752
  async function runLoop(opts) {
5439
- const sleep = opts.sleep ?? realSleep;
5753
+ const sleep2 = opts.sleep ?? realSleep;
5440
5754
  const sliceMs = opts.sliceMs ?? 200;
5441
5755
  while (!opts.shouldStop()) {
5442
5756
  try {
@@ -5447,7 +5761,7 @@ async function runLoop(opts) {
5447
5761
  let waited = 0;
5448
5762
  while (waited < opts.intervalMs && !opts.shouldStop()) {
5449
5763
  const chunk = Math.min(sliceMs, opts.intervalMs - waited);
5450
- await sleep(chunk);
5764
+ await sleep2(chunk);
5451
5765
  waited += chunk;
5452
5766
  }
5453
5767
  }
@@ -5529,7 +5843,7 @@ async function stopDaemon(opts = {}) {
5529
5843
  } finally {
5530
5844
  store.close();
5531
5845
  }
5532
- const sleep = opts.sleep ?? realSleep;
5846
+ const sleep2 = opts.sleep ?? realSleep;
5533
5847
  try {
5534
5848
  process.kill(state.pid, "SIGTERM");
5535
5849
  } catch {
@@ -5538,7 +5852,7 @@ async function stopDaemon(opts = {}) {
5538
5852
  }
5539
5853
  const steps = Math.max(1, Math.ceil((opts.timeoutMs ?? 6000) / 100));
5540
5854
  for (let i = 0;i < steps; i++) {
5541
- await sleep(100);
5855
+ await sleep2(100);
5542
5856
  if (!isAlive(state.pid)) {
5543
5857
  removePid(path);
5544
5858
  return { wasRunning: true, stopped: true, forced: false, pid: state.pid };
@@ -5547,7 +5861,7 @@ async function stopDaemon(opts = {}) {
5547
5861
  try {
5548
5862
  process.kill(state.pid, "SIGKILL");
5549
5863
  } catch {}
5550
- await sleep(150);
5864
+ await sleep2(150);
5551
5865
  removePid(path);
5552
5866
  return { wasRunning: true, stopped: !isAlive(state.pid), forced: true, pid: state.pid };
5553
5867
  }
@@ -5716,10 +6030,10 @@ async function startDaemon(opts) {
5716
6030
  stdio: ["ignore", out, out]
5717
6031
  });
5718
6032
  child.unref();
5719
- const sleep = opts.sleep ?? realSleep;
6033
+ const sleep2 = opts.sleep ?? realSleep;
5720
6034
  const deadline = Math.max(1, Math.ceil((opts.waitMs ?? 4000) / 100));
5721
6035
  for (let i = 0;i < deadline; i++) {
5722
- await sleep(100);
6036
+ await sleep2(100);
5723
6037
  const current = isDaemonRunning();
5724
6038
  if (current.running)
5725
6039
  return { started: true, alreadyRunning: false, pid: current.pid };
@@ -6561,7 +6875,7 @@ function buildScriptInventoryReport(store, opts = {}) {
6561
6875
  // package.json
6562
6876
  var package_default = {
6563
6877
  name: "@hasna/loops",
6564
- version: "0.3.59",
6878
+ version: "0.3.60",
6565
6879
  description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
6566
6880
  type: "module",
6567
6881
  main: "dist/index.js",
@@ -9168,7 +9482,7 @@ function eventMetadata(event) {
9168
9482
  return metadata;
9169
9483
  return {};
9170
9484
  }
9171
- function stringField(value) {
9485
+ function stringField2(value) {
9172
9486
  return typeof value === "string" && value.trim() ? value.trim() : undefined;
9173
9487
  }
9174
9488
  function slugSegment2(value, fallback = "event") {
@@ -9179,14 +9493,14 @@ function stableSuffix(value) {
9179
9493
  }
9180
9494
  function taskEventField(data, keys) {
9181
9495
  for (const key of keys) {
9182
- const direct = stringField(data[key]);
9496
+ const direct = stringField2(data[key]);
9183
9497
  if (direct)
9184
9498
  return direct;
9185
9499
  }
9186
9500
  const task = data.task;
9187
9501
  if (task && typeof task === "object" && !Array.isArray(task)) {
9188
9502
  for (const key of keys) {
9189
- const direct = stringField(task[key]);
9503
+ const direct = stringField2(task[key]);
9190
9504
  if (direct)
9191
9505
  return direct;
9192
9506
  }
@@ -9194,7 +9508,7 @@ function taskEventField(data, keys) {
9194
9508
  const payload = data.payload;
9195
9509
  if (payload && typeof payload === "object" && !Array.isArray(payload)) {
9196
9510
  for (const key of keys) {
9197
- const direct = stringField(payload[key]);
9511
+ const direct = stringField2(payload[key]);
9198
9512
  if (direct)
9199
9513
  return direct;
9200
9514
  }
@@ -9894,11 +10208,11 @@ async function readEventEnvelopeInput(opts = {}) {
9894
10208
  const event = JSON.parse(raw);
9895
10209
  if (!event || typeof event !== "object" || Array.isArray(event))
9896
10210
  throw new Error("event JSON must be an object");
9897
- if (!stringField(event.id))
10211
+ if (!stringField2(event.id))
9898
10212
  throw new Error("event.id is required");
9899
- if (!stringField(event.type))
10213
+ if (!stringField2(event.type))
9900
10214
  throw new Error("event.type is required");
9901
- if (!stringField(event.source))
10215
+ if (!stringField2(event.source))
9902
10216
  throw new Error("event.source is required");
9903
10217
  return event;
9904
10218
  }
@@ -10238,8 +10552,8 @@ function routeGenericEvent(event, opts) {
10238
10552
  eventId: event.id,
10239
10553
  eventType: event.type,
10240
10554
  eventSource: event.source,
10241
- eventSubject: stringField(event.subject),
10242
- eventMessage: stringField(event.message),
10555
+ eventSubject: stringField2(event.subject),
10556
+ eventMessage: stringField2(event.message),
10243
10557
  eventJson: JSON.stringify(event),
10244
10558
  projectPath,
10245
10559
  routeProjectPath,
@@ -10285,9 +10599,9 @@ function routeGenericEvent(event, opts) {
10285
10599
  },
10286
10600
  subjectRef: {
10287
10601
  kind: "event",
10288
- id: stringField(event.subject) ?? event.id,
10602
+ id: stringField2(event.subject) ?? event.id,
10289
10603
  path: routeProjectPath,
10290
- raw: { message: stringField(event.message) }
10604
+ raw: { message: stringField2(event.message) }
10291
10605
  },
10292
10606
  intent: "route",
10293
10607
  scope: {
@@ -10311,7 +10625,7 @@ function routeGenericEvent(event, opts) {
10311
10625
  invocationId: "<created-invocation-id>",
10312
10626
  sourceType: event.type,
10313
10627
  sourceRef: event.id,
10314
- subjectRef: stringField(event.subject) ?? event.id,
10628
+ subjectRef: stringField2(event.subject) ?? event.id,
10315
10629
  projectKey: routeProjectPath,
10316
10630
  projectGroup,
10317
10631
  priority: 0,
@@ -10455,14 +10769,14 @@ function routeGenericEvent(event, opts) {
10455
10769
  }
10456
10770
  function taskField(task, keys) {
10457
10771
  for (const key of keys) {
10458
- const value = stringField(task[key]);
10772
+ const value = stringField2(task[key]);
10459
10773
  if (value)
10460
10774
  return value;
10461
10775
  }
10462
10776
  return;
10463
10777
  }
10464
10778
  function taskListId(task) {
10465
- 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);
10466
10780
  }
10467
10781
  function taskProjectId(task) {
10468
10782
  return taskField(task, ["project_id", "projectId"]);
@@ -10526,12 +10840,12 @@ function compactDrainResult(result) {
10526
10840
  kind: result.kind,
10527
10841
  taskId: event?.subject,
10528
10842
  eventId: event?.id,
10529
- idempotencyKey: stringField(value.idempotencyKey),
10530
- reason: stringField(value.reason) ?? throttle?.reason,
10531
- loopId: stringField(loop?.id),
10532
- loopName: stringField(loop?.name),
10533
- workflowId: stringField(workflow?.id),
10534
- 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),
10535
10849
  providerRouting,
10536
10850
  requeue,
10537
10851
  queuedAtSource: value.queuedAtSource
@@ -11036,7 +11350,7 @@ routes.command("show <id>").description("show one admission work item").action((
11036
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) => {
11037
11351
  const store = new Store;
11038
11352
  try {
11039
- const reason = stringField(opts.reason);
11353
+ const reason = stringField2(opts.reason);
11040
11354
  if (!reason)
11041
11355
  throw new Error("routes requeue requires --reason <text>");
11042
11356
  const item = store.requeueWorkflowWorkItem(id, { reason });