@kynver-app/runtime 0.1.16 → 0.1.18
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/dist/cli.js +554 -237
- package/dist/cli.js.map +4 -4
- package/dist/index.js +560 -240
- package/dist/index.js.map +4 -4
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -393,12 +393,12 @@ var DEFAULT_CRITICAL_FREE_BYTES = 15 * 1024 * 1024 * 1024;
|
|
|
393
393
|
var DEFAULT_MAX_USED_PERCENT = 80;
|
|
394
394
|
var DEFAULT_HARD_MAX_USED_PERCENT = 90;
|
|
395
395
|
function observeRunnerDiskGate(input = {}) {
|
|
396
|
-
const
|
|
396
|
+
const path16 = input.diskPath?.trim() || "/";
|
|
397
397
|
const warnBelowBytes = input.diskFreeWarnBytes ?? DEFAULT_WARN_FREE_BYTES;
|
|
398
398
|
const criticalBelowBytes = input.diskFreeCriticalBytes ?? DEFAULT_CRITICAL_FREE_BYTES;
|
|
399
399
|
const maxUsedPercent = input.diskMaxUsedPercent ?? DEFAULT_MAX_USED_PERCENT;
|
|
400
400
|
const hardMaxUsedPercent = input.diskHardMaxUsedPercent ?? DEFAULT_HARD_MAX_USED_PERCENT;
|
|
401
|
-
const stats = statfsSync(
|
|
401
|
+
const stats = statfsSync(path16);
|
|
402
402
|
const freeBytes = Number(stats.bavail) * Number(stats.bsize);
|
|
403
403
|
const totalBytes = Number(stats.blocks) * Number(stats.bsize);
|
|
404
404
|
const usedPercent = totalBytes > 0 ? (totalBytes - freeBytes) / totalBytes * 100 : 100;
|
|
@@ -418,7 +418,7 @@ function observeRunnerDiskGate(input = {}) {
|
|
|
418
418
|
}
|
|
419
419
|
return {
|
|
420
420
|
ok,
|
|
421
|
-
path:
|
|
421
|
+
path: path16,
|
|
422
422
|
freeBytes,
|
|
423
423
|
totalBytes,
|
|
424
424
|
usedPercent,
|
|
@@ -605,6 +605,52 @@ function summarizeEvent(event) {
|
|
|
605
605
|
return void 0;
|
|
606
606
|
}
|
|
607
607
|
|
|
608
|
+
// src/exit-classify.ts
|
|
609
|
+
var FAILURE_PATTERNS = [
|
|
610
|
+
{
|
|
611
|
+
test: /\b(?:invalid|unknown|unsupported|unrecognized)\b[^.\n]*\bmodel\b/i,
|
|
612
|
+
label: "provider rejected the requested model"
|
|
613
|
+
},
|
|
614
|
+
{
|
|
615
|
+
test: /\bmodel\b[^.\n]*\b(?:not\s+(?:found|supported|available|recognized|valid)|is\s+not\s+valid|does\s+not\s+exist)/i,
|
|
616
|
+
label: "provider rejected the requested model"
|
|
617
|
+
},
|
|
618
|
+
{
|
|
619
|
+
test: /\b(?:did you mean|available models|choose (?:a|one of)|supported models)\b/i,
|
|
620
|
+
label: "provider rejected the requested model"
|
|
621
|
+
},
|
|
622
|
+
{
|
|
623
|
+
test: /model preflight failed/i,
|
|
624
|
+
label: "model/provider preflight failed"
|
|
625
|
+
},
|
|
626
|
+
{
|
|
627
|
+
test: /\b(?:command not found|ENOENT|is the .*CLI on PATH|executable not found|no such file or directory)\b/i,
|
|
628
|
+
label: "provider CLI is missing or not on PATH"
|
|
629
|
+
},
|
|
630
|
+
{
|
|
631
|
+
test: /\bfailed to spawn\b/i,
|
|
632
|
+
label: "provider failed to spawn the worker process"
|
|
633
|
+
},
|
|
634
|
+
{
|
|
635
|
+
test: /\b(?:not logged in|unauthorized|authentication (?:failed|required)|invalid api key|missing api key|401)\b/i,
|
|
636
|
+
label: "provider authentication failed"
|
|
637
|
+
}
|
|
638
|
+
];
|
|
639
|
+
function tidy(errorText, max = 240) {
|
|
640
|
+
const oneLine2 = errorText.replace(/\s+/g, " ").trim();
|
|
641
|
+
return oneLine2.length > max ? `${oneLine2.slice(0, max - 1)}\u2026` : oneLine2;
|
|
642
|
+
}
|
|
643
|
+
function classifyExitFailure(errorText) {
|
|
644
|
+
const text = (errorText ?? "").trim();
|
|
645
|
+
if (!text) return null;
|
|
646
|
+
for (const pattern of FAILURE_PATTERNS) {
|
|
647
|
+
if (pattern.test.test(text)) {
|
|
648
|
+
return { blocked: true, reason: `${pattern.label}: ${tidy(text)}` };
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
return null;
|
|
652
|
+
}
|
|
653
|
+
|
|
608
654
|
// src/git.ts
|
|
609
655
|
import { spawnSync } from "node:child_process";
|
|
610
656
|
function git(cwd, args, options = {}) {
|
|
@@ -720,7 +766,15 @@ var STALE_MS = 6e5;
|
|
|
720
766
|
function computeAttention(input) {
|
|
721
767
|
const now = Date.now();
|
|
722
768
|
if (input.finalResult) return { state: "done", reason: "final result recorded" };
|
|
723
|
-
if (!input.alive)
|
|
769
|
+
if (!input.alive) {
|
|
770
|
+
const classified = classifyExitFailure(input.error);
|
|
771
|
+
if (classified) return { state: "blocked", reason: classified.reason };
|
|
772
|
+
const tail = input.error?.trim();
|
|
773
|
+
return {
|
|
774
|
+
state: "needs_attention",
|
|
775
|
+
reason: tail ? `process exited without a final result: ${tail}` : "process exited without a final result"
|
|
776
|
+
};
|
|
777
|
+
}
|
|
724
778
|
if (input.heartbeatBlocker) {
|
|
725
779
|
return { state: "blocked", reason: `worker heartbeat reported blocker: ${input.heartbeatBlocker}` };
|
|
726
780
|
}
|
|
@@ -750,6 +804,7 @@ function computeWorkerStatus(worker, options = {}) {
|
|
|
750
804
|
fileMtime(worker.stderrPath),
|
|
751
805
|
fileMtime(worker.heartbeatPath)
|
|
752
806
|
]);
|
|
807
|
+
const error = parsed.error || (!alive && !parsed.finalResult ? tailFile(worker.stderrPath, 10).trim() || void 0 : void 0);
|
|
753
808
|
const attention = computeAttention({
|
|
754
809
|
alive,
|
|
755
810
|
finalResult: parsed.finalResult,
|
|
@@ -758,7 +813,8 @@ function computeWorkerStatus(worker, options = {}) {
|
|
|
758
813
|
heartbeatBytes,
|
|
759
814
|
lastActivityAt,
|
|
760
815
|
heartbeatBlocker: heartbeat.heartbeatBlocker,
|
|
761
|
-
startedAt: worker.startedAt
|
|
816
|
+
startedAt: worker.startedAt,
|
|
817
|
+
error
|
|
762
818
|
});
|
|
763
819
|
return {
|
|
764
820
|
runId: worker.runId,
|
|
@@ -783,7 +839,7 @@ function computeWorkerStatus(worker, options = {}) {
|
|
|
783
839
|
lastHeartbeatSummary: heartbeat.lastHeartbeatSummary,
|
|
784
840
|
heartbeatBlocker: heartbeat.heartbeatBlocker,
|
|
785
841
|
finalResult: parsed.finalResult,
|
|
786
|
-
error
|
|
842
|
+
error,
|
|
787
843
|
changedFiles,
|
|
788
844
|
gitAncestry
|
|
789
845
|
};
|
|
@@ -908,8 +964,8 @@ function observeRunnerResourceGate(input) {
|
|
|
908
964
|
}
|
|
909
965
|
|
|
910
966
|
// src/supervisor.ts
|
|
911
|
-
import { existsSync as
|
|
912
|
-
import
|
|
967
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync3 } from "node:fs";
|
|
968
|
+
import path9 from "node:path";
|
|
913
969
|
|
|
914
970
|
// src/prompt.ts
|
|
915
971
|
function buildPrompt(input) {
|
|
@@ -943,10 +999,74 @@ function buildPrompt(input) {
|
|
|
943
999
|
// src/providers/claude.ts
|
|
944
1000
|
import { closeSync, openSync } from "node:fs";
|
|
945
1001
|
import { spawn } from "node:child_process";
|
|
1002
|
+
|
|
1003
|
+
// src/providers/model-preflight.ts
|
|
1004
|
+
var REASONING_SUFFIX_RE = /-(?:thinking(?:-(?:high|medium|low|minimal|max|none))?|high|medium|low|minimal)$/i;
|
|
1005
|
+
function stripReasoningSuffix(model) {
|
|
1006
|
+
return model.replace(REASONING_SUFFIX_RE, "");
|
|
1007
|
+
}
|
|
1008
|
+
var FOREIGN_MODEL_RE = /^(?:gpt-|gpt5|o1|o3|o4|gemini-|grok-|composer|deepseek|llama|mistral|qwen|command-)/i;
|
|
1009
|
+
function looksLikeClaudeModel(model) {
|
|
1010
|
+
return /^claude[-_]/i.test(model) || /^(?:opus|sonnet|haiku)\b/i.test(model);
|
|
1011
|
+
}
|
|
1012
|
+
function preflightClaudeModel(model, defaultModel) {
|
|
1013
|
+
const requested = (model ?? "").trim();
|
|
1014
|
+
if (!requested) {
|
|
1015
|
+
return { ok: true, model: defaultModel, normalized: false };
|
|
1016
|
+
}
|
|
1017
|
+
const stripped = stripReasoningSuffix(requested).trim();
|
|
1018
|
+
const launch = stripped || defaultModel;
|
|
1019
|
+
if (FOREIGN_MODEL_RE.test(launch) || !looksLikeClaudeModel(launch) && launch !== defaultModel) {
|
|
1020
|
+
return {
|
|
1021
|
+
ok: false,
|
|
1022
|
+
model: requested,
|
|
1023
|
+
normalized: false,
|
|
1024
|
+
requested,
|
|
1025
|
+
note: `model "${requested}" is not a Claude model \u2014 the "claude" provider drives the Claude CLI, which only accepts claude-* model ids (got "${launch}"). Pick a Claude model or switch the worker provider.`
|
|
1026
|
+
};
|
|
1027
|
+
}
|
|
1028
|
+
if (launch !== requested) {
|
|
1029
|
+
return {
|
|
1030
|
+
ok: true,
|
|
1031
|
+
model: launch,
|
|
1032
|
+
normalized: true,
|
|
1033
|
+
requested,
|
|
1034
|
+
note: `normalized model "${requested}" \u2192 "${launch}" (the Claude CLI rejects reasoning-effort suffixes)`
|
|
1035
|
+
};
|
|
1036
|
+
}
|
|
1037
|
+
return { ok: true, model: launch, normalized: false };
|
|
1038
|
+
}
|
|
1039
|
+
function preflightCursorModel(model, defaultModel) {
|
|
1040
|
+
const requested = (model ?? "").trim();
|
|
1041
|
+
if (!requested) {
|
|
1042
|
+
return { ok: true, model: defaultModel, normalized: false };
|
|
1043
|
+
}
|
|
1044
|
+
if (looksLikeClaudeModel(requested)) {
|
|
1045
|
+
return {
|
|
1046
|
+
ok: false,
|
|
1047
|
+
model: requested,
|
|
1048
|
+
normalized: false,
|
|
1049
|
+
requested,
|
|
1050
|
+
note: `model "${requested}" is a Claude model but the worker provider is "cursor". Switch the provider to "claude" or pick a Cursor model.`
|
|
1051
|
+
};
|
|
1052
|
+
}
|
|
1053
|
+
return { ok: true, model: requested, normalized: false };
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
// src/providers/claude.ts
|
|
1057
|
+
var CLAUDE_DEFAULT_MODEL = "claude-opus-4-7";
|
|
946
1058
|
var claudeProvider = {
|
|
947
1059
|
name: "claude",
|
|
1060
|
+
defaultModel: CLAUDE_DEFAULT_MODEL,
|
|
1061
|
+
preflightModel(model) {
|
|
1062
|
+
return preflightClaudeModel(model, CLAUDE_DEFAULT_MODEL);
|
|
1063
|
+
},
|
|
948
1064
|
start(opts) {
|
|
949
|
-
const
|
|
1065
|
+
const preflight = preflightClaudeModel(opts.model, CLAUDE_DEFAULT_MODEL);
|
|
1066
|
+
if (!preflight.ok) {
|
|
1067
|
+
throw new Error(`claude provider model preflight failed: ${preflight.note}`);
|
|
1068
|
+
}
|
|
1069
|
+
const model = preflight.model;
|
|
950
1070
|
const stdoutFd = openSync(opts.stdoutPath, "a");
|
|
951
1071
|
const stderrFd = openSync(opts.stderrPath, "a");
|
|
952
1072
|
const child = spawn(
|
|
@@ -1022,8 +1142,16 @@ function resolveAgentBin() {
|
|
|
1022
1142
|
}
|
|
1023
1143
|
var cursorProvider = {
|
|
1024
1144
|
name: "cursor",
|
|
1145
|
+
defaultModel: DEFAULT_CURSOR_MODEL,
|
|
1146
|
+
preflightModel(model) {
|
|
1147
|
+
return preflightCursorModel(model, DEFAULT_CURSOR_MODEL);
|
|
1148
|
+
},
|
|
1025
1149
|
start(opts) {
|
|
1026
|
-
const
|
|
1150
|
+
const preflight = preflightCursorModel(opts.model, DEFAULT_CURSOR_MODEL);
|
|
1151
|
+
if (!preflight.ok) {
|
|
1152
|
+
throw new Error(`cursor provider model preflight failed: ${preflight.note}`);
|
|
1153
|
+
}
|
|
1154
|
+
const model = preflight.model;
|
|
1027
1155
|
const stdoutFd = openSync2(opts.stdoutPath, "a");
|
|
1028
1156
|
const stderrFd = openSync2(opts.stderrPath, "a");
|
|
1029
1157
|
const agentBin = resolveAgentBin();
|
|
@@ -1083,6 +1211,369 @@ function resolveWorkerProvider(name) {
|
|
|
1083
1211
|
return provider;
|
|
1084
1212
|
}
|
|
1085
1213
|
|
|
1214
|
+
// src/auto-complete.ts
|
|
1215
|
+
import { spawn as spawn3 } from "node:child_process";
|
|
1216
|
+
import { existsSync as existsSync8, openSync as openSync3, closeSync as closeSync3 } from "node:fs";
|
|
1217
|
+
import path8 from "node:path";
|
|
1218
|
+
import { fileURLToPath } from "node:url";
|
|
1219
|
+
|
|
1220
|
+
// src/worker-ops.ts
|
|
1221
|
+
import path7 from "node:path";
|
|
1222
|
+
async function postCompletion(url, secret, body) {
|
|
1223
|
+
const res = await fetch(url, {
|
|
1224
|
+
method: "POST",
|
|
1225
|
+
headers: buildHarnessCallbackHeaders(secret),
|
|
1226
|
+
body: JSON.stringify(body)
|
|
1227
|
+
});
|
|
1228
|
+
let parsed = null;
|
|
1229
|
+
try {
|
|
1230
|
+
parsed = await res.json();
|
|
1231
|
+
} catch {
|
|
1232
|
+
parsed = null;
|
|
1233
|
+
}
|
|
1234
|
+
return { ok: res.ok, status: res.status, parsed };
|
|
1235
|
+
}
|
|
1236
|
+
function completionErrorText(parsed) {
|
|
1237
|
+
if (parsed && typeof parsed === "object") {
|
|
1238
|
+
const err = parsed.error;
|
|
1239
|
+
if (typeof err === "string" && err.trim()) return err.trim();
|
|
1240
|
+
}
|
|
1241
|
+
return void 0;
|
|
1242
|
+
}
|
|
1243
|
+
function persistCompletionBlocker(worker, reason) {
|
|
1244
|
+
const current = worker.completionBlocker;
|
|
1245
|
+
if ((current ?? void 0) === (reason ?? void 0)) return;
|
|
1246
|
+
if (reason) worker.completionBlocker = reason;
|
|
1247
|
+
else delete worker.completionBlocker;
|
|
1248
|
+
saveWorker(worker.runId, worker);
|
|
1249
|
+
}
|
|
1250
|
+
async function tryCompleteWorker(args) {
|
|
1251
|
+
const worker = loadWorker(String(args.run), String(args.name));
|
|
1252
|
+
const status = computeWorkerStatus(worker);
|
|
1253
|
+
const agentOsId = (args.agentOsId ? String(args.agentOsId) : worker.agentOsId) || "";
|
|
1254
|
+
const taskId = (args.taskId ? String(args.taskId) : worker.taskId) || null;
|
|
1255
|
+
if (!agentOsId) {
|
|
1256
|
+
return { ok: false, reason: "missing agentOsId" };
|
|
1257
|
+
}
|
|
1258
|
+
if (!isFinishedWorkerStatus(status)) {
|
|
1259
|
+
return { ok: true, skipped: true, reason: "worker-not-finished" };
|
|
1260
|
+
}
|
|
1261
|
+
const base = resolveBaseUrl(args.baseUrl ? String(args.baseUrl) : void 0);
|
|
1262
|
+
const explicitSecret = args.secret ? String(args.secret) : void 0;
|
|
1263
|
+
let secret = await resolveCallbackSecretWithMint(explicitSecret, agentOsId, { baseUrl: base });
|
|
1264
|
+
const url = `${base}/api/agent-os/by-id/${encodeURIComponent(agentOsId)}/harness/completion`;
|
|
1265
|
+
const body = {
|
|
1266
|
+
source: "openclaw-harness",
|
|
1267
|
+
agentOsId,
|
|
1268
|
+
runId: worker.runId,
|
|
1269
|
+
workerName: worker.name,
|
|
1270
|
+
taskId,
|
|
1271
|
+
startedAt: worker.startedAt,
|
|
1272
|
+
finishedAt: status.lastActivityAt || (/* @__PURE__ */ new Date()).toISOString(),
|
|
1273
|
+
status
|
|
1274
|
+
};
|
|
1275
|
+
let result = await postCompletion(url, secret, body);
|
|
1276
|
+
if ((result.status === 401 || result.status === 403) && !explicitSecret) {
|
|
1277
|
+
const refreshed = await refreshRunnerToken(agentOsId, { baseUrl: base });
|
|
1278
|
+
if (refreshed && refreshed !== secret) {
|
|
1279
|
+
secret = refreshed;
|
|
1280
|
+
result = await postCompletion(url, secret, body);
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
if (result.ok) {
|
|
1284
|
+
persistCompletionBlocker(worker, void 0);
|
|
1285
|
+
return { ok: true, httpStatus: result.status, response: result.parsed };
|
|
1286
|
+
}
|
|
1287
|
+
const authRejected = result.status === 401 || result.status === 403;
|
|
1288
|
+
const detail = completionErrorText(result.parsed) ?? (authRejected ? "runner token unauthorized" : "non-2xx response");
|
|
1289
|
+
const reason = authRejected ? `completion replay rejected (${result.status}): ${detail}` : `completion replay failed (${result.status}): ${detail}`;
|
|
1290
|
+
persistCompletionBlocker(worker, reason);
|
|
1291
|
+
return { ok: false, httpStatus: result.status, response: result.parsed, completionBlocked: true };
|
|
1292
|
+
}
|
|
1293
|
+
async function completeWorker(args) {
|
|
1294
|
+
try {
|
|
1295
|
+
const worker = loadWorker(String(args.run), String(args.name));
|
|
1296
|
+
const status = computeWorkerStatus(worker);
|
|
1297
|
+
const agentOsId = (args.agentOsId ? String(args.agentOsId) : worker.agentOsId) || "";
|
|
1298
|
+
const taskId = (args.taskId ? String(args.taskId) : worker.taskId) || null;
|
|
1299
|
+
if (!agentOsId) {
|
|
1300
|
+
console.error("worker complete requires --agent-os-id (or an agentOsId persisted at worker start)");
|
|
1301
|
+
process.exit(1);
|
|
1302
|
+
}
|
|
1303
|
+
if (!isFinishedWorkerStatus(status)) {
|
|
1304
|
+
console.log(
|
|
1305
|
+
JSON.stringify(
|
|
1306
|
+
{
|
|
1307
|
+
worker: worker.name,
|
|
1308
|
+
runId: worker.runId,
|
|
1309
|
+
status: "skipped",
|
|
1310
|
+
reason: "worker-not-finished",
|
|
1311
|
+
workerStatus: status.status,
|
|
1312
|
+
alive: status.alive
|
|
1313
|
+
},
|
|
1314
|
+
null,
|
|
1315
|
+
2
|
|
1316
|
+
)
|
|
1317
|
+
);
|
|
1318
|
+
return;
|
|
1319
|
+
}
|
|
1320
|
+
const result = await tryCompleteWorker(args);
|
|
1321
|
+
console.log(
|
|
1322
|
+
JSON.stringify(
|
|
1323
|
+
{
|
|
1324
|
+
worker: worker.name,
|
|
1325
|
+
runId: worker.runId,
|
|
1326
|
+
agentOsId,
|
|
1327
|
+
taskId,
|
|
1328
|
+
httpStatus: result.httpStatus,
|
|
1329
|
+
response: result.response
|
|
1330
|
+
},
|
|
1331
|
+
null,
|
|
1332
|
+
2
|
|
1333
|
+
)
|
|
1334
|
+
);
|
|
1335
|
+
if (!result.ok) process.exit(1);
|
|
1336
|
+
} catch (error) {
|
|
1337
|
+
console.error(`worker complete failed: ${error.message}`);
|
|
1338
|
+
process.exit(1);
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
function workerStatus(args) {
|
|
1342
|
+
const worker = loadWorker(String(args.run), String(args.name));
|
|
1343
|
+
const status = computeWorkerStatus(worker);
|
|
1344
|
+
writeJson(path7.join(worker.workerDir, "last-status.json"), status);
|
|
1345
|
+
console.log(JSON.stringify(status, null, 2));
|
|
1346
|
+
}
|
|
1347
|
+
function runStatus(args) {
|
|
1348
|
+
const run = loadRun(String(args.run));
|
|
1349
|
+
const names = Object.keys(run.workers || {});
|
|
1350
|
+
const workers = names.map((name) => {
|
|
1351
|
+
const worker = readJson(
|
|
1352
|
+
path7.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
1353
|
+
void 0
|
|
1354
|
+
);
|
|
1355
|
+
if (!worker) {
|
|
1356
|
+
return { worker: name, status: "missing", attention: "needs_attention", attentionReason: "worker.json not found" };
|
|
1357
|
+
}
|
|
1358
|
+
const status = computeWorkerStatus(worker, { base: run.base });
|
|
1359
|
+
const rawBlocker = worker.completionBlocker;
|
|
1360
|
+
const completionBlocker = typeof rawBlocker === "string" && rawBlocker ? rawBlocker : void 0;
|
|
1361
|
+
return {
|
|
1362
|
+
worker: status.worker,
|
|
1363
|
+
status: completionBlocker ? "blocked" : status.status,
|
|
1364
|
+
attention: completionBlocker ? "blocked" : status.attention.state,
|
|
1365
|
+
attentionReason: completionBlocker ?? status.attention.reason,
|
|
1366
|
+
pid: status.pid,
|
|
1367
|
+
alive: status.alive,
|
|
1368
|
+
currentTool: status.currentTool,
|
|
1369
|
+
lastActivityAt: status.lastActivityAt,
|
|
1370
|
+
lastHeartbeatPhase: status.lastHeartbeatPhase,
|
|
1371
|
+
lastHeartbeatSummary: status.lastHeartbeatSummary,
|
|
1372
|
+
heartbeatBlocker: status.heartbeatBlocker,
|
|
1373
|
+
changedFileCount: status.changedFiles.length,
|
|
1374
|
+
branch: status.branch,
|
|
1375
|
+
ancestry: status.gitAncestry.relation,
|
|
1376
|
+
ancestryChecked: status.gitAncestry.checked
|
|
1377
|
+
};
|
|
1378
|
+
});
|
|
1379
|
+
const board = {
|
|
1380
|
+
runId: run.id,
|
|
1381
|
+
name: run.name,
|
|
1382
|
+
status: deriveRunStatus(run.status, workers),
|
|
1383
|
+
repo: run.repo,
|
|
1384
|
+
workerCount: workers.length,
|
|
1385
|
+
needsAttention: workers.filter((w) => w.attention && w.attention !== "ok" && w.attention !== "done").map((w) => w.worker),
|
|
1386
|
+
workers
|
|
1387
|
+
};
|
|
1388
|
+
writeJson(path7.join(runDirectory(run.id), "last-board.json"), board);
|
|
1389
|
+
console.log(JSON.stringify(board, null, 2));
|
|
1390
|
+
}
|
|
1391
|
+
function tailWorker(args) {
|
|
1392
|
+
const worker = loadWorker(String(args.run), String(args.name));
|
|
1393
|
+
const raw = tailFile(worker.stdoutPath, Number(args.lines || 40));
|
|
1394
|
+
if (args.raw === true || args.raw === "true") {
|
|
1395
|
+
process.stdout.write(raw);
|
|
1396
|
+
return;
|
|
1397
|
+
}
|
|
1398
|
+
for (const line of raw.split("\n").filter(Boolean)) {
|
|
1399
|
+
const event = safeJson(line);
|
|
1400
|
+
const summary = event ? summarizeEvent(event) : line;
|
|
1401
|
+
if (summary) console.log(summary);
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
function stopWorker(args) {
|
|
1405
|
+
const worker = loadWorker(String(args.run), String(args.name));
|
|
1406
|
+
if (!isPidAlive(worker.pid)) {
|
|
1407
|
+
console.log(JSON.stringify({ worker: worker.name, pid: worker.pid, status: "not_running" }, null, 2));
|
|
1408
|
+
return;
|
|
1409
|
+
}
|
|
1410
|
+
killWorkerProcess(worker.pid, "SIGTERM");
|
|
1411
|
+
sleepMs(1500);
|
|
1412
|
+
if (isPidAlive(worker.pid)) {
|
|
1413
|
+
killWorkerProcess(worker.pid, "SIGKILL");
|
|
1414
|
+
console.log(JSON.stringify({ worker: worker.name, pid: worker.pid, status: "sigkill_sent" }, null, 2));
|
|
1415
|
+
return;
|
|
1416
|
+
}
|
|
1417
|
+
console.log(JSON.stringify({ worker: worker.name, pid: worker.pid, status: "stopped" }, null, 2));
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
// src/auto-complete.ts
|
|
1421
|
+
var DEFAULT_POLL_MS = 5e3;
|
|
1422
|
+
var DEFAULT_MAX_TOTAL_MS = 6 * 60 * 60 * 1e3;
|
|
1423
|
+
var DEFAULT_COMPLETE_ATTEMPTS = 3;
|
|
1424
|
+
var DEFAULT_COMPLETE_BACKOFF_MS = 5e3;
|
|
1425
|
+
function readArgs(raw) {
|
|
1426
|
+
return {
|
|
1427
|
+
run: String(raw.run || ""),
|
|
1428
|
+
name: String(raw.name || ""),
|
|
1429
|
+
agentOsId: raw.agentOsId ? String(raw.agentOsId) : void 0,
|
|
1430
|
+
pollMs: Number(raw.pollMs) > 0 ? Math.floor(Number(raw.pollMs)) : void 0,
|
|
1431
|
+
maxTotalMs: Number(raw.maxTotalMs) > 0 ? Math.floor(Number(raw.maxTotalMs)) : void 0,
|
|
1432
|
+
completeAttempts: Number(raw.completeAttempts) > 0 ? Math.floor(Number(raw.completeAttempts)) : void 0,
|
|
1433
|
+
completeBackoffMs: Number(raw.completeBackoffMs) > 0 ? Math.floor(Number(raw.completeBackoffMs)) : void 0,
|
|
1434
|
+
baseUrl: raw.baseUrl ? String(raw.baseUrl) : void 0,
|
|
1435
|
+
secret: raw.secret ? String(raw.secret) : void 0
|
|
1436
|
+
};
|
|
1437
|
+
}
|
|
1438
|
+
async function autoCompleteWorker(raw) {
|
|
1439
|
+
const args = readArgs(raw);
|
|
1440
|
+
const pollMs = args.pollMs ?? DEFAULT_POLL_MS;
|
|
1441
|
+
const maxTotalMs = args.maxTotalMs ?? DEFAULT_MAX_TOTAL_MS;
|
|
1442
|
+
const completeAttempts = args.completeAttempts ?? DEFAULT_COMPLETE_ATTEMPTS;
|
|
1443
|
+
const completeBackoffMs = args.completeBackoffMs ?? DEFAULT_COMPLETE_BACKOFF_MS;
|
|
1444
|
+
const worker = loadWorker(args.run, args.name);
|
|
1445
|
+
if (!worker.agentOsId || !worker.taskId) {
|
|
1446
|
+
return {
|
|
1447
|
+
worker: worker.name,
|
|
1448
|
+
runId: worker.runId,
|
|
1449
|
+
outcome: "missing_link",
|
|
1450
|
+
attempts: 0,
|
|
1451
|
+
reason: "worker has no agentOsId/taskId \u2014 nothing to attribute completion to"
|
|
1452
|
+
};
|
|
1453
|
+
}
|
|
1454
|
+
const startMs = Date.now();
|
|
1455
|
+
while (true) {
|
|
1456
|
+
const status = computeWorkerStatus(worker);
|
|
1457
|
+
if (isFinishedWorkerStatus(status)) break;
|
|
1458
|
+
if (!isPidAlive(worker.pid)) break;
|
|
1459
|
+
if (Date.now() - startMs > maxTotalMs) {
|
|
1460
|
+
return {
|
|
1461
|
+
worker: worker.name,
|
|
1462
|
+
runId: worker.runId,
|
|
1463
|
+
outcome: "timed_out",
|
|
1464
|
+
attempts: 0,
|
|
1465
|
+
reason: `worker did not finish within ${maxTotalMs}ms`
|
|
1466
|
+
};
|
|
1467
|
+
}
|
|
1468
|
+
sleepMs(pollMs);
|
|
1469
|
+
}
|
|
1470
|
+
let lastHttpStatus;
|
|
1471
|
+
let lastReason;
|
|
1472
|
+
for (let attempt = 1; attempt <= completeAttempts; attempt++) {
|
|
1473
|
+
const result = await tryCompleteWorker({
|
|
1474
|
+
run: args.run,
|
|
1475
|
+
name: args.name,
|
|
1476
|
+
...args.agentOsId ? { agentOsId: args.agentOsId } : {},
|
|
1477
|
+
...args.baseUrl ? { baseUrl: args.baseUrl } : {},
|
|
1478
|
+
...args.secret ? { secret: args.secret } : {}
|
|
1479
|
+
});
|
|
1480
|
+
lastHttpStatus = result.httpStatus;
|
|
1481
|
+
if (result.ok) {
|
|
1482
|
+
return {
|
|
1483
|
+
worker: worker.name,
|
|
1484
|
+
runId: worker.runId,
|
|
1485
|
+
outcome: "completed",
|
|
1486
|
+
httpStatus: result.httpStatus,
|
|
1487
|
+
attempts: attempt
|
|
1488
|
+
};
|
|
1489
|
+
}
|
|
1490
|
+
const authRejected = result.httpStatus === 401 || result.httpStatus === 403;
|
|
1491
|
+
if (authRejected) {
|
|
1492
|
+
lastReason = typeof result.reason === "string" ? result.reason : "completion replay refused";
|
|
1493
|
+
return {
|
|
1494
|
+
worker: worker.name,
|
|
1495
|
+
runId: worker.runId,
|
|
1496
|
+
outcome: "blocked",
|
|
1497
|
+
httpStatus: result.httpStatus,
|
|
1498
|
+
attempts: attempt,
|
|
1499
|
+
reason: lastReason
|
|
1500
|
+
};
|
|
1501
|
+
}
|
|
1502
|
+
lastReason = typeof result.reason === "string" ? result.reason : "transient failure";
|
|
1503
|
+
if (attempt < completeAttempts) sleepMs(completeBackoffMs);
|
|
1504
|
+
}
|
|
1505
|
+
return {
|
|
1506
|
+
worker: worker.name,
|
|
1507
|
+
runId: worker.runId,
|
|
1508
|
+
outcome: "blocked",
|
|
1509
|
+
httpStatus: lastHttpStatus,
|
|
1510
|
+
attempts: completeAttempts,
|
|
1511
|
+
reason: lastReason ?? "completion failed after retries"
|
|
1512
|
+
};
|
|
1513
|
+
}
|
|
1514
|
+
async function autoCompleteWorkerCli(raw) {
|
|
1515
|
+
try {
|
|
1516
|
+
const outcome = await autoCompleteWorker(raw);
|
|
1517
|
+
console.log(JSON.stringify(outcome, null, 2));
|
|
1518
|
+
if (outcome.outcome === "missing_link" || outcome.outcome === "timed_out") {
|
|
1519
|
+
process.exitCode = 1;
|
|
1520
|
+
}
|
|
1521
|
+
} catch (error) {
|
|
1522
|
+
console.error(`worker auto-complete failed: ${error.message}`);
|
|
1523
|
+
process.exitCode = 1;
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
function resolveDefaultCliPath() {
|
|
1527
|
+
return path8.join(fileURLToPath(new URL(".", import.meta.url)), "cli.js");
|
|
1528
|
+
}
|
|
1529
|
+
function spawnCompletionSidecar(opts) {
|
|
1530
|
+
const cliPath = opts.cliPath ?? resolveDefaultCliPath();
|
|
1531
|
+
if (!existsSync8(cliPath)) return void 0;
|
|
1532
|
+
const logPath = path8.join(opts.workerDir, "auto-complete.log");
|
|
1533
|
+
let logFd;
|
|
1534
|
+
try {
|
|
1535
|
+
logFd = openSync3(logPath, "a");
|
|
1536
|
+
} catch {
|
|
1537
|
+
logFd = void 0;
|
|
1538
|
+
}
|
|
1539
|
+
const stdio = [
|
|
1540
|
+
"ignore",
|
|
1541
|
+
logFd ?? "ignore",
|
|
1542
|
+
logFd ?? "ignore"
|
|
1543
|
+
];
|
|
1544
|
+
const nodeExecutable = opts.nodeExecutable ?? process.execPath;
|
|
1545
|
+
const args = [
|
|
1546
|
+
cliPath,
|
|
1547
|
+
"worker",
|
|
1548
|
+
"auto-complete",
|
|
1549
|
+
"--run",
|
|
1550
|
+
opts.runId,
|
|
1551
|
+
"--name",
|
|
1552
|
+
opts.workerName
|
|
1553
|
+
];
|
|
1554
|
+
if (opts.agentOsId) args.push("--agent-os-id", opts.agentOsId);
|
|
1555
|
+
if (opts.baseUrl) args.push("--base-url", opts.baseUrl);
|
|
1556
|
+
if (opts.secret) args.push("--secret", opts.secret);
|
|
1557
|
+
try {
|
|
1558
|
+
const child = spawn3(nodeExecutable, args, {
|
|
1559
|
+
detached: true,
|
|
1560
|
+
stdio,
|
|
1561
|
+
env: process.env
|
|
1562
|
+
});
|
|
1563
|
+
if (logFd !== void 0) closeSync3(logFd);
|
|
1564
|
+
child.unref();
|
|
1565
|
+
return { pid: child.pid, logPath, cliPath };
|
|
1566
|
+
} catch {
|
|
1567
|
+
if (logFd !== void 0) {
|
|
1568
|
+
try {
|
|
1569
|
+
closeSync3(logFd);
|
|
1570
|
+
} catch {
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
return void 0;
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1086
1577
|
// src/supervisor.ts
|
|
1087
1578
|
function spawnWorkerProcess(run, opts) {
|
|
1088
1579
|
const rawName = typeof opts.name === "string" ? opts.name.trim() : "";
|
|
@@ -1092,31 +1583,44 @@ function spawnWorkerProcess(run, opts) {
|
|
|
1092
1583
|
const name = safeSlug(rawName);
|
|
1093
1584
|
if (run.workers?.[name]) throw new Error(`worker already exists in run ${run.id}: ${name}`);
|
|
1094
1585
|
if (!opts.task) throw new Error(`missing task text for worker ${name}`);
|
|
1586
|
+
const provider = resolveWorkerProvider(opts.provider);
|
|
1587
|
+
let launchModel = opts.model;
|
|
1588
|
+
if (provider.preflightModel) {
|
|
1589
|
+
const preflight = provider.preflightModel(opts.model);
|
|
1590
|
+
if (!preflight.ok) {
|
|
1591
|
+
throw new Error(
|
|
1592
|
+
`model preflight failed for provider "${provider.name}": ${preflight.note ?? "invalid model/provider combination"}`
|
|
1593
|
+
);
|
|
1594
|
+
}
|
|
1595
|
+
if (preflight.normalized) {
|
|
1596
|
+
console.error(`[supervisor] ${name}: ${preflight.note}`);
|
|
1597
|
+
}
|
|
1598
|
+
launchModel = preflight.model;
|
|
1599
|
+
}
|
|
1095
1600
|
const { worktreesDir } = getPaths();
|
|
1096
|
-
const workerDir =
|
|
1601
|
+
const workerDir = path9.join(runDirectory(run.id), "workers", name);
|
|
1097
1602
|
mkdirSync3(workerDir, { recursive: true });
|
|
1098
|
-
const worktreePath =
|
|
1603
|
+
const worktreePath = path9.join(worktreesDir, run.id, name);
|
|
1099
1604
|
const branch = opts.branch || `agent/${run.id}/${name}`;
|
|
1100
|
-
if (
|
|
1605
|
+
if (existsSync9(worktreePath)) throw new Error(`worktree path already exists: ${worktreePath}`);
|
|
1101
1606
|
git(run.repo, ["fetch", "origin", "--prune"], { allowFailure: true });
|
|
1102
1607
|
git(run.repo, ["worktree", "add", "-b", branch, worktreePath, run.baseCommit], { throwError: true });
|
|
1103
|
-
const stdoutPath =
|
|
1104
|
-
const stderrPath =
|
|
1105
|
-
const heartbeatPath =
|
|
1608
|
+
const stdoutPath = path9.join(workerDir, "stdout.jsonl");
|
|
1609
|
+
const stderrPath = path9.join(workerDir, "stderr.log");
|
|
1610
|
+
const heartbeatPath = path9.join(workerDir, "heartbeat.jsonl");
|
|
1106
1611
|
const prompt = buildPrompt({
|
|
1107
1612
|
task: opts.task,
|
|
1108
1613
|
ownedPaths: opts.ownedPaths || [],
|
|
1109
1614
|
worktreePath,
|
|
1110
1615
|
heartbeatPath
|
|
1111
1616
|
});
|
|
1112
|
-
const provider = resolveWorkerProvider(opts.provider);
|
|
1113
1617
|
let started;
|
|
1114
1618
|
try {
|
|
1115
1619
|
started = provider.start({
|
|
1116
1620
|
name,
|
|
1117
1621
|
task: opts.task,
|
|
1118
1622
|
ownedPaths: opts.ownedPaths,
|
|
1119
|
-
model:
|
|
1623
|
+
model: launchModel,
|
|
1120
1624
|
branch,
|
|
1121
1625
|
worktreePath,
|
|
1122
1626
|
workerDir,
|
|
@@ -1130,7 +1634,7 @@ function spawnWorkerProcess(run, opts) {
|
|
|
1130
1634
|
git(run.repo, ["branch", "-D", branch], { allowFailure: true });
|
|
1131
1635
|
throw error;
|
|
1132
1636
|
}
|
|
1133
|
-
const model = started.model ||
|
|
1637
|
+
const model = started.model || launchModel || provider.defaultModel || "claude-opus-4-7";
|
|
1134
1638
|
const worker = {
|
|
1135
1639
|
name,
|
|
1136
1640
|
runId: run.id,
|
|
@@ -1152,9 +1656,20 @@ function spawnWorkerProcess(run, opts) {
|
|
|
1152
1656
|
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1153
1657
|
};
|
|
1154
1658
|
saveWorker(run.id, worker);
|
|
1155
|
-
run.workers = { ...run.workers || {}, [name]: { workerDir, statusPath:
|
|
1659
|
+
run.workers = { ...run.workers || {}, [name]: { workerDir, statusPath: path9.join(workerDir, "worker.json") } };
|
|
1156
1660
|
run.status = "running";
|
|
1157
1661
|
saveRun(run);
|
|
1662
|
+
if (worker.agentOsId && worker.taskId) {
|
|
1663
|
+
try {
|
|
1664
|
+
spawnCompletionSidecar({
|
|
1665
|
+
runId: run.id,
|
|
1666
|
+
workerName: name,
|
|
1667
|
+
workerDir,
|
|
1668
|
+
agentOsId: worker.agentOsId
|
|
1669
|
+
});
|
|
1670
|
+
} catch {
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1158
1673
|
return worker;
|
|
1159
1674
|
}
|
|
1160
1675
|
function startWorker(args) {
|
|
@@ -1364,7 +1879,7 @@ function redactHarness(text, secret) {
|
|
|
1364
1879
|
}
|
|
1365
1880
|
|
|
1366
1881
|
// src/validate.ts
|
|
1367
|
-
import
|
|
1882
|
+
import path10 from "node:path";
|
|
1368
1883
|
var RUN_ID_RE = /^[a-z0-9][a-z0-9._-]{0,127}$/i;
|
|
1369
1884
|
var WORKER_NAME_RE = /^[a-z0-9][a-z0-9._-]{0,63}$/i;
|
|
1370
1885
|
function validateRunId(runId) {
|
|
@@ -1378,15 +1893,15 @@ function validateWorkerName(name) {
|
|
|
1378
1893
|
return trimmed;
|
|
1379
1894
|
}
|
|
1380
1895
|
function validateRepo(repo) {
|
|
1381
|
-
const resolved =
|
|
1896
|
+
const resolved = path10.resolve(repo);
|
|
1382
1897
|
if (resolved.includes("..")) throw new Error("repo path must not contain .. segments");
|
|
1383
1898
|
return resolved;
|
|
1384
1899
|
}
|
|
1385
1900
|
function validateOwnedPaths(repoRoot, ownedPaths) {
|
|
1386
1901
|
return ownedPaths.map((owned) => {
|
|
1387
|
-
const resolved =
|
|
1388
|
-
const rel =
|
|
1389
|
-
if (rel.startsWith("..") ||
|
|
1902
|
+
const resolved = path10.resolve(repoRoot, owned);
|
|
1903
|
+
const rel = path10.relative(repoRoot, resolved);
|
|
1904
|
+
if (rel.startsWith("..") || path10.isAbsolute(rel)) {
|
|
1390
1905
|
throw new Error(`owned path escapes repo: ${owned}`);
|
|
1391
1906
|
}
|
|
1392
1907
|
return resolved;
|
|
@@ -1398,14 +1913,14 @@ function validateTailLines(lines) {
|
|
|
1398
1913
|
}
|
|
1399
1914
|
|
|
1400
1915
|
// src/worktree.ts
|
|
1401
|
-
import { existsSync as
|
|
1402
|
-
import
|
|
1916
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync4 } from "node:fs";
|
|
1917
|
+
import path11 from "node:path";
|
|
1403
1918
|
function createRun(args) {
|
|
1404
1919
|
const repo = validateRepo(required(String(args.repo || ""), "--repo"));
|
|
1405
1920
|
ensureGitRepo(repo);
|
|
1406
1921
|
const id = args.id ? validateRunId(String(args.id)) : timestampSlug(String(args.name || "run"));
|
|
1407
1922
|
const dir = runDirectory(id);
|
|
1408
|
-
if (
|
|
1923
|
+
if (existsSync10(dir)) failExists(`run already exists: ${id}`);
|
|
1409
1924
|
mkdirSync4(dir, { recursive: true });
|
|
1410
1925
|
const base = String(args.base || "origin/main");
|
|
1411
1926
|
const baseCommit = git(repo, ["rev-parse", base]).trim();
|
|
@@ -1419,12 +1934,12 @@ function createRun(args) {
|
|
|
1419
1934
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1420
1935
|
workers: {}
|
|
1421
1936
|
};
|
|
1422
|
-
writeJson(
|
|
1937
|
+
writeJson(path11.join(dir, "run.json"), run);
|
|
1423
1938
|
console.log(JSON.stringify({ runId: id, runDir: dir, repo, base, baseCommit }, null, 2));
|
|
1424
1939
|
}
|
|
1425
1940
|
function listRuns() {
|
|
1426
1941
|
const { runsDir } = getPaths();
|
|
1427
|
-
const rows = listRunIds(runsDir).map((id) => readJson(
|
|
1942
|
+
const rows = listRunIds(runsDir).map((id) => readJson(path11.join(runDirectory(id), "run.json"), void 0)).filter(Boolean).map((run) => ({
|
|
1428
1943
|
id: run.id,
|
|
1429
1944
|
name: run.name,
|
|
1430
1945
|
status: run.status,
|
|
@@ -1439,7 +1954,7 @@ function failExists(message) {
|
|
|
1439
1954
|
}
|
|
1440
1955
|
|
|
1441
1956
|
// src/sweep.ts
|
|
1442
|
-
import
|
|
1957
|
+
import path12 from "node:path";
|
|
1443
1958
|
async function sweepRun(args) {
|
|
1444
1959
|
const pipeline = args.pipeline === true || args.pipeline === "true";
|
|
1445
1960
|
try {
|
|
@@ -1451,7 +1966,7 @@ async function sweepRun(args) {
|
|
|
1451
1966
|
const releasedLocalOrphans = [];
|
|
1452
1967
|
for (const name of Object.keys(run.workers || {})) {
|
|
1453
1968
|
const worker = readJson(
|
|
1454
|
-
|
|
1969
|
+
path12.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
1455
1970
|
void 0
|
|
1456
1971
|
);
|
|
1457
1972
|
if (!worker || !worker.dispatched || !worker.taskId) continue;
|
|
@@ -1493,215 +2008,15 @@ async function sweepRun(args) {
|
|
|
1493
2008
|
}
|
|
1494
2009
|
}
|
|
1495
2010
|
|
|
1496
|
-
// src/worker-ops.ts
|
|
1497
|
-
import path11 from "node:path";
|
|
1498
|
-
async function postCompletion(url, secret, body) {
|
|
1499
|
-
const res = await fetch(url, {
|
|
1500
|
-
method: "POST",
|
|
1501
|
-
headers: buildHarnessCallbackHeaders(secret),
|
|
1502
|
-
body: JSON.stringify(body)
|
|
1503
|
-
});
|
|
1504
|
-
let parsed = null;
|
|
1505
|
-
try {
|
|
1506
|
-
parsed = await res.json();
|
|
1507
|
-
} catch {
|
|
1508
|
-
parsed = null;
|
|
1509
|
-
}
|
|
1510
|
-
return { ok: res.ok, status: res.status, parsed };
|
|
1511
|
-
}
|
|
1512
|
-
function completionErrorText(parsed) {
|
|
1513
|
-
if (parsed && typeof parsed === "object") {
|
|
1514
|
-
const err = parsed.error;
|
|
1515
|
-
if (typeof err === "string" && err.trim()) return err.trim();
|
|
1516
|
-
}
|
|
1517
|
-
return void 0;
|
|
1518
|
-
}
|
|
1519
|
-
function persistCompletionBlocker(worker, reason) {
|
|
1520
|
-
const current = worker.completionBlocker;
|
|
1521
|
-
if ((current ?? void 0) === (reason ?? void 0)) return;
|
|
1522
|
-
if (reason) worker.completionBlocker = reason;
|
|
1523
|
-
else delete worker.completionBlocker;
|
|
1524
|
-
saveWorker(worker.runId, worker);
|
|
1525
|
-
}
|
|
1526
|
-
async function tryCompleteWorker(args) {
|
|
1527
|
-
const worker = loadWorker(String(args.run), String(args.name));
|
|
1528
|
-
const status = computeWorkerStatus(worker);
|
|
1529
|
-
const agentOsId = (args.agentOsId ? String(args.agentOsId) : worker.agentOsId) || "";
|
|
1530
|
-
const taskId = (args.taskId ? String(args.taskId) : worker.taskId) || null;
|
|
1531
|
-
if (!agentOsId) {
|
|
1532
|
-
return { ok: false, reason: "missing agentOsId" };
|
|
1533
|
-
}
|
|
1534
|
-
if (!isFinishedWorkerStatus(status)) {
|
|
1535
|
-
return { ok: true, skipped: true, reason: "worker-not-finished" };
|
|
1536
|
-
}
|
|
1537
|
-
const base = resolveBaseUrl(args.baseUrl ? String(args.baseUrl) : void 0);
|
|
1538
|
-
const explicitSecret = args.secret ? String(args.secret) : void 0;
|
|
1539
|
-
let secret = await resolveCallbackSecretWithMint(explicitSecret, agentOsId, { baseUrl: base });
|
|
1540
|
-
const url = `${base}/api/agent-os/by-id/${encodeURIComponent(agentOsId)}/harness/completion`;
|
|
1541
|
-
const body = {
|
|
1542
|
-
source: "openclaw-harness",
|
|
1543
|
-
agentOsId,
|
|
1544
|
-
runId: worker.runId,
|
|
1545
|
-
workerName: worker.name,
|
|
1546
|
-
taskId,
|
|
1547
|
-
startedAt: worker.startedAt,
|
|
1548
|
-
finishedAt: status.lastActivityAt || (/* @__PURE__ */ new Date()).toISOString(),
|
|
1549
|
-
status
|
|
1550
|
-
};
|
|
1551
|
-
let result = await postCompletion(url, secret, body);
|
|
1552
|
-
if ((result.status === 401 || result.status === 403) && !explicitSecret) {
|
|
1553
|
-
const refreshed = await refreshRunnerToken(agentOsId, { baseUrl: base });
|
|
1554
|
-
if (refreshed && refreshed !== secret) {
|
|
1555
|
-
secret = refreshed;
|
|
1556
|
-
result = await postCompletion(url, secret, body);
|
|
1557
|
-
}
|
|
1558
|
-
}
|
|
1559
|
-
if (result.ok) {
|
|
1560
|
-
persistCompletionBlocker(worker, void 0);
|
|
1561
|
-
return { ok: true, httpStatus: result.status, response: result.parsed };
|
|
1562
|
-
}
|
|
1563
|
-
const authRejected = result.status === 401 || result.status === 403;
|
|
1564
|
-
const detail = completionErrorText(result.parsed) ?? (authRejected ? "runner token unauthorized" : "non-2xx response");
|
|
1565
|
-
const reason = authRejected ? `completion replay rejected (${result.status}): ${detail}` : `completion replay failed (${result.status}): ${detail}`;
|
|
1566
|
-
persistCompletionBlocker(worker, reason);
|
|
1567
|
-
return { ok: false, httpStatus: result.status, response: result.parsed, completionBlocked: true };
|
|
1568
|
-
}
|
|
1569
|
-
async function completeWorker(args) {
|
|
1570
|
-
try {
|
|
1571
|
-
const worker = loadWorker(String(args.run), String(args.name));
|
|
1572
|
-
const status = computeWorkerStatus(worker);
|
|
1573
|
-
const agentOsId = (args.agentOsId ? String(args.agentOsId) : worker.agentOsId) || "";
|
|
1574
|
-
const taskId = (args.taskId ? String(args.taskId) : worker.taskId) || null;
|
|
1575
|
-
if (!agentOsId) {
|
|
1576
|
-
console.error("worker complete requires --agent-os-id (or an agentOsId persisted at worker start)");
|
|
1577
|
-
process.exit(1);
|
|
1578
|
-
}
|
|
1579
|
-
if (!isFinishedWorkerStatus(status)) {
|
|
1580
|
-
console.log(
|
|
1581
|
-
JSON.stringify(
|
|
1582
|
-
{
|
|
1583
|
-
worker: worker.name,
|
|
1584
|
-
runId: worker.runId,
|
|
1585
|
-
status: "skipped",
|
|
1586
|
-
reason: "worker-not-finished",
|
|
1587
|
-
workerStatus: status.status,
|
|
1588
|
-
alive: status.alive
|
|
1589
|
-
},
|
|
1590
|
-
null,
|
|
1591
|
-
2
|
|
1592
|
-
)
|
|
1593
|
-
);
|
|
1594
|
-
return;
|
|
1595
|
-
}
|
|
1596
|
-
const result = await tryCompleteWorker(args);
|
|
1597
|
-
console.log(
|
|
1598
|
-
JSON.stringify(
|
|
1599
|
-
{
|
|
1600
|
-
worker: worker.name,
|
|
1601
|
-
runId: worker.runId,
|
|
1602
|
-
agentOsId,
|
|
1603
|
-
taskId,
|
|
1604
|
-
httpStatus: result.httpStatus,
|
|
1605
|
-
response: result.response
|
|
1606
|
-
},
|
|
1607
|
-
null,
|
|
1608
|
-
2
|
|
1609
|
-
)
|
|
1610
|
-
);
|
|
1611
|
-
if (!result.ok) process.exit(1);
|
|
1612
|
-
} catch (error) {
|
|
1613
|
-
console.error(`worker complete failed: ${error.message}`);
|
|
1614
|
-
process.exit(1);
|
|
1615
|
-
}
|
|
1616
|
-
}
|
|
1617
|
-
function workerStatus(args) {
|
|
1618
|
-
const worker = loadWorker(String(args.run), String(args.name));
|
|
1619
|
-
const status = computeWorkerStatus(worker);
|
|
1620
|
-
writeJson(path11.join(worker.workerDir, "last-status.json"), status);
|
|
1621
|
-
console.log(JSON.stringify(status, null, 2));
|
|
1622
|
-
}
|
|
1623
|
-
function runStatus(args) {
|
|
1624
|
-
const run = loadRun(String(args.run));
|
|
1625
|
-
const names = Object.keys(run.workers || {});
|
|
1626
|
-
const workers = names.map((name) => {
|
|
1627
|
-
const worker = readJson(
|
|
1628
|
-
path11.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
1629
|
-
void 0
|
|
1630
|
-
);
|
|
1631
|
-
if (!worker) {
|
|
1632
|
-
return { worker: name, status: "missing", attention: "needs_attention", attentionReason: "worker.json not found" };
|
|
1633
|
-
}
|
|
1634
|
-
const status = computeWorkerStatus(worker, { base: run.base });
|
|
1635
|
-
const rawBlocker = worker.completionBlocker;
|
|
1636
|
-
const completionBlocker = typeof rawBlocker === "string" && rawBlocker ? rawBlocker : void 0;
|
|
1637
|
-
return {
|
|
1638
|
-
worker: status.worker,
|
|
1639
|
-
status: completionBlocker ? "blocked" : status.status,
|
|
1640
|
-
attention: completionBlocker ? "blocked" : status.attention.state,
|
|
1641
|
-
attentionReason: completionBlocker ?? status.attention.reason,
|
|
1642
|
-
pid: status.pid,
|
|
1643
|
-
alive: status.alive,
|
|
1644
|
-
currentTool: status.currentTool,
|
|
1645
|
-
lastActivityAt: status.lastActivityAt,
|
|
1646
|
-
lastHeartbeatPhase: status.lastHeartbeatPhase,
|
|
1647
|
-
lastHeartbeatSummary: status.lastHeartbeatSummary,
|
|
1648
|
-
heartbeatBlocker: status.heartbeatBlocker,
|
|
1649
|
-
changedFileCount: status.changedFiles.length,
|
|
1650
|
-
branch: status.branch,
|
|
1651
|
-
ancestry: status.gitAncestry.relation,
|
|
1652
|
-
ancestryChecked: status.gitAncestry.checked
|
|
1653
|
-
};
|
|
1654
|
-
});
|
|
1655
|
-
const board = {
|
|
1656
|
-
runId: run.id,
|
|
1657
|
-
name: run.name,
|
|
1658
|
-
status: deriveRunStatus(run.status, workers),
|
|
1659
|
-
repo: run.repo,
|
|
1660
|
-
workerCount: workers.length,
|
|
1661
|
-
needsAttention: workers.filter((w) => w.attention && w.attention !== "ok" && w.attention !== "done").map((w) => w.worker),
|
|
1662
|
-
workers
|
|
1663
|
-
};
|
|
1664
|
-
writeJson(path11.join(runDirectory(run.id), "last-board.json"), board);
|
|
1665
|
-
console.log(JSON.stringify(board, null, 2));
|
|
1666
|
-
}
|
|
1667
|
-
function tailWorker(args) {
|
|
1668
|
-
const worker = loadWorker(String(args.run), String(args.name));
|
|
1669
|
-
const raw = tailFile(worker.stdoutPath, Number(args.lines || 40));
|
|
1670
|
-
if (args.raw === true || args.raw === "true") {
|
|
1671
|
-
process.stdout.write(raw);
|
|
1672
|
-
return;
|
|
1673
|
-
}
|
|
1674
|
-
for (const line of raw.split("\n").filter(Boolean)) {
|
|
1675
|
-
const event = safeJson(line);
|
|
1676
|
-
const summary = event ? summarizeEvent(event) : line;
|
|
1677
|
-
if (summary) console.log(summary);
|
|
1678
|
-
}
|
|
1679
|
-
}
|
|
1680
|
-
function stopWorker(args) {
|
|
1681
|
-
const worker = loadWorker(String(args.run), String(args.name));
|
|
1682
|
-
if (!isPidAlive(worker.pid)) {
|
|
1683
|
-
console.log(JSON.stringify({ worker: worker.name, pid: worker.pid, status: "not_running" }, null, 2));
|
|
1684
|
-
return;
|
|
1685
|
-
}
|
|
1686
|
-
killWorkerProcess(worker.pid, "SIGTERM");
|
|
1687
|
-
sleepMs(1500);
|
|
1688
|
-
if (isPidAlive(worker.pid)) {
|
|
1689
|
-
killWorkerProcess(worker.pid, "SIGKILL");
|
|
1690
|
-
console.log(JSON.stringify({ worker: worker.name, pid: worker.pid, status: "sigkill_sent" }, null, 2));
|
|
1691
|
-
return;
|
|
1692
|
-
}
|
|
1693
|
-
console.log(JSON.stringify({ worker: worker.name, pid: worker.pid, status: "stopped" }, null, 2));
|
|
1694
|
-
}
|
|
1695
|
-
|
|
1696
2011
|
// src/cli.ts
|
|
1697
2012
|
import { mkdirSync as mkdirSync5, realpathSync } from "node:fs";
|
|
1698
|
-
import { fileURLToPath } from "node:url";
|
|
2013
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
1699
2014
|
|
|
1700
2015
|
// src/pipeline-tick.ts
|
|
1701
|
-
import
|
|
2016
|
+
import path15 from "node:path";
|
|
1702
2017
|
|
|
1703
2018
|
// src/finalize.ts
|
|
1704
|
-
import
|
|
2019
|
+
import path13 from "node:path";
|
|
1705
2020
|
var ACTIVE_RUN_STATUSES = /* @__PURE__ */ new Set(["running", "dispatching", "pending", "queued"]);
|
|
1706
2021
|
function terminalStatusFor(run) {
|
|
1707
2022
|
const names = Object.keys(run.workers || {});
|
|
@@ -1711,7 +2026,7 @@ function terminalStatusFor(run) {
|
|
|
1711
2026
|
let anyCompletionBlocked = false;
|
|
1712
2027
|
for (const name of names) {
|
|
1713
2028
|
const worker = readJson(
|
|
1714
|
-
|
|
2029
|
+
path13.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
1715
2030
|
void 0
|
|
1716
2031
|
);
|
|
1717
2032
|
if (!worker) continue;
|
|
@@ -1744,7 +2059,7 @@ function finalizeStaleRuns() {
|
|
|
1744
2059
|
}
|
|
1745
2060
|
|
|
1746
2061
|
// src/plan-progress-daemon-sync.ts
|
|
1747
|
-
import
|
|
2062
|
+
import path14 from "node:path";
|
|
1748
2063
|
|
|
1749
2064
|
// src/plan-progress-sync.ts
|
|
1750
2065
|
async function syncPlanProgress(args) {
|
|
@@ -1768,7 +2083,7 @@ async function syncActiveWorkerPlanProgress(runId, args) {
|
|
|
1768
2083
|
const outcomes = [];
|
|
1769
2084
|
for (const name of Object.keys(run.workers || {})) {
|
|
1770
2085
|
const worker = readJson(
|
|
1771
|
-
|
|
2086
|
+
path14.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
1772
2087
|
void 0
|
|
1773
2088
|
);
|
|
1774
2089
|
if (!worker?.dispatched || !worker.taskId) continue;
|
|
@@ -1822,7 +2137,7 @@ async function completeFinishedWorkers(runId, args) {
|
|
|
1822
2137
|
const outcomes = [];
|
|
1823
2138
|
for (const name of Object.keys(run.workers || {})) {
|
|
1824
2139
|
const worker = readJson(
|
|
1825
|
-
|
|
2140
|
+
path15.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
1826
2141
|
void 0
|
|
1827
2142
|
);
|
|
1828
2143
|
if (!worker?.taskId) continue;
|
|
@@ -2065,6 +2380,7 @@ function usage(code = 0) {
|
|
|
2065
2380
|
" kynver worker tail --run RUN_ID --name worker [--lines 40] [--raw]",
|
|
2066
2381
|
" kynver worker stop --run RUN_ID --name worker",
|
|
2067
2382
|
" kynver worker complete --run RUN_ID --name worker [--agent-os-id AOS_ID] [--task-id TASK_ID] [--base-url URL] [--secret SECRET]",
|
|
2383
|
+
" kynver worker auto-complete --run RUN_ID --name worker [--agent-os-id AOS_ID] [--poll-ms 5000] [--max-total-ms 21600000] [--complete-attempts 3] [--complete-backoff-ms 5000] [--base-url URL] [--secret SECRET]",
|
|
2068
2384
|
" kynver plan progress --plan PLAN_ID --row ROW_KEY --role ROLE --status STATUS [--task TASK_ID] [--note NOTE] [--evidence type:value] [--agent-os-id AOS_ID]",
|
|
2069
2385
|
" kynver plan verify --plan PLAN_ID [--worktree PATH] [--task TASK_ID] [--human-override]"
|
|
2070
2386
|
].join("\n")
|
|
@@ -2103,9 +2419,10 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
2103
2419
|
if (scope === "worker" && action === "tail") return tailWorker(args);
|
|
2104
2420
|
if (scope === "worker" && action === "stop") return stopWorker(args);
|
|
2105
2421
|
if (scope === "worker" && action === "complete") return void await completeWorker(args);
|
|
2422
|
+
if (scope === "worker" && action === "auto-complete") return void await autoCompleteWorkerCli(args);
|
|
2106
2423
|
unknownCommand(scope, action);
|
|
2107
2424
|
}
|
|
2108
|
-
var isCliEntry = process.argv[1] && realpathSync.native(process.argv[1]) === realpathSync.native(
|
|
2425
|
+
var isCliEntry = process.argv[1] && realpathSync.native(process.argv[1]) === realpathSync.native(fileURLToPath2(import.meta.url));
|
|
2109
2426
|
if (isCliEntry) {
|
|
2110
2427
|
void main().catch((error) => {
|
|
2111
2428
|
console.error(error);
|
|
@@ -2114,6 +2431,8 @@ if (isCliEntry) {
|
|
|
2114
2431
|
}
|
|
2115
2432
|
export {
|
|
2116
2433
|
DEFAULT_DISPATCH_LEASE_MS,
|
|
2434
|
+
autoCompleteWorker,
|
|
2435
|
+
autoCompleteWorkerCli,
|
|
2117
2436
|
buildDispatchTaskText,
|
|
2118
2437
|
buildPrompt,
|
|
2119
2438
|
completeWorker,
|
|
@@ -2140,6 +2459,7 @@ export {
|
|
|
2140
2459
|
runDaemon,
|
|
2141
2460
|
runStatus,
|
|
2142
2461
|
saveUserConfig,
|
|
2462
|
+
spawnCompletionSidecar,
|
|
2143
2463
|
spawnWorkerProcess,
|
|
2144
2464
|
startWorker,
|
|
2145
2465
|
stopWorker,
|