@kynver-app/runtime 0.1.13 → 0.1.17

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 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";
@@ -207,6 +207,18 @@ async function resolveCallbackSecretWithMint(argsSecret, agentOsId, opts) {
207
207
  "requires --secret, KYNVER_RUNNER_TOKEN, a scoped runner token (`kynver runner credential`), ~/.kynver/credentials runnerToken, KYNVER_API_KEY with an API base URL to mint one, or (legacy) KYNVER_RUNTIME_SECRET / OPENCLAW_CRON_SECRET"
208
208
  );
209
209
  }
210
+ async function refreshRunnerToken(agentOsId, opts) {
211
+ const apiKey = loadApiKey();
212
+ const baseUrl = resolveConfiguredBaseUrl(opts?.baseUrl);
213
+ if (!apiKey || !agentOsId || !baseUrl) return null;
214
+ try {
215
+ const token = await fetchRunnerCredential(agentOsId, { baseUrl, apiKey });
216
+ saveRunnerToken(agentOsId, token);
217
+ return token;
218
+ } catch {
219
+ return null;
220
+ }
221
+ }
210
222
  async function fetchRunnerCredential(agentOsId, opts) {
211
223
  const apiKey = opts?.apiKey || loadApiKey();
212
224
  if (!apiKey) throw new Error("API key required \u2014 run `kynver login` first");
@@ -380,12 +392,12 @@ var DEFAULT_CRITICAL_FREE_BYTES = 15 * 1024 * 1024 * 1024;
380
392
  var DEFAULT_MAX_USED_PERCENT = 80;
381
393
  var DEFAULT_HARD_MAX_USED_PERCENT = 90;
382
394
  function observeRunnerDiskGate(input = {}) {
383
- const path15 = input.diskPath?.trim() || "/";
395
+ const path16 = input.diskPath?.trim() || "/";
384
396
  const warnBelowBytes = input.diskFreeWarnBytes ?? DEFAULT_WARN_FREE_BYTES;
385
397
  const criticalBelowBytes = input.diskFreeCriticalBytes ?? DEFAULT_CRITICAL_FREE_BYTES;
386
398
  const maxUsedPercent = input.diskMaxUsedPercent ?? DEFAULT_MAX_USED_PERCENT;
387
399
  const hardMaxUsedPercent = input.diskHardMaxUsedPercent ?? DEFAULT_HARD_MAX_USED_PERCENT;
388
- const stats = statfsSync(path15);
400
+ const stats = statfsSync(path16);
389
401
  const freeBytes = Number(stats.bavail) * Number(stats.bsize);
390
402
  const totalBytes = Number(stats.blocks) * Number(stats.bsize);
391
403
  const usedPercent = totalBytes > 0 ? (totalBytes - freeBytes) / totalBytes * 100 : 100;
@@ -405,7 +417,7 @@ function observeRunnerDiskGate(input = {}) {
405
417
  }
406
418
  return {
407
419
  ok,
408
- path: path15,
420
+ path: path16,
409
421
  freeBytes,
410
422
  totalBytes,
411
423
  usedPercent,
@@ -895,8 +907,8 @@ function observeRunnerResourceGate(input) {
895
907
  }
896
908
 
897
909
  // src/supervisor.ts
898
- import { existsSync as existsSync8, mkdirSync as mkdirSync3 } from "node:fs";
899
- import path7 from "node:path";
910
+ import { existsSync as existsSync9, mkdirSync as mkdirSync3 } from "node:fs";
911
+ import path9 from "node:path";
900
912
 
901
913
  // src/prompt.ts
902
914
  function buildPrompt(input) {
@@ -1070,6 +1082,369 @@ function resolveWorkerProvider(name) {
1070
1082
  return provider;
1071
1083
  }
1072
1084
 
1085
+ // src/auto-complete.ts
1086
+ import { spawn as spawn3 } from "node:child_process";
1087
+ import { existsSync as existsSync8, openSync as openSync3, closeSync as closeSync3 } from "node:fs";
1088
+ import path8 from "node:path";
1089
+ import { fileURLToPath } from "node:url";
1090
+
1091
+ // src/worker-ops.ts
1092
+ import path7 from "node:path";
1093
+ async function postCompletion(url, secret, body) {
1094
+ const res = await fetch(url, {
1095
+ method: "POST",
1096
+ headers: buildHarnessCallbackHeaders(secret),
1097
+ body: JSON.stringify(body)
1098
+ });
1099
+ let parsed = null;
1100
+ try {
1101
+ parsed = await res.json();
1102
+ } catch {
1103
+ parsed = null;
1104
+ }
1105
+ return { ok: res.ok, status: res.status, parsed };
1106
+ }
1107
+ function completionErrorText(parsed) {
1108
+ if (parsed && typeof parsed === "object") {
1109
+ const err = parsed.error;
1110
+ if (typeof err === "string" && err.trim()) return err.trim();
1111
+ }
1112
+ return void 0;
1113
+ }
1114
+ function persistCompletionBlocker(worker, reason) {
1115
+ const current = worker.completionBlocker;
1116
+ if ((current ?? void 0) === (reason ?? void 0)) return;
1117
+ if (reason) worker.completionBlocker = reason;
1118
+ else delete worker.completionBlocker;
1119
+ saveWorker(worker.runId, worker);
1120
+ }
1121
+ async function tryCompleteWorker(args) {
1122
+ const worker = loadWorker(String(args.run), String(args.name));
1123
+ const status = computeWorkerStatus(worker);
1124
+ const agentOsId = (args.agentOsId ? String(args.agentOsId) : worker.agentOsId) || "";
1125
+ const taskId = (args.taskId ? String(args.taskId) : worker.taskId) || null;
1126
+ if (!agentOsId) {
1127
+ return { ok: false, reason: "missing agentOsId" };
1128
+ }
1129
+ if (!isFinishedWorkerStatus(status)) {
1130
+ return { ok: true, skipped: true, reason: "worker-not-finished" };
1131
+ }
1132
+ const base = resolveBaseUrl(args.baseUrl ? String(args.baseUrl) : void 0);
1133
+ const explicitSecret = args.secret ? String(args.secret) : void 0;
1134
+ let secret = await resolveCallbackSecretWithMint(explicitSecret, agentOsId, { baseUrl: base });
1135
+ const url = `${base}/api/agent-os/by-id/${encodeURIComponent(agentOsId)}/harness/completion`;
1136
+ const body = {
1137
+ source: "openclaw-harness",
1138
+ agentOsId,
1139
+ runId: worker.runId,
1140
+ workerName: worker.name,
1141
+ taskId,
1142
+ startedAt: worker.startedAt,
1143
+ finishedAt: status.lastActivityAt || (/* @__PURE__ */ new Date()).toISOString(),
1144
+ status
1145
+ };
1146
+ let result = await postCompletion(url, secret, body);
1147
+ if ((result.status === 401 || result.status === 403) && !explicitSecret) {
1148
+ const refreshed = await refreshRunnerToken(agentOsId, { baseUrl: base });
1149
+ if (refreshed && refreshed !== secret) {
1150
+ secret = refreshed;
1151
+ result = await postCompletion(url, secret, body);
1152
+ }
1153
+ }
1154
+ if (result.ok) {
1155
+ persistCompletionBlocker(worker, void 0);
1156
+ return { ok: true, httpStatus: result.status, response: result.parsed };
1157
+ }
1158
+ const authRejected = result.status === 401 || result.status === 403;
1159
+ const detail = completionErrorText(result.parsed) ?? (authRejected ? "runner token unauthorized" : "non-2xx response");
1160
+ const reason = authRejected ? `completion replay rejected (${result.status}): ${detail}` : `completion replay failed (${result.status}): ${detail}`;
1161
+ persistCompletionBlocker(worker, reason);
1162
+ return { ok: false, httpStatus: result.status, response: result.parsed, completionBlocked: true };
1163
+ }
1164
+ async function completeWorker(args) {
1165
+ try {
1166
+ const worker = loadWorker(String(args.run), String(args.name));
1167
+ const status = computeWorkerStatus(worker);
1168
+ const agentOsId = (args.agentOsId ? String(args.agentOsId) : worker.agentOsId) || "";
1169
+ const taskId = (args.taskId ? String(args.taskId) : worker.taskId) || null;
1170
+ if (!agentOsId) {
1171
+ console.error("worker complete requires --agent-os-id (or an agentOsId persisted at worker start)");
1172
+ process.exit(1);
1173
+ }
1174
+ if (!isFinishedWorkerStatus(status)) {
1175
+ console.log(
1176
+ JSON.stringify(
1177
+ {
1178
+ worker: worker.name,
1179
+ runId: worker.runId,
1180
+ status: "skipped",
1181
+ reason: "worker-not-finished",
1182
+ workerStatus: status.status,
1183
+ alive: status.alive
1184
+ },
1185
+ null,
1186
+ 2
1187
+ )
1188
+ );
1189
+ return;
1190
+ }
1191
+ const result = await tryCompleteWorker(args);
1192
+ console.log(
1193
+ JSON.stringify(
1194
+ {
1195
+ worker: worker.name,
1196
+ runId: worker.runId,
1197
+ agentOsId,
1198
+ taskId,
1199
+ httpStatus: result.httpStatus,
1200
+ response: result.response
1201
+ },
1202
+ null,
1203
+ 2
1204
+ )
1205
+ );
1206
+ if (!result.ok) process.exit(1);
1207
+ } catch (error) {
1208
+ console.error(`worker complete failed: ${error.message}`);
1209
+ process.exit(1);
1210
+ }
1211
+ }
1212
+ function workerStatus(args) {
1213
+ const worker = loadWorker(String(args.run), String(args.name));
1214
+ const status = computeWorkerStatus(worker);
1215
+ writeJson(path7.join(worker.workerDir, "last-status.json"), status);
1216
+ console.log(JSON.stringify(status, null, 2));
1217
+ }
1218
+ function runStatus(args) {
1219
+ const run = loadRun(String(args.run));
1220
+ const names = Object.keys(run.workers || {});
1221
+ const workers = names.map((name) => {
1222
+ const worker = readJson(
1223
+ path7.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
1224
+ void 0
1225
+ );
1226
+ if (!worker) {
1227
+ return { worker: name, status: "missing", attention: "needs_attention", attentionReason: "worker.json not found" };
1228
+ }
1229
+ const status = computeWorkerStatus(worker, { base: run.base });
1230
+ const rawBlocker = worker.completionBlocker;
1231
+ const completionBlocker = typeof rawBlocker === "string" && rawBlocker ? rawBlocker : void 0;
1232
+ return {
1233
+ worker: status.worker,
1234
+ status: completionBlocker ? "blocked" : status.status,
1235
+ attention: completionBlocker ? "blocked" : status.attention.state,
1236
+ attentionReason: completionBlocker ?? status.attention.reason,
1237
+ pid: status.pid,
1238
+ alive: status.alive,
1239
+ currentTool: status.currentTool,
1240
+ lastActivityAt: status.lastActivityAt,
1241
+ lastHeartbeatPhase: status.lastHeartbeatPhase,
1242
+ lastHeartbeatSummary: status.lastHeartbeatSummary,
1243
+ heartbeatBlocker: status.heartbeatBlocker,
1244
+ changedFileCount: status.changedFiles.length,
1245
+ branch: status.branch,
1246
+ ancestry: status.gitAncestry.relation,
1247
+ ancestryChecked: status.gitAncestry.checked
1248
+ };
1249
+ });
1250
+ const board = {
1251
+ runId: run.id,
1252
+ name: run.name,
1253
+ status: deriveRunStatus(run.status, workers),
1254
+ repo: run.repo,
1255
+ workerCount: workers.length,
1256
+ needsAttention: workers.filter((w) => w.attention && w.attention !== "ok" && w.attention !== "done").map((w) => w.worker),
1257
+ workers
1258
+ };
1259
+ writeJson(path7.join(runDirectory(run.id), "last-board.json"), board);
1260
+ console.log(JSON.stringify(board, null, 2));
1261
+ }
1262
+ function tailWorker(args) {
1263
+ const worker = loadWorker(String(args.run), String(args.name));
1264
+ const raw = tailFile(worker.stdoutPath, Number(args.lines || 40));
1265
+ if (args.raw === true || args.raw === "true") {
1266
+ process.stdout.write(raw);
1267
+ return;
1268
+ }
1269
+ for (const line of raw.split("\n").filter(Boolean)) {
1270
+ const event = safeJson(line);
1271
+ const summary = event ? summarizeEvent(event) : line;
1272
+ if (summary) console.log(summary);
1273
+ }
1274
+ }
1275
+ function stopWorker(args) {
1276
+ const worker = loadWorker(String(args.run), String(args.name));
1277
+ if (!isPidAlive(worker.pid)) {
1278
+ console.log(JSON.stringify({ worker: worker.name, pid: worker.pid, status: "not_running" }, null, 2));
1279
+ return;
1280
+ }
1281
+ killWorkerProcess(worker.pid, "SIGTERM");
1282
+ sleepMs(1500);
1283
+ if (isPidAlive(worker.pid)) {
1284
+ killWorkerProcess(worker.pid, "SIGKILL");
1285
+ console.log(JSON.stringify({ worker: worker.name, pid: worker.pid, status: "sigkill_sent" }, null, 2));
1286
+ return;
1287
+ }
1288
+ console.log(JSON.stringify({ worker: worker.name, pid: worker.pid, status: "stopped" }, null, 2));
1289
+ }
1290
+
1291
+ // src/auto-complete.ts
1292
+ var DEFAULT_POLL_MS = 5e3;
1293
+ var DEFAULT_MAX_TOTAL_MS = 6 * 60 * 60 * 1e3;
1294
+ var DEFAULT_COMPLETE_ATTEMPTS = 3;
1295
+ var DEFAULT_COMPLETE_BACKOFF_MS = 5e3;
1296
+ function readArgs(raw) {
1297
+ return {
1298
+ run: String(raw.run || ""),
1299
+ name: String(raw.name || ""),
1300
+ agentOsId: raw.agentOsId ? String(raw.agentOsId) : void 0,
1301
+ pollMs: Number(raw.pollMs) > 0 ? Math.floor(Number(raw.pollMs)) : void 0,
1302
+ maxTotalMs: Number(raw.maxTotalMs) > 0 ? Math.floor(Number(raw.maxTotalMs)) : void 0,
1303
+ completeAttempts: Number(raw.completeAttempts) > 0 ? Math.floor(Number(raw.completeAttempts)) : void 0,
1304
+ completeBackoffMs: Number(raw.completeBackoffMs) > 0 ? Math.floor(Number(raw.completeBackoffMs)) : void 0,
1305
+ baseUrl: raw.baseUrl ? String(raw.baseUrl) : void 0,
1306
+ secret: raw.secret ? String(raw.secret) : void 0
1307
+ };
1308
+ }
1309
+ async function autoCompleteWorker(raw) {
1310
+ const args = readArgs(raw);
1311
+ const pollMs = args.pollMs ?? DEFAULT_POLL_MS;
1312
+ const maxTotalMs = args.maxTotalMs ?? DEFAULT_MAX_TOTAL_MS;
1313
+ const completeAttempts = args.completeAttempts ?? DEFAULT_COMPLETE_ATTEMPTS;
1314
+ const completeBackoffMs = args.completeBackoffMs ?? DEFAULT_COMPLETE_BACKOFF_MS;
1315
+ const worker = loadWorker(args.run, args.name);
1316
+ if (!worker.agentOsId || !worker.taskId) {
1317
+ return {
1318
+ worker: worker.name,
1319
+ runId: worker.runId,
1320
+ outcome: "missing_link",
1321
+ attempts: 0,
1322
+ reason: "worker has no agentOsId/taskId \u2014 nothing to attribute completion to"
1323
+ };
1324
+ }
1325
+ const startMs = Date.now();
1326
+ while (true) {
1327
+ const status = computeWorkerStatus(worker);
1328
+ if (isFinishedWorkerStatus(status)) break;
1329
+ if (!isPidAlive(worker.pid)) break;
1330
+ if (Date.now() - startMs > maxTotalMs) {
1331
+ return {
1332
+ worker: worker.name,
1333
+ runId: worker.runId,
1334
+ outcome: "timed_out",
1335
+ attempts: 0,
1336
+ reason: `worker did not finish within ${maxTotalMs}ms`
1337
+ };
1338
+ }
1339
+ sleepMs(pollMs);
1340
+ }
1341
+ let lastHttpStatus;
1342
+ let lastReason;
1343
+ for (let attempt = 1; attempt <= completeAttempts; attempt++) {
1344
+ const result = await tryCompleteWorker({
1345
+ run: args.run,
1346
+ name: args.name,
1347
+ ...args.agentOsId ? { agentOsId: args.agentOsId } : {},
1348
+ ...args.baseUrl ? { baseUrl: args.baseUrl } : {},
1349
+ ...args.secret ? { secret: args.secret } : {}
1350
+ });
1351
+ lastHttpStatus = result.httpStatus;
1352
+ if (result.ok) {
1353
+ return {
1354
+ worker: worker.name,
1355
+ runId: worker.runId,
1356
+ outcome: "completed",
1357
+ httpStatus: result.httpStatus,
1358
+ attempts: attempt
1359
+ };
1360
+ }
1361
+ const authRejected = result.httpStatus === 401 || result.httpStatus === 403;
1362
+ if (authRejected) {
1363
+ lastReason = typeof result.reason === "string" ? result.reason : "completion replay refused";
1364
+ return {
1365
+ worker: worker.name,
1366
+ runId: worker.runId,
1367
+ outcome: "blocked",
1368
+ httpStatus: result.httpStatus,
1369
+ attempts: attempt,
1370
+ reason: lastReason
1371
+ };
1372
+ }
1373
+ lastReason = typeof result.reason === "string" ? result.reason : "transient failure";
1374
+ if (attempt < completeAttempts) sleepMs(completeBackoffMs);
1375
+ }
1376
+ return {
1377
+ worker: worker.name,
1378
+ runId: worker.runId,
1379
+ outcome: "blocked",
1380
+ httpStatus: lastHttpStatus,
1381
+ attempts: completeAttempts,
1382
+ reason: lastReason ?? "completion failed after retries"
1383
+ };
1384
+ }
1385
+ async function autoCompleteWorkerCli(raw) {
1386
+ try {
1387
+ const outcome = await autoCompleteWorker(raw);
1388
+ console.log(JSON.stringify(outcome, null, 2));
1389
+ if (outcome.outcome === "missing_link" || outcome.outcome === "timed_out") {
1390
+ process.exitCode = 1;
1391
+ }
1392
+ } catch (error) {
1393
+ console.error(`worker auto-complete failed: ${error.message}`);
1394
+ process.exitCode = 1;
1395
+ }
1396
+ }
1397
+ function resolveDefaultCliPath() {
1398
+ return path8.join(fileURLToPath(new URL(".", import.meta.url)), "cli.js");
1399
+ }
1400
+ function spawnCompletionSidecar(opts) {
1401
+ const cliPath = opts.cliPath ?? resolveDefaultCliPath();
1402
+ if (!existsSync8(cliPath)) return void 0;
1403
+ const logPath = path8.join(opts.workerDir, "auto-complete.log");
1404
+ let logFd;
1405
+ try {
1406
+ logFd = openSync3(logPath, "a");
1407
+ } catch {
1408
+ logFd = void 0;
1409
+ }
1410
+ const stdio = [
1411
+ "ignore",
1412
+ logFd ?? "ignore",
1413
+ logFd ?? "ignore"
1414
+ ];
1415
+ const nodeExecutable = opts.nodeExecutable ?? process.execPath;
1416
+ const args = [
1417
+ cliPath,
1418
+ "worker",
1419
+ "auto-complete",
1420
+ "--run",
1421
+ opts.runId,
1422
+ "--name",
1423
+ opts.workerName
1424
+ ];
1425
+ if (opts.agentOsId) args.push("--agent-os-id", opts.agentOsId);
1426
+ if (opts.baseUrl) args.push("--base-url", opts.baseUrl);
1427
+ if (opts.secret) args.push("--secret", opts.secret);
1428
+ try {
1429
+ const child = spawn3(nodeExecutable, args, {
1430
+ detached: true,
1431
+ stdio,
1432
+ env: process.env
1433
+ });
1434
+ if (logFd !== void 0) closeSync3(logFd);
1435
+ child.unref();
1436
+ return { pid: child.pid, logPath, cliPath };
1437
+ } catch {
1438
+ if (logFd !== void 0) {
1439
+ try {
1440
+ closeSync3(logFd);
1441
+ } catch {
1442
+ }
1443
+ }
1444
+ return void 0;
1445
+ }
1446
+ }
1447
+
1073
1448
  // src/supervisor.ts
1074
1449
  function spawnWorkerProcess(run, opts) {
1075
1450
  const rawName = typeof opts.name === "string" ? opts.name.trim() : "";
@@ -1080,16 +1455,16 @@ function spawnWorkerProcess(run, opts) {
1080
1455
  if (run.workers?.[name]) throw new Error(`worker already exists in run ${run.id}: ${name}`);
1081
1456
  if (!opts.task) throw new Error(`missing task text for worker ${name}`);
1082
1457
  const { worktreesDir } = getPaths();
1083
- const workerDir = path7.join(runDirectory(run.id), "workers", name);
1458
+ const workerDir = path9.join(runDirectory(run.id), "workers", name);
1084
1459
  mkdirSync3(workerDir, { recursive: true });
1085
- const worktreePath = path7.join(worktreesDir, run.id, name);
1460
+ const worktreePath = path9.join(worktreesDir, run.id, name);
1086
1461
  const branch = opts.branch || `agent/${run.id}/${name}`;
1087
- if (existsSync8(worktreePath)) throw new Error(`worktree path already exists: ${worktreePath}`);
1462
+ if (existsSync9(worktreePath)) throw new Error(`worktree path already exists: ${worktreePath}`);
1088
1463
  git(run.repo, ["fetch", "origin", "--prune"], { allowFailure: true });
1089
1464
  git(run.repo, ["worktree", "add", "-b", branch, worktreePath, run.baseCommit], { throwError: true });
1090
- const stdoutPath = path7.join(workerDir, "stdout.jsonl");
1091
- const stderrPath = path7.join(workerDir, "stderr.log");
1092
- const heartbeatPath = path7.join(workerDir, "heartbeat.jsonl");
1465
+ const stdoutPath = path9.join(workerDir, "stdout.jsonl");
1466
+ const stderrPath = path9.join(workerDir, "stderr.log");
1467
+ const heartbeatPath = path9.join(workerDir, "heartbeat.jsonl");
1093
1468
  const prompt = buildPrompt({
1094
1469
  task: opts.task,
1095
1470
  ownedPaths: opts.ownedPaths || [],
@@ -1139,9 +1514,20 @@ function spawnWorkerProcess(run, opts) {
1139
1514
  startedAt: (/* @__PURE__ */ new Date()).toISOString()
1140
1515
  };
1141
1516
  saveWorker(run.id, worker);
1142
- run.workers = { ...run.workers || {}, [name]: { workerDir, statusPath: path7.join(workerDir, "worker.json") } };
1517
+ run.workers = { ...run.workers || {}, [name]: { workerDir, statusPath: path9.join(workerDir, "worker.json") } };
1143
1518
  run.status = "running";
1144
1519
  saveRun(run);
1520
+ if (worker.agentOsId && worker.taskId) {
1521
+ try {
1522
+ spawnCompletionSidecar({
1523
+ runId: run.id,
1524
+ workerName: name,
1525
+ workerDir,
1526
+ agentOsId: worker.agentOsId
1527
+ });
1528
+ } catch {
1529
+ }
1530
+ }
1145
1531
  return worker;
1146
1532
  }
1147
1533
  function startWorker(args) {
@@ -1342,7 +1728,7 @@ async function dispatchRun(args) {
1342
1728
  }
1343
1729
 
1344
1730
  // src/sweep.ts
1345
- import path8 from "node:path";
1731
+ import path10 from "node:path";
1346
1732
  async function sweepRun(args) {
1347
1733
  const pipeline = args.pipeline === true || args.pipeline === "true";
1348
1734
  try {
@@ -1354,7 +1740,7 @@ async function sweepRun(args) {
1354
1740
  const releasedLocalOrphans = [];
1355
1741
  for (const name of Object.keys(run.workers || {})) {
1356
1742
  const worker = readJson(
1357
- path8.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
1743
+ path10.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
1358
1744
  void 0
1359
1745
  );
1360
1746
  if (!worker || !worker.dispatched || !worker.taskId) continue;
@@ -1397,11 +1783,11 @@ async function sweepRun(args) {
1397
1783
  }
1398
1784
 
1399
1785
  // src/worktree.ts
1400
- import { existsSync as existsSync9, mkdirSync as mkdirSync4 } from "node:fs";
1401
- import path10 from "node:path";
1786
+ import { existsSync as existsSync10, mkdirSync as mkdirSync4 } from "node:fs";
1787
+ import path12 from "node:path";
1402
1788
 
1403
1789
  // src/validate.ts
1404
- import path9 from "node:path";
1790
+ import path11 from "node:path";
1405
1791
  var RUN_ID_RE = /^[a-z0-9][a-z0-9._-]{0,127}$/i;
1406
1792
  function validateRunId(runId) {
1407
1793
  const trimmed = runId.trim();
@@ -1409,7 +1795,7 @@ function validateRunId(runId) {
1409
1795
  return trimmed;
1410
1796
  }
1411
1797
  function validateRepo(repo) {
1412
- const resolved = path9.resolve(repo);
1798
+ const resolved = path11.resolve(repo);
1413
1799
  if (resolved.includes("..")) throw new Error("repo path must not contain .. segments");
1414
1800
  return resolved;
1415
1801
  }
@@ -1420,7 +1806,7 @@ function createRun(args) {
1420
1806
  ensureGitRepo(repo);
1421
1807
  const id = args.id ? validateRunId(String(args.id)) : timestampSlug(String(args.name || "run"));
1422
1808
  const dir = runDirectory(id);
1423
- if (existsSync9(dir)) failExists(`run already exists: ${id}`);
1809
+ if (existsSync10(dir)) failExists(`run already exists: ${id}`);
1424
1810
  mkdirSync4(dir, { recursive: true });
1425
1811
  const base = String(args.base || "origin/main");
1426
1812
  const baseCommit = git(repo, ["rev-parse", base]).trim();
@@ -1434,12 +1820,12 @@ function createRun(args) {
1434
1820
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1435
1821
  workers: {}
1436
1822
  };
1437
- writeJson(path10.join(dir, "run.json"), run);
1823
+ writeJson(path12.join(dir, "run.json"), run);
1438
1824
  console.log(JSON.stringify({ runId: id, runDir: dir, repo, base, baseCommit }, null, 2));
1439
1825
  }
1440
1826
  function listRuns() {
1441
1827
  const { runsDir } = getPaths();
1442
- const rows = listRunIds(runsDir).map((id) => readJson(path10.join(runDirectory(id), "run.json"), void 0)).filter(Boolean).map((run) => ({
1828
+ const rows = listRunIds(runsDir).map((id) => readJson(path12.join(runDirectory(id), "run.json"), void 0)).filter(Boolean).map((run) => ({
1443
1829
  id: run.id,
1444
1830
  name: run.name,
1445
1831
  status: run.status,
@@ -1453,184 +1839,21 @@ function failExists(message) {
1453
1839
  process.exit(1);
1454
1840
  }
1455
1841
 
1456
- // src/worker-ops.ts
1457
- import path11 from "node:path";
1458
- async function tryCompleteWorker(args) {
1459
- const worker = loadWorker(String(args.run), String(args.name));
1460
- const status = computeWorkerStatus(worker);
1461
- const agentOsId = (args.agentOsId ? String(args.agentOsId) : worker.agentOsId) || "";
1462
- const taskId = (args.taskId ? String(args.taskId) : worker.taskId) || null;
1463
- if (!agentOsId) {
1464
- return { ok: false, reason: "missing agentOsId" };
1465
- }
1466
- if (!isFinishedWorkerStatus(status)) {
1467
- return { ok: true, skipped: true, reason: "worker-not-finished" };
1468
- }
1469
- const base = resolveBaseUrl(args.baseUrl ? String(args.baseUrl) : void 0);
1470
- const secret = await resolveCallbackSecretWithMint(args.secret ? String(args.secret) : void 0, agentOsId, { baseUrl: base });
1471
- const url = `${base}/api/agent-os/by-id/${encodeURIComponent(agentOsId)}/harness/completion`;
1472
- const body = {
1473
- source: "openclaw-harness",
1474
- agentOsId,
1475
- runId: worker.runId,
1476
- workerName: worker.name,
1477
- taskId,
1478
- startedAt: worker.startedAt,
1479
- finishedAt: status.lastActivityAt || (/* @__PURE__ */ new Date()).toISOString(),
1480
- status
1481
- };
1482
- const res = await fetch(url, {
1483
- method: "POST",
1484
- headers: buildHarnessCallbackHeaders(secret),
1485
- body: JSON.stringify(body)
1486
- });
1487
- let parsed = null;
1488
- try {
1489
- parsed = await res.json();
1490
- } catch {
1491
- parsed = null;
1492
- }
1493
- return { ok: res.ok, httpStatus: res.status, response: parsed };
1494
- }
1495
- async function completeWorker(args) {
1496
- try {
1497
- const worker = loadWorker(String(args.run), String(args.name));
1498
- const status = computeWorkerStatus(worker);
1499
- const agentOsId = (args.agentOsId ? String(args.agentOsId) : worker.agentOsId) || "";
1500
- const taskId = (args.taskId ? String(args.taskId) : worker.taskId) || null;
1501
- if (!agentOsId) {
1502
- console.error("worker complete requires --agent-os-id (or an agentOsId persisted at worker start)");
1503
- process.exit(1);
1504
- }
1505
- if (!isFinishedWorkerStatus(status)) {
1506
- console.log(
1507
- JSON.stringify(
1508
- {
1509
- worker: worker.name,
1510
- runId: worker.runId,
1511
- status: "skipped",
1512
- reason: "worker-not-finished",
1513
- workerStatus: status.status,
1514
- alive: status.alive
1515
- },
1516
- null,
1517
- 2
1518
- )
1519
- );
1520
- return;
1521
- }
1522
- const result = await tryCompleteWorker(args);
1523
- console.log(
1524
- JSON.stringify(
1525
- {
1526
- worker: worker.name,
1527
- runId: worker.runId,
1528
- agentOsId,
1529
- taskId,
1530
- httpStatus: result.httpStatus,
1531
- response: result.response
1532
- },
1533
- null,
1534
- 2
1535
- )
1536
- );
1537
- if (!result.ok) process.exit(1);
1538
- } catch (error) {
1539
- console.error(`worker complete failed: ${error.message}`);
1540
- process.exit(1);
1541
- }
1542
- }
1543
- function workerStatus(args) {
1544
- const worker = loadWorker(String(args.run), String(args.name));
1545
- const status = computeWorkerStatus(worker);
1546
- writeJson(path11.join(worker.workerDir, "last-status.json"), status);
1547
- console.log(JSON.stringify(status, null, 2));
1548
- }
1549
- function runStatus(args) {
1550
- const run = loadRun(String(args.run));
1551
- const names = Object.keys(run.workers || {});
1552
- const workers = names.map((name) => {
1553
- const worker = readJson(
1554
- path11.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
1555
- void 0
1556
- );
1557
- if (!worker) {
1558
- return { worker: name, status: "missing", attention: "needs_attention", attentionReason: "worker.json not found" };
1559
- }
1560
- const status = computeWorkerStatus(worker, { base: run.base });
1561
- return {
1562
- worker: status.worker,
1563
- status: status.status,
1564
- attention: status.attention.state,
1565
- attentionReason: status.attention.reason,
1566
- pid: status.pid,
1567
- alive: status.alive,
1568
- currentTool: status.currentTool,
1569
- lastActivityAt: status.lastActivityAt,
1570
- lastHeartbeatPhase: status.lastHeartbeatPhase,
1571
- lastHeartbeatSummary: status.lastHeartbeatSummary,
1572
- heartbeatBlocker: status.heartbeatBlocker,
1573
- changedFileCount: status.changedFiles.length,
1574
- branch: status.branch,
1575
- ancestry: status.gitAncestry.relation,
1576
- ancestryChecked: status.gitAncestry.checked
1577
- };
1578
- });
1579
- const board = {
1580
- runId: run.id,
1581
- name: run.name,
1582
- status: deriveRunStatus(run.status, workers),
1583
- repo: run.repo,
1584
- workerCount: workers.length,
1585
- needsAttention: workers.filter((w) => w.attention && w.attention !== "ok" && w.attention !== "done").map((w) => w.worker),
1586
- workers
1587
- };
1588
- writeJson(path11.join(runDirectory(run.id), "last-board.json"), board);
1589
- console.log(JSON.stringify(board, null, 2));
1590
- }
1591
- function tailWorker(args) {
1592
- const worker = loadWorker(String(args.run), String(args.name));
1593
- const raw = tailFile(worker.stdoutPath, Number(args.lines || 40));
1594
- if (args.raw === true || args.raw === "true") {
1595
- process.stdout.write(raw);
1596
- return;
1597
- }
1598
- for (const line of raw.split("\n").filter(Boolean)) {
1599
- const event = safeJson(line);
1600
- const summary = event ? summarizeEvent(event) : line;
1601
- if (summary) console.log(summary);
1602
- }
1603
- }
1604
- function stopWorker(args) {
1605
- const worker = loadWorker(String(args.run), String(args.name));
1606
- if (!isPidAlive(worker.pid)) {
1607
- console.log(JSON.stringify({ worker: worker.name, pid: worker.pid, status: "not_running" }, null, 2));
1608
- return;
1609
- }
1610
- killWorkerProcess(worker.pid, "SIGTERM");
1611
- sleepMs(1500);
1612
- if (isPidAlive(worker.pid)) {
1613
- killWorkerProcess(worker.pid, "SIGKILL");
1614
- console.log(JSON.stringify({ worker: worker.name, pid: worker.pid, status: "sigkill_sent" }, null, 2));
1615
- return;
1616
- }
1617
- console.log(JSON.stringify({ worker: worker.name, pid: worker.pid, status: "stopped" }, null, 2));
1618
- }
1619
-
1620
1842
  // src/pipeline-tick.ts
1621
- import path14 from "node:path";
1843
+ import path15 from "node:path";
1622
1844
 
1623
1845
  // src/finalize.ts
1624
- import path12 from "node:path";
1846
+ import path13 from "node:path";
1625
1847
  var ACTIVE_RUN_STATUSES = /* @__PURE__ */ new Set(["running", "dispatching", "pending", "queued"]);
1626
1848
  function terminalStatusFor(run) {
1627
1849
  const names = Object.keys(run.workers || {});
1628
1850
  if (names.length === 0) return "failed";
1629
1851
  let anyAlive = false;
1630
1852
  let anyResult = false;
1853
+ let anyCompletionBlocked = false;
1631
1854
  for (const name of names) {
1632
1855
  const worker = readJson(
1633
- path12.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
1856
+ path13.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
1634
1857
  void 0
1635
1858
  );
1636
1859
  if (!worker) continue;
@@ -1639,9 +1862,13 @@ function terminalStatusFor(run) {
1639
1862
  anyAlive = true;
1640
1863
  break;
1641
1864
  }
1865
+ if (typeof worker.completionBlocker === "string" && worker.completionBlocker) {
1866
+ anyCompletionBlocked = true;
1867
+ }
1642
1868
  if (status.finalResult) anyResult = true;
1643
1869
  }
1644
1870
  if (anyAlive) return null;
1871
+ if (anyCompletionBlocked) return null;
1645
1872
  return anyResult ? "completed" : "failed";
1646
1873
  }
1647
1874
  function finalizeStaleRuns() {
@@ -1659,7 +1886,7 @@ function finalizeStaleRuns() {
1659
1886
  }
1660
1887
 
1661
1888
  // src/plan-progress-daemon-sync.ts
1662
- import path13 from "node:path";
1889
+ import path14 from "node:path";
1663
1890
 
1664
1891
  // src/plan-progress-sync.ts
1665
1892
  async function syncPlanProgress(args) {
@@ -1683,7 +1910,7 @@ async function syncActiveWorkerPlanProgress(runId, args) {
1683
1910
  const outcomes = [];
1684
1911
  for (const name of Object.keys(run.workers || {})) {
1685
1912
  const worker = readJson(
1686
- path13.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
1913
+ path14.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
1687
1914
  void 0
1688
1915
  );
1689
1916
  if (!worker?.dispatched || !worker.taskId) continue;
@@ -1737,12 +1964,13 @@ async function completeFinishedWorkers(runId, args) {
1737
1964
  const outcomes = [];
1738
1965
  for (const name of Object.keys(run.workers || {})) {
1739
1966
  const worker = readJson(
1740
- path14.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
1967
+ path15.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
1741
1968
  void 0
1742
1969
  );
1743
- if (!worker?.dispatched || !worker.taskId) continue;
1970
+ if (!worker?.taskId) continue;
1744
1971
  const status = computeWorkerStatus(worker);
1745
1972
  if (!isFinishedWorkerStatus(status)) continue;
1973
+ if (!worker.dispatched && !status.finalResult) continue;
1746
1974
  const result = await tryCompleteWorker({
1747
1975
  run: runId,
1748
1976
  name,
@@ -1770,8 +1998,8 @@ async function runPipelineTick(args) {
1770
1998
  const agentOsId = String(required(String(args.agentOsId || ""), "--agent-os-id"));
1771
1999
  const execute = args.execute !== false && args.execute !== "false";
1772
2000
  runStatus({ run: runId });
1773
- const finalizedStaleRuns = finalizeStaleRuns();
1774
2001
  const completedWorkers = await completeFinishedWorkers(runId, args);
2002
+ const finalizedStaleRuns = finalizeStaleRuns();
1775
2003
  const planProgressSync = await syncActiveWorkerPlanProgress(runId, args);
1776
2004
  const workspacePrefs = await fetchWorkspaceRuntimePreferences(agentOsId, args);
1777
2005
  const resourceGate = observeRunnerResourceGate({
@@ -1979,6 +2207,7 @@ function usage(code = 0) {
1979
2207
  " kynver worker tail --run RUN_ID --name worker [--lines 40] [--raw]",
1980
2208
  " kynver worker stop --run RUN_ID --name worker",
1981
2209
  " kynver worker complete --run RUN_ID --name worker [--agent-os-id AOS_ID] [--task-id TASK_ID] [--base-url URL] [--secret SECRET]",
2210
+ " 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]",
1982
2211
  " 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]",
1983
2212
  " kynver plan verify --plan PLAN_ID [--worktree PATH] [--task TASK_ID] [--human-override]"
1984
2213
  ].join("\n")
@@ -2017,9 +2246,10 @@ async function main(argv = process.argv.slice(2)) {
2017
2246
  if (scope === "worker" && action === "tail") return tailWorker(args);
2018
2247
  if (scope === "worker" && action === "stop") return stopWorker(args);
2019
2248
  if (scope === "worker" && action === "complete") return void await completeWorker(args);
2249
+ if (scope === "worker" && action === "auto-complete") return void await autoCompleteWorkerCli(args);
2020
2250
  unknownCommand(scope, action);
2021
2251
  }
2022
- var isCliEntry = process.argv[1] && realpathSync.native(process.argv[1]) === realpathSync.native(fileURLToPath(import.meta.url));
2252
+ var isCliEntry = process.argv[1] && realpathSync.native(process.argv[1]) === realpathSync.native(fileURLToPath2(import.meta.url));
2023
2253
  if (isCliEntry) {
2024
2254
  void main().catch((error) => {
2025
2255
  console.error(error);