@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/mcp/index.js
CHANGED
|
@@ -56,8 +56,12 @@ function nowIso() {
|
|
|
56
56
|
import { mkdirSync } from "fs";
|
|
57
57
|
import { homedir } from "os";
|
|
58
58
|
import { join } from "path";
|
|
59
|
+
function homeDir() {
|
|
60
|
+
const home = process.env.HOME?.trim();
|
|
61
|
+
return home ? home : homedir();
|
|
62
|
+
}
|
|
59
63
|
function dataDir() {
|
|
60
|
-
return process.env.LOOPS_DATA_DIR || join(
|
|
64
|
+
return process.env.LOOPS_DATA_DIR || join(homeDir(), ".hasna", "loops");
|
|
61
65
|
}
|
|
62
66
|
function ensureDataDir() {
|
|
63
67
|
const dir = dataDir();
|
|
@@ -74,10 +78,10 @@ function daemonLogPath() {
|
|
|
74
78
|
return join(dataDir(), "daemon.log");
|
|
75
79
|
}
|
|
76
80
|
function systemdServicePath() {
|
|
77
|
-
return join(
|
|
81
|
+
return join(homeDir(), ".config", "systemd", "user", "loops-daemon.service");
|
|
78
82
|
}
|
|
79
83
|
function launchdPlistPath() {
|
|
80
|
-
return join(
|
|
84
|
+
return join(homeDir(), "Library", "LaunchAgents", "com.hasna.loops.daemon.plist");
|
|
81
85
|
}
|
|
82
86
|
|
|
83
87
|
// src/lib/process-identity.ts
|
|
@@ -581,7 +585,7 @@ import { isAbsolute, resolve } from "path";
|
|
|
581
585
|
|
|
582
586
|
// src/lib/agent-adapter.ts
|
|
583
587
|
import { spawn } from "child_process";
|
|
584
|
-
var
|
|
588
|
+
var UNSAFE_CODEWITH_EXEC_EXTRA_ARGS = new Set([
|
|
585
589
|
"e",
|
|
586
590
|
"exec",
|
|
587
591
|
"agent",
|
|
@@ -630,12 +634,12 @@ function validateAgentOptions(target, label, capabilities) {
|
|
|
630
634
|
throw new Error(`${label}.agent is not supported for provider codex`);
|
|
631
635
|
}
|
|
632
636
|
if (provider === "codewith" && target.agent !== undefined) {
|
|
633
|
-
throw new Error(`${label}.agent is not supported for provider codewith
|
|
637
|
+
throw new Error(`${label}.agent is not supported for provider codewith`);
|
|
634
638
|
}
|
|
635
639
|
if (provider === "codewith") {
|
|
636
|
-
const unsafe = target.extraArgs?.find((arg) =>
|
|
640
|
+
const unsafe = target.extraArgs?.find((arg) => UNSAFE_CODEWITH_EXEC_EXTRA_ARGS.has(arg));
|
|
637
641
|
if (unsafe) {
|
|
638
|
-
throw new Error(`${label}.extraArgs cannot include ${unsafe}; codewith
|
|
642
|
+
throw new Error(`${label}.extraArgs cannot include ${unsafe}; codewith exec launch flags are managed by the adapter`);
|
|
639
643
|
}
|
|
640
644
|
}
|
|
641
645
|
if (target.addDirs?.length && !["codewith", "codex"].includes(provider)) {
|
|
@@ -718,7 +722,10 @@ function buildAgentInvocation(target) {
|
|
|
718
722
|
args.push(...target.authProfile ? ["--auth-profile", target.authProfile] : []);
|
|
719
723
|
if (target.variant)
|
|
720
724
|
args.push("-c", `model_reasoning_effort=${configStringValue(target.variant)}`);
|
|
721
|
-
|
|
725
|
+
const sandbox = codewithLikeSandbox(target);
|
|
726
|
+
if (sandbox === "workspace-write")
|
|
727
|
+
args.push("-c", "sandbox_workspace_write.network_access=true");
|
|
728
|
+
args.push("--ask-for-approval", "never", "exec", "--json", "--ephemeral", "--sandbox", sandbox, "--skip-git-repo-check");
|
|
722
729
|
if (target.cwd)
|
|
723
730
|
args.push("--cd", target.cwd);
|
|
724
731
|
for (const dir of target.addDirs ?? [])
|
|
@@ -726,11 +733,7 @@ function buildAgentInvocation(target) {
|
|
|
726
733
|
if (target.model)
|
|
727
734
|
args.push("--model", target.model);
|
|
728
735
|
args.push(...target.extraArgs ?? []);
|
|
729
|
-
|
|
730
|
-
if (target.cwd)
|
|
731
|
-
args.push("--cwd", target.cwd);
|
|
732
|
-
args.push(target.prompt);
|
|
733
|
-
return { command: "codewith", args };
|
|
736
|
+
return { command: "codewith", args, stdin: target.prompt };
|
|
734
737
|
}
|
|
735
738
|
case "codex": {
|
|
736
739
|
if (target.variant)
|
|
@@ -783,7 +786,7 @@ function adapterFor(provider, capabilities) {
|
|
|
783
786
|
var PROVIDER_ADAPTERS = {
|
|
784
787
|
claude: adapterFor("claude", { sandbox: [], durable: false, remote: true, promptChannel: "stdin" }),
|
|
785
788
|
cursor: adapterFor("cursor", { sandbox: CURSOR_SANDBOXES, durable: false, remote: true, promptChannel: "stdin" }),
|
|
786
|
-
codewith: adapterFor("codewith", { sandbox: CODEX_LIKE_SANDBOXES, durable:
|
|
789
|
+
codewith: adapterFor("codewith", { sandbox: CODEX_LIKE_SANDBOXES, durable: false, remote: true, promptChannel: "stdin" }),
|
|
787
790
|
codex: adapterFor("codex", { sandbox: CODEX_LIKE_SANDBOXES, durable: false, remote: true, promptChannel: "stdin" }),
|
|
788
791
|
aicopilot: adapterFor("aicopilot", { sandbox: [], durable: false, remote: true, promptChannel: "stdin" }),
|
|
789
792
|
opencode: adapterFor("opencode", { sandbox: [], durable: false, remote: true, promptChannel: "stdin" })
|
|
@@ -1475,6 +1478,21 @@ function workItemStatusForLoopRun(status, attempt, maxAttempts) {
|
|
|
1475
1478
|
function scrubbedOrNull(value) {
|
|
1476
1479
|
return value == null ? null : scrubSecrets(value);
|
|
1477
1480
|
}
|
|
1481
|
+
var MAX_PERSISTED_RUN_OUTPUT_CHARS = 64 * 1024;
|
|
1482
|
+
function clampPersistedRunOutput(value) {
|
|
1483
|
+
if (value == null || value.length <= MAX_PERSISTED_RUN_OUTPUT_CHARS)
|
|
1484
|
+
return value;
|
|
1485
|
+
const half = Math.floor(MAX_PERSISTED_RUN_OUTPUT_CHARS / 2);
|
|
1486
|
+
const head = value.slice(0, half);
|
|
1487
|
+
const tail = value.slice(value.length - half);
|
|
1488
|
+
const omitted = value.length - head.length - tail.length;
|
|
1489
|
+
return `${head}
|
|
1490
|
+
\u2026[${omitted} chars truncated by loops run-output retention]\u2026
|
|
1491
|
+
${tail}`;
|
|
1492
|
+
}
|
|
1493
|
+
function persistedRunOutput(value) {
|
|
1494
|
+
return clampPersistedRunOutput(scrubbedOrNull(value));
|
|
1495
|
+
}
|
|
1478
1496
|
function chmodIfExists(path, mode) {
|
|
1479
1497
|
try {
|
|
1480
1498
|
if (existsSync(path))
|
|
@@ -3221,8 +3239,8 @@ class Store {
|
|
|
3221
3239
|
))`).run({
|
|
3222
3240
|
$workflowRunId: workflowRunId,
|
|
3223
3241
|
$stepId: stepId,
|
|
3224
|
-
$stdout: progress.stdout === undefined ? null :
|
|
3225
|
-
$stderr: progress.stderr === undefined ? null :
|
|
3242
|
+
$stdout: progress.stdout === undefined ? null : persistedRunOutput(progress.stdout),
|
|
3243
|
+
$stderr: progress.stderr === undefined ? null : persistedRunOutput(progress.stderr),
|
|
3226
3244
|
$updated: now,
|
|
3227
3245
|
$daemonLeaseId: opts.daemonLeaseId ?? null,
|
|
3228
3246
|
$now: now
|
|
@@ -3283,8 +3301,8 @@ class Store {
|
|
|
3283
3301
|
$finished: finishedAt,
|
|
3284
3302
|
$exitCode: patch.exitCode ?? null,
|
|
3285
3303
|
$durationMs: patch.durationMs ?? null,
|
|
3286
|
-
$stdout:
|
|
3287
|
-
$stderr:
|
|
3304
|
+
$stdout: persistedRunOutput(patch.stdout),
|
|
3305
|
+
$stderr: persistedRunOutput(patch.stderr),
|
|
3288
3306
|
$error: error ?? null,
|
|
3289
3307
|
$updated: finishedAt,
|
|
3290
3308
|
$daemonLeaseId: opts.daemonLeaseId ?? null,
|
|
@@ -3662,8 +3680,8 @@ class Store {
|
|
|
3662
3680
|
$pid: patch.pid ?? null,
|
|
3663
3681
|
$exitCode: patch.exitCode ?? null,
|
|
3664
3682
|
$durationMs: patch.durationMs ?? null,
|
|
3665
|
-
$stdout:
|
|
3666
|
-
$stderr:
|
|
3683
|
+
$stdout: persistedRunOutput(patch.stdout),
|
|
3684
|
+
$stderr: persistedRunOutput(patch.stderr),
|
|
3667
3685
|
$error: error ?? null,
|
|
3668
3686
|
$updated: finishedAt,
|
|
3669
3687
|
$claimedBy: opts.claimedBy ?? null,
|
|
@@ -4060,8 +4078,8 @@ class Store {
|
|
|
4060
4078
|
$processStartedAt: run.processStartedAt ?? null,
|
|
4061
4079
|
$exitCode: run.exitCode ?? null,
|
|
4062
4080
|
$durationMs: run.durationMs ?? null,
|
|
4063
|
-
$stdout:
|
|
4064
|
-
$stderr:
|
|
4081
|
+
$stdout: persistedRunOutput(run.stdout),
|
|
4082
|
+
$stderr: persistedRunOutput(run.stderr),
|
|
4065
4083
|
$error: scrubbedOrNull(run.error),
|
|
4066
4084
|
$goalRunId: run.goalRunId ?? null,
|
|
4067
4085
|
$created: run.createdAt,
|
|
@@ -4744,11 +4762,6 @@ var TRANSPORT_ENV_KEYS = new Set([
|
|
|
4744
4762
|
"USER",
|
|
4745
4763
|
"XDG_RUNTIME_DIR"
|
|
4746
4764
|
]);
|
|
4747
|
-
function boundedText(text, maxBytes) {
|
|
4748
|
-
const buffer = new BoundedOutputBuffer(maxBytes);
|
|
4749
|
-
buffer.append(text);
|
|
4750
|
-
return buffer.value();
|
|
4751
|
-
}
|
|
4752
4765
|
function buildResult(status, startedAt, fields = {}) {
|
|
4753
4766
|
const finishedAt = fields.finishedAt ?? nowIso();
|
|
4754
4767
|
return {
|
|
@@ -4786,6 +4799,20 @@ function notifySpawn(pid, opts) {
|
|
|
4786
4799
|
function shellQuote(value) {
|
|
4787
4800
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
4788
4801
|
}
|
|
4802
|
+
function codewithProfileCandidateFromLine(line) {
|
|
4803
|
+
const cols = line.trim().split(/\s+/);
|
|
4804
|
+
if (cols[0] === "*")
|
|
4805
|
+
return cols[1];
|
|
4806
|
+
return cols[0];
|
|
4807
|
+
}
|
|
4808
|
+
function codewithProfileForError(profile) {
|
|
4809
|
+
return /[\u0000-\u001F\u007F]/.test(profile) ? JSON.stringify(profile) ?? profile : profile;
|
|
4810
|
+
}
|
|
4811
|
+
function assertCodewithAuthProfileSupported(profile) {
|
|
4812
|
+
if (profile.includes("\x00")) {
|
|
4813
|
+
throw new Error(`codewith auth profile contains unsupported NUL byte: ${codewithProfileForError(profile)}`);
|
|
4814
|
+
}
|
|
4815
|
+
}
|
|
4789
4816
|
function metadataEnv(metadata) {
|
|
4790
4817
|
const env = {};
|
|
4791
4818
|
if (metadata.loopId)
|
|
@@ -4812,21 +4839,6 @@ function metadataEnv(metadata) {
|
|
|
4812
4839
|
env.LOOPS_GOAL_NODE_KEY = metadata.goalNodeKey;
|
|
4813
4840
|
return env;
|
|
4814
4841
|
}
|
|
4815
|
-
function codewithAgentIdempotencyKey(metadata) {
|
|
4816
|
-
const parts = [
|
|
4817
|
-
"openloops",
|
|
4818
|
-
metadata.workflowRunId ? `workflow-run:${metadata.workflowRunId}` : undefined,
|
|
4819
|
-
metadata.workflowStepId ? `step:${metadata.workflowStepId}` : undefined,
|
|
4820
|
-
metadata.runId ? `loop-run:${metadata.runId}` : undefined,
|
|
4821
|
-
metadata.loopId ? `loop:${metadata.loopId}` : undefined,
|
|
4822
|
-
metadata.scheduledFor ? `scheduled:${metadata.scheduledFor}` : undefined,
|
|
4823
|
-
metadata.goalId ? `goal:${metadata.goalId}` : undefined,
|
|
4824
|
-
metadata.goalNodeKey ? `node:${metadata.goalNodeKey}` : undefined
|
|
4825
|
-
].filter(Boolean);
|
|
4826
|
-
if (parts.length > 1)
|
|
4827
|
-
return parts.join(":");
|
|
4828
|
-
return `openloops:adhoc:${randomBytes2(16).toString("hex")}`;
|
|
4829
|
-
}
|
|
4830
4842
|
function allowlistEnv(allowlist) {
|
|
4831
4843
|
const env = {};
|
|
4832
4844
|
if (allowlist?.tools?.length)
|
|
@@ -4867,6 +4879,9 @@ function commandSpec(target, opts) {
|
|
|
4867
4879
|
};
|
|
4868
4880
|
}
|
|
4869
4881
|
const agentTarget = target;
|
|
4882
|
+
if (agentTarget.provider === "codewith" && agentTarget.authProfile) {
|
|
4883
|
+
assertCodewithAuthProfileSupported(agentTarget.authProfile);
|
|
4884
|
+
}
|
|
4870
4885
|
const adapter = providerAdapter(agentTarget.provider);
|
|
4871
4886
|
const invocation = adapter.buildInvocation(agentTarget);
|
|
4872
4887
|
return {
|
|
@@ -4881,8 +4896,7 @@ function commandSpec(target, opts) {
|
|
|
4881
4896
|
preflightAnyOf: invocation.preflightAnyOf,
|
|
4882
4897
|
stdin: invocation.stdin,
|
|
4883
4898
|
allowlist: agentTarget.allowlist,
|
|
4884
|
-
worktree: agentTarget.worktree
|
|
4885
|
-
codewithDurableAgent: adapter.capabilities.durable ? { target: agentTarget } : undefined
|
|
4899
|
+
worktree: agentTarget.worktree
|
|
4886
4900
|
};
|
|
4887
4901
|
}
|
|
4888
4902
|
function composeExecutionEnv(spec, metadata, opts, accountEnv) {
|
|
@@ -5040,7 +5054,8 @@ function remotePreflightScript(spec, metadata) {
|
|
|
5040
5054
|
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");
|
|
5041
5055
|
}
|
|
5042
5056
|
if (spec.nativeAuthProfile?.provider === "codewith") {
|
|
5043
|
-
|
|
5057
|
+
const profileForError = codewithProfileForError(spec.nativeAuthProfile.profile);
|
|
5058
|
+
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");
|
|
5044
5059
|
}
|
|
5045
5060
|
return lines.join(`
|
|
5046
5061
|
`);
|
|
@@ -5065,9 +5080,9 @@ function assertCodewithProfileListed(profile, result) {
|
|
|
5065
5080
|
const detail = (result.stderr || result.stdout || `exit ${result.status ?? "unknown"}`).trim();
|
|
5066
5081
|
throw new Error(`codewith auth profile preflight failed${detail ? `: ${detail}` : ""}`);
|
|
5067
5082
|
}
|
|
5068
|
-
const profiles = new Set((result.stdout || "").split(/\r?\n/).slice(1).map(
|
|
5083
|
+
const profiles = new Set((result.stdout || "").split(/\r?\n/).slice(1).map(codewithProfileCandidateFromLine).filter(Boolean));
|
|
5069
5084
|
if (!profiles.has(profile)) {
|
|
5070
|
-
throw new Error(`codewith auth profile not found: ${profile}`);
|
|
5085
|
+
throw new Error(`codewith auth profile not found: ${codewithProfileForError(profile)}`);
|
|
5071
5086
|
}
|
|
5072
5087
|
}
|
|
5073
5088
|
function preflightNativeAuthProfileSync(spec, env) {
|
|
@@ -5092,266 +5107,6 @@ async function preflightNativeAuthProfile(spec, env) {
|
|
|
5092
5107
|
const result = await spawnCapture(spec.command, ["profile", "list"], { env, timeoutMs: 15000 });
|
|
5093
5108
|
assertCodewithProfileListed(spec.nativeAuthProfile.profile, result);
|
|
5094
5109
|
}
|
|
5095
|
-
function codewithAgentStartArgs(target, idempotencyKey) {
|
|
5096
|
-
const args = providerAdapter(target.provider).buildInvocation(target).args;
|
|
5097
|
-
const startIndex = args.findIndex((arg, index) => arg === "start" && args[index - 1] === "agent");
|
|
5098
|
-
if (startIndex === -1)
|
|
5099
|
-
throw new Error("internal error: codewith durable agent args missing agent start");
|
|
5100
|
-
args.splice(startIndex + 1, 0, "--idempotency-key", idempotencyKey);
|
|
5101
|
-
return args;
|
|
5102
|
-
}
|
|
5103
|
-
function codewithAgentControlArgs(target, command, agentId) {
|
|
5104
|
-
return [
|
|
5105
|
-
...target.authProfile ? ["--auth-profile", target.authProfile] : [],
|
|
5106
|
-
"agent",
|
|
5107
|
-
command,
|
|
5108
|
-
...command === "logs" ? ["--limit", "20"] : [],
|
|
5109
|
-
"--json",
|
|
5110
|
-
agentId
|
|
5111
|
-
];
|
|
5112
|
-
}
|
|
5113
|
-
function extractFirstJsonObject(text) {
|
|
5114
|
-
let depth = 0;
|
|
5115
|
-
let start = -1;
|
|
5116
|
-
let inString = false;
|
|
5117
|
-
let escaped = false;
|
|
5118
|
-
for (let i = 0;i < text.length; i += 1) {
|
|
5119
|
-
const ch = text[i];
|
|
5120
|
-
if (inString) {
|
|
5121
|
-
if (escaped)
|
|
5122
|
-
escaped = false;
|
|
5123
|
-
else if (ch === "\\")
|
|
5124
|
-
escaped = true;
|
|
5125
|
-
else if (ch === '"')
|
|
5126
|
-
inString = false;
|
|
5127
|
-
continue;
|
|
5128
|
-
}
|
|
5129
|
-
if (ch === '"') {
|
|
5130
|
-
inString = true;
|
|
5131
|
-
} else if (ch === "{") {
|
|
5132
|
-
if (depth === 0)
|
|
5133
|
-
start = i;
|
|
5134
|
-
depth += 1;
|
|
5135
|
-
} else if (ch === "}") {
|
|
5136
|
-
if (depth > 0) {
|
|
5137
|
-
depth -= 1;
|
|
5138
|
-
if (depth === 0 && start !== -1)
|
|
5139
|
-
return text.slice(start, i + 1);
|
|
5140
|
-
}
|
|
5141
|
-
}
|
|
5142
|
-
}
|
|
5143
|
-
return;
|
|
5144
|
-
}
|
|
5145
|
-
function parseJsonOutput(stdout, label) {
|
|
5146
|
-
const raw = stdout || "{}";
|
|
5147
|
-
const candidates = [raw.trim()];
|
|
5148
|
-
const scanned = extractFirstJsonObject(raw);
|
|
5149
|
-
if (scanned && scanned !== raw.trim())
|
|
5150
|
-
candidates.push(scanned);
|
|
5151
|
-
let lastError;
|
|
5152
|
-
for (const candidate of candidates) {
|
|
5153
|
-
try {
|
|
5154
|
-
const value = JSON.parse(candidate);
|
|
5155
|
-
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
5156
|
-
throw new Error("not an object");
|
|
5157
|
-
return value;
|
|
5158
|
-
} catch (error) {
|
|
5159
|
-
lastError = error;
|
|
5160
|
-
}
|
|
5161
|
-
}
|
|
5162
|
-
throw new Error(`${label} did not return JSON${lastError instanceof Error ? `: ${lastError.message}` : ""}`);
|
|
5163
|
-
}
|
|
5164
|
-
function recordField(value, key) {
|
|
5165
|
-
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
5166
|
-
return;
|
|
5167
|
-
const field = value[key];
|
|
5168
|
-
return field && typeof field === "object" && !Array.isArray(field) ? field : undefined;
|
|
5169
|
-
}
|
|
5170
|
-
function stringField(value, key) {
|
|
5171
|
-
const field = value?.[key];
|
|
5172
|
-
return typeof field === "string" ? field : undefined;
|
|
5173
|
-
}
|
|
5174
|
-
function numberField(value, key) {
|
|
5175
|
-
const field = value?.[key];
|
|
5176
|
-
return typeof field === "number" && Number.isFinite(field) ? field : undefined;
|
|
5177
|
-
}
|
|
5178
|
-
function codewithAgentStatus(readJson) {
|
|
5179
|
-
return stringField(recordField(readJson, "agent"), "status") ?? stringField(recordField(readJson, "statusSnapshot"), "status");
|
|
5180
|
-
}
|
|
5181
|
-
function codewithAgentLastEventSeq(readJson) {
|
|
5182
|
-
const agentSeq = numberField(recordField(readJson, "agent"), "lastEventSeq");
|
|
5183
|
-
const snapshotSeq = numberField(recordField(readJson, "statusSnapshot"), "lastEventSeq");
|
|
5184
|
-
if (agentSeq !== undefined && snapshotSeq !== undefined)
|
|
5185
|
-
return Math.max(agentSeq, snapshotSeq);
|
|
5186
|
-
return agentSeq ?? snapshotSeq;
|
|
5187
|
-
}
|
|
5188
|
-
function codewithAgentEvidence(startJson, readJson, logsJson) {
|
|
5189
|
-
const agent = recordField(readJson, "agent") ?? recordField(startJson, "agent");
|
|
5190
|
-
const statusSnapshot = recordField(readJson, "statusSnapshot");
|
|
5191
|
-
const events = Array.isArray(logsJson?.data) ? logsJson.data.filter((event) => Boolean(event) && typeof event === "object" && !Array.isArray(event)).map((event) => ({
|
|
5192
|
-
seq: numberField(event, "seq"),
|
|
5193
|
-
eventType: stringField(event, "eventType"),
|
|
5194
|
-
createdAt: numberField(event, "createdAt")
|
|
5195
|
-
})) : undefined;
|
|
5196
|
-
return JSON.stringify({
|
|
5197
|
-
codewithAgent: {
|
|
5198
|
-
agentId: stringField(agent, "agentId"),
|
|
5199
|
-
status: stringField(agent, "status"),
|
|
5200
|
-
desiredState: stringField(agent, "desiredState"),
|
|
5201
|
-
statusReason: stringField(agent, "statusReason"),
|
|
5202
|
-
threadId: stringField(agent, "threadId"),
|
|
5203
|
-
rolloutPath: stringField(agent, "rolloutPath"),
|
|
5204
|
-
pid: numberField(agent, "pid"),
|
|
5205
|
-
exitCode: numberField(agent, "exitCode"),
|
|
5206
|
-
created: typeof startJson.created === "boolean" ? startJson.created : undefined
|
|
5207
|
-
},
|
|
5208
|
-
statusSnapshot: statusSnapshot ? {
|
|
5209
|
-
seq: numberField(statusSnapshot, "seq"),
|
|
5210
|
-
status: stringField(statusSnapshot, "status"),
|
|
5211
|
-
summary: stringField(statusSnapshot, "summary"),
|
|
5212
|
-
pendingInteractionCount: numberField(statusSnapshot, "pendingInteractionCount"),
|
|
5213
|
-
lastEventSeq: numberField(statusSnapshot, "lastEventSeq")
|
|
5214
|
-
} : undefined,
|
|
5215
|
-
events
|
|
5216
|
-
}, null, 2);
|
|
5217
|
-
}
|
|
5218
|
-
function codewithAgentProgress(readJson) {
|
|
5219
|
-
const agent = recordField(readJson, "agent");
|
|
5220
|
-
const statusSnapshot = recordField(readJson, "statusSnapshot");
|
|
5221
|
-
return {
|
|
5222
|
-
provider: "codewith",
|
|
5223
|
-
agentId: stringField(agent, "agentId"),
|
|
5224
|
-
status: stringField(agent, "status") ?? stringField(statusSnapshot, "status"),
|
|
5225
|
-
summary: stringField(statusSnapshot, "summary"),
|
|
5226
|
-
statusReason: stringField(agent, "statusReason"),
|
|
5227
|
-
threadId: stringField(agent, "threadId"),
|
|
5228
|
-
rolloutPath: stringField(agent, "rolloutPath"),
|
|
5229
|
-
pid: numberField(agent, "pid"),
|
|
5230
|
-
lastEventSeq: codewithAgentLastEventSeq(readJson)
|
|
5231
|
-
};
|
|
5232
|
-
}
|
|
5233
|
-
function sleep(ms) {
|
|
5234
|
-
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
5235
|
-
}
|
|
5236
|
-
async function executeCodewithDurableAgent(spec, metadata, opts, env, startedAt) {
|
|
5237
|
-
const target = spec.codewithDurableAgent?.target;
|
|
5238
|
-
if (!target)
|
|
5239
|
-
throw new Error("internal error: missing codewith durable target");
|
|
5240
|
-
const maxOutputBytes = opts.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
|
|
5241
|
-
const idempotencyKey = codewithAgentIdempotencyKey(metadata);
|
|
5242
|
-
const start = await spawnCapture(spec.command, codewithAgentStartArgs(target, idempotencyKey), {
|
|
5243
|
-
cwd: spec.cwd,
|
|
5244
|
-
env,
|
|
5245
|
-
timeoutMs: 30000,
|
|
5246
|
-
maxOutputBytes
|
|
5247
|
-
});
|
|
5248
|
-
const stderr = new BoundedOutputBuffer(maxOutputBytes);
|
|
5249
|
-
stderr.append(start.stderr);
|
|
5250
|
-
if (start.error || (start.status ?? 1) !== 0) {
|
|
5251
|
-
return failureResult(startedAt, start.error ?? `codewith agent start exited with code ${start.status ?? "unknown"}`, {
|
|
5252
|
-
exitCode: start.status ?? undefined,
|
|
5253
|
-
stdout: start.stdout,
|
|
5254
|
-
stderr: stderr.value()
|
|
5255
|
-
});
|
|
5256
|
-
}
|
|
5257
|
-
let startJson;
|
|
5258
|
-
try {
|
|
5259
|
-
startJson = parseJsonOutput(start.stdout || "{}", "codewith agent start");
|
|
5260
|
-
} catch (error) {
|
|
5261
|
-
return failureResult(startedAt, error instanceof Error ? error.message : String(error), { stdout: start.stdout, stderr: stderr.value() });
|
|
5262
|
-
}
|
|
5263
|
-
const agentId = stringField(recordField(startJson, "agent"), "agentId");
|
|
5264
|
-
if (!agentId) {
|
|
5265
|
-
return failureResult(startedAt, "codewith agent start did not return agent.agentId", { stdout: start.stdout, stderr: stderr.value() });
|
|
5266
|
-
}
|
|
5267
|
-
opts.onAgentProgress?.(codewithAgentProgress(startJson));
|
|
5268
|
-
const stopAgent = async () => {
|
|
5269
|
-
await spawnCapture(spec.command, codewithAgentControlArgs(target, "stop", agentId), {
|
|
5270
|
-
cwd: spec.cwd,
|
|
5271
|
-
env,
|
|
5272
|
-
timeoutMs: 15000,
|
|
5273
|
-
maxOutputBytes
|
|
5274
|
-
});
|
|
5275
|
-
};
|
|
5276
|
-
const pollMs = Math.max(100, Number(opts.env?.LOOPS_CODEWITH_AGENT_POLL_MS ?? process.env.LOOPS_CODEWITH_AGENT_POLL_MS ?? 2000) || 2000);
|
|
5277
|
-
let lastReadJson = startJson;
|
|
5278
|
-
let lastLogsJson;
|
|
5279
|
-
let lastFingerprint;
|
|
5280
|
-
let lastProgressAt = Date.now();
|
|
5281
|
-
const evidence = () => boundedText(codewithAgentEvidence(startJson, lastReadJson, lastLogsJson), maxOutputBytes);
|
|
5282
|
-
while (true) {
|
|
5283
|
-
if (opts.signal?.aborted) {
|
|
5284
|
-
await stopAgent();
|
|
5285
|
-
return failureResult(startedAt, "cancelled", { stdout: evidence(), stderr: stderr.value() });
|
|
5286
|
-
}
|
|
5287
|
-
const read = await spawnCapture(spec.command, codewithAgentControlArgs(target, "read", agentId), {
|
|
5288
|
-
cwd: spec.cwd,
|
|
5289
|
-
env,
|
|
5290
|
-
timeoutMs: 30000,
|
|
5291
|
-
maxOutputBytes
|
|
5292
|
-
});
|
|
5293
|
-
stderr.append(read.stderr);
|
|
5294
|
-
if (read.error || (read.status ?? 1) !== 0) {
|
|
5295
|
-
return failureResult(startedAt, read.error ?? `codewith agent read exited with code ${read.status ?? "unknown"}`, {
|
|
5296
|
-
exitCode: read.status ?? undefined,
|
|
5297
|
-
stdout: evidence(),
|
|
5298
|
-
stderr: stderr.value()
|
|
5299
|
-
});
|
|
5300
|
-
}
|
|
5301
|
-
try {
|
|
5302
|
-
lastReadJson = parseJsonOutput(read.stdout || "{}", "codewith agent read");
|
|
5303
|
-
} catch (error) {
|
|
5304
|
-
return failureResult(startedAt, error instanceof Error ? error.message : String(error), {
|
|
5305
|
-
stdout: boundedText(read.stdout, maxOutputBytes),
|
|
5306
|
-
stderr: stderr.value()
|
|
5307
|
-
});
|
|
5308
|
-
}
|
|
5309
|
-
const status = codewithAgentStatus(lastReadJson);
|
|
5310
|
-
const fingerprint = JSON.stringify({
|
|
5311
|
-
status,
|
|
5312
|
-
agentLastEventSeq: numberField(recordField(lastReadJson, "agent"), "lastEventSeq"),
|
|
5313
|
-
snapshot: recordField(lastReadJson, "statusSnapshot") ?? null
|
|
5314
|
-
});
|
|
5315
|
-
if (fingerprint !== lastFingerprint) {
|
|
5316
|
-
lastFingerprint = fingerprint;
|
|
5317
|
-
lastProgressAt = Date.now();
|
|
5318
|
-
opts.onAgentProgress?.(codewithAgentProgress(lastReadJson));
|
|
5319
|
-
}
|
|
5320
|
-
if (status === "completed" || status === "failed" || status === "cancelled") {
|
|
5321
|
-
const logs = await spawnCapture(spec.command, codewithAgentControlArgs(target, "logs", agentId), {
|
|
5322
|
-
cwd: spec.cwd,
|
|
5323
|
-
env,
|
|
5324
|
-
timeoutMs: 30000,
|
|
5325
|
-
maxOutputBytes
|
|
5326
|
-
});
|
|
5327
|
-
if (!logs.error && (logs.status ?? 1) === 0) {
|
|
5328
|
-
try {
|
|
5329
|
-
lastLogsJson = parseJsonOutput(logs.stdout || "{}", "codewith agent logs");
|
|
5330
|
-
} catch {
|
|
5331
|
-
lastLogsJson = undefined;
|
|
5332
|
-
}
|
|
5333
|
-
} else {
|
|
5334
|
-
stderr.append(logs.stderr || logs.error || "");
|
|
5335
|
-
}
|
|
5336
|
-
if (status === "completed") {
|
|
5337
|
-
return successResult(startedAt, { exitCode: 0, stdout: evidence(), stderr: stderr.value() });
|
|
5338
|
-
}
|
|
5339
|
-
return failureResult(startedAt, stringField(recordField(lastReadJson, "agent"), "statusReason") ?? `codewith agent ${status}`, { exitCode: 1, stdout: evidence(), stderr: stderr.value() });
|
|
5340
|
-
}
|
|
5341
|
-
if (typeof spec.timeoutMs === "number" && new Date(nowIso()).getTime() - new Date(startedAt).getTime() >= spec.timeoutMs) {
|
|
5342
|
-
await stopAgent();
|
|
5343
|
-
return timeoutResult(startedAt, `timed out after ${spec.timeoutMs}ms`, { stdout: evidence(), stderr: stderr.value() });
|
|
5344
|
-
}
|
|
5345
|
-
if (typeof spec.idleTimeoutMs === "number" && Date.now() - lastProgressAt >= spec.idleTimeoutMs) {
|
|
5346
|
-
await stopAgent();
|
|
5347
|
-
return timeoutResult(startedAt, `idle timed out after ${spec.idleTimeoutMs}ms without agent progress`, {
|
|
5348
|
-
stdout: evidence(),
|
|
5349
|
-
stderr: stderr.value()
|
|
5350
|
-
});
|
|
5351
|
-
}
|
|
5352
|
-
await sleep(pollMs);
|
|
5353
|
-
}
|
|
5354
|
-
}
|
|
5355
5110
|
function resolvedDirEquals(left, right) {
|
|
5356
5111
|
try {
|
|
5357
5112
|
return realpathSync(left) === realpathSync(right);
|
|
@@ -5585,9 +5340,6 @@ function preflightTarget(target, metadata = {}, opts = {}) {
|
|
|
5585
5340
|
async function executeTarget(target, metadata = {}, opts = {}) {
|
|
5586
5341
|
let spec = commandSpec(target, opts);
|
|
5587
5342
|
const machine = resolvedMachine(opts);
|
|
5588
|
-
if (machine && !machine.local && spec.codewithDurableAgent) {
|
|
5589
|
-
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");
|
|
5590
|
-
}
|
|
5591
5343
|
if (machine && !machine.local) {
|
|
5592
5344
|
const remoteFallbackSpec = spec.worktree?.enabled && spec.worktree.mode === "auto" ? worktreeFallbackSpec(target, opts, spec.worktree.originalCwd) : undefined;
|
|
5593
5345
|
return executeRemoteSpec(spec, machine, metadata, opts, remoteFallbackSpec);
|
|
@@ -5618,9 +5370,6 @@ async function executeTarget(target, metadata = {}, opts = {}) {
|
|
|
5618
5370
|
if (worktreeEntry?.fallbackCwd) {
|
|
5619
5371
|
spec = worktreeFallbackSpec(target, opts, worktreeEntry.fallbackCwd) ?? spec;
|
|
5620
5372
|
}
|
|
5621
|
-
if (spec.codewithDurableAgent) {
|
|
5622
|
-
return executeCodewithDurableAgent(spec, metadata, opts, env, startedAt);
|
|
5623
|
-
}
|
|
5624
5373
|
const child = spawn2(spec.command, spec.args, {
|
|
5625
5374
|
cwd: spec.cwd,
|
|
5626
5375
|
env,
|
|
@@ -7719,7 +7468,7 @@ async function tick(deps) {
|
|
|
7719
7468
|
// package.json
|
|
7720
7469
|
var package_default = {
|
|
7721
7470
|
name: "@hasna/loops",
|
|
7722
|
-
version: "0.4.
|
|
7471
|
+
version: "0.4.10",
|
|
7723
7472
|
description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
|
|
7724
7473
|
type: "module",
|
|
7725
7474
|
main: "dist/index.js",
|