@hasna/loops 0.4.13 → 0.4.22
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 +99 -3
- package/README.md +171 -39
- package/dist/api/index.d.ts +36 -0
- package/dist/api/index.js +1518 -15
- package/dist/cli/index.js +7280 -3213
- package/dist/cli/ui.d.ts +44 -0
- package/dist/daemon/index.js +1433 -303
- package/dist/generated/storage-kit/health.d.ts +19 -0
- package/dist/generated/storage-kit/index.d.ts +7 -0
- package/dist/generated/storage-kit/migrations.d.ts +47 -0
- package/dist/generated/storage-kit/mode.d.ts +47 -0
- package/dist/generated/storage-kit/pool.d.ts +33 -0
- package/dist/generated/storage-kit/query.d.ts +35 -0
- package/dist/generated/storage-kit/tls.d.ts +25 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +8689 -4837
- package/dist/lib/cloud/mode.d.ts +17 -0
- package/dist/lib/cloud/resolve.d.ts +16 -0
- package/dist/lib/cloud/storage.d.ts +82 -0
- package/dist/lib/cloud/transport.d.ts +148 -0
- package/dist/lib/format.d.ts +2 -1
- package/dist/lib/health.d.ts +83 -3
- package/dist/lib/migration.d.ts +40 -0
- package/dist/lib/mode.js +15 -3
- package/dist/lib/route/index.d.ts +2 -0
- package/dist/lib/route/policies.d.ts +51 -0
- package/dist/lib/route/provider-admission.d.ts +37 -0
- package/dist/lib/route/throttle.d.ts +4 -0
- package/dist/lib/route/types.d.ts +8 -0
- package/dist/lib/run-receipts.d.ts +14 -0
- package/dist/lib/scheduler.d.ts +5 -2
- package/dist/lib/storage/contract.d.ts +7 -1
- package/dist/lib/storage/index.d.ts +1 -0
- package/dist/lib/storage/index.js +2028 -231
- package/dist/lib/storage/pg-executor.d.ts +27 -0
- package/dist/lib/storage/pg-runner-claim.d.ts +40 -0
- package/dist/lib/storage/postgres-loop-storage.d.ts +120 -0
- package/dist/lib/storage/postgres-schema.js +29 -0
- package/dist/lib/storage/postgres.js +29 -0
- package/dist/lib/storage/sqlite.d.ts +6 -0
- package/dist/lib/storage/sqlite.js +748 -26
- package/dist/lib/store/index.d.ts +268 -0
- package/dist/lib/store.d.ts +282 -1
- package/dist/lib/store.js +746 -26
- package/dist/lib/template-kit.d.ts +25 -0
- package/dist/lib/templates.d.ts +16 -1
- package/dist/mcp/http.d.ts +16 -0
- package/dist/mcp/index.js +2885 -634
- package/dist/runner/index.js +198 -32
- package/dist/sdk/http.d.ts +182 -0
- package/dist/sdk/http.js +166 -0
- package/dist/sdk/index.d.ts +55 -18
- package/dist/sdk/index.js +2734 -617
- package/dist/serve/index.d.ts +4 -0
- package/dist/serve/index.js +9095 -0
- package/dist/types.d.ts +52 -0
- package/docs/AUTOMATION_RUNTIME_DESIGN.md +442 -1
- package/docs/CUTOVER-RUNBOOK.md +78 -0
- package/docs/DEPLOYMENT_MODES.md +35 -18
- package/docs/RUNTIME_BOUNDARY.md +203 -0
- package/docs/USAGE.md +195 -38
- package/package.json +15 -3
package/dist/runner/index.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// package.json
|
|
4
4
|
var package_default = {
|
|
5
5
|
name: "@hasna/loops",
|
|
6
|
-
version: "0.4.
|
|
6
|
+
version: "0.4.22",
|
|
7
7
|
description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
|
|
8
8
|
type: "module",
|
|
9
9
|
main: "dist/index.js",
|
|
@@ -12,6 +12,7 @@ var package_default = {
|
|
|
12
12
|
loops: "dist/cli/index.js",
|
|
13
13
|
"loops-daemon": "dist/daemon/index.js",
|
|
14
14
|
"loops-api": "dist/api/index.js",
|
|
15
|
+
"loops-serve": "dist/serve/index.js",
|
|
15
16
|
"loops-runner": "dist/runner/index.js",
|
|
16
17
|
"loops-mcp": "dist/mcp/index.js"
|
|
17
18
|
},
|
|
@@ -24,6 +25,14 @@ var package_default = {
|
|
|
24
25
|
types: "./dist/sdk/index.d.ts",
|
|
25
26
|
import: "./dist/sdk/index.js"
|
|
26
27
|
},
|
|
28
|
+
"./sdk/http": {
|
|
29
|
+
types: "./dist/sdk/http.d.ts",
|
|
30
|
+
import: "./dist/sdk/http.js"
|
|
31
|
+
},
|
|
32
|
+
"./serve": {
|
|
33
|
+
types: "./dist/serve/index.d.ts",
|
|
34
|
+
import: "./dist/serve/index.js"
|
|
35
|
+
},
|
|
27
36
|
"./mcp": {
|
|
28
37
|
types: "./dist/mcp/index.d.ts",
|
|
29
38
|
import: "./dist/mcp/index.js"
|
|
@@ -69,7 +78,7 @@ var package_default = {
|
|
|
69
78
|
"LICENSE"
|
|
70
79
|
],
|
|
71
80
|
scripts: {
|
|
72
|
-
build: "rm -rf dist && bun build src/cli/index.ts src/daemon/index.ts src/api/index.ts src/runner/index.ts src/mcp/index.ts src/index.ts src/sdk/index.ts src/lib/store.ts src/lib/mode.ts src/lib/storage/index.ts src/lib/storage/contract.ts src/lib/storage/sqlite.ts src/lib/storage/postgres.ts src/lib/storage/postgres-schema.ts --root src --outdir dist --target bun --packages external && chmod +x dist/cli/index.js dist/daemon/index.js dist/api/index.js dist/runner/index.js dist/mcp/index.js && tsc -p tsconfig.build.json --emitDeclarationOnly --outDir dist",
|
|
81
|
+
build: "rm -rf dist && bun build src/cli/index.ts src/daemon/index.ts src/api/index.ts src/serve/index.ts src/runner/index.ts src/mcp/index.ts src/index.ts src/sdk/index.ts src/sdk/http.ts src/lib/store.ts src/lib/mode.ts src/lib/storage/index.ts src/lib/storage/contract.ts src/lib/storage/sqlite.ts src/lib/storage/postgres.ts src/lib/storage/postgres-schema.ts --root src --outdir dist --target bun --packages external && chmod +x dist/cli/index.js dist/daemon/index.js dist/api/index.js dist/serve/index.js dist/runner/index.js dist/mcp/index.js && tsc -p tsconfig.build.json --emitDeclarationOnly --outDir dist",
|
|
73
82
|
"build:bin": "bun build src/cli/index.ts --compile --outfile dist/loops",
|
|
74
83
|
typecheck: "tsc --noEmit",
|
|
75
84
|
test: "bun test",
|
|
@@ -109,13 +118,16 @@ var package_default = {
|
|
|
109
118
|
"@openrouter/ai-sdk-provider": "2.9.1",
|
|
110
119
|
ai: "6.0.204",
|
|
111
120
|
commander: "^13.1.0",
|
|
121
|
+
pg: "^8.13.1",
|
|
112
122
|
zod: "4.4.3"
|
|
113
123
|
},
|
|
114
124
|
optionalDependencies: {
|
|
115
|
-
"@hasna/machines": "0.0.49"
|
|
125
|
+
"@hasna/machines": "0.0.49",
|
|
126
|
+
"@hasna/contracts": "^0.4.2"
|
|
116
127
|
},
|
|
117
128
|
devDependencies: {
|
|
118
129
|
"@types/bun": "latest",
|
|
130
|
+
"@types/pg": "^8.11.10",
|
|
119
131
|
typescript: "^5.7.3"
|
|
120
132
|
},
|
|
121
133
|
publishConfig: {
|
|
@@ -1086,6 +1098,7 @@ var TRANSPORT_ENV_KEYS = new Set([
|
|
|
1086
1098
|
"LOGNAME",
|
|
1087
1099
|
"PATH",
|
|
1088
1100
|
"SHELL",
|
|
1101
|
+
"SHLVL",
|
|
1089
1102
|
"SSH_AGENT_PID",
|
|
1090
1103
|
"SSH_AUTH_SOCK",
|
|
1091
1104
|
"TERM",
|
|
@@ -1118,6 +1131,32 @@ function timeoutResult(startedAt, error, fields = {}) {
|
|
|
1118
1131
|
function successResult(startedAt, fields = {}) {
|
|
1119
1132
|
return buildResult("succeeded", startedAt, fields);
|
|
1120
1133
|
}
|
|
1134
|
+
function codewithJsonlHasTerminalSuccess(stdout) {
|
|
1135
|
+
for (const line of stdout.split(/\r?\n/)) {
|
|
1136
|
+
const trimmed = line.trim();
|
|
1137
|
+
if (!trimmed || !trimmed.startsWith("{"))
|
|
1138
|
+
continue;
|
|
1139
|
+
let event;
|
|
1140
|
+
try {
|
|
1141
|
+
event = JSON.parse(trimmed);
|
|
1142
|
+
} catch {
|
|
1143
|
+
continue;
|
|
1144
|
+
}
|
|
1145
|
+
if (!event || typeof event !== "object")
|
|
1146
|
+
continue;
|
|
1147
|
+
const record = event;
|
|
1148
|
+
if (record.type === "task_complete")
|
|
1149
|
+
return true;
|
|
1150
|
+
const payload = record.payload;
|
|
1151
|
+
if (payload && typeof payload === "object" && payload.type === "task_complete") {
|
|
1152
|
+
return true;
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
return false;
|
|
1156
|
+
}
|
|
1157
|
+
function codewithJsonlReconciledSuccess(spec, fields) {
|
|
1158
|
+
return spec.agentProvider === "codewith" && codewithJsonlHasTerminalSuccess(fields.stdout ?? "");
|
|
1159
|
+
}
|
|
1121
1160
|
function notifySpawn(pid, opts) {
|
|
1122
1161
|
if (!pid)
|
|
1123
1162
|
return;
|
|
@@ -1133,10 +1172,48 @@ function shellQuote(value) {
|
|
|
1133
1172
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
1134
1173
|
}
|
|
1135
1174
|
function codewithProfileCandidateFromLine(line) {
|
|
1136
|
-
const
|
|
1137
|
-
if (
|
|
1138
|
-
return
|
|
1139
|
-
|
|
1175
|
+
const trimmed = line.trim();
|
|
1176
|
+
if (!trimmed || trimmed === "No auth profiles saved.")
|
|
1177
|
+
return;
|
|
1178
|
+
const cols = trimmed.split(/\s+/);
|
|
1179
|
+
const candidate = cols[0] === "*" ? cols[1] : cols[0];
|
|
1180
|
+
if (candidate === "NAME" && (cols[1] === "ACCOUNT" || cols[2] === "ACCOUNT"))
|
|
1181
|
+
return;
|
|
1182
|
+
if (candidate === '"name":') {
|
|
1183
|
+
const jsonName = trimmed.match(/"name"\s*:\s*"([^"]+)"/)?.[1];
|
|
1184
|
+
if (jsonName)
|
|
1185
|
+
return jsonName;
|
|
1186
|
+
}
|
|
1187
|
+
return candidate;
|
|
1188
|
+
}
|
|
1189
|
+
function codewithProfileCandidatesFromJson(value) {
|
|
1190
|
+
const candidates = [];
|
|
1191
|
+
const root = value && typeof value === "object" ? value : undefined;
|
|
1192
|
+
const addProfile = (entry) => {
|
|
1193
|
+
if (!entry || typeof entry !== "object")
|
|
1194
|
+
return;
|
|
1195
|
+
const name = entry.name;
|
|
1196
|
+
if (typeof name === "string" && name)
|
|
1197
|
+
candidates.push(name);
|
|
1198
|
+
};
|
|
1199
|
+
const data = root?.data;
|
|
1200
|
+
if (Array.isArray(data))
|
|
1201
|
+
data.forEach(addProfile);
|
|
1202
|
+
const profiles = root?.profiles;
|
|
1203
|
+
if (Array.isArray(profiles))
|
|
1204
|
+
profiles.forEach(addProfile);
|
|
1205
|
+
return candidates;
|
|
1206
|
+
}
|
|
1207
|
+
function codewithProfileCandidatesFromOutput(output) {
|
|
1208
|
+
const trimmed = output.trim();
|
|
1209
|
+
const jsonStart = trimmed.indexOf("{");
|
|
1210
|
+
const jsonEnd = trimmed.lastIndexOf("}");
|
|
1211
|
+
if (jsonStart >= 0 && jsonEnd >= jsonStart) {
|
|
1212
|
+
try {
|
|
1213
|
+
return codewithProfileCandidatesFromJson(JSON.parse(trimmed.slice(jsonStart, jsonEnd + 1)));
|
|
1214
|
+
} catch {}
|
|
1215
|
+
}
|
|
1216
|
+
return output.split(/\r?\n/).map(codewithProfileCandidateFromLine).filter(Boolean);
|
|
1140
1217
|
}
|
|
1141
1218
|
function codewithProfileForError(profile) {
|
|
1142
1219
|
return /[\u0000-\u001F\u007F]/.test(profile) ? JSON.stringify(profile) ?? profile : profile;
|
|
@@ -1229,6 +1306,7 @@ function commandSpec(target, opts) {
|
|
|
1229
1306
|
preflightAnyOf: invocation.preflightAnyOf,
|
|
1230
1307
|
stdin: invocation.stdin,
|
|
1231
1308
|
allowlist: agentTarget.allowlist,
|
|
1309
|
+
agentProvider: agentTarget.provider,
|
|
1232
1310
|
worktree: agentTarget.worktree
|
|
1233
1311
|
};
|
|
1234
1312
|
}
|
|
@@ -1242,6 +1320,7 @@ function composeExecutionEnv(spec, metadata, opts, accountEnv) {
|
|
|
1242
1320
|
Object.assign(env, spec.env ?? {});
|
|
1243
1321
|
Object.assign(env, allowlistEnv(spec.allowlist));
|
|
1244
1322
|
env.PATH = normalizeExecutionPath(env);
|
|
1323
|
+
env.SHLVL ||= "1";
|
|
1245
1324
|
Object.assign(env, metadataEnv(metadata));
|
|
1246
1325
|
return env;
|
|
1247
1326
|
}
|
|
@@ -1313,7 +1392,7 @@ function remoteWorktreePrepareLines(worktree) {
|
|
|
1313
1392
|
return [
|
|
1314
1393
|
"__openloops_prepare_worktree() {",
|
|
1315
1394
|
` local repo=${shellQuote(repoRoot)} path=${shellQuote(path)} branch=${shellQuote(branch)}`,
|
|
1316
|
-
" local top expected_common actual_common current",
|
|
1395
|
+
" local top expected_common actual_common current status recovered",
|
|
1317
1396
|
' if [ -L "$path" ]; then echo "refusing symlinked worktree path $path" >&2; return 1; fi',
|
|
1318
1397
|
' if [ -e "$path" ]; then',
|
|
1319
1398
|
' top="$(git -C "$path" rev-parse --show-toplevel 2>/dev/null)" || { echo "refusing to reuse non-worktree path: $path" >&2; return 1; }',
|
|
@@ -1322,7 +1401,19 @@ function remoteWorktreePrepareLines(worktree) {
|
|
|
1322
1401
|
' actual_common="$(cd "$path" 2>/dev/null && cd "$(git rev-parse --git-common-dir 2>/dev/null)" 2>/dev/null && pwd -P)"',
|
|
1323
1402
|
' if [ -z "$expected_common" ] || [ "$expected_common" != "$actual_common" ]; then echo "existing worktree $path belongs to a different git common dir" >&2; return 1; fi',
|
|
1324
1403
|
' current="$(git -C "$path" branch --show-current 2>/dev/null)"',
|
|
1325
|
-
' if [ "$current" != "$branch" ]; then
|
|
1404
|
+
' if [ "$current" != "$branch" ]; then',
|
|
1405
|
+
' status="$(git -C "$path" status --porcelain=v1 --untracked-files=all 2>/dev/null)" || { echo "existing worktree $path is on branch ${current:-unknown}, expected $branch, and cleanliness could not be checked; inspect or remove the stale worktree" >&2; return 1; }',
|
|
1406
|
+
' if [ -n "$status" ]; then echo "existing worktree $path is on branch ${current:-unknown}, expected $branch, and has local changes; commit/stash them or remove the stale worktree before retrying" >&2; return 1; fi',
|
|
1407
|
+
' if git -C "$repo" show-ref --verify --quiet "refs/heads/$branch"; then',
|
|
1408
|
+
' git -C "$path" checkout "$branch" 1>&2 || { echo "existing worktree $path is clean but could not switch from branch ${current:-unknown} to expected $branch; remove/prune the stale worktree or free the branch before retrying" >&2; return 1; }',
|
|
1409
|
+
' elif [ -z "$current" ]; then',
|
|
1410
|
+
' git -C "$path" checkout -b "$branch" 1>&2 || { echo "existing worktree $path is clean but could not recreate expected branch $branch at the current detached HEAD; remove/prune the stale worktree before retrying" >&2; return 1; }',
|
|
1411
|
+
" else",
|
|
1412
|
+
' echo "existing worktree $path is on branch $current, expected $branch, but expected branch does not exist; remove/prune the stale worktree or recreate the expected branch before retrying" >&2; return 1',
|
|
1413
|
+
" fi",
|
|
1414
|
+
' recovered="$(git -C "$path" branch --show-current 2>/dev/null)"',
|
|
1415
|
+
' if [ "$recovered" != "$branch" ]; then echo "existing worktree $path branch recovery ended on ${recovered:-unknown}, expected $branch; remove/prune the stale worktree before retrying" >&2; return 1; fi',
|
|
1416
|
+
" fi",
|
|
1326
1417
|
" return 0",
|
|
1327
1418
|
" fi",
|
|
1328
1419
|
' git -C "$repo" rev-parse --is-inside-work-tree >/dev/null 2>&1 || { echo "worktree repoRoot is not a git repository: $repo" >&2; return 1; }',
|
|
@@ -1388,7 +1479,7 @@ function remotePreflightScript(spec, metadata) {
|
|
|
1388
1479
|
}
|
|
1389
1480
|
if (spec.nativeAuthProfile?.provider === "codewith") {
|
|
1390
1481
|
const profileForError = codewithProfileForError(spec.nativeAuthProfile.profile);
|
|
1391
|
-
lines.push(`__OPENLOOPS_CODEWITH_PROFILE=${shellQuote(spec.nativeAuthProfile.profile)}`, "export __OPENLOOPS_CODEWITH_PROFILE",
|
|
1482
|
+
lines.push(`__OPENLOOPS_CODEWITH_PROFILE=${shellQuote(spec.nativeAuthProfile.profile)}`, "export __OPENLOOPS_CODEWITH_PROFILE", "__openloops_codewith_profile_list_contains() {", ` printf '%s\\n' "$__OPENLOOPS_CODEWITH_PROFILES" | awk '{ line = $0; gsub(/^[[:space:]]+|[[:space:]]+$/, "", line); if (line == "" || line == "No auth profiles saved.") next; if (line ~ /"(data|profiles)"[[:space:]]*:[[:space:]]*\\[/) { in_profiles = 1; next } if (in_profiles && line ~ /^\\]/) { in_profiles = 0; next } json = line; if (in_profiles && json ~ /"name"[[:space:]]*:[[:space:]]*"/) { sub(/^.*"name"[[:space:]]*:[[:space:]]*"/, "", json); sub(/".*$/, "", json); if (json == ENVIRON["__OPENLOOPS_CODEWITH_PROFILE"]) found = 1; next } split(line, cols, /[[:space:]]+/); candidate = (cols[1] == "*" ? cols[2] : cols[1]); if (candidate == "NAME" && (cols[2] == "ACCOUNT" || cols[3] == "ACCOUNT")) next; if (candidate == ENVIRON["__OPENLOOPS_CODEWITH_PROFILE"]) found = 1 } END { exit(found ? 0 : 1) }'`, "}", `if __OPENLOOPS_CODEWITH_PROFILES="$(${shellQuote(spec.command)} profile list --json 2>/dev/null)" && __openloops_codewith_profile_list_contains; then`, " :", "else", ` __OPENLOOPS_CODEWITH_PROFILES="$(${shellQuote(spec.command)} profile list)" || {`, ` printf '%s\\n' ${shellQuote("codewith auth profile preflight failed")} >&2`, " exit 1", " }", " if ! __openloops_codewith_profile_list_contains; then", ` printf '%s\\n' ${shellQuote(`codewith auth profile not found: ${profileForError}`)} >&2`, " exit 1", " fi", "fi");
|
|
1392
1483
|
}
|
|
1393
1484
|
return lines.join(`
|
|
1394
1485
|
`);
|
|
@@ -1403,9 +1494,28 @@ function transportEnv(opts) {
|
|
|
1403
1494
|
env[key] = value;
|
|
1404
1495
|
}
|
|
1405
1496
|
env.PATH = normalizeExecutionPath(env);
|
|
1497
|
+
env.SHLVL ||= "1";
|
|
1406
1498
|
return env;
|
|
1407
1499
|
}
|
|
1500
|
+
function remotePreflightFailureMessage(machineId, detail) {
|
|
1501
|
+
const normalized = detail.trim();
|
|
1502
|
+
if (/Host key verification failed|REMOTE HOST IDENTIFICATION HAS CHANGED|Offending .*key in .*known_hosts/i.test(normalized)) {
|
|
1503
|
+
return [
|
|
1504
|
+
`remote preflight failed on ${machineId}: SSH host key verification failed.`,
|
|
1505
|
+
`Verify ${machineId}'s host identity and repair SSH known_hosts/trust material outside OpenLoops;`,
|
|
1506
|
+
"OpenLoops will not disable host-key checking or modify known_hosts automatically.",
|
|
1507
|
+
normalized ? `Transport detail: ${normalized}` : ""
|
|
1508
|
+
].filter(Boolean).join(" ");
|
|
1509
|
+
}
|
|
1510
|
+
return `remote preflight failed on ${machineId}${normalized ? `: ${normalized}` : ""}`;
|
|
1511
|
+
}
|
|
1408
1512
|
function assertCodewithProfileListed(profile, result) {
|
|
1513
|
+
const profiles = codewithProfileSetFromResult(result);
|
|
1514
|
+
if (!profiles.has(profile)) {
|
|
1515
|
+
throw new Error(`codewith auth profile not found: ${codewithProfileForError(profile)}`);
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
function codewithProfileSetFromResult(result) {
|
|
1409
1519
|
if (result.error) {
|
|
1410
1520
|
throw new Error(`codewith auth profile preflight failed: ${result.error}`);
|
|
1411
1521
|
}
|
|
@@ -1413,32 +1523,45 @@ function assertCodewithProfileListed(profile, result) {
|
|
|
1413
1523
|
const detail = (result.stderr || result.stdout || `exit ${result.status ?? "unknown"}`).trim();
|
|
1414
1524
|
throw new Error(`codewith auth profile preflight failed${detail ? `: ${detail}` : ""}`);
|
|
1415
1525
|
}
|
|
1416
|
-
|
|
1417
|
-
if (!profiles.has(profile)) {
|
|
1418
|
-
throw new Error(`codewith auth profile not found: ${codewithProfileForError(profile)}`);
|
|
1419
|
-
}
|
|
1526
|
+
return new Set(codewithProfileCandidatesFromOutput(result.stdout || ""));
|
|
1420
1527
|
}
|
|
1421
1528
|
function preflightNativeAuthProfileSync(spec, env) {
|
|
1422
1529
|
if (spec.nativeAuthProfile?.provider !== "codewith")
|
|
1423
1530
|
return;
|
|
1424
|
-
const
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1531
|
+
const runProfileList = (args) => {
|
|
1532
|
+
const result = spawnSync3(spec.command, args, {
|
|
1533
|
+
encoding: "utf8",
|
|
1534
|
+
env,
|
|
1535
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1536
|
+
timeout: 15000
|
|
1537
|
+
});
|
|
1538
|
+
return {
|
|
1539
|
+
status: result.status,
|
|
1540
|
+
stdout: result.stdout ?? "",
|
|
1541
|
+
stderr: result.stderr ?? "",
|
|
1542
|
+
error: result.error?.message
|
|
1543
|
+
};
|
|
1544
|
+
};
|
|
1545
|
+
const jsonResult = runProfileList(["profile", "list", "--json"]);
|
|
1546
|
+
try {
|
|
1547
|
+
if (codewithProfileSetFromResult(jsonResult).has(spec.nativeAuthProfile.profile))
|
|
1548
|
+
return;
|
|
1549
|
+
} catch {}
|
|
1550
|
+
assertCodewithProfileListed(spec.nativeAuthProfile.profile, runProfileList(["profile", "list"]));
|
|
1436
1551
|
}
|
|
1437
1552
|
async function preflightNativeAuthProfile(spec, env) {
|
|
1438
1553
|
if (spec.nativeAuthProfile?.provider !== "codewith")
|
|
1439
1554
|
return;
|
|
1440
|
-
const
|
|
1441
|
-
|
|
1555
|
+
const jsonResult = await spawnCapture(spec.command, ["profile", "list", "--json"], { env, timeoutMs: 15000 });
|
|
1556
|
+
try {
|
|
1557
|
+
if (codewithProfileSetFromResult(jsonResult).has(spec.nativeAuthProfile.profile))
|
|
1558
|
+
return;
|
|
1559
|
+
} catch {}
|
|
1560
|
+
const tableResult = await spawnCapture(spec.command, ["profile", "list"], { env, timeoutMs: 15000 });
|
|
1561
|
+
assertCodewithProfileListed(spec.nativeAuthProfile.profile, tableResult);
|
|
1562
|
+
}
|
|
1563
|
+
function spawnDetail(result) {
|
|
1564
|
+
return (result.stderr || result.stdout || result.error || "").toString().trim();
|
|
1442
1565
|
}
|
|
1443
1566
|
function resolvedDirEquals(left, right) {
|
|
1444
1567
|
try {
|
|
@@ -1489,8 +1612,45 @@ async function ensureLocalWorktree(worktree, env) {
|
|
|
1489
1612
|
}
|
|
1490
1613
|
const current = await git(["-C", path, "branch", "--show-current"]);
|
|
1491
1614
|
const actualBranch = current.stdout.trim();
|
|
1492
|
-
if (current.error || (current.status ?? 1) !== 0
|
|
1493
|
-
return { error: `existing worktree ${path}
|
|
1615
|
+
if (current.error || (current.status ?? 1) !== 0) {
|
|
1616
|
+
return { error: `existing worktree ${path} branch could not be inspected${spawnDetail(current) ? `: ${spawnDetail(current)}` : ""}` };
|
|
1617
|
+
}
|
|
1618
|
+
if (actualBranch !== branch) {
|
|
1619
|
+
const actualLabel = actualBranch || "unknown";
|
|
1620
|
+
const status = await git(["-C", path, "status", "--porcelain=v1", "--untracked-files=all"]);
|
|
1621
|
+
if (status.error || (status.status ?? 1) !== 0) {
|
|
1622
|
+
const detail = spawnDetail(status);
|
|
1623
|
+
return {
|
|
1624
|
+
error: `existing worktree ${path} is on branch ${actualLabel}, expected ${branch}, and cleanliness could not be checked; inspect or remove the stale worktree${detail ? `: ${detail}` : ""}`
|
|
1625
|
+
};
|
|
1626
|
+
}
|
|
1627
|
+
if (status.stdout.trim()) {
|
|
1628
|
+
return {
|
|
1629
|
+
error: `existing worktree ${path} is on branch ${actualLabel}, expected ${branch}, and has local changes; commit/stash them or remove the stale worktree before retrying`
|
|
1630
|
+
};
|
|
1631
|
+
}
|
|
1632
|
+
const hasBranch2 = await git(["-C", repoRoot, "show-ref", "--verify", "--quiet", `refs/heads/${branch}`]);
|
|
1633
|
+
const checkout = hasBranch2.status === 0 ? await git(["-C", path, "checkout", branch]) : actualBranch ? {
|
|
1634
|
+
status: 1,
|
|
1635
|
+
stdout: "",
|
|
1636
|
+
stderr: `existing worktree ${path} is on branch ${actualLabel}, expected ${branch}, but expected branch does not exist; remove/prune the stale worktree or recreate the expected branch before retrying`,
|
|
1637
|
+
signal: null,
|
|
1638
|
+
timedOut: false
|
|
1639
|
+
} : await git(["-C", path, "checkout", "-b", branch]);
|
|
1640
|
+
if (checkout.error || (checkout.status ?? 1) !== 0) {
|
|
1641
|
+
const detail = spawnDetail(checkout);
|
|
1642
|
+
return {
|
|
1643
|
+
error: `existing worktree ${path} is clean but could not recover branch ${branch} from ${actualLabel}; remove/prune the stale worktree before retrying${detail ? `: ${detail}` : ""}`
|
|
1644
|
+
};
|
|
1645
|
+
}
|
|
1646
|
+
const recovered = await git(["-C", path, "branch", "--show-current"]);
|
|
1647
|
+
if (recovered.error || (recovered.status ?? 1) !== 0 || recovered.stdout.trim() !== branch) {
|
|
1648
|
+
const recoveredBranch = recovered.stdout.trim() || "unknown";
|
|
1649
|
+
const detail = spawnDetail(recovered);
|
|
1650
|
+
return {
|
|
1651
|
+
error: `existing worktree ${path} branch recovery ended on ${recoveredBranch}, expected ${branch}; remove/prune the stale worktree before retrying${detail ? `: ${detail}` : ""}`
|
|
1652
|
+
};
|
|
1653
|
+
}
|
|
1494
1654
|
}
|
|
1495
1655
|
return { cwd: worktree.cwd };
|
|
1496
1656
|
}
|
|
@@ -1506,7 +1666,7 @@ async function ensureLocalWorktree(worktree, env) {
|
|
|
1506
1666
|
const hasBranch = await git(["-C", repoRoot, "show-ref", "--verify", "--quiet", `refs/heads/${branch}`]);
|
|
1507
1667
|
const add = hasBranch.status === 0 ? await git(["-C", repoRoot, "worktree", "add", path, branch]) : await git(["-C", repoRoot, "worktree", "add", "-b", branch, path, "HEAD"]);
|
|
1508
1668
|
if (add.error || (add.status ?? 1) !== 0) {
|
|
1509
|
-
const detail = (add
|
|
1669
|
+
const detail = spawnDetail(add);
|
|
1510
1670
|
return { error: `git worktree add failed for ${path}${detail ? `: ${detail}` : ""}` };
|
|
1511
1671
|
}
|
|
1512
1672
|
return { cwd: worktree.cwd };
|
|
@@ -1552,7 +1712,7 @@ function preflightRemoteSpec(spec, machine, metadata, opts) {
|
|
|
1552
1712
|
throw new Error(`remote preflight failed on ${machine.id}: ${result.error.message}`);
|
|
1553
1713
|
if ((result.status ?? 1) !== 0) {
|
|
1554
1714
|
const detail = (result.stderr || result.stdout || `exit ${result.status ?? "unknown"}`).trim();
|
|
1555
|
-
throw new Error(
|
|
1715
|
+
throw new Error(remotePreflightFailureMessage(machine.id, detail));
|
|
1556
1716
|
}
|
|
1557
1717
|
}
|
|
1558
1718
|
async function executeRemoteSpec(spec, machine, metadata, opts, fallbackSpec) {
|
|
@@ -1640,6 +1800,9 @@ async function executeRemoteSpec(spec, machine, metadata, opts, fallbackSpec) {
|
|
|
1640
1800
|
if (timedOut || idleTimedOut) {
|
|
1641
1801
|
return timeoutResult(startedAt, idleTimedOut ? `idle timed out after ${spec.idleTimeoutMs}ms without stdout/stderr` : `timed out after ${spec.timeoutMs}ms`, fields);
|
|
1642
1802
|
}
|
|
1803
|
+
if (!error && exitCode !== 0 && codewithJsonlReconciledSuccess(spec, fields)) {
|
|
1804
|
+
return successResult(startedAt, fields);
|
|
1805
|
+
}
|
|
1643
1806
|
if (error || exitCode !== 0) {
|
|
1644
1807
|
return failureResult(startedAt, error ?? `remote process on ${machine.id} exited with code ${exitCode ?? "unknown"}`, fields);
|
|
1645
1808
|
}
|
|
@@ -1775,6 +1938,9 @@ async function executeTarget(target, metadata = {}, opts = {}) {
|
|
|
1775
1938
|
if (timedOut || idleTimedOut) {
|
|
1776
1939
|
return timeoutResult(startedAt, idleTimedOut ? `idle timed out after ${spec.idleTimeoutMs}ms without stdout/stderr` : `timed out after ${spec.timeoutMs}ms`, fields);
|
|
1777
1940
|
}
|
|
1941
|
+
if (!error && exitCode !== 0 && codewithJsonlReconciledSuccess(spec, fields)) {
|
|
1942
|
+
return successResult(startedAt, fields);
|
|
1943
|
+
}
|
|
1778
1944
|
if (error || exitCode !== 0) {
|
|
1779
1945
|
return failureResult(startedAt, error ?? `process exited with code ${exitCode ?? "unknown"}`, fields);
|
|
1780
1946
|
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
export interface Foundation {
|
|
2
|
+
"status": string;
|
|
3
|
+
"version": string;
|
|
4
|
+
"mode": string;
|
|
5
|
+
"service"?: string;
|
|
6
|
+
"detail"?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface Loop {
|
|
9
|
+
"id": string;
|
|
10
|
+
"name": string;
|
|
11
|
+
"description"?: string | null;
|
|
12
|
+
"status": string;
|
|
13
|
+
"schedule"?: Record<string, unknown>;
|
|
14
|
+
"target"?: Record<string, unknown>;
|
|
15
|
+
"nextRunAt"?: string | null;
|
|
16
|
+
"createdAt"?: string;
|
|
17
|
+
"updatedAt"?: string;
|
|
18
|
+
}
|
|
19
|
+
export interface CreateLoopInput {
|
|
20
|
+
"name": string;
|
|
21
|
+
"description"?: string;
|
|
22
|
+
"schedule": Record<string, unknown>;
|
|
23
|
+
"target": Record<string, unknown>;
|
|
24
|
+
}
|
|
25
|
+
export interface UpdateLoopInput {
|
|
26
|
+
"status"?: "active" | "paused" | "stopped" | "expired";
|
|
27
|
+
"nextRunAt"?: string | null;
|
|
28
|
+
"retryScheduledFor"?: string | null;
|
|
29
|
+
"expiresAt"?: string | null;
|
|
30
|
+
}
|
|
31
|
+
export interface Run {
|
|
32
|
+
"id": string;
|
|
33
|
+
"loopId": string;
|
|
34
|
+
"status": string;
|
|
35
|
+
"attempt"?: number;
|
|
36
|
+
"scheduledFor"?: string;
|
|
37
|
+
"startedAt"?: string | null;
|
|
38
|
+
"finishedAt"?: string | null;
|
|
39
|
+
}
|
|
40
|
+
export interface LoopResponse {
|
|
41
|
+
"ok": boolean;
|
|
42
|
+
"loop": Loop;
|
|
43
|
+
}
|
|
44
|
+
export interface LoopListResponse {
|
|
45
|
+
"ok": boolean;
|
|
46
|
+
"loops": Array<Loop>;
|
|
47
|
+
}
|
|
48
|
+
export interface RunResponse {
|
|
49
|
+
"ok": boolean;
|
|
50
|
+
"run": Run;
|
|
51
|
+
}
|
|
52
|
+
export interface RunListResponse {
|
|
53
|
+
"ok": boolean;
|
|
54
|
+
"runs": Array<Run>;
|
|
55
|
+
}
|
|
56
|
+
export interface DeleteResponse {
|
|
57
|
+
"ok": boolean;
|
|
58
|
+
"deleted": boolean;
|
|
59
|
+
}
|
|
60
|
+
export type RunReceiptMachine = string | Record<string, unknown>;
|
|
61
|
+
export interface RunReceiptSummary {
|
|
62
|
+
"text"?: string;
|
|
63
|
+
"stdout_bytes": number;
|
|
64
|
+
"stderr_bytes": number;
|
|
65
|
+
"stdout_excerpt"?: string;
|
|
66
|
+
"stderr_excerpt"?: string;
|
|
67
|
+
"error"?: string;
|
|
68
|
+
"duration_ms"?: number;
|
|
69
|
+
}
|
|
70
|
+
export interface RunReceipt {
|
|
71
|
+
"loop_id": string;
|
|
72
|
+
"run_id": string;
|
|
73
|
+
"machine": RunReceiptMachine;
|
|
74
|
+
"repo": string;
|
|
75
|
+
"task_ids": Array<string>;
|
|
76
|
+
"knowledge_ids": Array<string>;
|
|
77
|
+
"digest_id": string;
|
|
78
|
+
"started_at": string | null;
|
|
79
|
+
"finished_at": string | null;
|
|
80
|
+
"status": string;
|
|
81
|
+
"exit_code": number | null;
|
|
82
|
+
"summary": RunReceiptSummary;
|
|
83
|
+
"evidence_paths": Array<string>;
|
|
84
|
+
"created_at": string;
|
|
85
|
+
"updated_at": string;
|
|
86
|
+
}
|
|
87
|
+
export interface WriteRunReceiptInput {
|
|
88
|
+
"loop_id"?: string;
|
|
89
|
+
"run_id": string;
|
|
90
|
+
"machine"?: RunReceiptMachine;
|
|
91
|
+
"repo"?: string;
|
|
92
|
+
"task_ids"?: Array<string>;
|
|
93
|
+
"knowledge_ids"?: Array<string>;
|
|
94
|
+
"digest_id"?: string;
|
|
95
|
+
"started_at"?: string | null;
|
|
96
|
+
"finished_at"?: string | null;
|
|
97
|
+
"status"?: string;
|
|
98
|
+
"exit_code"?: number | null;
|
|
99
|
+
"summary"?: string | RunReceiptSummary | null;
|
|
100
|
+
"evidence_paths"?: Array<string>;
|
|
101
|
+
"stdout"?: string;
|
|
102
|
+
"stderr"?: string;
|
|
103
|
+
"error"?: string;
|
|
104
|
+
"duration_ms"?: number;
|
|
105
|
+
}
|
|
106
|
+
export interface RunReceiptResponse {
|
|
107
|
+
"ok": boolean;
|
|
108
|
+
"receipt": RunReceipt;
|
|
109
|
+
}
|
|
110
|
+
export interface RunReceiptListResponse {
|
|
111
|
+
"ok": boolean;
|
|
112
|
+
"receipts": Array<RunReceipt>;
|
|
113
|
+
}
|
|
114
|
+
export interface LoopsClientOptions {
|
|
115
|
+
/** Base URL, e.g. process.env.APP_API_URL. */
|
|
116
|
+
baseUrl: string;
|
|
117
|
+
/** API key, e.g. process.env.APP_API_KEY. Sent as the 'x-api-key' header. */
|
|
118
|
+
apiKey?: string;
|
|
119
|
+
/** Custom fetch (defaults to global fetch). */
|
|
120
|
+
fetch?: typeof fetch;
|
|
121
|
+
/** Extra headers merged into every request. */
|
|
122
|
+
headers?: Record<string, string>;
|
|
123
|
+
}
|
|
124
|
+
export declare class ApiError extends Error {
|
|
125
|
+
readonly status: number;
|
|
126
|
+
readonly body: unknown;
|
|
127
|
+
constructor(status: number, message: string, body: unknown);
|
|
128
|
+
}
|
|
129
|
+
export declare class LoopsClient {
|
|
130
|
+
private readonly baseUrl;
|
|
131
|
+
private readonly apiKey;
|
|
132
|
+
private readonly fetchImpl;
|
|
133
|
+
private readonly baseHeaders;
|
|
134
|
+
constructor(options: LoopsClientOptions);
|
|
135
|
+
private request;
|
|
136
|
+
/** Liveness probe */
|
|
137
|
+
healthCheck(init?: RequestInit): Promise<Foundation>;
|
|
138
|
+
/** Readiness probe (storage reachable + migrated) */
|
|
139
|
+
readyCheck(init?: RequestInit): Promise<Foundation>;
|
|
140
|
+
/** List loops */
|
|
141
|
+
listLoops(query?: {
|
|
142
|
+
"status"?: "active" | "paused" | "stopped" | "expired";
|
|
143
|
+
"limit"?: number;
|
|
144
|
+
"includeArchived"?: boolean;
|
|
145
|
+
"archived"?: boolean;
|
|
146
|
+
}, init?: RequestInit): Promise<LoopListResponse>;
|
|
147
|
+
/** Create a loop */
|
|
148
|
+
createLoop(body: CreateLoopInput, init?: RequestInit): Promise<LoopResponse>;
|
|
149
|
+
/** Get a loop by id */
|
|
150
|
+
getLoop(id: string, init?: RequestInit): Promise<LoopResponse>;
|
|
151
|
+
/** Delete a loop */
|
|
152
|
+
deleteLoop(id: string, init?: RequestInit): Promise<DeleteResponse>;
|
|
153
|
+
/** Update a loop (status / schedule fields) */
|
|
154
|
+
updateLoop(id: string, body: UpdateLoopInput, init?: RequestInit): Promise<LoopResponse>;
|
|
155
|
+
/** Archive a loop */
|
|
156
|
+
archiveLoop(id: string, init?: RequestInit): Promise<LoopResponse>;
|
|
157
|
+
/** Unarchive a loop */
|
|
158
|
+
unarchiveLoop(id: string, init?: RequestInit): Promise<LoopResponse>;
|
|
159
|
+
/** List run receipts */
|
|
160
|
+
listRunReceipts(query?: {
|
|
161
|
+
"loopId"?: string;
|
|
162
|
+
"repo"?: string;
|
|
163
|
+
"taskId"?: string;
|
|
164
|
+
"knowledgeId"?: string;
|
|
165
|
+
"status"?: string;
|
|
166
|
+
"limit"?: number;
|
|
167
|
+
}, init?: RequestInit): Promise<RunReceiptListResponse>;
|
|
168
|
+
/** Write a run receipt */
|
|
169
|
+
writeRunReceipt(body: WriteRunReceiptInput, init?: RequestInit): Promise<RunReceiptResponse>;
|
|
170
|
+
/** Get a run receipt by run id */
|
|
171
|
+
getRunReceipt(runId: string, init?: RequestInit): Promise<RunReceiptResponse>;
|
|
172
|
+
/** List runs */
|
|
173
|
+
listRuns(query?: {
|
|
174
|
+
"loopId"?: string;
|
|
175
|
+
"status"?: string;
|
|
176
|
+
"limit"?: number;
|
|
177
|
+
}, init?: RequestInit): Promise<RunListResponse>;
|
|
178
|
+
/** Get a run by id */
|
|
179
|
+
getRun(id: string, init?: RequestInit): Promise<RunResponse>;
|
|
180
|
+
/** Service version */
|
|
181
|
+
getVersion(init?: RequestInit): Promise<Foundation>;
|
|
182
|
+
}
|