@hasna/loops 0.4.7 → 0.4.9
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 +34 -0
- package/README.md +4 -4
- package/dist/api/index.js +1 -1
- package/dist/cli/index.js +97 -353
- package/dist/daemon/index.js +25 -313
- package/dist/index.js +26 -308
- package/dist/lib/agent-adapter.d.ts +1 -1
- package/dist/lib/mode.js +1 -1
- package/dist/lib/route/pr-review.d.ts +11 -1
- package/dist/lib/storage/index.js +10 -11
- package/dist/lib/storage/sqlite.js +10 -11
- package/dist/lib/store.js +10 -11
- package/dist/mcp/index.js +12 -300
- package/dist/runner/index.js +12 -300
- package/dist/sdk/index.js +12 -300
- package/docs/USAGE.md +4 -4
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -579,7 +579,7 @@ import { isAbsolute, resolve } from "path";
|
|
|
579
579
|
|
|
580
580
|
// src/lib/agent-adapter.ts
|
|
581
581
|
import { spawn } from "child_process";
|
|
582
|
-
var
|
|
582
|
+
var UNSAFE_CODEWITH_EXEC_EXTRA_ARGS = new Set([
|
|
583
583
|
"e",
|
|
584
584
|
"exec",
|
|
585
585
|
"agent",
|
|
@@ -628,12 +628,12 @@ function validateAgentOptions(target, label, capabilities) {
|
|
|
628
628
|
throw new Error(`${label}.agent is not supported for provider codex`);
|
|
629
629
|
}
|
|
630
630
|
if (provider === "codewith" && target.agent !== undefined) {
|
|
631
|
-
throw new Error(`${label}.agent is not supported for provider codewith
|
|
631
|
+
throw new Error(`${label}.agent is not supported for provider codewith`);
|
|
632
632
|
}
|
|
633
633
|
if (provider === "codewith") {
|
|
634
|
-
const unsafe = target.extraArgs?.find((arg) =>
|
|
634
|
+
const unsafe = target.extraArgs?.find((arg) => UNSAFE_CODEWITH_EXEC_EXTRA_ARGS.has(arg));
|
|
635
635
|
if (unsafe) {
|
|
636
|
-
throw new Error(`${label}.extraArgs cannot include ${unsafe}; codewith
|
|
636
|
+
throw new Error(`${label}.extraArgs cannot include ${unsafe}; codewith exec launch flags are managed by the adapter`);
|
|
637
637
|
}
|
|
638
638
|
}
|
|
639
639
|
if (target.addDirs?.length && !["codewith", "codex"].includes(provider)) {
|
|
@@ -716,7 +716,10 @@ function buildAgentInvocation(target) {
|
|
|
716
716
|
args.push(...target.authProfile ? ["--auth-profile", target.authProfile] : []);
|
|
717
717
|
if (target.variant)
|
|
718
718
|
args.push("-c", `model_reasoning_effort=${configStringValue(target.variant)}`);
|
|
719
|
-
|
|
719
|
+
const sandbox = codewithLikeSandbox(target);
|
|
720
|
+
if (sandbox === "workspace-write")
|
|
721
|
+
args.push("-c", "sandbox_workspace_write.network_access=true");
|
|
722
|
+
args.push("--ask-for-approval", "never", "exec", "--json", "--ephemeral", "--sandbox", sandbox, "--skip-git-repo-check");
|
|
720
723
|
if (target.cwd)
|
|
721
724
|
args.push("--cd", target.cwd);
|
|
722
725
|
for (const dir of target.addDirs ?? [])
|
|
@@ -724,11 +727,7 @@ function buildAgentInvocation(target) {
|
|
|
724
727
|
if (target.model)
|
|
725
728
|
args.push("--model", target.model);
|
|
726
729
|
args.push(...target.extraArgs ?? []);
|
|
727
|
-
|
|
728
|
-
if (target.cwd)
|
|
729
|
-
args.push("--cwd", target.cwd);
|
|
730
|
-
args.push(target.prompt);
|
|
731
|
-
return { command: "codewith", args };
|
|
730
|
+
return { command: "codewith", args, stdin: target.prompt };
|
|
732
731
|
}
|
|
733
732
|
case "codex": {
|
|
734
733
|
if (target.variant)
|
|
@@ -781,7 +780,7 @@ function adapterFor(provider, capabilities) {
|
|
|
781
780
|
var PROVIDER_ADAPTERS = {
|
|
782
781
|
claude: adapterFor("claude", { sandbox: [], durable: false, remote: true, promptChannel: "stdin" }),
|
|
783
782
|
cursor: adapterFor("cursor", { sandbox: CURSOR_SANDBOXES, durable: false, remote: true, promptChannel: "stdin" }),
|
|
784
|
-
codewith: adapterFor("codewith", { sandbox: CODEX_LIKE_SANDBOXES, durable:
|
|
783
|
+
codewith: adapterFor("codewith", { sandbox: CODEX_LIKE_SANDBOXES, durable: false, remote: true, promptChannel: "stdin" }),
|
|
785
784
|
codex: adapterFor("codex", { sandbox: CODEX_LIKE_SANDBOXES, durable: false, remote: true, promptChannel: "stdin" }),
|
|
786
785
|
aicopilot: adapterFor("aicopilot", { sandbox: [], durable: false, remote: true, promptChannel: "stdin" }),
|
|
787
786
|
opencode: adapterFor("opencode", { sandbox: [], durable: false, remote: true, promptChannel: "stdin" })
|
|
@@ -4217,7 +4216,7 @@ class Store {
|
|
|
4217
4216
|
// package.json
|
|
4218
4217
|
var package_default = {
|
|
4219
4218
|
name: "@hasna/loops",
|
|
4220
|
-
version: "0.4.
|
|
4219
|
+
version: "0.4.9",
|
|
4221
4220
|
description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
|
|
4222
4221
|
type: "module",
|
|
4223
4222
|
main: "dist/index.js",
|
|
@@ -5009,11 +5008,6 @@ var TRANSPORT_ENV_KEYS = new Set([
|
|
|
5009
5008
|
"USER",
|
|
5010
5009
|
"XDG_RUNTIME_DIR"
|
|
5011
5010
|
]);
|
|
5012
|
-
function boundedText(text, maxBytes) {
|
|
5013
|
-
const buffer = new BoundedOutputBuffer(maxBytes);
|
|
5014
|
-
buffer.append(text);
|
|
5015
|
-
return buffer.value();
|
|
5016
|
-
}
|
|
5017
5011
|
function buildResult(status, startedAt, fields = {}) {
|
|
5018
5012
|
const finishedAt = fields.finishedAt ?? nowIso();
|
|
5019
5013
|
return {
|
|
@@ -5077,21 +5071,6 @@ function metadataEnv(metadata) {
|
|
|
5077
5071
|
env.LOOPS_GOAL_NODE_KEY = metadata.goalNodeKey;
|
|
5078
5072
|
return env;
|
|
5079
5073
|
}
|
|
5080
|
-
function codewithAgentIdempotencyKey(metadata) {
|
|
5081
|
-
const parts = [
|
|
5082
|
-
"openloops",
|
|
5083
|
-
metadata.workflowRunId ? `workflow-run:${metadata.workflowRunId}` : undefined,
|
|
5084
|
-
metadata.workflowStepId ? `step:${metadata.workflowStepId}` : undefined,
|
|
5085
|
-
metadata.runId ? `loop-run:${metadata.runId}` : undefined,
|
|
5086
|
-
metadata.loopId ? `loop:${metadata.loopId}` : undefined,
|
|
5087
|
-
metadata.scheduledFor ? `scheduled:${metadata.scheduledFor}` : undefined,
|
|
5088
|
-
metadata.goalId ? `goal:${metadata.goalId}` : undefined,
|
|
5089
|
-
metadata.goalNodeKey ? `node:${metadata.goalNodeKey}` : undefined
|
|
5090
|
-
].filter(Boolean);
|
|
5091
|
-
if (parts.length > 1)
|
|
5092
|
-
return parts.join(":");
|
|
5093
|
-
return `openloops:adhoc:${randomBytes2(16).toString("hex")}`;
|
|
5094
|
-
}
|
|
5095
5074
|
function allowlistEnv(allowlist) {
|
|
5096
5075
|
const env = {};
|
|
5097
5076
|
if (allowlist?.tools?.length)
|
|
@@ -5146,8 +5125,7 @@ function commandSpec(target, opts) {
|
|
|
5146
5125
|
preflightAnyOf: invocation.preflightAnyOf,
|
|
5147
5126
|
stdin: invocation.stdin,
|
|
5148
5127
|
allowlist: agentTarget.allowlist,
|
|
5149
|
-
worktree: agentTarget.worktree
|
|
5150
|
-
codewithDurableAgent: adapter.capabilities.durable ? { target: agentTarget } : undefined
|
|
5128
|
+
worktree: agentTarget.worktree
|
|
5151
5129
|
};
|
|
5152
5130
|
}
|
|
5153
5131
|
function composeExecutionEnv(spec, metadata, opts, accountEnv) {
|
|
@@ -5357,266 +5335,6 @@ async function preflightNativeAuthProfile(spec, env) {
|
|
|
5357
5335
|
const result = await spawnCapture(spec.command, ["profile", "list"], { env, timeoutMs: 15000 });
|
|
5358
5336
|
assertCodewithProfileListed(spec.nativeAuthProfile.profile, result);
|
|
5359
5337
|
}
|
|
5360
|
-
function codewithAgentStartArgs(target, idempotencyKey) {
|
|
5361
|
-
const args = providerAdapter(target.provider).buildInvocation(target).args;
|
|
5362
|
-
const startIndex = args.findIndex((arg, index) => arg === "start" && args[index - 1] === "agent");
|
|
5363
|
-
if (startIndex === -1)
|
|
5364
|
-
throw new Error("internal error: codewith durable agent args missing agent start");
|
|
5365
|
-
args.splice(startIndex + 1, 0, "--idempotency-key", idempotencyKey);
|
|
5366
|
-
return args;
|
|
5367
|
-
}
|
|
5368
|
-
function codewithAgentControlArgs(target, command, agentId) {
|
|
5369
|
-
return [
|
|
5370
|
-
...target.authProfile ? ["--auth-profile", target.authProfile] : [],
|
|
5371
|
-
"agent",
|
|
5372
|
-
command,
|
|
5373
|
-
...command === "logs" ? ["--limit", "20"] : [],
|
|
5374
|
-
"--json",
|
|
5375
|
-
agentId
|
|
5376
|
-
];
|
|
5377
|
-
}
|
|
5378
|
-
function extractFirstJsonObject(text) {
|
|
5379
|
-
let depth = 0;
|
|
5380
|
-
let start = -1;
|
|
5381
|
-
let inString = false;
|
|
5382
|
-
let escaped = false;
|
|
5383
|
-
for (let i = 0;i < text.length; i += 1) {
|
|
5384
|
-
const ch = text[i];
|
|
5385
|
-
if (inString) {
|
|
5386
|
-
if (escaped)
|
|
5387
|
-
escaped = false;
|
|
5388
|
-
else if (ch === "\\")
|
|
5389
|
-
escaped = true;
|
|
5390
|
-
else if (ch === '"')
|
|
5391
|
-
inString = false;
|
|
5392
|
-
continue;
|
|
5393
|
-
}
|
|
5394
|
-
if (ch === '"') {
|
|
5395
|
-
inString = true;
|
|
5396
|
-
} else if (ch === "{") {
|
|
5397
|
-
if (depth === 0)
|
|
5398
|
-
start = i;
|
|
5399
|
-
depth += 1;
|
|
5400
|
-
} else if (ch === "}") {
|
|
5401
|
-
if (depth > 0) {
|
|
5402
|
-
depth -= 1;
|
|
5403
|
-
if (depth === 0 && start !== -1)
|
|
5404
|
-
return text.slice(start, i + 1);
|
|
5405
|
-
}
|
|
5406
|
-
}
|
|
5407
|
-
}
|
|
5408
|
-
return;
|
|
5409
|
-
}
|
|
5410
|
-
function parseJsonOutput(stdout, label) {
|
|
5411
|
-
const raw = stdout || "{}";
|
|
5412
|
-
const candidates = [raw.trim()];
|
|
5413
|
-
const scanned = extractFirstJsonObject(raw);
|
|
5414
|
-
if (scanned && scanned !== raw.trim())
|
|
5415
|
-
candidates.push(scanned);
|
|
5416
|
-
let lastError;
|
|
5417
|
-
for (const candidate of candidates) {
|
|
5418
|
-
try {
|
|
5419
|
-
const value = JSON.parse(candidate);
|
|
5420
|
-
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
5421
|
-
throw new Error("not an object");
|
|
5422
|
-
return value;
|
|
5423
|
-
} catch (error) {
|
|
5424
|
-
lastError = error;
|
|
5425
|
-
}
|
|
5426
|
-
}
|
|
5427
|
-
throw new Error(`${label} did not return JSON${lastError instanceof Error ? `: ${lastError.message}` : ""}`);
|
|
5428
|
-
}
|
|
5429
|
-
function recordField(value, key) {
|
|
5430
|
-
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
5431
|
-
return;
|
|
5432
|
-
const field = value[key];
|
|
5433
|
-
return field && typeof field === "object" && !Array.isArray(field) ? field : undefined;
|
|
5434
|
-
}
|
|
5435
|
-
function stringField(value, key) {
|
|
5436
|
-
const field = value?.[key];
|
|
5437
|
-
return typeof field === "string" ? field : undefined;
|
|
5438
|
-
}
|
|
5439
|
-
function numberField(value, key) {
|
|
5440
|
-
const field = value?.[key];
|
|
5441
|
-
return typeof field === "number" && Number.isFinite(field) ? field : undefined;
|
|
5442
|
-
}
|
|
5443
|
-
function codewithAgentStatus(readJson) {
|
|
5444
|
-
return stringField(recordField(readJson, "agent"), "status") ?? stringField(recordField(readJson, "statusSnapshot"), "status");
|
|
5445
|
-
}
|
|
5446
|
-
function codewithAgentLastEventSeq(readJson) {
|
|
5447
|
-
const agentSeq = numberField(recordField(readJson, "agent"), "lastEventSeq");
|
|
5448
|
-
const snapshotSeq = numberField(recordField(readJson, "statusSnapshot"), "lastEventSeq");
|
|
5449
|
-
if (agentSeq !== undefined && snapshotSeq !== undefined)
|
|
5450
|
-
return Math.max(agentSeq, snapshotSeq);
|
|
5451
|
-
return agentSeq ?? snapshotSeq;
|
|
5452
|
-
}
|
|
5453
|
-
function codewithAgentEvidence(startJson, readJson, logsJson) {
|
|
5454
|
-
const agent = recordField(readJson, "agent") ?? recordField(startJson, "agent");
|
|
5455
|
-
const statusSnapshot = recordField(readJson, "statusSnapshot");
|
|
5456
|
-
const events = Array.isArray(logsJson?.data) ? logsJson.data.filter((event) => Boolean(event) && typeof event === "object" && !Array.isArray(event)).map((event) => ({
|
|
5457
|
-
seq: numberField(event, "seq"),
|
|
5458
|
-
eventType: stringField(event, "eventType"),
|
|
5459
|
-
createdAt: numberField(event, "createdAt")
|
|
5460
|
-
})) : undefined;
|
|
5461
|
-
return JSON.stringify({
|
|
5462
|
-
codewithAgent: {
|
|
5463
|
-
agentId: stringField(agent, "agentId"),
|
|
5464
|
-
status: stringField(agent, "status"),
|
|
5465
|
-
desiredState: stringField(agent, "desiredState"),
|
|
5466
|
-
statusReason: stringField(agent, "statusReason"),
|
|
5467
|
-
threadId: stringField(agent, "threadId"),
|
|
5468
|
-
rolloutPath: stringField(agent, "rolloutPath"),
|
|
5469
|
-
pid: numberField(agent, "pid"),
|
|
5470
|
-
exitCode: numberField(agent, "exitCode"),
|
|
5471
|
-
created: typeof startJson.created === "boolean" ? startJson.created : undefined
|
|
5472
|
-
},
|
|
5473
|
-
statusSnapshot: statusSnapshot ? {
|
|
5474
|
-
seq: numberField(statusSnapshot, "seq"),
|
|
5475
|
-
status: stringField(statusSnapshot, "status"),
|
|
5476
|
-
summary: stringField(statusSnapshot, "summary"),
|
|
5477
|
-
pendingInteractionCount: numberField(statusSnapshot, "pendingInteractionCount"),
|
|
5478
|
-
lastEventSeq: numberField(statusSnapshot, "lastEventSeq")
|
|
5479
|
-
} : undefined,
|
|
5480
|
-
events
|
|
5481
|
-
}, null, 2);
|
|
5482
|
-
}
|
|
5483
|
-
function codewithAgentProgress(readJson) {
|
|
5484
|
-
const agent = recordField(readJson, "agent");
|
|
5485
|
-
const statusSnapshot = recordField(readJson, "statusSnapshot");
|
|
5486
|
-
return {
|
|
5487
|
-
provider: "codewith",
|
|
5488
|
-
agentId: stringField(agent, "agentId"),
|
|
5489
|
-
status: stringField(agent, "status") ?? stringField(statusSnapshot, "status"),
|
|
5490
|
-
summary: stringField(statusSnapshot, "summary"),
|
|
5491
|
-
statusReason: stringField(agent, "statusReason"),
|
|
5492
|
-
threadId: stringField(agent, "threadId"),
|
|
5493
|
-
rolloutPath: stringField(agent, "rolloutPath"),
|
|
5494
|
-
pid: numberField(agent, "pid"),
|
|
5495
|
-
lastEventSeq: codewithAgentLastEventSeq(readJson)
|
|
5496
|
-
};
|
|
5497
|
-
}
|
|
5498
|
-
function sleep(ms) {
|
|
5499
|
-
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
5500
|
-
}
|
|
5501
|
-
async function executeCodewithDurableAgent(spec, metadata, opts, env, startedAt) {
|
|
5502
|
-
const target = spec.codewithDurableAgent?.target;
|
|
5503
|
-
if (!target)
|
|
5504
|
-
throw new Error("internal error: missing codewith durable target");
|
|
5505
|
-
const maxOutputBytes = opts.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
|
|
5506
|
-
const idempotencyKey = codewithAgentIdempotencyKey(metadata);
|
|
5507
|
-
const start = await spawnCapture(spec.command, codewithAgentStartArgs(target, idempotencyKey), {
|
|
5508
|
-
cwd: spec.cwd,
|
|
5509
|
-
env,
|
|
5510
|
-
timeoutMs: 30000,
|
|
5511
|
-
maxOutputBytes
|
|
5512
|
-
});
|
|
5513
|
-
const stderr = new BoundedOutputBuffer(maxOutputBytes);
|
|
5514
|
-
stderr.append(start.stderr);
|
|
5515
|
-
if (start.error || (start.status ?? 1) !== 0) {
|
|
5516
|
-
return failureResult(startedAt, start.error ?? `codewith agent start exited with code ${start.status ?? "unknown"}`, {
|
|
5517
|
-
exitCode: start.status ?? undefined,
|
|
5518
|
-
stdout: start.stdout,
|
|
5519
|
-
stderr: stderr.value()
|
|
5520
|
-
});
|
|
5521
|
-
}
|
|
5522
|
-
let startJson;
|
|
5523
|
-
try {
|
|
5524
|
-
startJson = parseJsonOutput(start.stdout || "{}", "codewith agent start");
|
|
5525
|
-
} catch (error) {
|
|
5526
|
-
return failureResult(startedAt, error instanceof Error ? error.message : String(error), { stdout: start.stdout, stderr: stderr.value() });
|
|
5527
|
-
}
|
|
5528
|
-
const agentId = stringField(recordField(startJson, "agent"), "agentId");
|
|
5529
|
-
if (!agentId) {
|
|
5530
|
-
return failureResult(startedAt, "codewith agent start did not return agent.agentId", { stdout: start.stdout, stderr: stderr.value() });
|
|
5531
|
-
}
|
|
5532
|
-
opts.onAgentProgress?.(codewithAgentProgress(startJson));
|
|
5533
|
-
const stopAgent = async () => {
|
|
5534
|
-
await spawnCapture(spec.command, codewithAgentControlArgs(target, "stop", agentId), {
|
|
5535
|
-
cwd: spec.cwd,
|
|
5536
|
-
env,
|
|
5537
|
-
timeoutMs: 15000,
|
|
5538
|
-
maxOutputBytes
|
|
5539
|
-
});
|
|
5540
|
-
};
|
|
5541
|
-
const pollMs = Math.max(100, Number(opts.env?.LOOPS_CODEWITH_AGENT_POLL_MS ?? process.env.LOOPS_CODEWITH_AGENT_POLL_MS ?? 2000) || 2000);
|
|
5542
|
-
let lastReadJson = startJson;
|
|
5543
|
-
let lastLogsJson;
|
|
5544
|
-
let lastFingerprint;
|
|
5545
|
-
let lastProgressAt = Date.now();
|
|
5546
|
-
const evidence = () => boundedText(codewithAgentEvidence(startJson, lastReadJson, lastLogsJson), maxOutputBytes);
|
|
5547
|
-
while (true) {
|
|
5548
|
-
if (opts.signal?.aborted) {
|
|
5549
|
-
await stopAgent();
|
|
5550
|
-
return failureResult(startedAt, "cancelled", { stdout: evidence(), stderr: stderr.value() });
|
|
5551
|
-
}
|
|
5552
|
-
const read = await spawnCapture(spec.command, codewithAgentControlArgs(target, "read", agentId), {
|
|
5553
|
-
cwd: spec.cwd,
|
|
5554
|
-
env,
|
|
5555
|
-
timeoutMs: 30000,
|
|
5556
|
-
maxOutputBytes
|
|
5557
|
-
});
|
|
5558
|
-
stderr.append(read.stderr);
|
|
5559
|
-
if (read.error || (read.status ?? 1) !== 0) {
|
|
5560
|
-
return failureResult(startedAt, read.error ?? `codewith agent read exited with code ${read.status ?? "unknown"}`, {
|
|
5561
|
-
exitCode: read.status ?? undefined,
|
|
5562
|
-
stdout: evidence(),
|
|
5563
|
-
stderr: stderr.value()
|
|
5564
|
-
});
|
|
5565
|
-
}
|
|
5566
|
-
try {
|
|
5567
|
-
lastReadJson = parseJsonOutput(read.stdout || "{}", "codewith agent read");
|
|
5568
|
-
} catch (error) {
|
|
5569
|
-
return failureResult(startedAt, error instanceof Error ? error.message : String(error), {
|
|
5570
|
-
stdout: boundedText(read.stdout, maxOutputBytes),
|
|
5571
|
-
stderr: stderr.value()
|
|
5572
|
-
});
|
|
5573
|
-
}
|
|
5574
|
-
const status = codewithAgentStatus(lastReadJson);
|
|
5575
|
-
const fingerprint = JSON.stringify({
|
|
5576
|
-
status,
|
|
5577
|
-
agentLastEventSeq: numberField(recordField(lastReadJson, "agent"), "lastEventSeq"),
|
|
5578
|
-
snapshot: recordField(lastReadJson, "statusSnapshot") ?? null
|
|
5579
|
-
});
|
|
5580
|
-
if (fingerprint !== lastFingerprint) {
|
|
5581
|
-
lastFingerprint = fingerprint;
|
|
5582
|
-
lastProgressAt = Date.now();
|
|
5583
|
-
opts.onAgentProgress?.(codewithAgentProgress(lastReadJson));
|
|
5584
|
-
}
|
|
5585
|
-
if (status === "completed" || status === "failed" || status === "cancelled") {
|
|
5586
|
-
const logs = await spawnCapture(spec.command, codewithAgentControlArgs(target, "logs", agentId), {
|
|
5587
|
-
cwd: spec.cwd,
|
|
5588
|
-
env,
|
|
5589
|
-
timeoutMs: 30000,
|
|
5590
|
-
maxOutputBytes
|
|
5591
|
-
});
|
|
5592
|
-
if (!logs.error && (logs.status ?? 1) === 0) {
|
|
5593
|
-
try {
|
|
5594
|
-
lastLogsJson = parseJsonOutput(logs.stdout || "{}", "codewith agent logs");
|
|
5595
|
-
} catch {
|
|
5596
|
-
lastLogsJson = undefined;
|
|
5597
|
-
}
|
|
5598
|
-
} else {
|
|
5599
|
-
stderr.append(logs.stderr || logs.error || "");
|
|
5600
|
-
}
|
|
5601
|
-
if (status === "completed") {
|
|
5602
|
-
return successResult(startedAt, { exitCode: 0, stdout: evidence(), stderr: stderr.value() });
|
|
5603
|
-
}
|
|
5604
|
-
return failureResult(startedAt, stringField(recordField(lastReadJson, "agent"), "statusReason") ?? `codewith agent ${status}`, { exitCode: 1, stdout: evidence(), stderr: stderr.value() });
|
|
5605
|
-
}
|
|
5606
|
-
if (typeof spec.timeoutMs === "number" && new Date(nowIso()).getTime() - new Date(startedAt).getTime() >= spec.timeoutMs) {
|
|
5607
|
-
await stopAgent();
|
|
5608
|
-
return timeoutResult(startedAt, `timed out after ${spec.timeoutMs}ms`, { stdout: evidence(), stderr: stderr.value() });
|
|
5609
|
-
}
|
|
5610
|
-
if (typeof spec.idleTimeoutMs === "number" && Date.now() - lastProgressAt >= spec.idleTimeoutMs) {
|
|
5611
|
-
await stopAgent();
|
|
5612
|
-
return timeoutResult(startedAt, `idle timed out after ${spec.idleTimeoutMs}ms without agent progress`, {
|
|
5613
|
-
stdout: evidence(),
|
|
5614
|
-
stderr: stderr.value()
|
|
5615
|
-
});
|
|
5616
|
-
}
|
|
5617
|
-
await sleep(pollMs);
|
|
5618
|
-
}
|
|
5619
|
-
}
|
|
5620
5338
|
function resolvedDirEquals(left, right) {
|
|
5621
5339
|
try {
|
|
5622
5340
|
return realpathSync(left) === realpathSync(right);
|
|
@@ -5850,9 +5568,6 @@ function preflightTarget(target, metadata = {}, opts = {}) {
|
|
|
5850
5568
|
async function executeTarget(target, metadata = {}, opts = {}) {
|
|
5851
5569
|
let spec = commandSpec(target, opts);
|
|
5852
5570
|
const machine = resolvedMachine(opts);
|
|
5853
|
-
if (machine && !machine.local && spec.codewithDurableAgent) {
|
|
5854
|
-
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");
|
|
5855
|
-
}
|
|
5856
5571
|
if (machine && !machine.local) {
|
|
5857
5572
|
const remoteFallbackSpec = spec.worktree?.enabled && spec.worktree.mode === "auto" ? worktreeFallbackSpec(target, opts, spec.worktree.originalCwd) : undefined;
|
|
5858
5573
|
return executeRemoteSpec(spec, machine, metadata, opts, remoteFallbackSpec);
|
|
@@ -5883,9 +5598,6 @@ async function executeTarget(target, metadata = {}, opts = {}) {
|
|
|
5883
5598
|
if (worktreeEntry?.fallbackCwd) {
|
|
5884
5599
|
spec = worktreeFallbackSpec(target, opts, worktreeEntry.fallbackCwd) ?? spec;
|
|
5885
5600
|
}
|
|
5886
|
-
if (spec.codewithDurableAgent) {
|
|
5887
|
-
return executeCodewithDurableAgent(spec, metadata, opts, env, startedAt);
|
|
5888
|
-
}
|
|
5889
5601
|
const child = spawn2(spec.command, spec.args, {
|
|
5890
5602
|
cwd: spec.cwd,
|
|
5891
5603
|
env,
|
|
@@ -11261,9 +10973,8 @@ function renderTaskLifecycleWorkflow(input) {
|
|
|
11261
10973
|
`) : "";
|
|
11262
10974
|
const shared = [
|
|
11263
10975
|
worktreePrompt(plan),
|
|
11264
|
-
...todosExactCommandsFragment(todosProjectPath, input.taskId, [
|
|
11265
|
-
|
|
11266
|
-
]),
|
|
10976
|
+
...todosExactCommandsFragment(todosProjectPath, input.taskId, []),
|
|
10977
|
+
"Use concrete task-specific text in lifecycle comments. Do not copy placeholder text into lifecycle comments; triage and planner comments must start with the exact stage marker when advancing or blocking the workflow.",
|
|
11267
10978
|
NO_TMUX_DISPATCH_FRAGMENT,
|
|
11268
10979
|
"Preserve unrelated user changes and keep scope tied to the task acceptance criteria.",
|
|
11269
10980
|
prReviewFollowUpFragment(input),
|
|
@@ -11274,6 +10985,8 @@ function renderTaskLifecycleWorkflow(input) {
|
|
|
11274
10985
|
`);
|
|
11275
10986
|
const gateMarker = (stage, state) => `openloops:${stage}=${state} task=${input.taskId}${input.eventId ? ` event=${input.eventId}` : ""}`;
|
|
11276
10987
|
const blockTaskCommand = `todos --project ${todosProjectPath} update ${input.taskId} --status blocked`;
|
|
10988
|
+
const markerCommentCommand = (stage, state, evidencePlaceholder) => `todos --project ${todosProjectPath} comment ${input.taskId} "${gateMarker(stage, state)}
|
|
10989
|
+
<${evidencePlaceholder}>"`;
|
|
11277
10990
|
const gateStopFragment = (stage, stops) => `The deterministic ${stage} gate will stop ${stops} unless the latest ${stage} marker is the exact go marker and the task has no blocked/completed/done/cancelled/failed/archived/no-auto/manual/approval-required state.`;
|
|
11278
10991
|
const triagePrompt = [
|
|
11279
10992
|
...boundedStepHeaderFragment(`Triage todos task ${input.taskId} for safe automated execution.`, "triage", "lifecycle"),
|
|
@@ -11281,9 +10994,12 @@ function renderTaskLifecycleWorkflow(input) {
|
|
|
11281
10994
|
"Decide whether the task is eligible for loop execution. Check status, dependencies, duplicate tasks, no-auto/manual/approval metadata, project path, acceptance criteria, and whether the requested work should be split before implementation.",
|
|
11282
10995
|
"Do not implement repo changes in this step.",
|
|
11283
10996
|
`If the task is eligible for automated planning, add a task comment whose first line is exactly: ${gateMarker("triage", "go")}`,
|
|
11284
|
-
|
|
10997
|
+
`Use this copy-safe marker comment command for triage go: ${markerCommentCommand("triage", "go", "task-specific triage evidence")}`,
|
|
10998
|
+
"Do not run a separate generic evidence comment before the marker; include the triage decision, duplicates/dependencies found, and any follow-up tasks created in that same marker comment.",
|
|
11285
10999
|
`If the task should not proceed automatically, run: ${blockTaskCommand}`,
|
|
11286
11000
|
`Then add a task comment whose first line is exactly: ${gateMarker("triage", "blocked")}`,
|
|
11001
|
+
`Use this copy-safe marker comment command for triage blocked: ${markerCommentCommand("triage", "blocked", "task-specific triage evidence")}`,
|
|
11002
|
+
"Do not run a separate generic blocker comment before the marker; include the blocker evidence in that same marker comment.",
|
|
11287
11003
|
gateStopFragment("triage", "later steps")
|
|
11288
11004
|
].join(`
|
|
11289
11005
|
`);
|
|
@@ -11292,17 +11008,19 @@ function renderTaskLifecycleWorkflow(input) {
|
|
|
11292
11008
|
shared,
|
|
11293
11009
|
"Read the triage comment and current task details.",
|
|
11294
11010
|
`If the task is ready for implementation, add a task comment whose first line is exactly: ${gateMarker("planner", "go")}`,
|
|
11295
|
-
|
|
11011
|
+
`Use this copy-safe marker comment command for planner go: ${markerCommentCommand("planner", "go", "task-specific plan/evidence")}`,
|
|
11012
|
+
"Do not run a separate generic evidence comment before the marker; in that same marker comment, include a concise implementation plan: files/areas to inspect, validation commands, risk checks, expected commit/PR behavior, and any cross-repo tasks that should be created separately.",
|
|
11296
11013
|
`Do not implement repo changes in this step. If the task is too broad or unsafe for automation, run: ${blockTaskCommand}`,
|
|
11297
11014
|
`Then add a task comment whose first line is exactly: ${gateMarker("planner", "blocked")}`,
|
|
11298
|
-
`
|
|
11015
|
+
`Use this copy-safe marker comment command for planner blocked: ${markerCommentCommand("planner", "blocked", "task-specific plan/evidence")}`,
|
|
11016
|
+
`Do not run a separate generic blocker comment before the marker; create smaller deduped tasks and record blocker evidence in that same marker comment. ${gateStopFragment("planner", "the worker")}`
|
|
11299
11017
|
].join(`
|
|
11300
11018
|
`);
|
|
11301
11019
|
const workerPrompt = [
|
|
11302
11020
|
...boundedStepHeaderFragment(`Complete todos task ${input.taskId} according to the planner evidence.`, "worker", "lifecycle"),
|
|
11303
11021
|
shared,
|
|
11304
11022
|
todosStartLine(todosProjectPath, input.taskId),
|
|
11305
|
-
"Read the triage and planner comments first. Implement only the scoped task, run focused validation, and record changed files, commits,
|
|
11023
|
+
"Read the triage and planner comments first. Implement only the scoped task, run focused validation, and record concrete worker evidence in todos: changed files, commits, validation results, blockers, and residual risks.",
|
|
11306
11024
|
input.prHandoff ? `When only GitHub network access is blocked after a successful commit/validation, record the handoff artifact at ${handoffArtifactPath} instead of repeatedly retrying push/PR creation.` : undefined,
|
|
11307
11025
|
WORKER_LEAVES_COMPLETION_FRAGMENT
|
|
11308
11026
|
].filter(Boolean).join(`
|
|
@@ -11310,7 +11028,7 @@ function renderTaskLifecycleWorkflow(input) {
|
|
|
11310
11028
|
const verifierPrompt = [
|
|
11311
11029
|
...boundedStepHeaderFragment(`Verify todos task ${input.taskId} after the full lifecycle worker step.`, "verifier", "lifecycle"),
|
|
11312
11030
|
shared,
|
|
11313
|
-
|
|
11031
|
+
"Before completion, record concrete verification evidence in todos with changed files, validation results, findings, and the task decision.",
|
|
11314
11032
|
todosDoneLine(todosProjectPath, input.taskId),
|
|
11315
11033
|
adversarialReviewFragment("triage, plan, worker evidence, repo state, commits, tests, and acceptance criteria", TASK_REVIEW_FOCUS),
|
|
11316
11034
|
verifierRuntimeGuidance(input),
|
|
@@ -24,7 +24,7 @@ export interface ProviderAdapter {
|
|
|
24
24
|
validate(target: AgentTarget, label?: string): void;
|
|
25
25
|
buildInvocation(target: AgentTarget): AgentInvocation;
|
|
26
26
|
}
|
|
27
|
-
export declare const
|
|
27
|
+
export declare const UNSAFE_CODEWITH_EXEC_EXTRA_ARGS: Set<string>;
|
|
28
28
|
export declare const PROVIDER_ADAPTERS: Record<AgentProvider, ProviderAdapter>;
|
|
29
29
|
export declare const AGENT_PROVIDERS: AgentProvider[];
|
|
30
30
|
export declare function providerAdapter(provider: AgentProvider): ProviderAdapter;
|
package/dist/lib/mode.js
CHANGED
|
@@ -8,4 +8,14 @@ export interface PrReviewRoutingDecision {
|
|
|
8
8
|
selectedReviewer?: string;
|
|
9
9
|
signals: string[];
|
|
10
10
|
}
|
|
11
|
-
|
|
11
|
+
/** A GitHub PR reference resolvable to `owner/repo#number`. */
|
|
12
|
+
export interface PrReference {
|
|
13
|
+
owner: string;
|
|
14
|
+
repo: string;
|
|
15
|
+
number: number;
|
|
16
|
+
}
|
|
17
|
+
/** Resolves a PR author login from a concrete `owner/repo#number` reference. */
|
|
18
|
+
export type PrAuthorResolver = (ref: PrReference) => string | undefined;
|
|
19
|
+
/** Extracts a concrete owner/repo/number PR reference from route evidence text. */
|
|
20
|
+
export declare function prReferenceFrom(text: string): PrReference | undefined;
|
|
21
|
+
export declare function prReviewRoutingDecision(data: Record<string, unknown>, metadata: Record<string, unknown>, opts: TodosTaskRouteOptions, resolveAuthor?: PrAuthorResolver): PrReviewRoutingDecision;
|
|
@@ -579,7 +579,7 @@ import { isAbsolute, resolve } from "path";
|
|
|
579
579
|
|
|
580
580
|
// src/lib/agent-adapter.ts
|
|
581
581
|
import { spawn } from "child_process";
|
|
582
|
-
var
|
|
582
|
+
var UNSAFE_CODEWITH_EXEC_EXTRA_ARGS = new Set([
|
|
583
583
|
"e",
|
|
584
584
|
"exec",
|
|
585
585
|
"agent",
|
|
@@ -628,12 +628,12 @@ function validateAgentOptions(target, label, capabilities) {
|
|
|
628
628
|
throw new Error(`${label}.agent is not supported for provider codex`);
|
|
629
629
|
}
|
|
630
630
|
if (provider === "codewith" && target.agent !== undefined) {
|
|
631
|
-
throw new Error(`${label}.agent is not supported for provider codewith
|
|
631
|
+
throw new Error(`${label}.agent is not supported for provider codewith`);
|
|
632
632
|
}
|
|
633
633
|
if (provider === "codewith") {
|
|
634
|
-
const unsafe = target.extraArgs?.find((arg) =>
|
|
634
|
+
const unsafe = target.extraArgs?.find((arg) => UNSAFE_CODEWITH_EXEC_EXTRA_ARGS.has(arg));
|
|
635
635
|
if (unsafe) {
|
|
636
|
-
throw new Error(`${label}.extraArgs cannot include ${unsafe}; codewith
|
|
636
|
+
throw new Error(`${label}.extraArgs cannot include ${unsafe}; codewith exec launch flags are managed by the adapter`);
|
|
637
637
|
}
|
|
638
638
|
}
|
|
639
639
|
if (target.addDirs?.length && !["codewith", "codex"].includes(provider)) {
|
|
@@ -716,7 +716,10 @@ function buildAgentInvocation(target) {
|
|
|
716
716
|
args.push(...target.authProfile ? ["--auth-profile", target.authProfile] : []);
|
|
717
717
|
if (target.variant)
|
|
718
718
|
args.push("-c", `model_reasoning_effort=${configStringValue(target.variant)}`);
|
|
719
|
-
|
|
719
|
+
const sandbox = codewithLikeSandbox(target);
|
|
720
|
+
if (sandbox === "workspace-write")
|
|
721
|
+
args.push("-c", "sandbox_workspace_write.network_access=true");
|
|
722
|
+
args.push("--ask-for-approval", "never", "exec", "--json", "--ephemeral", "--sandbox", sandbox, "--skip-git-repo-check");
|
|
720
723
|
if (target.cwd)
|
|
721
724
|
args.push("--cd", target.cwd);
|
|
722
725
|
for (const dir of target.addDirs ?? [])
|
|
@@ -724,11 +727,7 @@ function buildAgentInvocation(target) {
|
|
|
724
727
|
if (target.model)
|
|
725
728
|
args.push("--model", target.model);
|
|
726
729
|
args.push(...target.extraArgs ?? []);
|
|
727
|
-
|
|
728
|
-
if (target.cwd)
|
|
729
|
-
args.push("--cwd", target.cwd);
|
|
730
|
-
args.push(target.prompt);
|
|
731
|
-
return { command: "codewith", args };
|
|
730
|
+
return { command: "codewith", args, stdin: target.prompt };
|
|
732
731
|
}
|
|
733
732
|
case "codex": {
|
|
734
733
|
if (target.variant)
|
|
@@ -781,7 +780,7 @@ function adapterFor(provider, capabilities) {
|
|
|
781
780
|
var PROVIDER_ADAPTERS = {
|
|
782
781
|
claude: adapterFor("claude", { sandbox: [], durable: false, remote: true, promptChannel: "stdin" }),
|
|
783
782
|
cursor: adapterFor("cursor", { sandbox: CURSOR_SANDBOXES, durable: false, remote: true, promptChannel: "stdin" }),
|
|
784
|
-
codewith: adapterFor("codewith", { sandbox: CODEX_LIKE_SANDBOXES, durable:
|
|
783
|
+
codewith: adapterFor("codewith", { sandbox: CODEX_LIKE_SANDBOXES, durable: false, remote: true, promptChannel: "stdin" }),
|
|
785
784
|
codex: adapterFor("codex", { sandbox: CODEX_LIKE_SANDBOXES, durable: false, remote: true, promptChannel: "stdin" }),
|
|
786
785
|
aicopilot: adapterFor("aicopilot", { sandbox: [], durable: false, remote: true, promptChannel: "stdin" }),
|
|
787
786
|
opencode: adapterFor("opencode", { sandbox: [], durable: false, remote: true, promptChannel: "stdin" })
|
|
@@ -579,7 +579,7 @@ import { isAbsolute, resolve } from "path";
|
|
|
579
579
|
|
|
580
580
|
// src/lib/agent-adapter.ts
|
|
581
581
|
import { spawn } from "child_process";
|
|
582
|
-
var
|
|
582
|
+
var UNSAFE_CODEWITH_EXEC_EXTRA_ARGS = new Set([
|
|
583
583
|
"e",
|
|
584
584
|
"exec",
|
|
585
585
|
"agent",
|
|
@@ -628,12 +628,12 @@ function validateAgentOptions(target, label, capabilities) {
|
|
|
628
628
|
throw new Error(`${label}.agent is not supported for provider codex`);
|
|
629
629
|
}
|
|
630
630
|
if (provider === "codewith" && target.agent !== undefined) {
|
|
631
|
-
throw new Error(`${label}.agent is not supported for provider codewith
|
|
631
|
+
throw new Error(`${label}.agent is not supported for provider codewith`);
|
|
632
632
|
}
|
|
633
633
|
if (provider === "codewith") {
|
|
634
|
-
const unsafe = target.extraArgs?.find((arg) =>
|
|
634
|
+
const unsafe = target.extraArgs?.find((arg) => UNSAFE_CODEWITH_EXEC_EXTRA_ARGS.has(arg));
|
|
635
635
|
if (unsafe) {
|
|
636
|
-
throw new Error(`${label}.extraArgs cannot include ${unsafe}; codewith
|
|
636
|
+
throw new Error(`${label}.extraArgs cannot include ${unsafe}; codewith exec launch flags are managed by the adapter`);
|
|
637
637
|
}
|
|
638
638
|
}
|
|
639
639
|
if (target.addDirs?.length && !["codewith", "codex"].includes(provider)) {
|
|
@@ -716,7 +716,10 @@ function buildAgentInvocation(target) {
|
|
|
716
716
|
args.push(...target.authProfile ? ["--auth-profile", target.authProfile] : []);
|
|
717
717
|
if (target.variant)
|
|
718
718
|
args.push("-c", `model_reasoning_effort=${configStringValue(target.variant)}`);
|
|
719
|
-
|
|
719
|
+
const sandbox = codewithLikeSandbox(target);
|
|
720
|
+
if (sandbox === "workspace-write")
|
|
721
|
+
args.push("-c", "sandbox_workspace_write.network_access=true");
|
|
722
|
+
args.push("--ask-for-approval", "never", "exec", "--json", "--ephemeral", "--sandbox", sandbox, "--skip-git-repo-check");
|
|
720
723
|
if (target.cwd)
|
|
721
724
|
args.push("--cd", target.cwd);
|
|
722
725
|
for (const dir of target.addDirs ?? [])
|
|
@@ -724,11 +727,7 @@ function buildAgentInvocation(target) {
|
|
|
724
727
|
if (target.model)
|
|
725
728
|
args.push("--model", target.model);
|
|
726
729
|
args.push(...target.extraArgs ?? []);
|
|
727
|
-
|
|
728
|
-
if (target.cwd)
|
|
729
|
-
args.push("--cwd", target.cwd);
|
|
730
|
-
args.push(target.prompt);
|
|
731
|
-
return { command: "codewith", args };
|
|
730
|
+
return { command: "codewith", args, stdin: target.prompt };
|
|
732
731
|
}
|
|
733
732
|
case "codex": {
|
|
734
733
|
if (target.variant)
|
|
@@ -781,7 +780,7 @@ function adapterFor(provider, capabilities) {
|
|
|
781
780
|
var PROVIDER_ADAPTERS = {
|
|
782
781
|
claude: adapterFor("claude", { sandbox: [], durable: false, remote: true, promptChannel: "stdin" }),
|
|
783
782
|
cursor: adapterFor("cursor", { sandbox: CURSOR_SANDBOXES, durable: false, remote: true, promptChannel: "stdin" }),
|
|
784
|
-
codewith: adapterFor("codewith", { sandbox: CODEX_LIKE_SANDBOXES, durable:
|
|
783
|
+
codewith: adapterFor("codewith", { sandbox: CODEX_LIKE_SANDBOXES, durable: false, remote: true, promptChannel: "stdin" }),
|
|
785
784
|
codex: adapterFor("codex", { sandbox: CODEX_LIKE_SANDBOXES, durable: false, remote: true, promptChannel: "stdin" }),
|
|
786
785
|
aicopilot: adapterFor("aicopilot", { sandbox: [], durable: false, remote: true, promptChannel: "stdin" }),
|
|
787
786
|
opencode: adapterFor("opencode", { sandbox: [], durable: false, remote: true, promptChannel: "stdin" })
|