@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 +4 -4
- package/dist/cli/index.js +351 -37
- package/dist/daemon/index.js +330 -16
- package/dist/index.js +323 -9
- package/dist/lib/store.js +26 -1
- package/dist/mcp/index.js +323 -9
- package/dist/sdk/index.js +322 -8
- package/docs/USAGE.md +4 -4
- package/package.json +1 -1
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
|
|
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
|
|
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
|
|
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 `
|
|
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", "
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 (!
|
|
10211
|
+
if (!stringField2(event.id))
|
|
9898
10212
|
throw new Error("event.id is required");
|
|
9899
|
-
if (!
|
|
10213
|
+
if (!stringField2(event.type))
|
|
9900
10214
|
throw new Error("event.type is required");
|
|
9901
|
-
if (!
|
|
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:
|
|
10242
|
-
eventMessage:
|
|
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:
|
|
10602
|
+
id: stringField2(event.subject) ?? event.id,
|
|
10289
10603
|
path: routeProjectPath,
|
|
10290
|
-
raw: { 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:
|
|
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 =
|
|
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"]) ??
|
|
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:
|
|
10530
|
-
reason:
|
|
10531
|
-
loopId:
|
|
10532
|
-
loopName:
|
|
10533
|
-
workflowId:
|
|
10534
|
-
workflowName:
|
|
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 =
|
|
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 });
|