@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/cli.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { mkdirSync as mkdirSync5, realpathSync } from "node:fs";
|
|
5
|
-
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
6
6
|
|
|
7
7
|
// src/config.ts
|
|
8
8
|
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
@@ -392,12 +392,12 @@ var DEFAULT_CRITICAL_FREE_BYTES = 15 * 1024 * 1024 * 1024;
|
|
|
392
392
|
var DEFAULT_MAX_USED_PERCENT = 80;
|
|
393
393
|
var DEFAULT_HARD_MAX_USED_PERCENT = 90;
|
|
394
394
|
function observeRunnerDiskGate(input = {}) {
|
|
395
|
-
const
|
|
395
|
+
const path16 = input.diskPath?.trim() || "/";
|
|
396
396
|
const warnBelowBytes = input.diskFreeWarnBytes ?? DEFAULT_WARN_FREE_BYTES;
|
|
397
397
|
const criticalBelowBytes = input.diskFreeCriticalBytes ?? DEFAULT_CRITICAL_FREE_BYTES;
|
|
398
398
|
const maxUsedPercent = input.diskMaxUsedPercent ?? DEFAULT_MAX_USED_PERCENT;
|
|
399
399
|
const hardMaxUsedPercent = input.diskHardMaxUsedPercent ?? DEFAULT_HARD_MAX_USED_PERCENT;
|
|
400
|
-
const stats = statfsSync(
|
|
400
|
+
const stats = statfsSync(path16);
|
|
401
401
|
const freeBytes = Number(stats.bavail) * Number(stats.bsize);
|
|
402
402
|
const totalBytes = Number(stats.blocks) * Number(stats.bsize);
|
|
403
403
|
const usedPercent = totalBytes > 0 ? (totalBytes - freeBytes) / totalBytes * 100 : 100;
|
|
@@ -417,7 +417,7 @@ function observeRunnerDiskGate(input = {}) {
|
|
|
417
417
|
}
|
|
418
418
|
return {
|
|
419
419
|
ok,
|
|
420
|
-
path:
|
|
420
|
+
path: path16,
|
|
421
421
|
freeBytes,
|
|
422
422
|
totalBytes,
|
|
423
423
|
usedPercent,
|
|
@@ -604,6 +604,52 @@ function summarizeEvent(event) {
|
|
|
604
604
|
return void 0;
|
|
605
605
|
}
|
|
606
606
|
|
|
607
|
+
// src/exit-classify.ts
|
|
608
|
+
var FAILURE_PATTERNS = [
|
|
609
|
+
{
|
|
610
|
+
test: /\b(?:invalid|unknown|unsupported|unrecognized)\b[^.\n]*\bmodel\b/i,
|
|
611
|
+
label: "provider rejected the requested model"
|
|
612
|
+
},
|
|
613
|
+
{
|
|
614
|
+
test: /\bmodel\b[^.\n]*\b(?:not\s+(?:found|supported|available|recognized|valid)|is\s+not\s+valid|does\s+not\s+exist)/i,
|
|
615
|
+
label: "provider rejected the requested model"
|
|
616
|
+
},
|
|
617
|
+
{
|
|
618
|
+
test: /\b(?:did you mean|available models|choose (?:a|one of)|supported models)\b/i,
|
|
619
|
+
label: "provider rejected the requested model"
|
|
620
|
+
},
|
|
621
|
+
{
|
|
622
|
+
test: /model preflight failed/i,
|
|
623
|
+
label: "model/provider preflight failed"
|
|
624
|
+
},
|
|
625
|
+
{
|
|
626
|
+
test: /\b(?:command not found|ENOENT|is the .*CLI on PATH|executable not found|no such file or directory)\b/i,
|
|
627
|
+
label: "provider CLI is missing or not on PATH"
|
|
628
|
+
},
|
|
629
|
+
{
|
|
630
|
+
test: /\bfailed to spawn\b/i,
|
|
631
|
+
label: "provider failed to spawn the worker process"
|
|
632
|
+
},
|
|
633
|
+
{
|
|
634
|
+
test: /\b(?:not logged in|unauthorized|authentication (?:failed|required)|invalid api key|missing api key|401)\b/i,
|
|
635
|
+
label: "provider authentication failed"
|
|
636
|
+
}
|
|
637
|
+
];
|
|
638
|
+
function tidy(errorText, max = 240) {
|
|
639
|
+
const oneLine2 = errorText.replace(/\s+/g, " ").trim();
|
|
640
|
+
return oneLine2.length > max ? `${oneLine2.slice(0, max - 1)}\u2026` : oneLine2;
|
|
641
|
+
}
|
|
642
|
+
function classifyExitFailure(errorText) {
|
|
643
|
+
const text = (errorText ?? "").trim();
|
|
644
|
+
if (!text) return null;
|
|
645
|
+
for (const pattern of FAILURE_PATTERNS) {
|
|
646
|
+
if (pattern.test.test(text)) {
|
|
647
|
+
return { blocked: true, reason: `${pattern.label}: ${tidy(text)}` };
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
return null;
|
|
651
|
+
}
|
|
652
|
+
|
|
607
653
|
// src/git.ts
|
|
608
654
|
import { spawnSync } from "node:child_process";
|
|
609
655
|
function git(cwd, args, options = {}) {
|
|
@@ -719,7 +765,15 @@ var STALE_MS = 6e5;
|
|
|
719
765
|
function computeAttention(input) {
|
|
720
766
|
const now = Date.now();
|
|
721
767
|
if (input.finalResult) return { state: "done", reason: "final result recorded" };
|
|
722
|
-
if (!input.alive)
|
|
768
|
+
if (!input.alive) {
|
|
769
|
+
const classified = classifyExitFailure(input.error);
|
|
770
|
+
if (classified) return { state: "blocked", reason: classified.reason };
|
|
771
|
+
const tail = input.error?.trim();
|
|
772
|
+
return {
|
|
773
|
+
state: "needs_attention",
|
|
774
|
+
reason: tail ? `process exited without a final result: ${tail}` : "process exited without a final result"
|
|
775
|
+
};
|
|
776
|
+
}
|
|
723
777
|
if (input.heartbeatBlocker) {
|
|
724
778
|
return { state: "blocked", reason: `worker heartbeat reported blocker: ${input.heartbeatBlocker}` };
|
|
725
779
|
}
|
|
@@ -749,6 +803,7 @@ function computeWorkerStatus(worker, options = {}) {
|
|
|
749
803
|
fileMtime(worker.stderrPath),
|
|
750
804
|
fileMtime(worker.heartbeatPath)
|
|
751
805
|
]);
|
|
806
|
+
const error = parsed.error || (!alive && !parsed.finalResult ? tailFile(worker.stderrPath, 10).trim() || void 0 : void 0);
|
|
752
807
|
const attention = computeAttention({
|
|
753
808
|
alive,
|
|
754
809
|
finalResult: parsed.finalResult,
|
|
@@ -757,7 +812,8 @@ function computeWorkerStatus(worker, options = {}) {
|
|
|
757
812
|
heartbeatBytes,
|
|
758
813
|
lastActivityAt,
|
|
759
814
|
heartbeatBlocker: heartbeat.heartbeatBlocker,
|
|
760
|
-
startedAt: worker.startedAt
|
|
815
|
+
startedAt: worker.startedAt,
|
|
816
|
+
error
|
|
761
817
|
});
|
|
762
818
|
return {
|
|
763
819
|
runId: worker.runId,
|
|
@@ -782,7 +838,7 @@ function computeWorkerStatus(worker, options = {}) {
|
|
|
782
838
|
lastHeartbeatSummary: heartbeat.lastHeartbeatSummary,
|
|
783
839
|
heartbeatBlocker: heartbeat.heartbeatBlocker,
|
|
784
840
|
finalResult: parsed.finalResult,
|
|
785
|
-
error
|
|
841
|
+
error,
|
|
786
842
|
changedFiles,
|
|
787
843
|
gitAncestry
|
|
788
844
|
};
|
|
@@ -907,8 +963,8 @@ function observeRunnerResourceGate(input) {
|
|
|
907
963
|
}
|
|
908
964
|
|
|
909
965
|
// src/supervisor.ts
|
|
910
|
-
import { existsSync as
|
|
911
|
-
import
|
|
966
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync3 } from "node:fs";
|
|
967
|
+
import path9 from "node:path";
|
|
912
968
|
|
|
913
969
|
// src/prompt.ts
|
|
914
970
|
function buildPrompt(input) {
|
|
@@ -942,10 +998,74 @@ function buildPrompt(input) {
|
|
|
942
998
|
// src/providers/claude.ts
|
|
943
999
|
import { closeSync, openSync } from "node:fs";
|
|
944
1000
|
import { spawn } from "node:child_process";
|
|
1001
|
+
|
|
1002
|
+
// src/providers/model-preflight.ts
|
|
1003
|
+
var REASONING_SUFFIX_RE = /-(?:thinking(?:-(?:high|medium|low|minimal|max|none))?|high|medium|low|minimal)$/i;
|
|
1004
|
+
function stripReasoningSuffix(model) {
|
|
1005
|
+
return model.replace(REASONING_SUFFIX_RE, "");
|
|
1006
|
+
}
|
|
1007
|
+
var FOREIGN_MODEL_RE = /^(?:gpt-|gpt5|o1|o3|o4|gemini-|grok-|composer|deepseek|llama|mistral|qwen|command-)/i;
|
|
1008
|
+
function looksLikeClaudeModel(model) {
|
|
1009
|
+
return /^claude[-_]/i.test(model) || /^(?:opus|sonnet|haiku)\b/i.test(model);
|
|
1010
|
+
}
|
|
1011
|
+
function preflightClaudeModel(model, defaultModel) {
|
|
1012
|
+
const requested = (model ?? "").trim();
|
|
1013
|
+
if (!requested) {
|
|
1014
|
+
return { ok: true, model: defaultModel, normalized: false };
|
|
1015
|
+
}
|
|
1016
|
+
const stripped = stripReasoningSuffix(requested).trim();
|
|
1017
|
+
const launch = stripped || defaultModel;
|
|
1018
|
+
if (FOREIGN_MODEL_RE.test(launch) || !looksLikeClaudeModel(launch) && launch !== defaultModel) {
|
|
1019
|
+
return {
|
|
1020
|
+
ok: false,
|
|
1021
|
+
model: requested,
|
|
1022
|
+
normalized: false,
|
|
1023
|
+
requested,
|
|
1024
|
+
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.`
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
1027
|
+
if (launch !== requested) {
|
|
1028
|
+
return {
|
|
1029
|
+
ok: true,
|
|
1030
|
+
model: launch,
|
|
1031
|
+
normalized: true,
|
|
1032
|
+
requested,
|
|
1033
|
+
note: `normalized model "${requested}" \u2192 "${launch}" (the Claude CLI rejects reasoning-effort suffixes)`
|
|
1034
|
+
};
|
|
1035
|
+
}
|
|
1036
|
+
return { ok: true, model: launch, normalized: false };
|
|
1037
|
+
}
|
|
1038
|
+
function preflightCursorModel(model, defaultModel) {
|
|
1039
|
+
const requested = (model ?? "").trim();
|
|
1040
|
+
if (!requested) {
|
|
1041
|
+
return { ok: true, model: defaultModel, normalized: false };
|
|
1042
|
+
}
|
|
1043
|
+
if (looksLikeClaudeModel(requested)) {
|
|
1044
|
+
return {
|
|
1045
|
+
ok: false,
|
|
1046
|
+
model: requested,
|
|
1047
|
+
normalized: false,
|
|
1048
|
+
requested,
|
|
1049
|
+
note: `model "${requested}" is a Claude model but the worker provider is "cursor". Switch the provider to "claude" or pick a Cursor model.`
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
1052
|
+
return { ok: true, model: requested, normalized: false };
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
// src/providers/claude.ts
|
|
1056
|
+
var CLAUDE_DEFAULT_MODEL = "claude-opus-4-7";
|
|
945
1057
|
var claudeProvider = {
|
|
946
1058
|
name: "claude",
|
|
1059
|
+
defaultModel: CLAUDE_DEFAULT_MODEL,
|
|
1060
|
+
preflightModel(model) {
|
|
1061
|
+
return preflightClaudeModel(model, CLAUDE_DEFAULT_MODEL);
|
|
1062
|
+
},
|
|
947
1063
|
start(opts) {
|
|
948
|
-
const
|
|
1064
|
+
const preflight = preflightClaudeModel(opts.model, CLAUDE_DEFAULT_MODEL);
|
|
1065
|
+
if (!preflight.ok) {
|
|
1066
|
+
throw new Error(`claude provider model preflight failed: ${preflight.note}`);
|
|
1067
|
+
}
|
|
1068
|
+
const model = preflight.model;
|
|
949
1069
|
const stdoutFd = openSync(opts.stdoutPath, "a");
|
|
950
1070
|
const stderrFd = openSync(opts.stderrPath, "a");
|
|
951
1071
|
const child = spawn(
|
|
@@ -1021,8 +1141,16 @@ function resolveAgentBin() {
|
|
|
1021
1141
|
}
|
|
1022
1142
|
var cursorProvider = {
|
|
1023
1143
|
name: "cursor",
|
|
1144
|
+
defaultModel: DEFAULT_CURSOR_MODEL,
|
|
1145
|
+
preflightModel(model) {
|
|
1146
|
+
return preflightCursorModel(model, DEFAULT_CURSOR_MODEL);
|
|
1147
|
+
},
|
|
1024
1148
|
start(opts) {
|
|
1025
|
-
const
|
|
1149
|
+
const preflight = preflightCursorModel(opts.model, DEFAULT_CURSOR_MODEL);
|
|
1150
|
+
if (!preflight.ok) {
|
|
1151
|
+
throw new Error(`cursor provider model preflight failed: ${preflight.note}`);
|
|
1152
|
+
}
|
|
1153
|
+
const model = preflight.model;
|
|
1026
1154
|
const stdoutFd = openSync2(opts.stdoutPath, "a");
|
|
1027
1155
|
const stderrFd = openSync2(opts.stderrPath, "a");
|
|
1028
1156
|
const agentBin = resolveAgentBin();
|
|
@@ -1082,6 +1210,369 @@ function resolveWorkerProvider(name) {
|
|
|
1082
1210
|
return provider;
|
|
1083
1211
|
}
|
|
1084
1212
|
|
|
1213
|
+
// src/auto-complete.ts
|
|
1214
|
+
import { spawn as spawn3 } from "node:child_process";
|
|
1215
|
+
import { existsSync as existsSync8, openSync as openSync3, closeSync as closeSync3 } from "node:fs";
|
|
1216
|
+
import path8 from "node:path";
|
|
1217
|
+
import { fileURLToPath } from "node:url";
|
|
1218
|
+
|
|
1219
|
+
// src/worker-ops.ts
|
|
1220
|
+
import path7 from "node:path";
|
|
1221
|
+
async function postCompletion(url, secret, body) {
|
|
1222
|
+
const res = await fetch(url, {
|
|
1223
|
+
method: "POST",
|
|
1224
|
+
headers: buildHarnessCallbackHeaders(secret),
|
|
1225
|
+
body: JSON.stringify(body)
|
|
1226
|
+
});
|
|
1227
|
+
let parsed = null;
|
|
1228
|
+
try {
|
|
1229
|
+
parsed = await res.json();
|
|
1230
|
+
} catch {
|
|
1231
|
+
parsed = null;
|
|
1232
|
+
}
|
|
1233
|
+
return { ok: res.ok, status: res.status, parsed };
|
|
1234
|
+
}
|
|
1235
|
+
function completionErrorText(parsed) {
|
|
1236
|
+
if (parsed && typeof parsed === "object") {
|
|
1237
|
+
const err = parsed.error;
|
|
1238
|
+
if (typeof err === "string" && err.trim()) return err.trim();
|
|
1239
|
+
}
|
|
1240
|
+
return void 0;
|
|
1241
|
+
}
|
|
1242
|
+
function persistCompletionBlocker(worker, reason) {
|
|
1243
|
+
const current = worker.completionBlocker;
|
|
1244
|
+
if ((current ?? void 0) === (reason ?? void 0)) return;
|
|
1245
|
+
if (reason) worker.completionBlocker = reason;
|
|
1246
|
+
else delete worker.completionBlocker;
|
|
1247
|
+
saveWorker(worker.runId, worker);
|
|
1248
|
+
}
|
|
1249
|
+
async function tryCompleteWorker(args) {
|
|
1250
|
+
const worker = loadWorker(String(args.run), String(args.name));
|
|
1251
|
+
const status = computeWorkerStatus(worker);
|
|
1252
|
+
const agentOsId = (args.agentOsId ? String(args.agentOsId) : worker.agentOsId) || "";
|
|
1253
|
+
const taskId = (args.taskId ? String(args.taskId) : worker.taskId) || null;
|
|
1254
|
+
if (!agentOsId) {
|
|
1255
|
+
return { ok: false, reason: "missing agentOsId" };
|
|
1256
|
+
}
|
|
1257
|
+
if (!isFinishedWorkerStatus(status)) {
|
|
1258
|
+
return { ok: true, skipped: true, reason: "worker-not-finished" };
|
|
1259
|
+
}
|
|
1260
|
+
const base = resolveBaseUrl(args.baseUrl ? String(args.baseUrl) : void 0);
|
|
1261
|
+
const explicitSecret = args.secret ? String(args.secret) : void 0;
|
|
1262
|
+
let secret = await resolveCallbackSecretWithMint(explicitSecret, agentOsId, { baseUrl: base });
|
|
1263
|
+
const url = `${base}/api/agent-os/by-id/${encodeURIComponent(agentOsId)}/harness/completion`;
|
|
1264
|
+
const body = {
|
|
1265
|
+
source: "openclaw-harness",
|
|
1266
|
+
agentOsId,
|
|
1267
|
+
runId: worker.runId,
|
|
1268
|
+
workerName: worker.name,
|
|
1269
|
+
taskId,
|
|
1270
|
+
startedAt: worker.startedAt,
|
|
1271
|
+
finishedAt: status.lastActivityAt || (/* @__PURE__ */ new Date()).toISOString(),
|
|
1272
|
+
status
|
|
1273
|
+
};
|
|
1274
|
+
let result = await postCompletion(url, secret, body);
|
|
1275
|
+
if ((result.status === 401 || result.status === 403) && !explicitSecret) {
|
|
1276
|
+
const refreshed = await refreshRunnerToken(agentOsId, { baseUrl: base });
|
|
1277
|
+
if (refreshed && refreshed !== secret) {
|
|
1278
|
+
secret = refreshed;
|
|
1279
|
+
result = await postCompletion(url, secret, body);
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
if (result.ok) {
|
|
1283
|
+
persistCompletionBlocker(worker, void 0);
|
|
1284
|
+
return { ok: true, httpStatus: result.status, response: result.parsed };
|
|
1285
|
+
}
|
|
1286
|
+
const authRejected = result.status === 401 || result.status === 403;
|
|
1287
|
+
const detail = completionErrorText(result.parsed) ?? (authRejected ? "runner token unauthorized" : "non-2xx response");
|
|
1288
|
+
const reason = authRejected ? `completion replay rejected (${result.status}): ${detail}` : `completion replay failed (${result.status}): ${detail}`;
|
|
1289
|
+
persistCompletionBlocker(worker, reason);
|
|
1290
|
+
return { ok: false, httpStatus: result.status, response: result.parsed, completionBlocked: true };
|
|
1291
|
+
}
|
|
1292
|
+
async function completeWorker(args) {
|
|
1293
|
+
try {
|
|
1294
|
+
const worker = loadWorker(String(args.run), String(args.name));
|
|
1295
|
+
const status = computeWorkerStatus(worker);
|
|
1296
|
+
const agentOsId = (args.agentOsId ? String(args.agentOsId) : worker.agentOsId) || "";
|
|
1297
|
+
const taskId = (args.taskId ? String(args.taskId) : worker.taskId) || null;
|
|
1298
|
+
if (!agentOsId) {
|
|
1299
|
+
console.error("worker complete requires --agent-os-id (or an agentOsId persisted at worker start)");
|
|
1300
|
+
process.exit(1);
|
|
1301
|
+
}
|
|
1302
|
+
if (!isFinishedWorkerStatus(status)) {
|
|
1303
|
+
console.log(
|
|
1304
|
+
JSON.stringify(
|
|
1305
|
+
{
|
|
1306
|
+
worker: worker.name,
|
|
1307
|
+
runId: worker.runId,
|
|
1308
|
+
status: "skipped",
|
|
1309
|
+
reason: "worker-not-finished",
|
|
1310
|
+
workerStatus: status.status,
|
|
1311
|
+
alive: status.alive
|
|
1312
|
+
},
|
|
1313
|
+
null,
|
|
1314
|
+
2
|
|
1315
|
+
)
|
|
1316
|
+
);
|
|
1317
|
+
return;
|
|
1318
|
+
}
|
|
1319
|
+
const result = await tryCompleteWorker(args);
|
|
1320
|
+
console.log(
|
|
1321
|
+
JSON.stringify(
|
|
1322
|
+
{
|
|
1323
|
+
worker: worker.name,
|
|
1324
|
+
runId: worker.runId,
|
|
1325
|
+
agentOsId,
|
|
1326
|
+
taskId,
|
|
1327
|
+
httpStatus: result.httpStatus,
|
|
1328
|
+
response: result.response
|
|
1329
|
+
},
|
|
1330
|
+
null,
|
|
1331
|
+
2
|
|
1332
|
+
)
|
|
1333
|
+
);
|
|
1334
|
+
if (!result.ok) process.exit(1);
|
|
1335
|
+
} catch (error) {
|
|
1336
|
+
console.error(`worker complete failed: ${error.message}`);
|
|
1337
|
+
process.exit(1);
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
function workerStatus(args) {
|
|
1341
|
+
const worker = loadWorker(String(args.run), String(args.name));
|
|
1342
|
+
const status = computeWorkerStatus(worker);
|
|
1343
|
+
writeJson(path7.join(worker.workerDir, "last-status.json"), status);
|
|
1344
|
+
console.log(JSON.stringify(status, null, 2));
|
|
1345
|
+
}
|
|
1346
|
+
function runStatus(args) {
|
|
1347
|
+
const run = loadRun(String(args.run));
|
|
1348
|
+
const names = Object.keys(run.workers || {});
|
|
1349
|
+
const workers = names.map((name) => {
|
|
1350
|
+
const worker = readJson(
|
|
1351
|
+
path7.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
1352
|
+
void 0
|
|
1353
|
+
);
|
|
1354
|
+
if (!worker) {
|
|
1355
|
+
return { worker: name, status: "missing", attention: "needs_attention", attentionReason: "worker.json not found" };
|
|
1356
|
+
}
|
|
1357
|
+
const status = computeWorkerStatus(worker, { base: run.base });
|
|
1358
|
+
const rawBlocker = worker.completionBlocker;
|
|
1359
|
+
const completionBlocker = typeof rawBlocker === "string" && rawBlocker ? rawBlocker : void 0;
|
|
1360
|
+
return {
|
|
1361
|
+
worker: status.worker,
|
|
1362
|
+
status: completionBlocker ? "blocked" : status.status,
|
|
1363
|
+
attention: completionBlocker ? "blocked" : status.attention.state,
|
|
1364
|
+
attentionReason: completionBlocker ?? status.attention.reason,
|
|
1365
|
+
pid: status.pid,
|
|
1366
|
+
alive: status.alive,
|
|
1367
|
+
currentTool: status.currentTool,
|
|
1368
|
+
lastActivityAt: status.lastActivityAt,
|
|
1369
|
+
lastHeartbeatPhase: status.lastHeartbeatPhase,
|
|
1370
|
+
lastHeartbeatSummary: status.lastHeartbeatSummary,
|
|
1371
|
+
heartbeatBlocker: status.heartbeatBlocker,
|
|
1372
|
+
changedFileCount: status.changedFiles.length,
|
|
1373
|
+
branch: status.branch,
|
|
1374
|
+
ancestry: status.gitAncestry.relation,
|
|
1375
|
+
ancestryChecked: status.gitAncestry.checked
|
|
1376
|
+
};
|
|
1377
|
+
});
|
|
1378
|
+
const board = {
|
|
1379
|
+
runId: run.id,
|
|
1380
|
+
name: run.name,
|
|
1381
|
+
status: deriveRunStatus(run.status, workers),
|
|
1382
|
+
repo: run.repo,
|
|
1383
|
+
workerCount: workers.length,
|
|
1384
|
+
needsAttention: workers.filter((w) => w.attention && w.attention !== "ok" && w.attention !== "done").map((w) => w.worker),
|
|
1385
|
+
workers
|
|
1386
|
+
};
|
|
1387
|
+
writeJson(path7.join(runDirectory(run.id), "last-board.json"), board);
|
|
1388
|
+
console.log(JSON.stringify(board, null, 2));
|
|
1389
|
+
}
|
|
1390
|
+
function tailWorker(args) {
|
|
1391
|
+
const worker = loadWorker(String(args.run), String(args.name));
|
|
1392
|
+
const raw = tailFile(worker.stdoutPath, Number(args.lines || 40));
|
|
1393
|
+
if (args.raw === true || args.raw === "true") {
|
|
1394
|
+
process.stdout.write(raw);
|
|
1395
|
+
return;
|
|
1396
|
+
}
|
|
1397
|
+
for (const line of raw.split("\n").filter(Boolean)) {
|
|
1398
|
+
const event = safeJson(line);
|
|
1399
|
+
const summary = event ? summarizeEvent(event) : line;
|
|
1400
|
+
if (summary) console.log(summary);
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
function stopWorker(args) {
|
|
1404
|
+
const worker = loadWorker(String(args.run), String(args.name));
|
|
1405
|
+
if (!isPidAlive(worker.pid)) {
|
|
1406
|
+
console.log(JSON.stringify({ worker: worker.name, pid: worker.pid, status: "not_running" }, null, 2));
|
|
1407
|
+
return;
|
|
1408
|
+
}
|
|
1409
|
+
killWorkerProcess(worker.pid, "SIGTERM");
|
|
1410
|
+
sleepMs(1500);
|
|
1411
|
+
if (isPidAlive(worker.pid)) {
|
|
1412
|
+
killWorkerProcess(worker.pid, "SIGKILL");
|
|
1413
|
+
console.log(JSON.stringify({ worker: worker.name, pid: worker.pid, status: "sigkill_sent" }, null, 2));
|
|
1414
|
+
return;
|
|
1415
|
+
}
|
|
1416
|
+
console.log(JSON.stringify({ worker: worker.name, pid: worker.pid, status: "stopped" }, null, 2));
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
// src/auto-complete.ts
|
|
1420
|
+
var DEFAULT_POLL_MS = 5e3;
|
|
1421
|
+
var DEFAULT_MAX_TOTAL_MS = 6 * 60 * 60 * 1e3;
|
|
1422
|
+
var DEFAULT_COMPLETE_ATTEMPTS = 3;
|
|
1423
|
+
var DEFAULT_COMPLETE_BACKOFF_MS = 5e3;
|
|
1424
|
+
function readArgs(raw) {
|
|
1425
|
+
return {
|
|
1426
|
+
run: String(raw.run || ""),
|
|
1427
|
+
name: String(raw.name || ""),
|
|
1428
|
+
agentOsId: raw.agentOsId ? String(raw.agentOsId) : void 0,
|
|
1429
|
+
pollMs: Number(raw.pollMs) > 0 ? Math.floor(Number(raw.pollMs)) : void 0,
|
|
1430
|
+
maxTotalMs: Number(raw.maxTotalMs) > 0 ? Math.floor(Number(raw.maxTotalMs)) : void 0,
|
|
1431
|
+
completeAttempts: Number(raw.completeAttempts) > 0 ? Math.floor(Number(raw.completeAttempts)) : void 0,
|
|
1432
|
+
completeBackoffMs: Number(raw.completeBackoffMs) > 0 ? Math.floor(Number(raw.completeBackoffMs)) : void 0,
|
|
1433
|
+
baseUrl: raw.baseUrl ? String(raw.baseUrl) : void 0,
|
|
1434
|
+
secret: raw.secret ? String(raw.secret) : void 0
|
|
1435
|
+
};
|
|
1436
|
+
}
|
|
1437
|
+
async function autoCompleteWorker(raw) {
|
|
1438
|
+
const args = readArgs(raw);
|
|
1439
|
+
const pollMs = args.pollMs ?? DEFAULT_POLL_MS;
|
|
1440
|
+
const maxTotalMs = args.maxTotalMs ?? DEFAULT_MAX_TOTAL_MS;
|
|
1441
|
+
const completeAttempts = args.completeAttempts ?? DEFAULT_COMPLETE_ATTEMPTS;
|
|
1442
|
+
const completeBackoffMs = args.completeBackoffMs ?? DEFAULT_COMPLETE_BACKOFF_MS;
|
|
1443
|
+
const worker = loadWorker(args.run, args.name);
|
|
1444
|
+
if (!worker.agentOsId || !worker.taskId) {
|
|
1445
|
+
return {
|
|
1446
|
+
worker: worker.name,
|
|
1447
|
+
runId: worker.runId,
|
|
1448
|
+
outcome: "missing_link",
|
|
1449
|
+
attempts: 0,
|
|
1450
|
+
reason: "worker has no agentOsId/taskId \u2014 nothing to attribute completion to"
|
|
1451
|
+
};
|
|
1452
|
+
}
|
|
1453
|
+
const startMs = Date.now();
|
|
1454
|
+
while (true) {
|
|
1455
|
+
const status = computeWorkerStatus(worker);
|
|
1456
|
+
if (isFinishedWorkerStatus(status)) break;
|
|
1457
|
+
if (!isPidAlive(worker.pid)) break;
|
|
1458
|
+
if (Date.now() - startMs > maxTotalMs) {
|
|
1459
|
+
return {
|
|
1460
|
+
worker: worker.name,
|
|
1461
|
+
runId: worker.runId,
|
|
1462
|
+
outcome: "timed_out",
|
|
1463
|
+
attempts: 0,
|
|
1464
|
+
reason: `worker did not finish within ${maxTotalMs}ms`
|
|
1465
|
+
};
|
|
1466
|
+
}
|
|
1467
|
+
sleepMs(pollMs);
|
|
1468
|
+
}
|
|
1469
|
+
let lastHttpStatus;
|
|
1470
|
+
let lastReason;
|
|
1471
|
+
for (let attempt = 1; attempt <= completeAttempts; attempt++) {
|
|
1472
|
+
const result = await tryCompleteWorker({
|
|
1473
|
+
run: args.run,
|
|
1474
|
+
name: args.name,
|
|
1475
|
+
...args.agentOsId ? { agentOsId: args.agentOsId } : {},
|
|
1476
|
+
...args.baseUrl ? { baseUrl: args.baseUrl } : {},
|
|
1477
|
+
...args.secret ? { secret: args.secret } : {}
|
|
1478
|
+
});
|
|
1479
|
+
lastHttpStatus = result.httpStatus;
|
|
1480
|
+
if (result.ok) {
|
|
1481
|
+
return {
|
|
1482
|
+
worker: worker.name,
|
|
1483
|
+
runId: worker.runId,
|
|
1484
|
+
outcome: "completed",
|
|
1485
|
+
httpStatus: result.httpStatus,
|
|
1486
|
+
attempts: attempt
|
|
1487
|
+
};
|
|
1488
|
+
}
|
|
1489
|
+
const authRejected = result.httpStatus === 401 || result.httpStatus === 403;
|
|
1490
|
+
if (authRejected) {
|
|
1491
|
+
lastReason = typeof result.reason === "string" ? result.reason : "completion replay refused";
|
|
1492
|
+
return {
|
|
1493
|
+
worker: worker.name,
|
|
1494
|
+
runId: worker.runId,
|
|
1495
|
+
outcome: "blocked",
|
|
1496
|
+
httpStatus: result.httpStatus,
|
|
1497
|
+
attempts: attempt,
|
|
1498
|
+
reason: lastReason
|
|
1499
|
+
};
|
|
1500
|
+
}
|
|
1501
|
+
lastReason = typeof result.reason === "string" ? result.reason : "transient failure";
|
|
1502
|
+
if (attempt < completeAttempts) sleepMs(completeBackoffMs);
|
|
1503
|
+
}
|
|
1504
|
+
return {
|
|
1505
|
+
worker: worker.name,
|
|
1506
|
+
runId: worker.runId,
|
|
1507
|
+
outcome: "blocked",
|
|
1508
|
+
httpStatus: lastHttpStatus,
|
|
1509
|
+
attempts: completeAttempts,
|
|
1510
|
+
reason: lastReason ?? "completion failed after retries"
|
|
1511
|
+
};
|
|
1512
|
+
}
|
|
1513
|
+
async function autoCompleteWorkerCli(raw) {
|
|
1514
|
+
try {
|
|
1515
|
+
const outcome = await autoCompleteWorker(raw);
|
|
1516
|
+
console.log(JSON.stringify(outcome, null, 2));
|
|
1517
|
+
if (outcome.outcome === "missing_link" || outcome.outcome === "timed_out") {
|
|
1518
|
+
process.exitCode = 1;
|
|
1519
|
+
}
|
|
1520
|
+
} catch (error) {
|
|
1521
|
+
console.error(`worker auto-complete failed: ${error.message}`);
|
|
1522
|
+
process.exitCode = 1;
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
function resolveDefaultCliPath() {
|
|
1526
|
+
return path8.join(fileURLToPath(new URL(".", import.meta.url)), "cli.js");
|
|
1527
|
+
}
|
|
1528
|
+
function spawnCompletionSidecar(opts) {
|
|
1529
|
+
const cliPath = opts.cliPath ?? resolveDefaultCliPath();
|
|
1530
|
+
if (!existsSync8(cliPath)) return void 0;
|
|
1531
|
+
const logPath = path8.join(opts.workerDir, "auto-complete.log");
|
|
1532
|
+
let logFd;
|
|
1533
|
+
try {
|
|
1534
|
+
logFd = openSync3(logPath, "a");
|
|
1535
|
+
} catch {
|
|
1536
|
+
logFd = void 0;
|
|
1537
|
+
}
|
|
1538
|
+
const stdio = [
|
|
1539
|
+
"ignore",
|
|
1540
|
+
logFd ?? "ignore",
|
|
1541
|
+
logFd ?? "ignore"
|
|
1542
|
+
];
|
|
1543
|
+
const nodeExecutable = opts.nodeExecutable ?? process.execPath;
|
|
1544
|
+
const args = [
|
|
1545
|
+
cliPath,
|
|
1546
|
+
"worker",
|
|
1547
|
+
"auto-complete",
|
|
1548
|
+
"--run",
|
|
1549
|
+
opts.runId,
|
|
1550
|
+
"--name",
|
|
1551
|
+
opts.workerName
|
|
1552
|
+
];
|
|
1553
|
+
if (opts.agentOsId) args.push("--agent-os-id", opts.agentOsId);
|
|
1554
|
+
if (opts.baseUrl) args.push("--base-url", opts.baseUrl);
|
|
1555
|
+
if (opts.secret) args.push("--secret", opts.secret);
|
|
1556
|
+
try {
|
|
1557
|
+
const child = spawn3(nodeExecutable, args, {
|
|
1558
|
+
detached: true,
|
|
1559
|
+
stdio,
|
|
1560
|
+
env: process.env
|
|
1561
|
+
});
|
|
1562
|
+
if (logFd !== void 0) closeSync3(logFd);
|
|
1563
|
+
child.unref();
|
|
1564
|
+
return { pid: child.pid, logPath, cliPath };
|
|
1565
|
+
} catch {
|
|
1566
|
+
if (logFd !== void 0) {
|
|
1567
|
+
try {
|
|
1568
|
+
closeSync3(logFd);
|
|
1569
|
+
} catch {
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
return void 0;
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1085
1576
|
// src/supervisor.ts
|
|
1086
1577
|
function spawnWorkerProcess(run, opts) {
|
|
1087
1578
|
const rawName = typeof opts.name === "string" ? opts.name.trim() : "";
|
|
@@ -1091,31 +1582,44 @@ function spawnWorkerProcess(run, opts) {
|
|
|
1091
1582
|
const name = safeSlug(rawName);
|
|
1092
1583
|
if (run.workers?.[name]) throw new Error(`worker already exists in run ${run.id}: ${name}`);
|
|
1093
1584
|
if (!opts.task) throw new Error(`missing task text for worker ${name}`);
|
|
1585
|
+
const provider = resolveWorkerProvider(opts.provider);
|
|
1586
|
+
let launchModel = opts.model;
|
|
1587
|
+
if (provider.preflightModel) {
|
|
1588
|
+
const preflight = provider.preflightModel(opts.model);
|
|
1589
|
+
if (!preflight.ok) {
|
|
1590
|
+
throw new Error(
|
|
1591
|
+
`model preflight failed for provider "${provider.name}": ${preflight.note ?? "invalid model/provider combination"}`
|
|
1592
|
+
);
|
|
1593
|
+
}
|
|
1594
|
+
if (preflight.normalized) {
|
|
1595
|
+
console.error(`[supervisor] ${name}: ${preflight.note}`);
|
|
1596
|
+
}
|
|
1597
|
+
launchModel = preflight.model;
|
|
1598
|
+
}
|
|
1094
1599
|
const { worktreesDir } = getPaths();
|
|
1095
|
-
const workerDir =
|
|
1600
|
+
const workerDir = path9.join(runDirectory(run.id), "workers", name);
|
|
1096
1601
|
mkdirSync3(workerDir, { recursive: true });
|
|
1097
|
-
const worktreePath =
|
|
1602
|
+
const worktreePath = path9.join(worktreesDir, run.id, name);
|
|
1098
1603
|
const branch = opts.branch || `agent/${run.id}/${name}`;
|
|
1099
|
-
if (
|
|
1604
|
+
if (existsSync9(worktreePath)) throw new Error(`worktree path already exists: ${worktreePath}`);
|
|
1100
1605
|
git(run.repo, ["fetch", "origin", "--prune"], { allowFailure: true });
|
|
1101
1606
|
git(run.repo, ["worktree", "add", "-b", branch, worktreePath, run.baseCommit], { throwError: true });
|
|
1102
|
-
const stdoutPath =
|
|
1103
|
-
const stderrPath =
|
|
1104
|
-
const heartbeatPath =
|
|
1607
|
+
const stdoutPath = path9.join(workerDir, "stdout.jsonl");
|
|
1608
|
+
const stderrPath = path9.join(workerDir, "stderr.log");
|
|
1609
|
+
const heartbeatPath = path9.join(workerDir, "heartbeat.jsonl");
|
|
1105
1610
|
const prompt = buildPrompt({
|
|
1106
1611
|
task: opts.task,
|
|
1107
1612
|
ownedPaths: opts.ownedPaths || [],
|
|
1108
1613
|
worktreePath,
|
|
1109
1614
|
heartbeatPath
|
|
1110
1615
|
});
|
|
1111
|
-
const provider = resolveWorkerProvider(opts.provider);
|
|
1112
1616
|
let started;
|
|
1113
1617
|
try {
|
|
1114
1618
|
started = provider.start({
|
|
1115
1619
|
name,
|
|
1116
1620
|
task: opts.task,
|
|
1117
1621
|
ownedPaths: opts.ownedPaths,
|
|
1118
|
-
model:
|
|
1622
|
+
model: launchModel,
|
|
1119
1623
|
branch,
|
|
1120
1624
|
worktreePath,
|
|
1121
1625
|
workerDir,
|
|
@@ -1129,7 +1633,7 @@ function spawnWorkerProcess(run, opts) {
|
|
|
1129
1633
|
git(run.repo, ["branch", "-D", branch], { allowFailure: true });
|
|
1130
1634
|
throw error;
|
|
1131
1635
|
}
|
|
1132
|
-
const model = started.model ||
|
|
1636
|
+
const model = started.model || launchModel || provider.defaultModel || "claude-opus-4-7";
|
|
1133
1637
|
const worker = {
|
|
1134
1638
|
name,
|
|
1135
1639
|
runId: run.id,
|
|
@@ -1151,9 +1655,20 @@ function spawnWorkerProcess(run, opts) {
|
|
|
1151
1655
|
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1152
1656
|
};
|
|
1153
1657
|
saveWorker(run.id, worker);
|
|
1154
|
-
run.workers = { ...run.workers || {}, [name]: { workerDir, statusPath:
|
|
1658
|
+
run.workers = { ...run.workers || {}, [name]: { workerDir, statusPath: path9.join(workerDir, "worker.json") } };
|
|
1155
1659
|
run.status = "running";
|
|
1156
1660
|
saveRun(run);
|
|
1661
|
+
if (worker.agentOsId && worker.taskId) {
|
|
1662
|
+
try {
|
|
1663
|
+
spawnCompletionSidecar({
|
|
1664
|
+
runId: run.id,
|
|
1665
|
+
workerName: name,
|
|
1666
|
+
workerDir,
|
|
1667
|
+
agentOsId: worker.agentOsId
|
|
1668
|
+
});
|
|
1669
|
+
} catch {
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1157
1672
|
return worker;
|
|
1158
1673
|
}
|
|
1159
1674
|
function startWorker(args) {
|
|
@@ -1354,7 +1869,7 @@ async function dispatchRun(args) {
|
|
|
1354
1869
|
}
|
|
1355
1870
|
|
|
1356
1871
|
// src/sweep.ts
|
|
1357
|
-
import
|
|
1872
|
+
import path10 from "node:path";
|
|
1358
1873
|
async function sweepRun(args) {
|
|
1359
1874
|
const pipeline = args.pipeline === true || args.pipeline === "true";
|
|
1360
1875
|
try {
|
|
@@ -1366,7 +1881,7 @@ async function sweepRun(args) {
|
|
|
1366
1881
|
const releasedLocalOrphans = [];
|
|
1367
1882
|
for (const name of Object.keys(run.workers || {})) {
|
|
1368
1883
|
const worker = readJson(
|
|
1369
|
-
|
|
1884
|
+
path10.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
1370
1885
|
void 0
|
|
1371
1886
|
);
|
|
1372
1887
|
if (!worker || !worker.dispatched || !worker.taskId) continue;
|
|
@@ -1409,11 +1924,11 @@ async function sweepRun(args) {
|
|
|
1409
1924
|
}
|
|
1410
1925
|
|
|
1411
1926
|
// src/worktree.ts
|
|
1412
|
-
import { existsSync as
|
|
1413
|
-
import
|
|
1927
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync4 } from "node:fs";
|
|
1928
|
+
import path12 from "node:path";
|
|
1414
1929
|
|
|
1415
1930
|
// src/validate.ts
|
|
1416
|
-
import
|
|
1931
|
+
import path11 from "node:path";
|
|
1417
1932
|
var RUN_ID_RE = /^[a-z0-9][a-z0-9._-]{0,127}$/i;
|
|
1418
1933
|
function validateRunId(runId) {
|
|
1419
1934
|
const trimmed = runId.trim();
|
|
@@ -1421,7 +1936,7 @@ function validateRunId(runId) {
|
|
|
1421
1936
|
return trimmed;
|
|
1422
1937
|
}
|
|
1423
1938
|
function validateRepo(repo) {
|
|
1424
|
-
const resolved =
|
|
1939
|
+
const resolved = path11.resolve(repo);
|
|
1425
1940
|
if (resolved.includes("..")) throw new Error("repo path must not contain .. segments");
|
|
1426
1941
|
return resolved;
|
|
1427
1942
|
}
|
|
@@ -1432,7 +1947,7 @@ function createRun(args) {
|
|
|
1432
1947
|
ensureGitRepo(repo);
|
|
1433
1948
|
const id = args.id ? validateRunId(String(args.id)) : timestampSlug(String(args.name || "run"));
|
|
1434
1949
|
const dir = runDirectory(id);
|
|
1435
|
-
if (
|
|
1950
|
+
if (existsSync10(dir)) failExists(`run already exists: ${id}`);
|
|
1436
1951
|
mkdirSync4(dir, { recursive: true });
|
|
1437
1952
|
const base = String(args.base || "origin/main");
|
|
1438
1953
|
const baseCommit = git(repo, ["rev-parse", base]).trim();
|
|
@@ -1446,12 +1961,12 @@ function createRun(args) {
|
|
|
1446
1961
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1447
1962
|
workers: {}
|
|
1448
1963
|
};
|
|
1449
|
-
writeJson(
|
|
1964
|
+
writeJson(path12.join(dir, "run.json"), run);
|
|
1450
1965
|
console.log(JSON.stringify({ runId: id, runDir: dir, repo, base, baseCommit }, null, 2));
|
|
1451
1966
|
}
|
|
1452
1967
|
function listRuns() {
|
|
1453
1968
|
const { runsDir } = getPaths();
|
|
1454
|
-
const rows = listRunIds(runsDir).map((id) => readJson(
|
|
1969
|
+
const rows = listRunIds(runsDir).map((id) => readJson(path12.join(runDirectory(id), "run.json"), void 0)).filter(Boolean).map((run) => ({
|
|
1455
1970
|
id: run.id,
|
|
1456
1971
|
name: run.name,
|
|
1457
1972
|
status: run.status,
|
|
@@ -1465,211 +1980,11 @@ function failExists(message) {
|
|
|
1465
1980
|
process.exit(1);
|
|
1466
1981
|
}
|
|
1467
1982
|
|
|
1468
|
-
// src/worker-ops.ts
|
|
1469
|
-
import path11 from "node:path";
|
|
1470
|
-
async function postCompletion(url, secret, body) {
|
|
1471
|
-
const res = await fetch(url, {
|
|
1472
|
-
method: "POST",
|
|
1473
|
-
headers: buildHarnessCallbackHeaders(secret),
|
|
1474
|
-
body: JSON.stringify(body)
|
|
1475
|
-
});
|
|
1476
|
-
let parsed = null;
|
|
1477
|
-
try {
|
|
1478
|
-
parsed = await res.json();
|
|
1479
|
-
} catch {
|
|
1480
|
-
parsed = null;
|
|
1481
|
-
}
|
|
1482
|
-
return { ok: res.ok, status: res.status, parsed };
|
|
1483
|
-
}
|
|
1484
|
-
function completionErrorText(parsed) {
|
|
1485
|
-
if (parsed && typeof parsed === "object") {
|
|
1486
|
-
const err = parsed.error;
|
|
1487
|
-
if (typeof err === "string" && err.trim()) return err.trim();
|
|
1488
|
-
}
|
|
1489
|
-
return void 0;
|
|
1490
|
-
}
|
|
1491
|
-
function persistCompletionBlocker(worker, reason) {
|
|
1492
|
-
const current = worker.completionBlocker;
|
|
1493
|
-
if ((current ?? void 0) === (reason ?? void 0)) return;
|
|
1494
|
-
if (reason) worker.completionBlocker = reason;
|
|
1495
|
-
else delete worker.completionBlocker;
|
|
1496
|
-
saveWorker(worker.runId, worker);
|
|
1497
|
-
}
|
|
1498
|
-
async function tryCompleteWorker(args) {
|
|
1499
|
-
const worker = loadWorker(String(args.run), String(args.name));
|
|
1500
|
-
const status = computeWorkerStatus(worker);
|
|
1501
|
-
const agentOsId = (args.agentOsId ? String(args.agentOsId) : worker.agentOsId) || "";
|
|
1502
|
-
const taskId = (args.taskId ? String(args.taskId) : worker.taskId) || null;
|
|
1503
|
-
if (!agentOsId) {
|
|
1504
|
-
return { ok: false, reason: "missing agentOsId" };
|
|
1505
|
-
}
|
|
1506
|
-
if (!isFinishedWorkerStatus(status)) {
|
|
1507
|
-
return { ok: true, skipped: true, reason: "worker-not-finished" };
|
|
1508
|
-
}
|
|
1509
|
-
const base = resolveBaseUrl(args.baseUrl ? String(args.baseUrl) : void 0);
|
|
1510
|
-
const explicitSecret = args.secret ? String(args.secret) : void 0;
|
|
1511
|
-
let secret = await resolveCallbackSecretWithMint(explicitSecret, agentOsId, { baseUrl: base });
|
|
1512
|
-
const url = `${base}/api/agent-os/by-id/${encodeURIComponent(agentOsId)}/harness/completion`;
|
|
1513
|
-
const body = {
|
|
1514
|
-
source: "openclaw-harness",
|
|
1515
|
-
agentOsId,
|
|
1516
|
-
runId: worker.runId,
|
|
1517
|
-
workerName: worker.name,
|
|
1518
|
-
taskId,
|
|
1519
|
-
startedAt: worker.startedAt,
|
|
1520
|
-
finishedAt: status.lastActivityAt || (/* @__PURE__ */ new Date()).toISOString(),
|
|
1521
|
-
status
|
|
1522
|
-
};
|
|
1523
|
-
let result = await postCompletion(url, secret, body);
|
|
1524
|
-
if ((result.status === 401 || result.status === 403) && !explicitSecret) {
|
|
1525
|
-
const refreshed = await refreshRunnerToken(agentOsId, { baseUrl: base });
|
|
1526
|
-
if (refreshed && refreshed !== secret) {
|
|
1527
|
-
secret = refreshed;
|
|
1528
|
-
result = await postCompletion(url, secret, body);
|
|
1529
|
-
}
|
|
1530
|
-
}
|
|
1531
|
-
if (result.ok) {
|
|
1532
|
-
persistCompletionBlocker(worker, void 0);
|
|
1533
|
-
return { ok: true, httpStatus: result.status, response: result.parsed };
|
|
1534
|
-
}
|
|
1535
|
-
const authRejected = result.status === 401 || result.status === 403;
|
|
1536
|
-
const detail = completionErrorText(result.parsed) ?? (authRejected ? "runner token unauthorized" : "non-2xx response");
|
|
1537
|
-
const reason = authRejected ? `completion replay rejected (${result.status}): ${detail}` : `completion replay failed (${result.status}): ${detail}`;
|
|
1538
|
-
persistCompletionBlocker(worker, reason);
|
|
1539
|
-
return { ok: false, httpStatus: result.status, response: result.parsed, completionBlocked: true };
|
|
1540
|
-
}
|
|
1541
|
-
async function completeWorker(args) {
|
|
1542
|
-
try {
|
|
1543
|
-
const worker = loadWorker(String(args.run), String(args.name));
|
|
1544
|
-
const status = computeWorkerStatus(worker);
|
|
1545
|
-
const agentOsId = (args.agentOsId ? String(args.agentOsId) : worker.agentOsId) || "";
|
|
1546
|
-
const taskId = (args.taskId ? String(args.taskId) : worker.taskId) || null;
|
|
1547
|
-
if (!agentOsId) {
|
|
1548
|
-
console.error("worker complete requires --agent-os-id (or an agentOsId persisted at worker start)");
|
|
1549
|
-
process.exit(1);
|
|
1550
|
-
}
|
|
1551
|
-
if (!isFinishedWorkerStatus(status)) {
|
|
1552
|
-
console.log(
|
|
1553
|
-
JSON.stringify(
|
|
1554
|
-
{
|
|
1555
|
-
worker: worker.name,
|
|
1556
|
-
runId: worker.runId,
|
|
1557
|
-
status: "skipped",
|
|
1558
|
-
reason: "worker-not-finished",
|
|
1559
|
-
workerStatus: status.status,
|
|
1560
|
-
alive: status.alive
|
|
1561
|
-
},
|
|
1562
|
-
null,
|
|
1563
|
-
2
|
|
1564
|
-
)
|
|
1565
|
-
);
|
|
1566
|
-
return;
|
|
1567
|
-
}
|
|
1568
|
-
const result = await tryCompleteWorker(args);
|
|
1569
|
-
console.log(
|
|
1570
|
-
JSON.stringify(
|
|
1571
|
-
{
|
|
1572
|
-
worker: worker.name,
|
|
1573
|
-
runId: worker.runId,
|
|
1574
|
-
agentOsId,
|
|
1575
|
-
taskId,
|
|
1576
|
-
httpStatus: result.httpStatus,
|
|
1577
|
-
response: result.response
|
|
1578
|
-
},
|
|
1579
|
-
null,
|
|
1580
|
-
2
|
|
1581
|
-
)
|
|
1582
|
-
);
|
|
1583
|
-
if (!result.ok) process.exit(1);
|
|
1584
|
-
} catch (error) {
|
|
1585
|
-
console.error(`worker complete failed: ${error.message}`);
|
|
1586
|
-
process.exit(1);
|
|
1587
|
-
}
|
|
1588
|
-
}
|
|
1589
|
-
function workerStatus(args) {
|
|
1590
|
-
const worker = loadWorker(String(args.run), String(args.name));
|
|
1591
|
-
const status = computeWorkerStatus(worker);
|
|
1592
|
-
writeJson(path11.join(worker.workerDir, "last-status.json"), status);
|
|
1593
|
-
console.log(JSON.stringify(status, null, 2));
|
|
1594
|
-
}
|
|
1595
|
-
function runStatus(args) {
|
|
1596
|
-
const run = loadRun(String(args.run));
|
|
1597
|
-
const names = Object.keys(run.workers || {});
|
|
1598
|
-
const workers = names.map((name) => {
|
|
1599
|
-
const worker = readJson(
|
|
1600
|
-
path11.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
1601
|
-
void 0
|
|
1602
|
-
);
|
|
1603
|
-
if (!worker) {
|
|
1604
|
-
return { worker: name, status: "missing", attention: "needs_attention", attentionReason: "worker.json not found" };
|
|
1605
|
-
}
|
|
1606
|
-
const status = computeWorkerStatus(worker, { base: run.base });
|
|
1607
|
-
const rawBlocker = worker.completionBlocker;
|
|
1608
|
-
const completionBlocker = typeof rawBlocker === "string" && rawBlocker ? rawBlocker : void 0;
|
|
1609
|
-
return {
|
|
1610
|
-
worker: status.worker,
|
|
1611
|
-
status: completionBlocker ? "blocked" : status.status,
|
|
1612
|
-
attention: completionBlocker ? "blocked" : status.attention.state,
|
|
1613
|
-
attentionReason: completionBlocker ?? status.attention.reason,
|
|
1614
|
-
pid: status.pid,
|
|
1615
|
-
alive: status.alive,
|
|
1616
|
-
currentTool: status.currentTool,
|
|
1617
|
-
lastActivityAt: status.lastActivityAt,
|
|
1618
|
-
lastHeartbeatPhase: status.lastHeartbeatPhase,
|
|
1619
|
-
lastHeartbeatSummary: status.lastHeartbeatSummary,
|
|
1620
|
-
heartbeatBlocker: status.heartbeatBlocker,
|
|
1621
|
-
changedFileCount: status.changedFiles.length,
|
|
1622
|
-
branch: status.branch,
|
|
1623
|
-
ancestry: status.gitAncestry.relation,
|
|
1624
|
-
ancestryChecked: status.gitAncestry.checked
|
|
1625
|
-
};
|
|
1626
|
-
});
|
|
1627
|
-
const board = {
|
|
1628
|
-
runId: run.id,
|
|
1629
|
-
name: run.name,
|
|
1630
|
-
status: deriveRunStatus(run.status, workers),
|
|
1631
|
-
repo: run.repo,
|
|
1632
|
-
workerCount: workers.length,
|
|
1633
|
-
needsAttention: workers.filter((w) => w.attention && w.attention !== "ok" && w.attention !== "done").map((w) => w.worker),
|
|
1634
|
-
workers
|
|
1635
|
-
};
|
|
1636
|
-
writeJson(path11.join(runDirectory(run.id), "last-board.json"), board);
|
|
1637
|
-
console.log(JSON.stringify(board, null, 2));
|
|
1638
|
-
}
|
|
1639
|
-
function tailWorker(args) {
|
|
1640
|
-
const worker = loadWorker(String(args.run), String(args.name));
|
|
1641
|
-
const raw = tailFile(worker.stdoutPath, Number(args.lines || 40));
|
|
1642
|
-
if (args.raw === true || args.raw === "true") {
|
|
1643
|
-
process.stdout.write(raw);
|
|
1644
|
-
return;
|
|
1645
|
-
}
|
|
1646
|
-
for (const line of raw.split("\n").filter(Boolean)) {
|
|
1647
|
-
const event = safeJson(line);
|
|
1648
|
-
const summary = event ? summarizeEvent(event) : line;
|
|
1649
|
-
if (summary) console.log(summary);
|
|
1650
|
-
}
|
|
1651
|
-
}
|
|
1652
|
-
function stopWorker(args) {
|
|
1653
|
-
const worker = loadWorker(String(args.run), String(args.name));
|
|
1654
|
-
if (!isPidAlive(worker.pid)) {
|
|
1655
|
-
console.log(JSON.stringify({ worker: worker.name, pid: worker.pid, status: "not_running" }, null, 2));
|
|
1656
|
-
return;
|
|
1657
|
-
}
|
|
1658
|
-
killWorkerProcess(worker.pid, "SIGTERM");
|
|
1659
|
-
sleepMs(1500);
|
|
1660
|
-
if (isPidAlive(worker.pid)) {
|
|
1661
|
-
killWorkerProcess(worker.pid, "SIGKILL");
|
|
1662
|
-
console.log(JSON.stringify({ worker: worker.name, pid: worker.pid, status: "sigkill_sent" }, null, 2));
|
|
1663
|
-
return;
|
|
1664
|
-
}
|
|
1665
|
-
console.log(JSON.stringify({ worker: worker.name, pid: worker.pid, status: "stopped" }, null, 2));
|
|
1666
|
-
}
|
|
1667
|
-
|
|
1668
1983
|
// src/pipeline-tick.ts
|
|
1669
|
-
import
|
|
1984
|
+
import path15 from "node:path";
|
|
1670
1985
|
|
|
1671
1986
|
// src/finalize.ts
|
|
1672
|
-
import
|
|
1987
|
+
import path13 from "node:path";
|
|
1673
1988
|
var ACTIVE_RUN_STATUSES = /* @__PURE__ */ new Set(["running", "dispatching", "pending", "queued"]);
|
|
1674
1989
|
function terminalStatusFor(run) {
|
|
1675
1990
|
const names = Object.keys(run.workers || {});
|
|
@@ -1679,7 +1994,7 @@ function terminalStatusFor(run) {
|
|
|
1679
1994
|
let anyCompletionBlocked = false;
|
|
1680
1995
|
for (const name of names) {
|
|
1681
1996
|
const worker = readJson(
|
|
1682
|
-
|
|
1997
|
+
path13.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
1683
1998
|
void 0
|
|
1684
1999
|
);
|
|
1685
2000
|
if (!worker) continue;
|
|
@@ -1712,7 +2027,7 @@ function finalizeStaleRuns() {
|
|
|
1712
2027
|
}
|
|
1713
2028
|
|
|
1714
2029
|
// src/plan-progress-daemon-sync.ts
|
|
1715
|
-
import
|
|
2030
|
+
import path14 from "node:path";
|
|
1716
2031
|
|
|
1717
2032
|
// src/plan-progress-sync.ts
|
|
1718
2033
|
async function syncPlanProgress(args) {
|
|
@@ -1736,7 +2051,7 @@ async function syncActiveWorkerPlanProgress(runId, args) {
|
|
|
1736
2051
|
const outcomes = [];
|
|
1737
2052
|
for (const name of Object.keys(run.workers || {})) {
|
|
1738
2053
|
const worker = readJson(
|
|
1739
|
-
|
|
2054
|
+
path14.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
1740
2055
|
void 0
|
|
1741
2056
|
);
|
|
1742
2057
|
if (!worker?.dispatched || !worker.taskId) continue;
|
|
@@ -1790,7 +2105,7 @@ async function completeFinishedWorkers(runId, args) {
|
|
|
1790
2105
|
const outcomes = [];
|
|
1791
2106
|
for (const name of Object.keys(run.workers || {})) {
|
|
1792
2107
|
const worker = readJson(
|
|
1793
|
-
|
|
2108
|
+
path15.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
1794
2109
|
void 0
|
|
1795
2110
|
);
|
|
1796
2111
|
if (!worker?.taskId) continue;
|
|
@@ -2033,6 +2348,7 @@ function usage(code = 0) {
|
|
|
2033
2348
|
" kynver worker tail --run RUN_ID --name worker [--lines 40] [--raw]",
|
|
2034
2349
|
" kynver worker stop --run RUN_ID --name worker",
|
|
2035
2350
|
" kynver worker complete --run RUN_ID --name worker [--agent-os-id AOS_ID] [--task-id TASK_ID] [--base-url URL] [--secret SECRET]",
|
|
2351
|
+
" 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]",
|
|
2036
2352
|
" 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]",
|
|
2037
2353
|
" kynver plan verify --plan PLAN_ID [--worktree PATH] [--task TASK_ID] [--human-override]"
|
|
2038
2354
|
].join("\n")
|
|
@@ -2071,9 +2387,10 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
2071
2387
|
if (scope === "worker" && action === "tail") return tailWorker(args);
|
|
2072
2388
|
if (scope === "worker" && action === "stop") return stopWorker(args);
|
|
2073
2389
|
if (scope === "worker" && action === "complete") return void await completeWorker(args);
|
|
2390
|
+
if (scope === "worker" && action === "auto-complete") return void await autoCompleteWorkerCli(args);
|
|
2074
2391
|
unknownCommand(scope, action);
|
|
2075
2392
|
}
|
|
2076
|
-
var isCliEntry = process.argv[1] && realpathSync.native(process.argv[1]) === realpathSync.native(
|
|
2393
|
+
var isCliEntry = process.argv[1] && realpathSync.native(process.argv[1]) === realpathSync.native(fileURLToPath2(import.meta.url));
|
|
2077
2394
|
if (isCliEntry) {
|
|
2078
2395
|
void main().catch((error) => {
|
|
2079
2396
|
console.error(error);
|