@hasna/loops 0.4.8 → 0.4.10
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/CHANGELOG.md +24 -0
- package/README.md +4 -4
- package/dist/api/index.js +1 -1
- package/dist/cli/index.js +226 -368
- package/dist/daemon/index.js +76 -327
- package/dist/index.js +63 -314
- package/dist/lib/agent-adapter.d.ts +1 -1
- package/dist/lib/mode.js +1 -1
- package/dist/lib/route/pr-review.d.ts +18 -1
- package/dist/lib/storage/index.js +40 -22
- package/dist/lib/storage/sqlite.js +40 -22
- package/dist/lib/store.js +40 -22
- package/dist/mcp/index.js +63 -314
- package/dist/runner/index.js +33 -303
- package/dist/sdk/index.js +63 -314
- package/docs/USAGE.md +4 -4
- package/package.json +1 -1
package/dist/runner/index.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// package.json
|
|
4
4
|
var package_default = {
|
|
5
5
|
name: "@hasna/loops",
|
|
6
|
-
version: "0.4.
|
|
6
|
+
version: "0.4.10",
|
|
7
7
|
description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
|
|
8
8
|
type: "module",
|
|
9
9
|
main: "dist/index.js",
|
|
@@ -365,7 +365,7 @@ function scrubSecretsDeep(value) {
|
|
|
365
365
|
}
|
|
366
366
|
|
|
367
367
|
// src/lib/agent-adapter.ts
|
|
368
|
-
var
|
|
368
|
+
var UNSAFE_CODEWITH_EXEC_EXTRA_ARGS = new Set([
|
|
369
369
|
"e",
|
|
370
370
|
"exec",
|
|
371
371
|
"agent",
|
|
@@ -414,12 +414,12 @@ function validateAgentOptions(target, label, capabilities) {
|
|
|
414
414
|
throw new Error(`${label}.agent is not supported for provider codex`);
|
|
415
415
|
}
|
|
416
416
|
if (provider === "codewith" && target.agent !== undefined) {
|
|
417
|
-
throw new Error(`${label}.agent is not supported for provider codewith
|
|
417
|
+
throw new Error(`${label}.agent is not supported for provider codewith`);
|
|
418
418
|
}
|
|
419
419
|
if (provider === "codewith") {
|
|
420
|
-
const unsafe = target.extraArgs?.find((arg) =>
|
|
420
|
+
const unsafe = target.extraArgs?.find((arg) => UNSAFE_CODEWITH_EXEC_EXTRA_ARGS.has(arg));
|
|
421
421
|
if (unsafe) {
|
|
422
|
-
throw new Error(`${label}.extraArgs cannot include ${unsafe}; codewith
|
|
422
|
+
throw new Error(`${label}.extraArgs cannot include ${unsafe}; codewith exec launch flags are managed by the adapter`);
|
|
423
423
|
}
|
|
424
424
|
}
|
|
425
425
|
if (target.addDirs?.length && !["codewith", "codex"].includes(provider)) {
|
|
@@ -502,7 +502,10 @@ function buildAgentInvocation(target) {
|
|
|
502
502
|
args.push(...target.authProfile ? ["--auth-profile", target.authProfile] : []);
|
|
503
503
|
if (target.variant)
|
|
504
504
|
args.push("-c", `model_reasoning_effort=${configStringValue(target.variant)}`);
|
|
505
|
-
|
|
505
|
+
const sandbox = codewithLikeSandbox(target);
|
|
506
|
+
if (sandbox === "workspace-write")
|
|
507
|
+
args.push("-c", "sandbox_workspace_write.network_access=true");
|
|
508
|
+
args.push("--ask-for-approval", "never", "exec", "--json", "--ephemeral", "--sandbox", sandbox, "--skip-git-repo-check");
|
|
506
509
|
if (target.cwd)
|
|
507
510
|
args.push("--cd", target.cwd);
|
|
508
511
|
for (const dir of target.addDirs ?? [])
|
|
@@ -510,11 +513,7 @@ function buildAgentInvocation(target) {
|
|
|
510
513
|
if (target.model)
|
|
511
514
|
args.push("--model", target.model);
|
|
512
515
|
args.push(...target.extraArgs ?? []);
|
|
513
|
-
|
|
514
|
-
if (target.cwd)
|
|
515
|
-
args.push("--cwd", target.cwd);
|
|
516
|
-
args.push(target.prompt);
|
|
517
|
-
return { command: "codewith", args };
|
|
516
|
+
return { command: "codewith", args, stdin: target.prompt };
|
|
518
517
|
}
|
|
519
518
|
case "codex": {
|
|
520
519
|
if (target.variant)
|
|
@@ -567,7 +566,7 @@ function adapterFor(provider, capabilities) {
|
|
|
567
566
|
var PROVIDER_ADAPTERS = {
|
|
568
567
|
claude: adapterFor("claude", { sandbox: [], durable: false, remote: true, promptChannel: "stdin" }),
|
|
569
568
|
cursor: adapterFor("cursor", { sandbox: CURSOR_SANDBOXES, durable: false, remote: true, promptChannel: "stdin" }),
|
|
570
|
-
codewith: adapterFor("codewith", { sandbox: CODEX_LIKE_SANDBOXES, durable:
|
|
569
|
+
codewith: adapterFor("codewith", { sandbox: CODEX_LIKE_SANDBOXES, durable: false, remote: true, promptChannel: "stdin" }),
|
|
571
570
|
codex: adapterFor("codex", { sandbox: CODEX_LIKE_SANDBOXES, durable: false, remote: true, promptChannel: "stdin" }),
|
|
572
571
|
aicopilot: adapterFor("aicopilot", { sandbox: [], durable: false, remote: true, promptChannel: "stdin" }),
|
|
573
572
|
opencode: adapterFor("opencode", { sandbox: [], durable: false, remote: true, promptChannel: "stdin" })
|
|
@@ -1041,11 +1040,6 @@ var TRANSPORT_ENV_KEYS = new Set([
|
|
|
1041
1040
|
"USER",
|
|
1042
1041
|
"XDG_RUNTIME_DIR"
|
|
1043
1042
|
]);
|
|
1044
|
-
function boundedText(text, maxBytes) {
|
|
1045
|
-
const buffer = new BoundedOutputBuffer(maxBytes);
|
|
1046
|
-
buffer.append(text);
|
|
1047
|
-
return buffer.value();
|
|
1048
|
-
}
|
|
1049
1043
|
function buildResult(status, startedAt, fields = {}) {
|
|
1050
1044
|
const finishedAt = fields.finishedAt ?? nowIso();
|
|
1051
1045
|
return {
|
|
@@ -1083,6 +1077,20 @@ function notifySpawn(pid, opts) {
|
|
|
1083
1077
|
function shellQuote(value) {
|
|
1084
1078
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
1085
1079
|
}
|
|
1080
|
+
function codewithProfileCandidateFromLine(line) {
|
|
1081
|
+
const cols = line.trim().split(/\s+/);
|
|
1082
|
+
if (cols[0] === "*")
|
|
1083
|
+
return cols[1];
|
|
1084
|
+
return cols[0];
|
|
1085
|
+
}
|
|
1086
|
+
function codewithProfileForError(profile) {
|
|
1087
|
+
return /[\u0000-\u001F\u007F]/.test(profile) ? JSON.stringify(profile) ?? profile : profile;
|
|
1088
|
+
}
|
|
1089
|
+
function assertCodewithAuthProfileSupported(profile) {
|
|
1090
|
+
if (profile.includes("\x00")) {
|
|
1091
|
+
throw new Error(`codewith auth profile contains unsupported NUL byte: ${codewithProfileForError(profile)}`);
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1086
1094
|
function metadataEnv(metadata) {
|
|
1087
1095
|
const env = {};
|
|
1088
1096
|
if (metadata.loopId)
|
|
@@ -1109,21 +1117,6 @@ function metadataEnv(metadata) {
|
|
|
1109
1117
|
env.LOOPS_GOAL_NODE_KEY = metadata.goalNodeKey;
|
|
1110
1118
|
return env;
|
|
1111
1119
|
}
|
|
1112
|
-
function codewithAgentIdempotencyKey(metadata) {
|
|
1113
|
-
const parts = [
|
|
1114
|
-
"openloops",
|
|
1115
|
-
metadata.workflowRunId ? `workflow-run:${metadata.workflowRunId}` : undefined,
|
|
1116
|
-
metadata.workflowStepId ? `step:${metadata.workflowStepId}` : undefined,
|
|
1117
|
-
metadata.runId ? `loop-run:${metadata.runId}` : undefined,
|
|
1118
|
-
metadata.loopId ? `loop:${metadata.loopId}` : undefined,
|
|
1119
|
-
metadata.scheduledFor ? `scheduled:${metadata.scheduledFor}` : undefined,
|
|
1120
|
-
metadata.goalId ? `goal:${metadata.goalId}` : undefined,
|
|
1121
|
-
metadata.goalNodeKey ? `node:${metadata.goalNodeKey}` : undefined
|
|
1122
|
-
].filter(Boolean);
|
|
1123
|
-
if (parts.length > 1)
|
|
1124
|
-
return parts.join(":");
|
|
1125
|
-
return `openloops:adhoc:${randomBytes2(16).toString("hex")}`;
|
|
1126
|
-
}
|
|
1127
1120
|
function allowlistEnv(allowlist) {
|
|
1128
1121
|
const env = {};
|
|
1129
1122
|
if (allowlist?.tools?.length)
|
|
@@ -1164,6 +1157,9 @@ function commandSpec(target, opts) {
|
|
|
1164
1157
|
};
|
|
1165
1158
|
}
|
|
1166
1159
|
const agentTarget = target;
|
|
1160
|
+
if (agentTarget.provider === "codewith" && agentTarget.authProfile) {
|
|
1161
|
+
assertCodewithAuthProfileSupported(agentTarget.authProfile);
|
|
1162
|
+
}
|
|
1167
1163
|
const adapter = providerAdapter(agentTarget.provider);
|
|
1168
1164
|
const invocation = adapter.buildInvocation(agentTarget);
|
|
1169
1165
|
return {
|
|
@@ -1178,8 +1174,7 @@ function commandSpec(target, opts) {
|
|
|
1178
1174
|
preflightAnyOf: invocation.preflightAnyOf,
|
|
1179
1175
|
stdin: invocation.stdin,
|
|
1180
1176
|
allowlist: agentTarget.allowlist,
|
|
1181
|
-
worktree: agentTarget.worktree
|
|
1182
|
-
codewithDurableAgent: adapter.capabilities.durable ? { target: agentTarget } : undefined
|
|
1177
|
+
worktree: agentTarget.worktree
|
|
1183
1178
|
};
|
|
1184
1179
|
}
|
|
1185
1180
|
function composeExecutionEnv(spec, metadata, opts, accountEnv) {
|
|
@@ -1337,7 +1332,8 @@ function remotePreflightScript(spec, metadata) {
|
|
|
1337
1332
|
lines.push(`if ! ${spec.preflightAnyOf.map((command) => `command -v ${shellQuote(command)} >/dev/null 2>&1`).join(" && ! ")}; then`, ` echo 'none of required executables found: ${spec.preflightAnyOf.join(", ")}' >&2`, " exit 127", "fi");
|
|
1338
1333
|
}
|
|
1339
1334
|
if (spec.nativeAuthProfile?.provider === "codewith") {
|
|
1340
|
-
|
|
1335
|
+
const profileForError = codewithProfileForError(spec.nativeAuthProfile.profile);
|
|
1336
|
+
lines.push(`__OPENLOOPS_CODEWITH_PROFILE=${shellQuote(spec.nativeAuthProfile.profile)}`, "export __OPENLOOPS_CODEWITH_PROFILE", `__OPENLOOPS_CODEWITH_PROFILES="$(${shellQuote(spec.command)} profile list)" || {`, ` printf '%s\\n' ${shellQuote("codewith auth profile preflight failed")} >&2`, " exit 1", "}", `if ! printf '%s\\n' "$__OPENLOOPS_CODEWITH_PROFILES" | awk 'NR > 1 { candidate = ($1 == "*" ? $2 : $1); if (candidate == ENVIRON["__OPENLOOPS_CODEWITH_PROFILE"]) found = 1 } END { exit(found ? 0 : 1) }'; then`, ` printf '%s\\n' ${shellQuote(`codewith auth profile not found: ${profileForError}`)} >&2`, " exit 1", "fi");
|
|
1341
1337
|
}
|
|
1342
1338
|
return lines.join(`
|
|
1343
1339
|
`);
|
|
@@ -1362,9 +1358,9 @@ function assertCodewithProfileListed(profile, result) {
|
|
|
1362
1358
|
const detail = (result.stderr || result.stdout || `exit ${result.status ?? "unknown"}`).trim();
|
|
1363
1359
|
throw new Error(`codewith auth profile preflight failed${detail ? `: ${detail}` : ""}`);
|
|
1364
1360
|
}
|
|
1365
|
-
const profiles = new Set((result.stdout || "").split(/\r?\n/).slice(1).map(
|
|
1361
|
+
const profiles = new Set((result.stdout || "").split(/\r?\n/).slice(1).map(codewithProfileCandidateFromLine).filter(Boolean));
|
|
1366
1362
|
if (!profiles.has(profile)) {
|
|
1367
|
-
throw new Error(`codewith auth profile not found: ${profile}`);
|
|
1363
|
+
throw new Error(`codewith auth profile not found: ${codewithProfileForError(profile)}`);
|
|
1368
1364
|
}
|
|
1369
1365
|
}
|
|
1370
1366
|
function preflightNativeAuthProfileSync(spec, env) {
|
|
@@ -1389,266 +1385,6 @@ async function preflightNativeAuthProfile(spec, env) {
|
|
|
1389
1385
|
const result = await spawnCapture(spec.command, ["profile", "list"], { env, timeoutMs: 15000 });
|
|
1390
1386
|
assertCodewithProfileListed(spec.nativeAuthProfile.profile, result);
|
|
1391
1387
|
}
|
|
1392
|
-
function codewithAgentStartArgs(target, idempotencyKey) {
|
|
1393
|
-
const args = providerAdapter(target.provider).buildInvocation(target).args;
|
|
1394
|
-
const startIndex = args.findIndex((arg, index) => arg === "start" && args[index - 1] === "agent");
|
|
1395
|
-
if (startIndex === -1)
|
|
1396
|
-
throw new Error("internal error: codewith durable agent args missing agent start");
|
|
1397
|
-
args.splice(startIndex + 1, 0, "--idempotency-key", idempotencyKey);
|
|
1398
|
-
return args;
|
|
1399
|
-
}
|
|
1400
|
-
function codewithAgentControlArgs(target, command, agentId) {
|
|
1401
|
-
return [
|
|
1402
|
-
...target.authProfile ? ["--auth-profile", target.authProfile] : [],
|
|
1403
|
-
"agent",
|
|
1404
|
-
command,
|
|
1405
|
-
...command === "logs" ? ["--limit", "20"] : [],
|
|
1406
|
-
"--json",
|
|
1407
|
-
agentId
|
|
1408
|
-
];
|
|
1409
|
-
}
|
|
1410
|
-
function extractFirstJsonObject(text) {
|
|
1411
|
-
let depth = 0;
|
|
1412
|
-
let start = -1;
|
|
1413
|
-
let inString = false;
|
|
1414
|
-
let escaped = false;
|
|
1415
|
-
for (let i = 0;i < text.length; i += 1) {
|
|
1416
|
-
const ch = text[i];
|
|
1417
|
-
if (inString) {
|
|
1418
|
-
if (escaped)
|
|
1419
|
-
escaped = false;
|
|
1420
|
-
else if (ch === "\\")
|
|
1421
|
-
escaped = true;
|
|
1422
|
-
else if (ch === '"')
|
|
1423
|
-
inString = false;
|
|
1424
|
-
continue;
|
|
1425
|
-
}
|
|
1426
|
-
if (ch === '"') {
|
|
1427
|
-
inString = true;
|
|
1428
|
-
} else if (ch === "{") {
|
|
1429
|
-
if (depth === 0)
|
|
1430
|
-
start = i;
|
|
1431
|
-
depth += 1;
|
|
1432
|
-
} else if (ch === "}") {
|
|
1433
|
-
if (depth > 0) {
|
|
1434
|
-
depth -= 1;
|
|
1435
|
-
if (depth === 0 && start !== -1)
|
|
1436
|
-
return text.slice(start, i + 1);
|
|
1437
|
-
}
|
|
1438
|
-
}
|
|
1439
|
-
}
|
|
1440
|
-
return;
|
|
1441
|
-
}
|
|
1442
|
-
function parseJsonOutput(stdout, label) {
|
|
1443
|
-
const raw = stdout || "{}";
|
|
1444
|
-
const candidates = [raw.trim()];
|
|
1445
|
-
const scanned = extractFirstJsonObject(raw);
|
|
1446
|
-
if (scanned && scanned !== raw.trim())
|
|
1447
|
-
candidates.push(scanned);
|
|
1448
|
-
let lastError;
|
|
1449
|
-
for (const candidate of candidates) {
|
|
1450
|
-
try {
|
|
1451
|
-
const value = JSON.parse(candidate);
|
|
1452
|
-
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
1453
|
-
throw new Error("not an object");
|
|
1454
|
-
return value;
|
|
1455
|
-
} catch (error) {
|
|
1456
|
-
lastError = error;
|
|
1457
|
-
}
|
|
1458
|
-
}
|
|
1459
|
-
throw new Error(`${label} did not return JSON${lastError instanceof Error ? `: ${lastError.message}` : ""}`);
|
|
1460
|
-
}
|
|
1461
|
-
function recordField(value, key) {
|
|
1462
|
-
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
1463
|
-
return;
|
|
1464
|
-
const field = value[key];
|
|
1465
|
-
return field && typeof field === "object" && !Array.isArray(field) ? field : undefined;
|
|
1466
|
-
}
|
|
1467
|
-
function stringField(value, key) {
|
|
1468
|
-
const field = value?.[key];
|
|
1469
|
-
return typeof field === "string" ? field : undefined;
|
|
1470
|
-
}
|
|
1471
|
-
function numberField(value, key) {
|
|
1472
|
-
const field = value?.[key];
|
|
1473
|
-
return typeof field === "number" && Number.isFinite(field) ? field : undefined;
|
|
1474
|
-
}
|
|
1475
|
-
function codewithAgentStatus(readJson) {
|
|
1476
|
-
return stringField(recordField(readJson, "agent"), "status") ?? stringField(recordField(readJson, "statusSnapshot"), "status");
|
|
1477
|
-
}
|
|
1478
|
-
function codewithAgentLastEventSeq(readJson) {
|
|
1479
|
-
const agentSeq = numberField(recordField(readJson, "agent"), "lastEventSeq");
|
|
1480
|
-
const snapshotSeq = numberField(recordField(readJson, "statusSnapshot"), "lastEventSeq");
|
|
1481
|
-
if (agentSeq !== undefined && snapshotSeq !== undefined)
|
|
1482
|
-
return Math.max(agentSeq, snapshotSeq);
|
|
1483
|
-
return agentSeq ?? snapshotSeq;
|
|
1484
|
-
}
|
|
1485
|
-
function codewithAgentEvidence(startJson, readJson, logsJson) {
|
|
1486
|
-
const agent = recordField(readJson, "agent") ?? recordField(startJson, "agent");
|
|
1487
|
-
const statusSnapshot = recordField(readJson, "statusSnapshot");
|
|
1488
|
-
const events = Array.isArray(logsJson?.data) ? logsJson.data.filter((event) => Boolean(event) && typeof event === "object" && !Array.isArray(event)).map((event) => ({
|
|
1489
|
-
seq: numberField(event, "seq"),
|
|
1490
|
-
eventType: stringField(event, "eventType"),
|
|
1491
|
-
createdAt: numberField(event, "createdAt")
|
|
1492
|
-
})) : undefined;
|
|
1493
|
-
return JSON.stringify({
|
|
1494
|
-
codewithAgent: {
|
|
1495
|
-
agentId: stringField(agent, "agentId"),
|
|
1496
|
-
status: stringField(agent, "status"),
|
|
1497
|
-
desiredState: stringField(agent, "desiredState"),
|
|
1498
|
-
statusReason: stringField(agent, "statusReason"),
|
|
1499
|
-
threadId: stringField(agent, "threadId"),
|
|
1500
|
-
rolloutPath: stringField(agent, "rolloutPath"),
|
|
1501
|
-
pid: numberField(agent, "pid"),
|
|
1502
|
-
exitCode: numberField(agent, "exitCode"),
|
|
1503
|
-
created: typeof startJson.created === "boolean" ? startJson.created : undefined
|
|
1504
|
-
},
|
|
1505
|
-
statusSnapshot: statusSnapshot ? {
|
|
1506
|
-
seq: numberField(statusSnapshot, "seq"),
|
|
1507
|
-
status: stringField(statusSnapshot, "status"),
|
|
1508
|
-
summary: stringField(statusSnapshot, "summary"),
|
|
1509
|
-
pendingInteractionCount: numberField(statusSnapshot, "pendingInteractionCount"),
|
|
1510
|
-
lastEventSeq: numberField(statusSnapshot, "lastEventSeq")
|
|
1511
|
-
} : undefined,
|
|
1512
|
-
events
|
|
1513
|
-
}, null, 2);
|
|
1514
|
-
}
|
|
1515
|
-
function codewithAgentProgress(readJson) {
|
|
1516
|
-
const agent = recordField(readJson, "agent");
|
|
1517
|
-
const statusSnapshot = recordField(readJson, "statusSnapshot");
|
|
1518
|
-
return {
|
|
1519
|
-
provider: "codewith",
|
|
1520
|
-
agentId: stringField(agent, "agentId"),
|
|
1521
|
-
status: stringField(agent, "status") ?? stringField(statusSnapshot, "status"),
|
|
1522
|
-
summary: stringField(statusSnapshot, "summary"),
|
|
1523
|
-
statusReason: stringField(agent, "statusReason"),
|
|
1524
|
-
threadId: stringField(agent, "threadId"),
|
|
1525
|
-
rolloutPath: stringField(agent, "rolloutPath"),
|
|
1526
|
-
pid: numberField(agent, "pid"),
|
|
1527
|
-
lastEventSeq: codewithAgentLastEventSeq(readJson)
|
|
1528
|
-
};
|
|
1529
|
-
}
|
|
1530
|
-
function sleep(ms) {
|
|
1531
|
-
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
1532
|
-
}
|
|
1533
|
-
async function executeCodewithDurableAgent(spec, metadata, opts, env, startedAt) {
|
|
1534
|
-
const target = spec.codewithDurableAgent?.target;
|
|
1535
|
-
if (!target)
|
|
1536
|
-
throw new Error("internal error: missing codewith durable target");
|
|
1537
|
-
const maxOutputBytes = opts.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
|
|
1538
|
-
const idempotencyKey = codewithAgentIdempotencyKey(metadata);
|
|
1539
|
-
const start = await spawnCapture(spec.command, codewithAgentStartArgs(target, idempotencyKey), {
|
|
1540
|
-
cwd: spec.cwd,
|
|
1541
|
-
env,
|
|
1542
|
-
timeoutMs: 30000,
|
|
1543
|
-
maxOutputBytes
|
|
1544
|
-
});
|
|
1545
|
-
const stderr = new BoundedOutputBuffer(maxOutputBytes);
|
|
1546
|
-
stderr.append(start.stderr);
|
|
1547
|
-
if (start.error || (start.status ?? 1) !== 0) {
|
|
1548
|
-
return failureResult(startedAt, start.error ?? `codewith agent start exited with code ${start.status ?? "unknown"}`, {
|
|
1549
|
-
exitCode: start.status ?? undefined,
|
|
1550
|
-
stdout: start.stdout,
|
|
1551
|
-
stderr: stderr.value()
|
|
1552
|
-
});
|
|
1553
|
-
}
|
|
1554
|
-
let startJson;
|
|
1555
|
-
try {
|
|
1556
|
-
startJson = parseJsonOutput(start.stdout || "{}", "codewith agent start");
|
|
1557
|
-
} catch (error) {
|
|
1558
|
-
return failureResult(startedAt, error instanceof Error ? error.message : String(error), { stdout: start.stdout, stderr: stderr.value() });
|
|
1559
|
-
}
|
|
1560
|
-
const agentId = stringField(recordField(startJson, "agent"), "agentId");
|
|
1561
|
-
if (!agentId) {
|
|
1562
|
-
return failureResult(startedAt, "codewith agent start did not return agent.agentId", { stdout: start.stdout, stderr: stderr.value() });
|
|
1563
|
-
}
|
|
1564
|
-
opts.onAgentProgress?.(codewithAgentProgress(startJson));
|
|
1565
|
-
const stopAgent = async () => {
|
|
1566
|
-
await spawnCapture(spec.command, codewithAgentControlArgs(target, "stop", agentId), {
|
|
1567
|
-
cwd: spec.cwd,
|
|
1568
|
-
env,
|
|
1569
|
-
timeoutMs: 15000,
|
|
1570
|
-
maxOutputBytes
|
|
1571
|
-
});
|
|
1572
|
-
};
|
|
1573
|
-
const pollMs = Math.max(100, Number(opts.env?.LOOPS_CODEWITH_AGENT_POLL_MS ?? process.env.LOOPS_CODEWITH_AGENT_POLL_MS ?? 2000) || 2000);
|
|
1574
|
-
let lastReadJson = startJson;
|
|
1575
|
-
let lastLogsJson;
|
|
1576
|
-
let lastFingerprint;
|
|
1577
|
-
let lastProgressAt = Date.now();
|
|
1578
|
-
const evidence = () => boundedText(codewithAgentEvidence(startJson, lastReadJson, lastLogsJson), maxOutputBytes);
|
|
1579
|
-
while (true) {
|
|
1580
|
-
if (opts.signal?.aborted) {
|
|
1581
|
-
await stopAgent();
|
|
1582
|
-
return failureResult(startedAt, "cancelled", { stdout: evidence(), stderr: stderr.value() });
|
|
1583
|
-
}
|
|
1584
|
-
const read = await spawnCapture(spec.command, codewithAgentControlArgs(target, "read", agentId), {
|
|
1585
|
-
cwd: spec.cwd,
|
|
1586
|
-
env,
|
|
1587
|
-
timeoutMs: 30000,
|
|
1588
|
-
maxOutputBytes
|
|
1589
|
-
});
|
|
1590
|
-
stderr.append(read.stderr);
|
|
1591
|
-
if (read.error || (read.status ?? 1) !== 0) {
|
|
1592
|
-
return failureResult(startedAt, read.error ?? `codewith agent read exited with code ${read.status ?? "unknown"}`, {
|
|
1593
|
-
exitCode: read.status ?? undefined,
|
|
1594
|
-
stdout: evidence(),
|
|
1595
|
-
stderr: stderr.value()
|
|
1596
|
-
});
|
|
1597
|
-
}
|
|
1598
|
-
try {
|
|
1599
|
-
lastReadJson = parseJsonOutput(read.stdout || "{}", "codewith agent read");
|
|
1600
|
-
} catch (error) {
|
|
1601
|
-
return failureResult(startedAt, error instanceof Error ? error.message : String(error), {
|
|
1602
|
-
stdout: boundedText(read.stdout, maxOutputBytes),
|
|
1603
|
-
stderr: stderr.value()
|
|
1604
|
-
});
|
|
1605
|
-
}
|
|
1606
|
-
const status = codewithAgentStatus(lastReadJson);
|
|
1607
|
-
const fingerprint = JSON.stringify({
|
|
1608
|
-
status,
|
|
1609
|
-
agentLastEventSeq: numberField(recordField(lastReadJson, "agent"), "lastEventSeq"),
|
|
1610
|
-
snapshot: recordField(lastReadJson, "statusSnapshot") ?? null
|
|
1611
|
-
});
|
|
1612
|
-
if (fingerprint !== lastFingerprint) {
|
|
1613
|
-
lastFingerprint = fingerprint;
|
|
1614
|
-
lastProgressAt = Date.now();
|
|
1615
|
-
opts.onAgentProgress?.(codewithAgentProgress(lastReadJson));
|
|
1616
|
-
}
|
|
1617
|
-
if (status === "completed" || status === "failed" || status === "cancelled") {
|
|
1618
|
-
const logs = await spawnCapture(spec.command, codewithAgentControlArgs(target, "logs", agentId), {
|
|
1619
|
-
cwd: spec.cwd,
|
|
1620
|
-
env,
|
|
1621
|
-
timeoutMs: 30000,
|
|
1622
|
-
maxOutputBytes
|
|
1623
|
-
});
|
|
1624
|
-
if (!logs.error && (logs.status ?? 1) === 0) {
|
|
1625
|
-
try {
|
|
1626
|
-
lastLogsJson = parseJsonOutput(logs.stdout || "{}", "codewith agent logs");
|
|
1627
|
-
} catch {
|
|
1628
|
-
lastLogsJson = undefined;
|
|
1629
|
-
}
|
|
1630
|
-
} else {
|
|
1631
|
-
stderr.append(logs.stderr || logs.error || "");
|
|
1632
|
-
}
|
|
1633
|
-
if (status === "completed") {
|
|
1634
|
-
return successResult(startedAt, { exitCode: 0, stdout: evidence(), stderr: stderr.value() });
|
|
1635
|
-
}
|
|
1636
|
-
return failureResult(startedAt, stringField(recordField(lastReadJson, "agent"), "statusReason") ?? `codewith agent ${status}`, { exitCode: 1, stdout: evidence(), stderr: stderr.value() });
|
|
1637
|
-
}
|
|
1638
|
-
if (typeof spec.timeoutMs === "number" && new Date(nowIso()).getTime() - new Date(startedAt).getTime() >= spec.timeoutMs) {
|
|
1639
|
-
await stopAgent();
|
|
1640
|
-
return timeoutResult(startedAt, `timed out after ${spec.timeoutMs}ms`, { stdout: evidence(), stderr: stderr.value() });
|
|
1641
|
-
}
|
|
1642
|
-
if (typeof spec.idleTimeoutMs === "number" && Date.now() - lastProgressAt >= spec.idleTimeoutMs) {
|
|
1643
|
-
await stopAgent();
|
|
1644
|
-
return timeoutResult(startedAt, `idle timed out after ${spec.idleTimeoutMs}ms without agent progress`, {
|
|
1645
|
-
stdout: evidence(),
|
|
1646
|
-
stderr: stderr.value()
|
|
1647
|
-
});
|
|
1648
|
-
}
|
|
1649
|
-
await sleep(pollMs);
|
|
1650
|
-
}
|
|
1651
|
-
}
|
|
1652
1388
|
function resolvedDirEquals(left, right) {
|
|
1653
1389
|
try {
|
|
1654
1390
|
return realpathSync(left) === realpathSync(right);
|
|
@@ -1882,9 +1618,6 @@ function preflightTarget(target, metadata = {}, opts = {}) {
|
|
|
1882
1618
|
async function executeTarget(target, metadata = {}, opts = {}) {
|
|
1883
1619
|
let spec = commandSpec(target, opts);
|
|
1884
1620
|
const machine = resolvedMachine(opts);
|
|
1885
|
-
if (machine && !machine.local && spec.codewithDurableAgent) {
|
|
1886
|
-
return failureResult(nowIso(), "remote Codewith durable background-agent steps require remote status polling support; run this Codewith step locally or add remote durable readback before dispatch");
|
|
1887
|
-
}
|
|
1888
1621
|
if (machine && !machine.local) {
|
|
1889
1622
|
const remoteFallbackSpec = spec.worktree?.enabled && spec.worktree.mode === "auto" ? worktreeFallbackSpec(target, opts, spec.worktree.originalCwd) : undefined;
|
|
1890
1623
|
return executeRemoteSpec(spec, machine, metadata, opts, remoteFallbackSpec);
|
|
@@ -1915,9 +1648,6 @@ async function executeTarget(target, metadata = {}, opts = {}) {
|
|
|
1915
1648
|
if (worktreeEntry?.fallbackCwd) {
|
|
1916
1649
|
spec = worktreeFallbackSpec(target, opts, worktreeEntry.fallbackCwd) ?? spec;
|
|
1917
1650
|
}
|
|
1918
|
-
if (spec.codewithDurableAgent) {
|
|
1919
|
-
return executeCodewithDurableAgent(spec, metadata, opts, env, startedAt);
|
|
1920
|
-
}
|
|
1921
1651
|
const child = spawn2(spec.command, spec.args, {
|
|
1922
1652
|
cwd: spec.cwd,
|
|
1923
1653
|
env,
|