@kynver-app/runtime 0.1.16 → 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/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 path15 = input.diskPath?.trim() || "/";
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(path15);
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: path15,
421
+ path: path16,
422
422
  freeBytes,
423
423
  totalBytes,
424
424
  usedPercent,
@@ -908,8 +908,8 @@ function observeRunnerResourceGate(input) {
908
908
  }
909
909
 
910
910
  // src/supervisor.ts
911
- import { existsSync as existsSync8, mkdirSync as mkdirSync3 } from "node:fs";
912
- import path7 from "node:path";
911
+ import { existsSync as existsSync9, mkdirSync as mkdirSync3 } from "node:fs";
912
+ import path9 from "node:path";
913
913
 
914
914
  // src/prompt.ts
915
915
  function buildPrompt(input) {
@@ -1083,6 +1083,369 @@ function resolveWorkerProvider(name) {
1083
1083
  return provider;
1084
1084
  }
1085
1085
 
1086
+ // src/auto-complete.ts
1087
+ import { spawn as spawn3 } from "node:child_process";
1088
+ import { existsSync as existsSync8, openSync as openSync3, closeSync as closeSync3 } from "node:fs";
1089
+ import path8 from "node:path";
1090
+ import { fileURLToPath } from "node:url";
1091
+
1092
+ // src/worker-ops.ts
1093
+ import path7 from "node:path";
1094
+ async function postCompletion(url, secret, body) {
1095
+ const res = await fetch(url, {
1096
+ method: "POST",
1097
+ headers: buildHarnessCallbackHeaders(secret),
1098
+ body: JSON.stringify(body)
1099
+ });
1100
+ let parsed = null;
1101
+ try {
1102
+ parsed = await res.json();
1103
+ } catch {
1104
+ parsed = null;
1105
+ }
1106
+ return { ok: res.ok, status: res.status, parsed };
1107
+ }
1108
+ function completionErrorText(parsed) {
1109
+ if (parsed && typeof parsed === "object") {
1110
+ const err = parsed.error;
1111
+ if (typeof err === "string" && err.trim()) return err.trim();
1112
+ }
1113
+ return void 0;
1114
+ }
1115
+ function persistCompletionBlocker(worker, reason) {
1116
+ const current = worker.completionBlocker;
1117
+ if ((current ?? void 0) === (reason ?? void 0)) return;
1118
+ if (reason) worker.completionBlocker = reason;
1119
+ else delete worker.completionBlocker;
1120
+ saveWorker(worker.runId, worker);
1121
+ }
1122
+ async function tryCompleteWorker(args) {
1123
+ const worker = loadWorker(String(args.run), String(args.name));
1124
+ const status = computeWorkerStatus(worker);
1125
+ const agentOsId = (args.agentOsId ? String(args.agentOsId) : worker.agentOsId) || "";
1126
+ const taskId = (args.taskId ? String(args.taskId) : worker.taskId) || null;
1127
+ if (!agentOsId) {
1128
+ return { ok: false, reason: "missing agentOsId" };
1129
+ }
1130
+ if (!isFinishedWorkerStatus(status)) {
1131
+ return { ok: true, skipped: true, reason: "worker-not-finished" };
1132
+ }
1133
+ const base = resolveBaseUrl(args.baseUrl ? String(args.baseUrl) : void 0);
1134
+ const explicitSecret = args.secret ? String(args.secret) : void 0;
1135
+ let secret = await resolveCallbackSecretWithMint(explicitSecret, agentOsId, { baseUrl: base });
1136
+ const url = `${base}/api/agent-os/by-id/${encodeURIComponent(agentOsId)}/harness/completion`;
1137
+ const body = {
1138
+ source: "openclaw-harness",
1139
+ agentOsId,
1140
+ runId: worker.runId,
1141
+ workerName: worker.name,
1142
+ taskId,
1143
+ startedAt: worker.startedAt,
1144
+ finishedAt: status.lastActivityAt || (/* @__PURE__ */ new Date()).toISOString(),
1145
+ status
1146
+ };
1147
+ let result = await postCompletion(url, secret, body);
1148
+ if ((result.status === 401 || result.status === 403) && !explicitSecret) {
1149
+ const refreshed = await refreshRunnerToken(agentOsId, { baseUrl: base });
1150
+ if (refreshed && refreshed !== secret) {
1151
+ secret = refreshed;
1152
+ result = await postCompletion(url, secret, body);
1153
+ }
1154
+ }
1155
+ if (result.ok) {
1156
+ persistCompletionBlocker(worker, void 0);
1157
+ return { ok: true, httpStatus: result.status, response: result.parsed };
1158
+ }
1159
+ const authRejected = result.status === 401 || result.status === 403;
1160
+ const detail = completionErrorText(result.parsed) ?? (authRejected ? "runner token unauthorized" : "non-2xx response");
1161
+ const reason = authRejected ? `completion replay rejected (${result.status}): ${detail}` : `completion replay failed (${result.status}): ${detail}`;
1162
+ persistCompletionBlocker(worker, reason);
1163
+ return { ok: false, httpStatus: result.status, response: result.parsed, completionBlocked: true };
1164
+ }
1165
+ async function completeWorker(args) {
1166
+ try {
1167
+ const worker = loadWorker(String(args.run), String(args.name));
1168
+ const status = computeWorkerStatus(worker);
1169
+ const agentOsId = (args.agentOsId ? String(args.agentOsId) : worker.agentOsId) || "";
1170
+ const taskId = (args.taskId ? String(args.taskId) : worker.taskId) || null;
1171
+ if (!agentOsId) {
1172
+ console.error("worker complete requires --agent-os-id (or an agentOsId persisted at worker start)");
1173
+ process.exit(1);
1174
+ }
1175
+ if (!isFinishedWorkerStatus(status)) {
1176
+ console.log(
1177
+ JSON.stringify(
1178
+ {
1179
+ worker: worker.name,
1180
+ runId: worker.runId,
1181
+ status: "skipped",
1182
+ reason: "worker-not-finished",
1183
+ workerStatus: status.status,
1184
+ alive: status.alive
1185
+ },
1186
+ null,
1187
+ 2
1188
+ )
1189
+ );
1190
+ return;
1191
+ }
1192
+ const result = await tryCompleteWorker(args);
1193
+ console.log(
1194
+ JSON.stringify(
1195
+ {
1196
+ worker: worker.name,
1197
+ runId: worker.runId,
1198
+ agentOsId,
1199
+ taskId,
1200
+ httpStatus: result.httpStatus,
1201
+ response: result.response
1202
+ },
1203
+ null,
1204
+ 2
1205
+ )
1206
+ );
1207
+ if (!result.ok) process.exit(1);
1208
+ } catch (error) {
1209
+ console.error(`worker complete failed: ${error.message}`);
1210
+ process.exit(1);
1211
+ }
1212
+ }
1213
+ function workerStatus(args) {
1214
+ const worker = loadWorker(String(args.run), String(args.name));
1215
+ const status = computeWorkerStatus(worker);
1216
+ writeJson(path7.join(worker.workerDir, "last-status.json"), status);
1217
+ console.log(JSON.stringify(status, null, 2));
1218
+ }
1219
+ function runStatus(args) {
1220
+ const run = loadRun(String(args.run));
1221
+ const names = Object.keys(run.workers || {});
1222
+ const workers = names.map((name) => {
1223
+ const worker = readJson(
1224
+ path7.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
1225
+ void 0
1226
+ );
1227
+ if (!worker) {
1228
+ return { worker: name, status: "missing", attention: "needs_attention", attentionReason: "worker.json not found" };
1229
+ }
1230
+ const status = computeWorkerStatus(worker, { base: run.base });
1231
+ const rawBlocker = worker.completionBlocker;
1232
+ const completionBlocker = typeof rawBlocker === "string" && rawBlocker ? rawBlocker : void 0;
1233
+ return {
1234
+ worker: status.worker,
1235
+ status: completionBlocker ? "blocked" : status.status,
1236
+ attention: completionBlocker ? "blocked" : status.attention.state,
1237
+ attentionReason: completionBlocker ?? status.attention.reason,
1238
+ pid: status.pid,
1239
+ alive: status.alive,
1240
+ currentTool: status.currentTool,
1241
+ lastActivityAt: status.lastActivityAt,
1242
+ lastHeartbeatPhase: status.lastHeartbeatPhase,
1243
+ lastHeartbeatSummary: status.lastHeartbeatSummary,
1244
+ heartbeatBlocker: status.heartbeatBlocker,
1245
+ changedFileCount: status.changedFiles.length,
1246
+ branch: status.branch,
1247
+ ancestry: status.gitAncestry.relation,
1248
+ ancestryChecked: status.gitAncestry.checked
1249
+ };
1250
+ });
1251
+ const board = {
1252
+ runId: run.id,
1253
+ name: run.name,
1254
+ status: deriveRunStatus(run.status, workers),
1255
+ repo: run.repo,
1256
+ workerCount: workers.length,
1257
+ needsAttention: workers.filter((w) => w.attention && w.attention !== "ok" && w.attention !== "done").map((w) => w.worker),
1258
+ workers
1259
+ };
1260
+ writeJson(path7.join(runDirectory(run.id), "last-board.json"), board);
1261
+ console.log(JSON.stringify(board, null, 2));
1262
+ }
1263
+ function tailWorker(args) {
1264
+ const worker = loadWorker(String(args.run), String(args.name));
1265
+ const raw = tailFile(worker.stdoutPath, Number(args.lines || 40));
1266
+ if (args.raw === true || args.raw === "true") {
1267
+ process.stdout.write(raw);
1268
+ return;
1269
+ }
1270
+ for (const line of raw.split("\n").filter(Boolean)) {
1271
+ const event = safeJson(line);
1272
+ const summary = event ? summarizeEvent(event) : line;
1273
+ if (summary) console.log(summary);
1274
+ }
1275
+ }
1276
+ function stopWorker(args) {
1277
+ const worker = loadWorker(String(args.run), String(args.name));
1278
+ if (!isPidAlive(worker.pid)) {
1279
+ console.log(JSON.stringify({ worker: worker.name, pid: worker.pid, status: "not_running" }, null, 2));
1280
+ return;
1281
+ }
1282
+ killWorkerProcess(worker.pid, "SIGTERM");
1283
+ sleepMs(1500);
1284
+ if (isPidAlive(worker.pid)) {
1285
+ killWorkerProcess(worker.pid, "SIGKILL");
1286
+ console.log(JSON.stringify({ worker: worker.name, pid: worker.pid, status: "sigkill_sent" }, null, 2));
1287
+ return;
1288
+ }
1289
+ console.log(JSON.stringify({ worker: worker.name, pid: worker.pid, status: "stopped" }, null, 2));
1290
+ }
1291
+
1292
+ // src/auto-complete.ts
1293
+ var DEFAULT_POLL_MS = 5e3;
1294
+ var DEFAULT_MAX_TOTAL_MS = 6 * 60 * 60 * 1e3;
1295
+ var DEFAULT_COMPLETE_ATTEMPTS = 3;
1296
+ var DEFAULT_COMPLETE_BACKOFF_MS = 5e3;
1297
+ function readArgs(raw) {
1298
+ return {
1299
+ run: String(raw.run || ""),
1300
+ name: String(raw.name || ""),
1301
+ agentOsId: raw.agentOsId ? String(raw.agentOsId) : void 0,
1302
+ pollMs: Number(raw.pollMs) > 0 ? Math.floor(Number(raw.pollMs)) : void 0,
1303
+ maxTotalMs: Number(raw.maxTotalMs) > 0 ? Math.floor(Number(raw.maxTotalMs)) : void 0,
1304
+ completeAttempts: Number(raw.completeAttempts) > 0 ? Math.floor(Number(raw.completeAttempts)) : void 0,
1305
+ completeBackoffMs: Number(raw.completeBackoffMs) > 0 ? Math.floor(Number(raw.completeBackoffMs)) : void 0,
1306
+ baseUrl: raw.baseUrl ? String(raw.baseUrl) : void 0,
1307
+ secret: raw.secret ? String(raw.secret) : void 0
1308
+ };
1309
+ }
1310
+ async function autoCompleteWorker(raw) {
1311
+ const args = readArgs(raw);
1312
+ const pollMs = args.pollMs ?? DEFAULT_POLL_MS;
1313
+ const maxTotalMs = args.maxTotalMs ?? DEFAULT_MAX_TOTAL_MS;
1314
+ const completeAttempts = args.completeAttempts ?? DEFAULT_COMPLETE_ATTEMPTS;
1315
+ const completeBackoffMs = args.completeBackoffMs ?? DEFAULT_COMPLETE_BACKOFF_MS;
1316
+ const worker = loadWorker(args.run, args.name);
1317
+ if (!worker.agentOsId || !worker.taskId) {
1318
+ return {
1319
+ worker: worker.name,
1320
+ runId: worker.runId,
1321
+ outcome: "missing_link",
1322
+ attempts: 0,
1323
+ reason: "worker has no agentOsId/taskId \u2014 nothing to attribute completion to"
1324
+ };
1325
+ }
1326
+ const startMs = Date.now();
1327
+ while (true) {
1328
+ const status = computeWorkerStatus(worker);
1329
+ if (isFinishedWorkerStatus(status)) break;
1330
+ if (!isPidAlive(worker.pid)) break;
1331
+ if (Date.now() - startMs > maxTotalMs) {
1332
+ return {
1333
+ worker: worker.name,
1334
+ runId: worker.runId,
1335
+ outcome: "timed_out",
1336
+ attempts: 0,
1337
+ reason: `worker did not finish within ${maxTotalMs}ms`
1338
+ };
1339
+ }
1340
+ sleepMs(pollMs);
1341
+ }
1342
+ let lastHttpStatus;
1343
+ let lastReason;
1344
+ for (let attempt = 1; attempt <= completeAttempts; attempt++) {
1345
+ const result = await tryCompleteWorker({
1346
+ run: args.run,
1347
+ name: args.name,
1348
+ ...args.agentOsId ? { agentOsId: args.agentOsId } : {},
1349
+ ...args.baseUrl ? { baseUrl: args.baseUrl } : {},
1350
+ ...args.secret ? { secret: args.secret } : {}
1351
+ });
1352
+ lastHttpStatus = result.httpStatus;
1353
+ if (result.ok) {
1354
+ return {
1355
+ worker: worker.name,
1356
+ runId: worker.runId,
1357
+ outcome: "completed",
1358
+ httpStatus: result.httpStatus,
1359
+ attempts: attempt
1360
+ };
1361
+ }
1362
+ const authRejected = result.httpStatus === 401 || result.httpStatus === 403;
1363
+ if (authRejected) {
1364
+ lastReason = typeof result.reason === "string" ? result.reason : "completion replay refused";
1365
+ return {
1366
+ worker: worker.name,
1367
+ runId: worker.runId,
1368
+ outcome: "blocked",
1369
+ httpStatus: result.httpStatus,
1370
+ attempts: attempt,
1371
+ reason: lastReason
1372
+ };
1373
+ }
1374
+ lastReason = typeof result.reason === "string" ? result.reason : "transient failure";
1375
+ if (attempt < completeAttempts) sleepMs(completeBackoffMs);
1376
+ }
1377
+ return {
1378
+ worker: worker.name,
1379
+ runId: worker.runId,
1380
+ outcome: "blocked",
1381
+ httpStatus: lastHttpStatus,
1382
+ attempts: completeAttempts,
1383
+ reason: lastReason ?? "completion failed after retries"
1384
+ };
1385
+ }
1386
+ async function autoCompleteWorkerCli(raw) {
1387
+ try {
1388
+ const outcome = await autoCompleteWorker(raw);
1389
+ console.log(JSON.stringify(outcome, null, 2));
1390
+ if (outcome.outcome === "missing_link" || outcome.outcome === "timed_out") {
1391
+ process.exitCode = 1;
1392
+ }
1393
+ } catch (error) {
1394
+ console.error(`worker auto-complete failed: ${error.message}`);
1395
+ process.exitCode = 1;
1396
+ }
1397
+ }
1398
+ function resolveDefaultCliPath() {
1399
+ return path8.join(fileURLToPath(new URL(".", import.meta.url)), "cli.js");
1400
+ }
1401
+ function spawnCompletionSidecar(opts) {
1402
+ const cliPath = opts.cliPath ?? resolveDefaultCliPath();
1403
+ if (!existsSync8(cliPath)) return void 0;
1404
+ const logPath = path8.join(opts.workerDir, "auto-complete.log");
1405
+ let logFd;
1406
+ try {
1407
+ logFd = openSync3(logPath, "a");
1408
+ } catch {
1409
+ logFd = void 0;
1410
+ }
1411
+ const stdio = [
1412
+ "ignore",
1413
+ logFd ?? "ignore",
1414
+ logFd ?? "ignore"
1415
+ ];
1416
+ const nodeExecutable = opts.nodeExecutable ?? process.execPath;
1417
+ const args = [
1418
+ cliPath,
1419
+ "worker",
1420
+ "auto-complete",
1421
+ "--run",
1422
+ opts.runId,
1423
+ "--name",
1424
+ opts.workerName
1425
+ ];
1426
+ if (opts.agentOsId) args.push("--agent-os-id", opts.agentOsId);
1427
+ if (opts.baseUrl) args.push("--base-url", opts.baseUrl);
1428
+ if (opts.secret) args.push("--secret", opts.secret);
1429
+ try {
1430
+ const child = spawn3(nodeExecutable, args, {
1431
+ detached: true,
1432
+ stdio,
1433
+ env: process.env
1434
+ });
1435
+ if (logFd !== void 0) closeSync3(logFd);
1436
+ child.unref();
1437
+ return { pid: child.pid, logPath, cliPath };
1438
+ } catch {
1439
+ if (logFd !== void 0) {
1440
+ try {
1441
+ closeSync3(logFd);
1442
+ } catch {
1443
+ }
1444
+ }
1445
+ return void 0;
1446
+ }
1447
+ }
1448
+
1086
1449
  // src/supervisor.ts
1087
1450
  function spawnWorkerProcess(run, opts) {
1088
1451
  const rawName = typeof opts.name === "string" ? opts.name.trim() : "";
@@ -1093,16 +1456,16 @@ function spawnWorkerProcess(run, opts) {
1093
1456
  if (run.workers?.[name]) throw new Error(`worker already exists in run ${run.id}: ${name}`);
1094
1457
  if (!opts.task) throw new Error(`missing task text for worker ${name}`);
1095
1458
  const { worktreesDir } = getPaths();
1096
- const workerDir = path7.join(runDirectory(run.id), "workers", name);
1459
+ const workerDir = path9.join(runDirectory(run.id), "workers", name);
1097
1460
  mkdirSync3(workerDir, { recursive: true });
1098
- const worktreePath = path7.join(worktreesDir, run.id, name);
1461
+ const worktreePath = path9.join(worktreesDir, run.id, name);
1099
1462
  const branch = opts.branch || `agent/${run.id}/${name}`;
1100
- if (existsSync8(worktreePath)) throw new Error(`worktree path already exists: ${worktreePath}`);
1463
+ if (existsSync9(worktreePath)) throw new Error(`worktree path already exists: ${worktreePath}`);
1101
1464
  git(run.repo, ["fetch", "origin", "--prune"], { allowFailure: true });
1102
1465
  git(run.repo, ["worktree", "add", "-b", branch, worktreePath, run.baseCommit], { throwError: true });
1103
- const stdoutPath = path7.join(workerDir, "stdout.jsonl");
1104
- const stderrPath = path7.join(workerDir, "stderr.log");
1105
- const heartbeatPath = path7.join(workerDir, "heartbeat.jsonl");
1466
+ const stdoutPath = path9.join(workerDir, "stdout.jsonl");
1467
+ const stderrPath = path9.join(workerDir, "stderr.log");
1468
+ const heartbeatPath = path9.join(workerDir, "heartbeat.jsonl");
1106
1469
  const prompt = buildPrompt({
1107
1470
  task: opts.task,
1108
1471
  ownedPaths: opts.ownedPaths || [],
@@ -1152,9 +1515,20 @@ function spawnWorkerProcess(run, opts) {
1152
1515
  startedAt: (/* @__PURE__ */ new Date()).toISOString()
1153
1516
  };
1154
1517
  saveWorker(run.id, worker);
1155
- run.workers = { ...run.workers || {}, [name]: { workerDir, statusPath: path7.join(workerDir, "worker.json") } };
1518
+ run.workers = { ...run.workers || {}, [name]: { workerDir, statusPath: path9.join(workerDir, "worker.json") } };
1156
1519
  run.status = "running";
1157
1520
  saveRun(run);
1521
+ if (worker.agentOsId && worker.taskId) {
1522
+ try {
1523
+ spawnCompletionSidecar({
1524
+ runId: run.id,
1525
+ workerName: name,
1526
+ workerDir,
1527
+ agentOsId: worker.agentOsId
1528
+ });
1529
+ } catch {
1530
+ }
1531
+ }
1158
1532
  return worker;
1159
1533
  }
1160
1534
  function startWorker(args) {
@@ -1364,7 +1738,7 @@ function redactHarness(text, secret) {
1364
1738
  }
1365
1739
 
1366
1740
  // src/validate.ts
1367
- import path8 from "node:path";
1741
+ import path10 from "node:path";
1368
1742
  var RUN_ID_RE = /^[a-z0-9][a-z0-9._-]{0,127}$/i;
1369
1743
  var WORKER_NAME_RE = /^[a-z0-9][a-z0-9._-]{0,63}$/i;
1370
1744
  function validateRunId(runId) {
@@ -1378,15 +1752,15 @@ function validateWorkerName(name) {
1378
1752
  return trimmed;
1379
1753
  }
1380
1754
  function validateRepo(repo) {
1381
- const resolved = path8.resolve(repo);
1755
+ const resolved = path10.resolve(repo);
1382
1756
  if (resolved.includes("..")) throw new Error("repo path must not contain .. segments");
1383
1757
  return resolved;
1384
1758
  }
1385
1759
  function validateOwnedPaths(repoRoot, ownedPaths) {
1386
1760
  return ownedPaths.map((owned) => {
1387
- const resolved = path8.resolve(repoRoot, owned);
1388
- const rel = path8.relative(repoRoot, resolved);
1389
- if (rel.startsWith("..") || path8.isAbsolute(rel)) {
1761
+ const resolved = path10.resolve(repoRoot, owned);
1762
+ const rel = path10.relative(repoRoot, resolved);
1763
+ if (rel.startsWith("..") || path10.isAbsolute(rel)) {
1390
1764
  throw new Error(`owned path escapes repo: ${owned}`);
1391
1765
  }
1392
1766
  return resolved;
@@ -1398,14 +1772,14 @@ function validateTailLines(lines) {
1398
1772
  }
1399
1773
 
1400
1774
  // src/worktree.ts
1401
- import { existsSync as existsSync9, mkdirSync as mkdirSync4 } from "node:fs";
1402
- import path9 from "node:path";
1775
+ import { existsSync as existsSync10, mkdirSync as mkdirSync4 } from "node:fs";
1776
+ import path11 from "node:path";
1403
1777
  function createRun(args) {
1404
1778
  const repo = validateRepo(required(String(args.repo || ""), "--repo"));
1405
1779
  ensureGitRepo(repo);
1406
1780
  const id = args.id ? validateRunId(String(args.id)) : timestampSlug(String(args.name || "run"));
1407
1781
  const dir = runDirectory(id);
1408
- if (existsSync9(dir)) failExists(`run already exists: ${id}`);
1782
+ if (existsSync10(dir)) failExists(`run already exists: ${id}`);
1409
1783
  mkdirSync4(dir, { recursive: true });
1410
1784
  const base = String(args.base || "origin/main");
1411
1785
  const baseCommit = git(repo, ["rev-parse", base]).trim();
@@ -1419,12 +1793,12 @@ function createRun(args) {
1419
1793
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1420
1794
  workers: {}
1421
1795
  };
1422
- writeJson(path9.join(dir, "run.json"), run);
1796
+ writeJson(path11.join(dir, "run.json"), run);
1423
1797
  console.log(JSON.stringify({ runId: id, runDir: dir, repo, base, baseCommit }, null, 2));
1424
1798
  }
1425
1799
  function listRuns() {
1426
1800
  const { runsDir } = getPaths();
1427
- const rows = listRunIds(runsDir).map((id) => readJson(path9.join(runDirectory(id), "run.json"), void 0)).filter(Boolean).map((run) => ({
1801
+ const rows = listRunIds(runsDir).map((id) => readJson(path11.join(runDirectory(id), "run.json"), void 0)).filter(Boolean).map((run) => ({
1428
1802
  id: run.id,
1429
1803
  name: run.name,
1430
1804
  status: run.status,
@@ -1439,7 +1813,7 @@ function failExists(message) {
1439
1813
  }
1440
1814
 
1441
1815
  // src/sweep.ts
1442
- import path10 from "node:path";
1816
+ import path12 from "node:path";
1443
1817
  async function sweepRun(args) {
1444
1818
  const pipeline = args.pipeline === true || args.pipeline === "true";
1445
1819
  try {
@@ -1451,7 +1825,7 @@ async function sweepRun(args) {
1451
1825
  const releasedLocalOrphans = [];
1452
1826
  for (const name of Object.keys(run.workers || {})) {
1453
1827
  const worker = readJson(
1454
- path10.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
1828
+ path12.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
1455
1829
  void 0
1456
1830
  );
1457
1831
  if (!worker || !worker.dispatched || !worker.taskId) continue;
@@ -1493,215 +1867,15 @@ async function sweepRun(args) {
1493
1867
  }
1494
1868
  }
1495
1869
 
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
1870
  // src/cli.ts
1697
1871
  import { mkdirSync as mkdirSync5, realpathSync } from "node:fs";
1698
- import { fileURLToPath } from "node:url";
1872
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
1699
1873
 
1700
1874
  // src/pipeline-tick.ts
1701
- import path14 from "node:path";
1875
+ import path15 from "node:path";
1702
1876
 
1703
1877
  // src/finalize.ts
1704
- import path12 from "node:path";
1878
+ import path13 from "node:path";
1705
1879
  var ACTIVE_RUN_STATUSES = /* @__PURE__ */ new Set(["running", "dispatching", "pending", "queued"]);
1706
1880
  function terminalStatusFor(run) {
1707
1881
  const names = Object.keys(run.workers || {});
@@ -1711,7 +1885,7 @@ function terminalStatusFor(run) {
1711
1885
  let anyCompletionBlocked = false;
1712
1886
  for (const name of names) {
1713
1887
  const worker = readJson(
1714
- path12.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
1888
+ path13.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
1715
1889
  void 0
1716
1890
  );
1717
1891
  if (!worker) continue;
@@ -1744,7 +1918,7 @@ function finalizeStaleRuns() {
1744
1918
  }
1745
1919
 
1746
1920
  // src/plan-progress-daemon-sync.ts
1747
- import path13 from "node:path";
1921
+ import path14 from "node:path";
1748
1922
 
1749
1923
  // src/plan-progress-sync.ts
1750
1924
  async function syncPlanProgress(args) {
@@ -1768,7 +1942,7 @@ async function syncActiveWorkerPlanProgress(runId, args) {
1768
1942
  const outcomes = [];
1769
1943
  for (const name of Object.keys(run.workers || {})) {
1770
1944
  const worker = readJson(
1771
- path13.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
1945
+ path14.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
1772
1946
  void 0
1773
1947
  );
1774
1948
  if (!worker?.dispatched || !worker.taskId) continue;
@@ -1822,7 +1996,7 @@ async function completeFinishedWorkers(runId, args) {
1822
1996
  const outcomes = [];
1823
1997
  for (const name of Object.keys(run.workers || {})) {
1824
1998
  const worker = readJson(
1825
- path14.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
1999
+ path15.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
1826
2000
  void 0
1827
2001
  );
1828
2002
  if (!worker?.taskId) continue;
@@ -2065,6 +2239,7 @@ function usage(code = 0) {
2065
2239
  " kynver worker tail --run RUN_ID --name worker [--lines 40] [--raw]",
2066
2240
  " kynver worker stop --run RUN_ID --name worker",
2067
2241
  " kynver worker complete --run RUN_ID --name worker [--agent-os-id AOS_ID] [--task-id TASK_ID] [--base-url URL] [--secret SECRET]",
2242
+ " 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
2243
  " 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
2244
  " kynver plan verify --plan PLAN_ID [--worktree PATH] [--task TASK_ID] [--human-override]"
2070
2245
  ].join("\n")
@@ -2103,9 +2278,10 @@ async function main(argv = process.argv.slice(2)) {
2103
2278
  if (scope === "worker" && action === "tail") return tailWorker(args);
2104
2279
  if (scope === "worker" && action === "stop") return stopWorker(args);
2105
2280
  if (scope === "worker" && action === "complete") return void await completeWorker(args);
2281
+ if (scope === "worker" && action === "auto-complete") return void await autoCompleteWorkerCli(args);
2106
2282
  unknownCommand(scope, action);
2107
2283
  }
2108
- var isCliEntry = process.argv[1] && realpathSync.native(process.argv[1]) === realpathSync.native(fileURLToPath(import.meta.url));
2284
+ var isCliEntry = process.argv[1] && realpathSync.native(process.argv[1]) === realpathSync.native(fileURLToPath2(import.meta.url));
2109
2285
  if (isCliEntry) {
2110
2286
  void main().catch((error) => {
2111
2287
  console.error(error);
@@ -2114,6 +2290,8 @@ if (isCliEntry) {
2114
2290
  }
2115
2291
  export {
2116
2292
  DEFAULT_DISPATCH_LEASE_MS,
2293
+ autoCompleteWorker,
2294
+ autoCompleteWorkerCli,
2117
2295
  buildDispatchTaskText,
2118
2296
  buildPrompt,
2119
2297
  completeWorker,
@@ -2140,6 +2318,7 @@ export {
2140
2318
  runDaemon,
2141
2319
  runStatus,
2142
2320
  saveUserConfig,
2321
+ spawnCompletionSidecar,
2143
2322
  spawnWorkerProcess,
2144
2323
  startWorker,
2145
2324
  stopWorker,