@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/sdk/index.js
CHANGED
|
@@ -54,8 +54,12 @@ function nowIso() {
|
|
|
54
54
|
import { mkdirSync } from "fs";
|
|
55
55
|
import { homedir } from "os";
|
|
56
56
|
import { join } from "path";
|
|
57
|
+
function homeDir() {
|
|
58
|
+
const home = process.env.HOME?.trim();
|
|
59
|
+
return home ? home : homedir();
|
|
60
|
+
}
|
|
57
61
|
function dataDir() {
|
|
58
|
-
return process.env.LOOPS_DATA_DIR || join(
|
|
62
|
+
return process.env.LOOPS_DATA_DIR || join(homeDir(), ".hasna", "loops");
|
|
59
63
|
}
|
|
60
64
|
function ensureDataDir() {
|
|
61
65
|
const dir = dataDir();
|
|
@@ -72,10 +76,10 @@ function daemonLogPath() {
|
|
|
72
76
|
return join(dataDir(), "daemon.log");
|
|
73
77
|
}
|
|
74
78
|
function systemdServicePath() {
|
|
75
|
-
return join(
|
|
79
|
+
return join(homeDir(), ".config", "systemd", "user", "loops-daemon.service");
|
|
76
80
|
}
|
|
77
81
|
function launchdPlistPath() {
|
|
78
|
-
return join(
|
|
82
|
+
return join(homeDir(), "Library", "LaunchAgents", "com.hasna.loops.daemon.plist");
|
|
79
83
|
}
|
|
80
84
|
|
|
81
85
|
// src/lib/process-identity.ts
|
|
@@ -579,7 +583,7 @@ import { isAbsolute, resolve } from "path";
|
|
|
579
583
|
|
|
580
584
|
// src/lib/agent-adapter.ts
|
|
581
585
|
import { spawn } from "child_process";
|
|
582
|
-
var
|
|
586
|
+
var UNSAFE_CODEWITH_EXEC_EXTRA_ARGS = new Set([
|
|
583
587
|
"e",
|
|
584
588
|
"exec",
|
|
585
589
|
"agent",
|
|
@@ -628,12 +632,12 @@ function validateAgentOptions(target, label, capabilities) {
|
|
|
628
632
|
throw new Error(`${label}.agent is not supported for provider codex`);
|
|
629
633
|
}
|
|
630
634
|
if (provider === "codewith" && target.agent !== undefined) {
|
|
631
|
-
throw new Error(`${label}.agent is not supported for provider codewith
|
|
635
|
+
throw new Error(`${label}.agent is not supported for provider codewith`);
|
|
632
636
|
}
|
|
633
637
|
if (provider === "codewith") {
|
|
634
|
-
const unsafe = target.extraArgs?.find((arg) =>
|
|
638
|
+
const unsafe = target.extraArgs?.find((arg) => UNSAFE_CODEWITH_EXEC_EXTRA_ARGS.has(arg));
|
|
635
639
|
if (unsafe) {
|
|
636
|
-
throw new Error(`${label}.extraArgs cannot include ${unsafe}; codewith
|
|
640
|
+
throw new Error(`${label}.extraArgs cannot include ${unsafe}; codewith exec launch flags are managed by the adapter`);
|
|
637
641
|
}
|
|
638
642
|
}
|
|
639
643
|
if (target.addDirs?.length && !["codewith", "codex"].includes(provider)) {
|
|
@@ -716,7 +720,10 @@ function buildAgentInvocation(target) {
|
|
|
716
720
|
args.push(...target.authProfile ? ["--auth-profile", target.authProfile] : []);
|
|
717
721
|
if (target.variant)
|
|
718
722
|
args.push("-c", `model_reasoning_effort=${configStringValue(target.variant)}`);
|
|
719
|
-
|
|
723
|
+
const sandbox = codewithLikeSandbox(target);
|
|
724
|
+
if (sandbox === "workspace-write")
|
|
725
|
+
args.push("-c", "sandbox_workspace_write.network_access=true");
|
|
726
|
+
args.push("--ask-for-approval", "never", "exec", "--json", "--ephemeral", "--sandbox", sandbox, "--skip-git-repo-check");
|
|
720
727
|
if (target.cwd)
|
|
721
728
|
args.push("--cd", target.cwd);
|
|
722
729
|
for (const dir of target.addDirs ?? [])
|
|
@@ -724,11 +731,7 @@ function buildAgentInvocation(target) {
|
|
|
724
731
|
if (target.model)
|
|
725
732
|
args.push("--model", target.model);
|
|
726
733
|
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 };
|
|
734
|
+
return { command: "codewith", args, stdin: target.prompt };
|
|
732
735
|
}
|
|
733
736
|
case "codex": {
|
|
734
737
|
if (target.variant)
|
|
@@ -781,7 +784,7 @@ function adapterFor(provider, capabilities) {
|
|
|
781
784
|
var PROVIDER_ADAPTERS = {
|
|
782
785
|
claude: adapterFor("claude", { sandbox: [], durable: false, remote: true, promptChannel: "stdin" }),
|
|
783
786
|
cursor: adapterFor("cursor", { sandbox: CURSOR_SANDBOXES, durable: false, remote: true, promptChannel: "stdin" }),
|
|
784
|
-
codewith: adapterFor("codewith", { sandbox: CODEX_LIKE_SANDBOXES, durable:
|
|
787
|
+
codewith: adapterFor("codewith", { sandbox: CODEX_LIKE_SANDBOXES, durable: false, remote: true, promptChannel: "stdin" }),
|
|
785
788
|
codex: adapterFor("codex", { sandbox: CODEX_LIKE_SANDBOXES, durable: false, remote: true, promptChannel: "stdin" }),
|
|
786
789
|
aicopilot: adapterFor("aicopilot", { sandbox: [], durable: false, remote: true, promptChannel: "stdin" }),
|
|
787
790
|
opencode: adapterFor("opencode", { sandbox: [], durable: false, remote: true, promptChannel: "stdin" })
|
|
@@ -1473,6 +1476,21 @@ function workItemStatusForLoopRun(status, attempt, maxAttempts) {
|
|
|
1473
1476
|
function scrubbedOrNull(value) {
|
|
1474
1477
|
return value == null ? null : scrubSecrets(value);
|
|
1475
1478
|
}
|
|
1479
|
+
var MAX_PERSISTED_RUN_OUTPUT_CHARS = 64 * 1024;
|
|
1480
|
+
function clampPersistedRunOutput(value) {
|
|
1481
|
+
if (value == null || value.length <= MAX_PERSISTED_RUN_OUTPUT_CHARS)
|
|
1482
|
+
return value;
|
|
1483
|
+
const half = Math.floor(MAX_PERSISTED_RUN_OUTPUT_CHARS / 2);
|
|
1484
|
+
const head = value.slice(0, half);
|
|
1485
|
+
const tail = value.slice(value.length - half);
|
|
1486
|
+
const omitted = value.length - head.length - tail.length;
|
|
1487
|
+
return `${head}
|
|
1488
|
+
\u2026[${omitted} chars truncated by loops run-output retention]\u2026
|
|
1489
|
+
${tail}`;
|
|
1490
|
+
}
|
|
1491
|
+
function persistedRunOutput(value) {
|
|
1492
|
+
return clampPersistedRunOutput(scrubbedOrNull(value));
|
|
1493
|
+
}
|
|
1476
1494
|
function chmodIfExists(path, mode) {
|
|
1477
1495
|
try {
|
|
1478
1496
|
if (existsSync(path))
|
|
@@ -3219,8 +3237,8 @@ class Store {
|
|
|
3219
3237
|
))`).run({
|
|
3220
3238
|
$workflowRunId: workflowRunId,
|
|
3221
3239
|
$stepId: stepId,
|
|
3222
|
-
$stdout: progress.stdout === undefined ? null :
|
|
3223
|
-
$stderr: progress.stderr === undefined ? null :
|
|
3240
|
+
$stdout: progress.stdout === undefined ? null : persistedRunOutput(progress.stdout),
|
|
3241
|
+
$stderr: progress.stderr === undefined ? null : persistedRunOutput(progress.stderr),
|
|
3224
3242
|
$updated: now,
|
|
3225
3243
|
$daemonLeaseId: opts.daemonLeaseId ?? null,
|
|
3226
3244
|
$now: now
|
|
@@ -3281,8 +3299,8 @@ class Store {
|
|
|
3281
3299
|
$finished: finishedAt,
|
|
3282
3300
|
$exitCode: patch.exitCode ?? null,
|
|
3283
3301
|
$durationMs: patch.durationMs ?? null,
|
|
3284
|
-
$stdout:
|
|
3285
|
-
$stderr:
|
|
3302
|
+
$stdout: persistedRunOutput(patch.stdout),
|
|
3303
|
+
$stderr: persistedRunOutput(patch.stderr),
|
|
3286
3304
|
$error: error ?? null,
|
|
3287
3305
|
$updated: finishedAt,
|
|
3288
3306
|
$daemonLeaseId: opts.daemonLeaseId ?? null,
|
|
@@ -3660,8 +3678,8 @@ class Store {
|
|
|
3660
3678
|
$pid: patch.pid ?? null,
|
|
3661
3679
|
$exitCode: patch.exitCode ?? null,
|
|
3662
3680
|
$durationMs: patch.durationMs ?? null,
|
|
3663
|
-
$stdout:
|
|
3664
|
-
$stderr:
|
|
3681
|
+
$stdout: persistedRunOutput(patch.stdout),
|
|
3682
|
+
$stderr: persistedRunOutput(patch.stderr),
|
|
3665
3683
|
$error: error ?? null,
|
|
3666
3684
|
$updated: finishedAt,
|
|
3667
3685
|
$claimedBy: opts.claimedBy ?? null,
|
|
@@ -4058,8 +4076,8 @@ class Store {
|
|
|
4058
4076
|
$processStartedAt: run.processStartedAt ?? null,
|
|
4059
4077
|
$exitCode: run.exitCode ?? null,
|
|
4060
4078
|
$durationMs: run.durationMs ?? null,
|
|
4061
|
-
$stdout:
|
|
4062
|
-
$stderr:
|
|
4079
|
+
$stdout: persistedRunOutput(run.stdout),
|
|
4080
|
+
$stderr: persistedRunOutput(run.stderr),
|
|
4063
4081
|
$error: scrubbedOrNull(run.error),
|
|
4064
4082
|
$goalRunId: run.goalRunId ?? null,
|
|
4065
4083
|
$created: run.createdAt,
|
|
@@ -4217,7 +4235,7 @@ class Store {
|
|
|
4217
4235
|
// package.json
|
|
4218
4236
|
var package_default = {
|
|
4219
4237
|
name: "@hasna/loops",
|
|
4220
|
-
version: "0.4.
|
|
4238
|
+
version: "0.4.10",
|
|
4221
4239
|
description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
|
|
4222
4240
|
type: "module",
|
|
4223
4241
|
main: "dist/index.js",
|
|
@@ -5004,11 +5022,6 @@ var TRANSPORT_ENV_KEYS = new Set([
|
|
|
5004
5022
|
"USER",
|
|
5005
5023
|
"XDG_RUNTIME_DIR"
|
|
5006
5024
|
]);
|
|
5007
|
-
function boundedText(text, maxBytes) {
|
|
5008
|
-
const buffer = new BoundedOutputBuffer(maxBytes);
|
|
5009
|
-
buffer.append(text);
|
|
5010
|
-
return buffer.value();
|
|
5011
|
-
}
|
|
5012
5025
|
function buildResult(status, startedAt, fields = {}) {
|
|
5013
5026
|
const finishedAt = fields.finishedAt ?? nowIso();
|
|
5014
5027
|
return {
|
|
@@ -5046,6 +5059,20 @@ function notifySpawn(pid, opts) {
|
|
|
5046
5059
|
function shellQuote(value) {
|
|
5047
5060
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
5048
5061
|
}
|
|
5062
|
+
function codewithProfileCandidateFromLine(line) {
|
|
5063
|
+
const cols = line.trim().split(/\s+/);
|
|
5064
|
+
if (cols[0] === "*")
|
|
5065
|
+
return cols[1];
|
|
5066
|
+
return cols[0];
|
|
5067
|
+
}
|
|
5068
|
+
function codewithProfileForError(profile) {
|
|
5069
|
+
return /[\u0000-\u001F\u007F]/.test(profile) ? JSON.stringify(profile) ?? profile : profile;
|
|
5070
|
+
}
|
|
5071
|
+
function assertCodewithAuthProfileSupported(profile) {
|
|
5072
|
+
if (profile.includes("\x00")) {
|
|
5073
|
+
throw new Error(`codewith auth profile contains unsupported NUL byte: ${codewithProfileForError(profile)}`);
|
|
5074
|
+
}
|
|
5075
|
+
}
|
|
5049
5076
|
function metadataEnv(metadata) {
|
|
5050
5077
|
const env = {};
|
|
5051
5078
|
if (metadata.loopId)
|
|
@@ -5072,21 +5099,6 @@ function metadataEnv(metadata) {
|
|
|
5072
5099
|
env.LOOPS_GOAL_NODE_KEY = metadata.goalNodeKey;
|
|
5073
5100
|
return env;
|
|
5074
5101
|
}
|
|
5075
|
-
function codewithAgentIdempotencyKey(metadata) {
|
|
5076
|
-
const parts = [
|
|
5077
|
-
"openloops",
|
|
5078
|
-
metadata.workflowRunId ? `workflow-run:${metadata.workflowRunId}` : undefined,
|
|
5079
|
-
metadata.workflowStepId ? `step:${metadata.workflowStepId}` : undefined,
|
|
5080
|
-
metadata.runId ? `loop-run:${metadata.runId}` : undefined,
|
|
5081
|
-
metadata.loopId ? `loop:${metadata.loopId}` : undefined,
|
|
5082
|
-
metadata.scheduledFor ? `scheduled:${metadata.scheduledFor}` : undefined,
|
|
5083
|
-
metadata.goalId ? `goal:${metadata.goalId}` : undefined,
|
|
5084
|
-
metadata.goalNodeKey ? `node:${metadata.goalNodeKey}` : undefined
|
|
5085
|
-
].filter(Boolean);
|
|
5086
|
-
if (parts.length > 1)
|
|
5087
|
-
return parts.join(":");
|
|
5088
|
-
return `openloops:adhoc:${randomBytes2(16).toString("hex")}`;
|
|
5089
|
-
}
|
|
5090
5102
|
function allowlistEnv(allowlist) {
|
|
5091
5103
|
const env = {};
|
|
5092
5104
|
if (allowlist?.tools?.length)
|
|
@@ -5127,6 +5139,9 @@ function commandSpec(target, opts) {
|
|
|
5127
5139
|
};
|
|
5128
5140
|
}
|
|
5129
5141
|
const agentTarget = target;
|
|
5142
|
+
if (agentTarget.provider === "codewith" && agentTarget.authProfile) {
|
|
5143
|
+
assertCodewithAuthProfileSupported(agentTarget.authProfile);
|
|
5144
|
+
}
|
|
5130
5145
|
const adapter = providerAdapter(agentTarget.provider);
|
|
5131
5146
|
const invocation = adapter.buildInvocation(agentTarget);
|
|
5132
5147
|
return {
|
|
@@ -5141,8 +5156,7 @@ function commandSpec(target, opts) {
|
|
|
5141
5156
|
preflightAnyOf: invocation.preflightAnyOf,
|
|
5142
5157
|
stdin: invocation.stdin,
|
|
5143
5158
|
allowlist: agentTarget.allowlist,
|
|
5144
|
-
worktree: agentTarget.worktree
|
|
5145
|
-
codewithDurableAgent: adapter.capabilities.durable ? { target: agentTarget } : undefined
|
|
5159
|
+
worktree: agentTarget.worktree
|
|
5146
5160
|
};
|
|
5147
5161
|
}
|
|
5148
5162
|
function composeExecutionEnv(spec, metadata, opts, accountEnv) {
|
|
@@ -5300,7 +5314,8 @@ function remotePreflightScript(spec, metadata) {
|
|
|
5300
5314
|
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");
|
|
5301
5315
|
}
|
|
5302
5316
|
if (spec.nativeAuthProfile?.provider === "codewith") {
|
|
5303
|
-
|
|
5317
|
+
const profileForError = codewithProfileForError(spec.nativeAuthProfile.profile);
|
|
5318
|
+
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");
|
|
5304
5319
|
}
|
|
5305
5320
|
return lines.join(`
|
|
5306
5321
|
`);
|
|
@@ -5325,9 +5340,9 @@ function assertCodewithProfileListed(profile, result) {
|
|
|
5325
5340
|
const detail = (result.stderr || result.stdout || `exit ${result.status ?? "unknown"}`).trim();
|
|
5326
5341
|
throw new Error(`codewith auth profile preflight failed${detail ? `: ${detail}` : ""}`);
|
|
5327
5342
|
}
|
|
5328
|
-
const profiles = new Set((result.stdout || "").split(/\r?\n/).slice(1).map(
|
|
5343
|
+
const profiles = new Set((result.stdout || "").split(/\r?\n/).slice(1).map(codewithProfileCandidateFromLine).filter(Boolean));
|
|
5329
5344
|
if (!profiles.has(profile)) {
|
|
5330
|
-
throw new Error(`codewith auth profile not found: ${profile}`);
|
|
5345
|
+
throw new Error(`codewith auth profile not found: ${codewithProfileForError(profile)}`);
|
|
5331
5346
|
}
|
|
5332
5347
|
}
|
|
5333
5348
|
function preflightNativeAuthProfileSync(spec, env) {
|
|
@@ -5352,266 +5367,6 @@ async function preflightNativeAuthProfile(spec, env) {
|
|
|
5352
5367
|
const result = await spawnCapture(spec.command, ["profile", "list"], { env, timeoutMs: 15000 });
|
|
5353
5368
|
assertCodewithProfileListed(spec.nativeAuthProfile.profile, result);
|
|
5354
5369
|
}
|
|
5355
|
-
function codewithAgentStartArgs(target, idempotencyKey) {
|
|
5356
|
-
const args = providerAdapter(target.provider).buildInvocation(target).args;
|
|
5357
|
-
const startIndex = args.findIndex((arg, index) => arg === "start" && args[index - 1] === "agent");
|
|
5358
|
-
if (startIndex === -1)
|
|
5359
|
-
throw new Error("internal error: codewith durable agent args missing agent start");
|
|
5360
|
-
args.splice(startIndex + 1, 0, "--idempotency-key", idempotencyKey);
|
|
5361
|
-
return args;
|
|
5362
|
-
}
|
|
5363
|
-
function codewithAgentControlArgs(target, command, agentId) {
|
|
5364
|
-
return [
|
|
5365
|
-
...target.authProfile ? ["--auth-profile", target.authProfile] : [],
|
|
5366
|
-
"agent",
|
|
5367
|
-
command,
|
|
5368
|
-
...command === "logs" ? ["--limit", "20"] : [],
|
|
5369
|
-
"--json",
|
|
5370
|
-
agentId
|
|
5371
|
-
];
|
|
5372
|
-
}
|
|
5373
|
-
function extractFirstJsonObject(text) {
|
|
5374
|
-
let depth = 0;
|
|
5375
|
-
let start = -1;
|
|
5376
|
-
let inString = false;
|
|
5377
|
-
let escaped = false;
|
|
5378
|
-
for (let i = 0;i < text.length; i += 1) {
|
|
5379
|
-
const ch = text[i];
|
|
5380
|
-
if (inString) {
|
|
5381
|
-
if (escaped)
|
|
5382
|
-
escaped = false;
|
|
5383
|
-
else if (ch === "\\")
|
|
5384
|
-
escaped = true;
|
|
5385
|
-
else if (ch === '"')
|
|
5386
|
-
inString = false;
|
|
5387
|
-
continue;
|
|
5388
|
-
}
|
|
5389
|
-
if (ch === '"') {
|
|
5390
|
-
inString = true;
|
|
5391
|
-
} else if (ch === "{") {
|
|
5392
|
-
if (depth === 0)
|
|
5393
|
-
start = i;
|
|
5394
|
-
depth += 1;
|
|
5395
|
-
} else if (ch === "}") {
|
|
5396
|
-
if (depth > 0) {
|
|
5397
|
-
depth -= 1;
|
|
5398
|
-
if (depth === 0 && start !== -1)
|
|
5399
|
-
return text.slice(start, i + 1);
|
|
5400
|
-
}
|
|
5401
|
-
}
|
|
5402
|
-
}
|
|
5403
|
-
return;
|
|
5404
|
-
}
|
|
5405
|
-
function parseJsonOutput(stdout, label) {
|
|
5406
|
-
const raw = stdout || "{}";
|
|
5407
|
-
const candidates = [raw.trim()];
|
|
5408
|
-
const scanned = extractFirstJsonObject(raw);
|
|
5409
|
-
if (scanned && scanned !== raw.trim())
|
|
5410
|
-
candidates.push(scanned);
|
|
5411
|
-
let lastError;
|
|
5412
|
-
for (const candidate of candidates) {
|
|
5413
|
-
try {
|
|
5414
|
-
const value = JSON.parse(candidate);
|
|
5415
|
-
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
5416
|
-
throw new Error("not an object");
|
|
5417
|
-
return value;
|
|
5418
|
-
} catch (error) {
|
|
5419
|
-
lastError = error;
|
|
5420
|
-
}
|
|
5421
|
-
}
|
|
5422
|
-
throw new Error(`${label} did not return JSON${lastError instanceof Error ? `: ${lastError.message}` : ""}`);
|
|
5423
|
-
}
|
|
5424
|
-
function recordField(value, key) {
|
|
5425
|
-
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
5426
|
-
return;
|
|
5427
|
-
const field = value[key];
|
|
5428
|
-
return field && typeof field === "object" && !Array.isArray(field) ? field : undefined;
|
|
5429
|
-
}
|
|
5430
|
-
function stringField(value, key) {
|
|
5431
|
-
const field = value?.[key];
|
|
5432
|
-
return typeof field === "string" ? field : undefined;
|
|
5433
|
-
}
|
|
5434
|
-
function numberField(value, key) {
|
|
5435
|
-
const field = value?.[key];
|
|
5436
|
-
return typeof field === "number" && Number.isFinite(field) ? field : undefined;
|
|
5437
|
-
}
|
|
5438
|
-
function codewithAgentStatus(readJson) {
|
|
5439
|
-
return stringField(recordField(readJson, "agent"), "status") ?? stringField(recordField(readJson, "statusSnapshot"), "status");
|
|
5440
|
-
}
|
|
5441
|
-
function codewithAgentLastEventSeq(readJson) {
|
|
5442
|
-
const agentSeq = numberField(recordField(readJson, "agent"), "lastEventSeq");
|
|
5443
|
-
const snapshotSeq = numberField(recordField(readJson, "statusSnapshot"), "lastEventSeq");
|
|
5444
|
-
if (agentSeq !== undefined && snapshotSeq !== undefined)
|
|
5445
|
-
return Math.max(agentSeq, snapshotSeq);
|
|
5446
|
-
return agentSeq ?? snapshotSeq;
|
|
5447
|
-
}
|
|
5448
|
-
function codewithAgentEvidence(startJson, readJson, logsJson) {
|
|
5449
|
-
const agent = recordField(readJson, "agent") ?? recordField(startJson, "agent");
|
|
5450
|
-
const statusSnapshot = recordField(readJson, "statusSnapshot");
|
|
5451
|
-
const events = Array.isArray(logsJson?.data) ? logsJson.data.filter((event) => Boolean(event) && typeof event === "object" && !Array.isArray(event)).map((event) => ({
|
|
5452
|
-
seq: numberField(event, "seq"),
|
|
5453
|
-
eventType: stringField(event, "eventType"),
|
|
5454
|
-
createdAt: numberField(event, "createdAt")
|
|
5455
|
-
})) : undefined;
|
|
5456
|
-
return JSON.stringify({
|
|
5457
|
-
codewithAgent: {
|
|
5458
|
-
agentId: stringField(agent, "agentId"),
|
|
5459
|
-
status: stringField(agent, "status"),
|
|
5460
|
-
desiredState: stringField(agent, "desiredState"),
|
|
5461
|
-
statusReason: stringField(agent, "statusReason"),
|
|
5462
|
-
threadId: stringField(agent, "threadId"),
|
|
5463
|
-
rolloutPath: stringField(agent, "rolloutPath"),
|
|
5464
|
-
pid: numberField(agent, "pid"),
|
|
5465
|
-
exitCode: numberField(agent, "exitCode"),
|
|
5466
|
-
created: typeof startJson.created === "boolean" ? startJson.created : undefined
|
|
5467
|
-
},
|
|
5468
|
-
statusSnapshot: statusSnapshot ? {
|
|
5469
|
-
seq: numberField(statusSnapshot, "seq"),
|
|
5470
|
-
status: stringField(statusSnapshot, "status"),
|
|
5471
|
-
summary: stringField(statusSnapshot, "summary"),
|
|
5472
|
-
pendingInteractionCount: numberField(statusSnapshot, "pendingInteractionCount"),
|
|
5473
|
-
lastEventSeq: numberField(statusSnapshot, "lastEventSeq")
|
|
5474
|
-
} : undefined,
|
|
5475
|
-
events
|
|
5476
|
-
}, null, 2);
|
|
5477
|
-
}
|
|
5478
|
-
function codewithAgentProgress(readJson) {
|
|
5479
|
-
const agent = recordField(readJson, "agent");
|
|
5480
|
-
const statusSnapshot = recordField(readJson, "statusSnapshot");
|
|
5481
|
-
return {
|
|
5482
|
-
provider: "codewith",
|
|
5483
|
-
agentId: stringField(agent, "agentId"),
|
|
5484
|
-
status: stringField(agent, "status") ?? stringField(statusSnapshot, "status"),
|
|
5485
|
-
summary: stringField(statusSnapshot, "summary"),
|
|
5486
|
-
statusReason: stringField(agent, "statusReason"),
|
|
5487
|
-
threadId: stringField(agent, "threadId"),
|
|
5488
|
-
rolloutPath: stringField(agent, "rolloutPath"),
|
|
5489
|
-
pid: numberField(agent, "pid"),
|
|
5490
|
-
lastEventSeq: codewithAgentLastEventSeq(readJson)
|
|
5491
|
-
};
|
|
5492
|
-
}
|
|
5493
|
-
function sleep(ms) {
|
|
5494
|
-
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
5495
|
-
}
|
|
5496
|
-
async function executeCodewithDurableAgent(spec, metadata, opts, env, startedAt) {
|
|
5497
|
-
const target = spec.codewithDurableAgent?.target;
|
|
5498
|
-
if (!target)
|
|
5499
|
-
throw new Error("internal error: missing codewith durable target");
|
|
5500
|
-
const maxOutputBytes = opts.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
|
|
5501
|
-
const idempotencyKey = codewithAgentIdempotencyKey(metadata);
|
|
5502
|
-
const start = await spawnCapture(spec.command, codewithAgentStartArgs(target, idempotencyKey), {
|
|
5503
|
-
cwd: spec.cwd,
|
|
5504
|
-
env,
|
|
5505
|
-
timeoutMs: 30000,
|
|
5506
|
-
maxOutputBytes
|
|
5507
|
-
});
|
|
5508
|
-
const stderr = new BoundedOutputBuffer(maxOutputBytes);
|
|
5509
|
-
stderr.append(start.stderr);
|
|
5510
|
-
if (start.error || (start.status ?? 1) !== 0) {
|
|
5511
|
-
return failureResult(startedAt, start.error ?? `codewith agent start exited with code ${start.status ?? "unknown"}`, {
|
|
5512
|
-
exitCode: start.status ?? undefined,
|
|
5513
|
-
stdout: start.stdout,
|
|
5514
|
-
stderr: stderr.value()
|
|
5515
|
-
});
|
|
5516
|
-
}
|
|
5517
|
-
let startJson;
|
|
5518
|
-
try {
|
|
5519
|
-
startJson = parseJsonOutput(start.stdout || "{}", "codewith agent start");
|
|
5520
|
-
} catch (error) {
|
|
5521
|
-
return failureResult(startedAt, error instanceof Error ? error.message : String(error), { stdout: start.stdout, stderr: stderr.value() });
|
|
5522
|
-
}
|
|
5523
|
-
const agentId = stringField(recordField(startJson, "agent"), "agentId");
|
|
5524
|
-
if (!agentId) {
|
|
5525
|
-
return failureResult(startedAt, "codewith agent start did not return agent.agentId", { stdout: start.stdout, stderr: stderr.value() });
|
|
5526
|
-
}
|
|
5527
|
-
opts.onAgentProgress?.(codewithAgentProgress(startJson));
|
|
5528
|
-
const stopAgent = async () => {
|
|
5529
|
-
await spawnCapture(spec.command, codewithAgentControlArgs(target, "stop", agentId), {
|
|
5530
|
-
cwd: spec.cwd,
|
|
5531
|
-
env,
|
|
5532
|
-
timeoutMs: 15000,
|
|
5533
|
-
maxOutputBytes
|
|
5534
|
-
});
|
|
5535
|
-
};
|
|
5536
|
-
const pollMs = Math.max(100, Number(opts.env?.LOOPS_CODEWITH_AGENT_POLL_MS ?? process.env.LOOPS_CODEWITH_AGENT_POLL_MS ?? 2000) || 2000);
|
|
5537
|
-
let lastReadJson = startJson;
|
|
5538
|
-
let lastLogsJson;
|
|
5539
|
-
let lastFingerprint;
|
|
5540
|
-
let lastProgressAt = Date.now();
|
|
5541
|
-
const evidence = () => boundedText(codewithAgentEvidence(startJson, lastReadJson, lastLogsJson), maxOutputBytes);
|
|
5542
|
-
while (true) {
|
|
5543
|
-
if (opts.signal?.aborted) {
|
|
5544
|
-
await stopAgent();
|
|
5545
|
-
return failureResult(startedAt, "cancelled", { stdout: evidence(), stderr: stderr.value() });
|
|
5546
|
-
}
|
|
5547
|
-
const read = await spawnCapture(spec.command, codewithAgentControlArgs(target, "read", agentId), {
|
|
5548
|
-
cwd: spec.cwd,
|
|
5549
|
-
env,
|
|
5550
|
-
timeoutMs: 30000,
|
|
5551
|
-
maxOutputBytes
|
|
5552
|
-
});
|
|
5553
|
-
stderr.append(read.stderr);
|
|
5554
|
-
if (read.error || (read.status ?? 1) !== 0) {
|
|
5555
|
-
return failureResult(startedAt, read.error ?? `codewith agent read exited with code ${read.status ?? "unknown"}`, {
|
|
5556
|
-
exitCode: read.status ?? undefined,
|
|
5557
|
-
stdout: evidence(),
|
|
5558
|
-
stderr: stderr.value()
|
|
5559
|
-
});
|
|
5560
|
-
}
|
|
5561
|
-
try {
|
|
5562
|
-
lastReadJson = parseJsonOutput(read.stdout || "{}", "codewith agent read");
|
|
5563
|
-
} catch (error) {
|
|
5564
|
-
return failureResult(startedAt, error instanceof Error ? error.message : String(error), {
|
|
5565
|
-
stdout: boundedText(read.stdout, maxOutputBytes),
|
|
5566
|
-
stderr: stderr.value()
|
|
5567
|
-
});
|
|
5568
|
-
}
|
|
5569
|
-
const status = codewithAgentStatus(lastReadJson);
|
|
5570
|
-
const fingerprint = JSON.stringify({
|
|
5571
|
-
status,
|
|
5572
|
-
agentLastEventSeq: numberField(recordField(lastReadJson, "agent"), "lastEventSeq"),
|
|
5573
|
-
snapshot: recordField(lastReadJson, "statusSnapshot") ?? null
|
|
5574
|
-
});
|
|
5575
|
-
if (fingerprint !== lastFingerprint) {
|
|
5576
|
-
lastFingerprint = fingerprint;
|
|
5577
|
-
lastProgressAt = Date.now();
|
|
5578
|
-
opts.onAgentProgress?.(codewithAgentProgress(lastReadJson));
|
|
5579
|
-
}
|
|
5580
|
-
if (status === "completed" || status === "failed" || status === "cancelled") {
|
|
5581
|
-
const logs = await spawnCapture(spec.command, codewithAgentControlArgs(target, "logs", agentId), {
|
|
5582
|
-
cwd: spec.cwd,
|
|
5583
|
-
env,
|
|
5584
|
-
timeoutMs: 30000,
|
|
5585
|
-
maxOutputBytes
|
|
5586
|
-
});
|
|
5587
|
-
if (!logs.error && (logs.status ?? 1) === 0) {
|
|
5588
|
-
try {
|
|
5589
|
-
lastLogsJson = parseJsonOutput(logs.stdout || "{}", "codewith agent logs");
|
|
5590
|
-
} catch {
|
|
5591
|
-
lastLogsJson = undefined;
|
|
5592
|
-
}
|
|
5593
|
-
} else {
|
|
5594
|
-
stderr.append(logs.stderr || logs.error || "");
|
|
5595
|
-
}
|
|
5596
|
-
if (status === "completed") {
|
|
5597
|
-
return successResult(startedAt, { exitCode: 0, stdout: evidence(), stderr: stderr.value() });
|
|
5598
|
-
}
|
|
5599
|
-
return failureResult(startedAt, stringField(recordField(lastReadJson, "agent"), "statusReason") ?? `codewith agent ${status}`, { exitCode: 1, stdout: evidence(), stderr: stderr.value() });
|
|
5600
|
-
}
|
|
5601
|
-
if (typeof spec.timeoutMs === "number" && new Date(nowIso()).getTime() - new Date(startedAt).getTime() >= spec.timeoutMs) {
|
|
5602
|
-
await stopAgent();
|
|
5603
|
-
return timeoutResult(startedAt, `timed out after ${spec.timeoutMs}ms`, { stdout: evidence(), stderr: stderr.value() });
|
|
5604
|
-
}
|
|
5605
|
-
if (typeof spec.idleTimeoutMs === "number" && Date.now() - lastProgressAt >= spec.idleTimeoutMs) {
|
|
5606
|
-
await stopAgent();
|
|
5607
|
-
return timeoutResult(startedAt, `idle timed out after ${spec.idleTimeoutMs}ms without agent progress`, {
|
|
5608
|
-
stdout: evidence(),
|
|
5609
|
-
stderr: stderr.value()
|
|
5610
|
-
});
|
|
5611
|
-
}
|
|
5612
|
-
await sleep(pollMs);
|
|
5613
|
-
}
|
|
5614
|
-
}
|
|
5615
5370
|
function resolvedDirEquals(left, right) {
|
|
5616
5371
|
try {
|
|
5617
5372
|
return realpathSync(left) === realpathSync(right);
|
|
@@ -5845,9 +5600,6 @@ function preflightTarget(target, metadata = {}, opts = {}) {
|
|
|
5845
5600
|
async function executeTarget(target, metadata = {}, opts = {}) {
|
|
5846
5601
|
let spec = commandSpec(target, opts);
|
|
5847
5602
|
const machine = resolvedMachine(opts);
|
|
5848
|
-
if (machine && !machine.local && spec.codewithDurableAgent) {
|
|
5849
|
-
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");
|
|
5850
|
-
}
|
|
5851
5603
|
if (machine && !machine.local) {
|
|
5852
5604
|
const remoteFallbackSpec = spec.worktree?.enabled && spec.worktree.mode === "auto" ? worktreeFallbackSpec(target, opts, spec.worktree.originalCwd) : undefined;
|
|
5853
5605
|
return executeRemoteSpec(spec, machine, metadata, opts, remoteFallbackSpec);
|
|
@@ -5878,9 +5630,6 @@ async function executeTarget(target, metadata = {}, opts = {}) {
|
|
|
5878
5630
|
if (worktreeEntry?.fallbackCwd) {
|
|
5879
5631
|
spec = worktreeFallbackSpec(target, opts, worktreeEntry.fallbackCwd) ?? spec;
|
|
5880
5632
|
}
|
|
5881
|
-
if (spec.codewithDurableAgent) {
|
|
5882
|
-
return executeCodewithDurableAgent(spec, metadata, opts, env, startedAt);
|
|
5883
|
-
}
|
|
5884
5633
|
const child = spawn2(spec.command, spec.args, {
|
|
5885
5634
|
cwd: spec.cwd,
|
|
5886
5635
|
env,
|
package/docs/USAGE.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 exec`
|
|
10
10
|
- `aicopilot run`
|
|
11
11
|
- `opencode run`
|
|
12
12
|
- `codex exec`
|
|
@@ -991,13 +991,13 @@ On Linux this writes a user systemd service. On macOS it writes a LaunchAgent pl
|
|
|
991
991
|
The adapters intentionally use provider command surfaces instead of pretending every agent has one SDK:
|
|
992
992
|
|
|
993
993
|
- Claude uses `claude -p --output-format json` and safe-mode/local setting sources by default.
|
|
994
|
-
- Codewith
|
|
994
|
+
- Codewith runs non-interactive `codewith --ask-for-approval never exec --json` sessions by default. exec starts a fresh session per invocation, avoiding the multi-megabyte rollout history that `codewith agent start` reloaded every turn (which drove `context_length_exceeded` silent no-ops), and it keeps network egress for gh/git — the `workspace-write` sandbox opts back into `sandbox_workspace_write.network_access`. Codewith exec is remote-capable like codex. OpenLoops rejects Codewith `extraArgs` that try to force `exec`, `--ephemeral`, `--json`, or other exec launch flags that the adapter already manages.
|
|
995
995
|
- AI Copilot and OpenCode use `run --format json --pure`. OpenCode requires an explicit provider/model id because ambient OpenCode config is machine-specific.
|
|
996
996
|
- 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.
|
|
997
997
|
- Codex uses `codex --ask-for-approval never exec --json --ephemeral --skip-git-repo-check`, with `--add-dir` for explicit extra writable directories where supported.
|
|
998
|
-
- Agent prompts are sent through child stdin instead of argv where the provider supports stdin
|
|
998
|
+
- Agent prompts are sent through child stdin instead of argv where the provider supports stdin, including Codewith `exec` (which reads instructions from stdin when no positional prompt is given), so the prompt never lands on argv.
|
|
999
999
|
- 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.
|
|
1000
|
-
- `--auth-profile` and step `authProfile` are provider-native auth selectors. They currently apply to Codewith and are passed to Codewith as `--auth-profile <name>`
|
|
1000
|
+
- `--auth-profile` and step `authProfile` are provider-native auth selectors. They currently apply to Codewith and are passed to Codewith as `--auth-profile <name>` on the `exec` invocation; they do not call OpenAccounts.
|
|
1001
1001
|
- `--sandbox` maps to provider-native sandbox flags. Codewith/Codex accept `read-only`, `workspace-write`, or `danger-full-access`; Cursor accepts `enabled` or `disabled`.
|
|
1002
1002
|
- `--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`.
|
|
1003
1003
|
- `--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`.
|