@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/index.js CHANGED
@@ -208,6 +208,18 @@ async function resolveCallbackSecretWithMint(argsSecret, agentOsId, opts) {
208
208
  "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"
209
209
  );
210
210
  }
211
+ async function refreshRunnerToken(agentOsId, opts) {
212
+ const apiKey = loadApiKey();
213
+ const baseUrl = resolveConfiguredBaseUrl(opts?.baseUrl);
214
+ if (!apiKey || !agentOsId || !baseUrl) return null;
215
+ try {
216
+ const token = await fetchRunnerCredential(agentOsId, { baseUrl, apiKey });
217
+ saveRunnerToken(agentOsId, token);
218
+ return token;
219
+ } catch {
220
+ return null;
221
+ }
222
+ }
211
223
  async function fetchRunnerCredential(agentOsId, opts) {
212
224
  const apiKey = opts?.apiKey || loadApiKey();
213
225
  if (!apiKey) throw new Error("API key required \u2014 run `kynver login` first");
@@ -381,12 +393,12 @@ var DEFAULT_CRITICAL_FREE_BYTES = 15 * 1024 * 1024 * 1024;
381
393
  var DEFAULT_MAX_USED_PERCENT = 80;
382
394
  var DEFAULT_HARD_MAX_USED_PERCENT = 90;
383
395
  function observeRunnerDiskGate(input = {}) {
384
- const path15 = input.diskPath?.trim() || "/";
396
+ const path16 = input.diskPath?.trim() || "/";
385
397
  const warnBelowBytes = input.diskFreeWarnBytes ?? DEFAULT_WARN_FREE_BYTES;
386
398
  const criticalBelowBytes = input.diskFreeCriticalBytes ?? DEFAULT_CRITICAL_FREE_BYTES;
387
399
  const maxUsedPercent = input.diskMaxUsedPercent ?? DEFAULT_MAX_USED_PERCENT;
388
400
  const hardMaxUsedPercent = input.diskHardMaxUsedPercent ?? DEFAULT_HARD_MAX_USED_PERCENT;
389
- const stats = statfsSync(path15);
401
+ const stats = statfsSync(path16);
390
402
  const freeBytes = Number(stats.bavail) * Number(stats.bsize);
391
403
  const totalBytes = Number(stats.blocks) * Number(stats.bsize);
392
404
  const usedPercent = totalBytes > 0 ? (totalBytes - freeBytes) / totalBytes * 100 : 100;
@@ -406,7 +418,7 @@ function observeRunnerDiskGate(input = {}) {
406
418
  }
407
419
  return {
408
420
  ok,
409
- path: path15,
421
+ path: path16,
410
422
  freeBytes,
411
423
  totalBytes,
412
424
  usedPercent,
@@ -896,8 +908,8 @@ function observeRunnerResourceGate(input) {
896
908
  }
897
909
 
898
910
  // src/supervisor.ts
899
- import { existsSync as existsSync8, mkdirSync as mkdirSync3 } from "node:fs";
900
- import path7 from "node:path";
911
+ import { existsSync as existsSync9, mkdirSync as mkdirSync3 } from "node:fs";
912
+ import path9 from "node:path";
901
913
 
902
914
  // src/prompt.ts
903
915
  function buildPrompt(input) {
@@ -1071,6 +1083,369 @@ function resolveWorkerProvider(name) {
1071
1083
  return provider;
1072
1084
  }
1073
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
+
1074
1449
  // src/supervisor.ts
1075
1450
  function spawnWorkerProcess(run, opts) {
1076
1451
  const rawName = typeof opts.name === "string" ? opts.name.trim() : "";
@@ -1081,16 +1456,16 @@ function spawnWorkerProcess(run, opts) {
1081
1456
  if (run.workers?.[name]) throw new Error(`worker already exists in run ${run.id}: ${name}`);
1082
1457
  if (!opts.task) throw new Error(`missing task text for worker ${name}`);
1083
1458
  const { worktreesDir } = getPaths();
1084
- const workerDir = path7.join(runDirectory(run.id), "workers", name);
1459
+ const workerDir = path9.join(runDirectory(run.id), "workers", name);
1085
1460
  mkdirSync3(workerDir, { recursive: true });
1086
- const worktreePath = path7.join(worktreesDir, run.id, name);
1461
+ const worktreePath = path9.join(worktreesDir, run.id, name);
1087
1462
  const branch = opts.branch || `agent/${run.id}/${name}`;
1088
- if (existsSync8(worktreePath)) throw new Error(`worktree path already exists: ${worktreePath}`);
1463
+ if (existsSync9(worktreePath)) throw new Error(`worktree path already exists: ${worktreePath}`);
1089
1464
  git(run.repo, ["fetch", "origin", "--prune"], { allowFailure: true });
1090
1465
  git(run.repo, ["worktree", "add", "-b", branch, worktreePath, run.baseCommit], { throwError: true });
1091
- const stdoutPath = path7.join(workerDir, "stdout.jsonl");
1092
- const stderrPath = path7.join(workerDir, "stderr.log");
1093
- 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");
1094
1469
  const prompt = buildPrompt({
1095
1470
  task: opts.task,
1096
1471
  ownedPaths: opts.ownedPaths || [],
@@ -1140,9 +1515,20 @@ function spawnWorkerProcess(run, opts) {
1140
1515
  startedAt: (/* @__PURE__ */ new Date()).toISOString()
1141
1516
  };
1142
1517
  saveWorker(run.id, worker);
1143
- 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") } };
1144
1519
  run.status = "running";
1145
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
+ }
1146
1532
  return worker;
1147
1533
  }
1148
1534
  function startWorker(args) {
@@ -1352,7 +1738,7 @@ function redactHarness(text, secret) {
1352
1738
  }
1353
1739
 
1354
1740
  // src/validate.ts
1355
- import path8 from "node:path";
1741
+ import path10 from "node:path";
1356
1742
  var RUN_ID_RE = /^[a-z0-9][a-z0-9._-]{0,127}$/i;
1357
1743
  var WORKER_NAME_RE = /^[a-z0-9][a-z0-9._-]{0,63}$/i;
1358
1744
  function validateRunId(runId) {
@@ -1366,15 +1752,15 @@ function validateWorkerName(name) {
1366
1752
  return trimmed;
1367
1753
  }
1368
1754
  function validateRepo(repo) {
1369
- const resolved = path8.resolve(repo);
1755
+ const resolved = path10.resolve(repo);
1370
1756
  if (resolved.includes("..")) throw new Error("repo path must not contain .. segments");
1371
1757
  return resolved;
1372
1758
  }
1373
1759
  function validateOwnedPaths(repoRoot, ownedPaths) {
1374
1760
  return ownedPaths.map((owned) => {
1375
- const resolved = path8.resolve(repoRoot, owned);
1376
- const rel = path8.relative(repoRoot, resolved);
1377
- 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)) {
1378
1764
  throw new Error(`owned path escapes repo: ${owned}`);
1379
1765
  }
1380
1766
  return resolved;
@@ -1386,14 +1772,14 @@ function validateTailLines(lines) {
1386
1772
  }
1387
1773
 
1388
1774
  // src/worktree.ts
1389
- import { existsSync as existsSync9, mkdirSync as mkdirSync4 } from "node:fs";
1390
- import path9 from "node:path";
1775
+ import { existsSync as existsSync10, mkdirSync as mkdirSync4 } from "node:fs";
1776
+ import path11 from "node:path";
1391
1777
  function createRun(args) {
1392
1778
  const repo = validateRepo(required(String(args.repo || ""), "--repo"));
1393
1779
  ensureGitRepo(repo);
1394
1780
  const id = args.id ? validateRunId(String(args.id)) : timestampSlug(String(args.name || "run"));
1395
1781
  const dir = runDirectory(id);
1396
- if (existsSync9(dir)) failExists(`run already exists: ${id}`);
1782
+ if (existsSync10(dir)) failExists(`run already exists: ${id}`);
1397
1783
  mkdirSync4(dir, { recursive: true });
1398
1784
  const base = String(args.base || "origin/main");
1399
1785
  const baseCommit = git(repo, ["rev-parse", base]).trim();
@@ -1407,12 +1793,12 @@ function createRun(args) {
1407
1793
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1408
1794
  workers: {}
1409
1795
  };
1410
- writeJson(path9.join(dir, "run.json"), run);
1796
+ writeJson(path11.join(dir, "run.json"), run);
1411
1797
  console.log(JSON.stringify({ runId: id, runDir: dir, repo, base, baseCommit }, null, 2));
1412
1798
  }
1413
1799
  function listRuns() {
1414
1800
  const { runsDir } = getPaths();
1415
- 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) => ({
1416
1802
  id: run.id,
1417
1803
  name: run.name,
1418
1804
  status: run.status,
@@ -1427,7 +1813,7 @@ function failExists(message) {
1427
1813
  }
1428
1814
 
1429
1815
  // src/sweep.ts
1430
- import path10 from "node:path";
1816
+ import path12 from "node:path";
1431
1817
  async function sweepRun(args) {
1432
1818
  const pipeline = args.pipeline === true || args.pipeline === "true";
1433
1819
  try {
@@ -1439,7 +1825,7 @@ async function sweepRun(args) {
1439
1825
  const releasedLocalOrphans = [];
1440
1826
  for (const name of Object.keys(run.workers || {})) {
1441
1827
  const worker = readJson(
1442
- path10.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
1828
+ path12.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
1443
1829
  void 0
1444
1830
  );
1445
1831
  if (!worker || !worker.dispatched || !worker.taskId) continue;
@@ -1481,188 +1867,25 @@ async function sweepRun(args) {
1481
1867
  }
1482
1868
  }
1483
1869
 
1484
- // src/worker-ops.ts
1485
- import path11 from "node:path";
1486
- async function tryCompleteWorker(args) {
1487
- const worker = loadWorker(String(args.run), String(args.name));
1488
- const status = computeWorkerStatus(worker);
1489
- const agentOsId = (args.agentOsId ? String(args.agentOsId) : worker.agentOsId) || "";
1490
- const taskId = (args.taskId ? String(args.taskId) : worker.taskId) || null;
1491
- if (!agentOsId) {
1492
- return { ok: false, reason: "missing agentOsId" };
1493
- }
1494
- if (!isFinishedWorkerStatus(status)) {
1495
- return { ok: true, skipped: true, reason: "worker-not-finished" };
1496
- }
1497
- const base = resolveBaseUrl(args.baseUrl ? String(args.baseUrl) : void 0);
1498
- const secret = await resolveCallbackSecretWithMint(args.secret ? String(args.secret) : void 0, agentOsId, { baseUrl: base });
1499
- const url = `${base}/api/agent-os/by-id/${encodeURIComponent(agentOsId)}/harness/completion`;
1500
- const body = {
1501
- source: "openclaw-harness",
1502
- agentOsId,
1503
- runId: worker.runId,
1504
- workerName: worker.name,
1505
- taskId,
1506
- startedAt: worker.startedAt,
1507
- finishedAt: status.lastActivityAt || (/* @__PURE__ */ new Date()).toISOString(),
1508
- status
1509
- };
1510
- const res = await fetch(url, {
1511
- method: "POST",
1512
- headers: buildHarnessCallbackHeaders(secret),
1513
- body: JSON.stringify(body)
1514
- });
1515
- let parsed = null;
1516
- try {
1517
- parsed = await res.json();
1518
- } catch {
1519
- parsed = null;
1520
- }
1521
- return { ok: res.ok, httpStatus: res.status, response: parsed };
1522
- }
1523
- async function completeWorker(args) {
1524
- try {
1525
- const worker = loadWorker(String(args.run), String(args.name));
1526
- const status = computeWorkerStatus(worker);
1527
- const agentOsId = (args.agentOsId ? String(args.agentOsId) : worker.agentOsId) || "";
1528
- const taskId = (args.taskId ? String(args.taskId) : worker.taskId) || null;
1529
- if (!agentOsId) {
1530
- console.error("worker complete requires --agent-os-id (or an agentOsId persisted at worker start)");
1531
- process.exit(1);
1532
- }
1533
- if (!isFinishedWorkerStatus(status)) {
1534
- console.log(
1535
- JSON.stringify(
1536
- {
1537
- worker: worker.name,
1538
- runId: worker.runId,
1539
- status: "skipped",
1540
- reason: "worker-not-finished",
1541
- workerStatus: status.status,
1542
- alive: status.alive
1543
- },
1544
- null,
1545
- 2
1546
- )
1547
- );
1548
- return;
1549
- }
1550
- const result = await tryCompleteWorker(args);
1551
- console.log(
1552
- JSON.stringify(
1553
- {
1554
- worker: worker.name,
1555
- runId: worker.runId,
1556
- agentOsId,
1557
- taskId,
1558
- httpStatus: result.httpStatus,
1559
- response: result.response
1560
- },
1561
- null,
1562
- 2
1563
- )
1564
- );
1565
- if (!result.ok) process.exit(1);
1566
- } catch (error) {
1567
- console.error(`worker complete failed: ${error.message}`);
1568
- process.exit(1);
1569
- }
1570
- }
1571
- function workerStatus(args) {
1572
- const worker = loadWorker(String(args.run), String(args.name));
1573
- const status = computeWorkerStatus(worker);
1574
- writeJson(path11.join(worker.workerDir, "last-status.json"), status);
1575
- console.log(JSON.stringify(status, null, 2));
1576
- }
1577
- function runStatus(args) {
1578
- const run = loadRun(String(args.run));
1579
- const names = Object.keys(run.workers || {});
1580
- const workers = names.map((name) => {
1581
- const worker = readJson(
1582
- path11.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
1583
- void 0
1584
- );
1585
- if (!worker) {
1586
- return { worker: name, status: "missing", attention: "needs_attention", attentionReason: "worker.json not found" };
1587
- }
1588
- const status = computeWorkerStatus(worker, { base: run.base });
1589
- return {
1590
- worker: status.worker,
1591
- status: status.status,
1592
- attention: status.attention.state,
1593
- attentionReason: status.attention.reason,
1594
- pid: status.pid,
1595
- alive: status.alive,
1596
- currentTool: status.currentTool,
1597
- lastActivityAt: status.lastActivityAt,
1598
- lastHeartbeatPhase: status.lastHeartbeatPhase,
1599
- lastHeartbeatSummary: status.lastHeartbeatSummary,
1600
- heartbeatBlocker: status.heartbeatBlocker,
1601
- changedFileCount: status.changedFiles.length,
1602
- branch: status.branch,
1603
- ancestry: status.gitAncestry.relation,
1604
- ancestryChecked: status.gitAncestry.checked
1605
- };
1606
- });
1607
- const board = {
1608
- runId: run.id,
1609
- name: run.name,
1610
- status: deriveRunStatus(run.status, workers),
1611
- repo: run.repo,
1612
- workerCount: workers.length,
1613
- needsAttention: workers.filter((w) => w.attention && w.attention !== "ok" && w.attention !== "done").map((w) => w.worker),
1614
- workers
1615
- };
1616
- writeJson(path11.join(runDirectory(run.id), "last-board.json"), board);
1617
- console.log(JSON.stringify(board, null, 2));
1618
- }
1619
- function tailWorker(args) {
1620
- const worker = loadWorker(String(args.run), String(args.name));
1621
- const raw = tailFile(worker.stdoutPath, Number(args.lines || 40));
1622
- if (args.raw === true || args.raw === "true") {
1623
- process.stdout.write(raw);
1624
- return;
1625
- }
1626
- for (const line of raw.split("\n").filter(Boolean)) {
1627
- const event = safeJson(line);
1628
- const summary = event ? summarizeEvent(event) : line;
1629
- if (summary) console.log(summary);
1630
- }
1631
- }
1632
- function stopWorker(args) {
1633
- const worker = loadWorker(String(args.run), String(args.name));
1634
- if (!isPidAlive(worker.pid)) {
1635
- console.log(JSON.stringify({ worker: worker.name, pid: worker.pid, status: "not_running" }, null, 2));
1636
- return;
1637
- }
1638
- killWorkerProcess(worker.pid, "SIGTERM");
1639
- sleepMs(1500);
1640
- if (isPidAlive(worker.pid)) {
1641
- killWorkerProcess(worker.pid, "SIGKILL");
1642
- console.log(JSON.stringify({ worker: worker.name, pid: worker.pid, status: "sigkill_sent" }, null, 2));
1643
- return;
1644
- }
1645
- console.log(JSON.stringify({ worker: worker.name, pid: worker.pid, status: "stopped" }, null, 2));
1646
- }
1647
-
1648
1870
  // src/cli.ts
1649
1871
  import { mkdirSync as mkdirSync5, realpathSync } from "node:fs";
1650
- import { fileURLToPath } from "node:url";
1872
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
1651
1873
 
1652
1874
  // src/pipeline-tick.ts
1653
- import path14 from "node:path";
1875
+ import path15 from "node:path";
1654
1876
 
1655
1877
  // src/finalize.ts
1656
- import path12 from "node:path";
1878
+ import path13 from "node:path";
1657
1879
  var ACTIVE_RUN_STATUSES = /* @__PURE__ */ new Set(["running", "dispatching", "pending", "queued"]);
1658
1880
  function terminalStatusFor(run) {
1659
1881
  const names = Object.keys(run.workers || {});
1660
1882
  if (names.length === 0) return "failed";
1661
1883
  let anyAlive = false;
1662
1884
  let anyResult = false;
1885
+ let anyCompletionBlocked = false;
1663
1886
  for (const name of names) {
1664
1887
  const worker = readJson(
1665
- path12.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
1888
+ path13.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
1666
1889
  void 0
1667
1890
  );
1668
1891
  if (!worker) continue;
@@ -1671,9 +1894,13 @@ function terminalStatusFor(run) {
1671
1894
  anyAlive = true;
1672
1895
  break;
1673
1896
  }
1897
+ if (typeof worker.completionBlocker === "string" && worker.completionBlocker) {
1898
+ anyCompletionBlocked = true;
1899
+ }
1674
1900
  if (status.finalResult) anyResult = true;
1675
1901
  }
1676
1902
  if (anyAlive) return null;
1903
+ if (anyCompletionBlocked) return null;
1677
1904
  return anyResult ? "completed" : "failed";
1678
1905
  }
1679
1906
  function finalizeStaleRuns() {
@@ -1691,7 +1918,7 @@ function finalizeStaleRuns() {
1691
1918
  }
1692
1919
 
1693
1920
  // src/plan-progress-daemon-sync.ts
1694
- import path13 from "node:path";
1921
+ import path14 from "node:path";
1695
1922
 
1696
1923
  // src/plan-progress-sync.ts
1697
1924
  async function syncPlanProgress(args) {
@@ -1715,7 +1942,7 @@ async function syncActiveWorkerPlanProgress(runId, args) {
1715
1942
  const outcomes = [];
1716
1943
  for (const name of Object.keys(run.workers || {})) {
1717
1944
  const worker = readJson(
1718
- path13.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
1945
+ path14.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
1719
1946
  void 0
1720
1947
  );
1721
1948
  if (!worker?.dispatched || !worker.taskId) continue;
@@ -1769,12 +1996,13 @@ async function completeFinishedWorkers(runId, args) {
1769
1996
  const outcomes = [];
1770
1997
  for (const name of Object.keys(run.workers || {})) {
1771
1998
  const worker = readJson(
1772
- path14.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
1999
+ path15.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
1773
2000
  void 0
1774
2001
  );
1775
- if (!worker?.dispatched || !worker.taskId) continue;
2002
+ if (!worker?.taskId) continue;
1776
2003
  const status = computeWorkerStatus(worker);
1777
2004
  if (!isFinishedWorkerStatus(status)) continue;
2005
+ if (!worker.dispatched && !status.finalResult) continue;
1778
2006
  const result = await tryCompleteWorker({
1779
2007
  run: runId,
1780
2008
  name,
@@ -1802,8 +2030,8 @@ async function runPipelineTick(args) {
1802
2030
  const agentOsId = String(required(String(args.agentOsId || ""), "--agent-os-id"));
1803
2031
  const execute = args.execute !== false && args.execute !== "false";
1804
2032
  runStatus({ run: runId });
1805
- const finalizedStaleRuns = finalizeStaleRuns();
1806
2033
  const completedWorkers = await completeFinishedWorkers(runId, args);
2034
+ const finalizedStaleRuns = finalizeStaleRuns();
1807
2035
  const planProgressSync = await syncActiveWorkerPlanProgress(runId, args);
1808
2036
  const workspacePrefs = await fetchWorkspaceRuntimePreferences(agentOsId, args);
1809
2037
  const resourceGate = observeRunnerResourceGate({
@@ -2011,6 +2239,7 @@ function usage(code = 0) {
2011
2239
  " kynver worker tail --run RUN_ID --name worker [--lines 40] [--raw]",
2012
2240
  " kynver worker stop --run RUN_ID --name worker",
2013
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]",
2014
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]",
2015
2244
  " kynver plan verify --plan PLAN_ID [--worktree PATH] [--task TASK_ID] [--human-override]"
2016
2245
  ].join("\n")
@@ -2049,9 +2278,10 @@ async function main(argv = process.argv.slice(2)) {
2049
2278
  if (scope === "worker" && action === "tail") return tailWorker(args);
2050
2279
  if (scope === "worker" && action === "stop") return stopWorker(args);
2051
2280
  if (scope === "worker" && action === "complete") return void await completeWorker(args);
2281
+ if (scope === "worker" && action === "auto-complete") return void await autoCompleteWorkerCli(args);
2052
2282
  unknownCommand(scope, action);
2053
2283
  }
2054
- 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));
2055
2285
  if (isCliEntry) {
2056
2286
  void main().catch((error) => {
2057
2287
  console.error(error);
@@ -2060,6 +2290,8 @@ if (isCliEntry) {
2060
2290
  }
2061
2291
  export {
2062
2292
  DEFAULT_DISPATCH_LEASE_MS,
2293
+ autoCompleteWorker,
2294
+ autoCompleteWorkerCli,
2063
2295
  buildDispatchTaskText,
2064
2296
  buildPrompt,
2065
2297
  completeWorker,
@@ -2086,6 +2318,7 @@ export {
2086
2318
  runDaemon,
2087
2319
  runStatus,
2088
2320
  saveUserConfig,
2321
+ spawnCompletionSidecar,
2089
2322
  spawnWorkerProcess,
2090
2323
  startWorker,
2091
2324
  stopWorker,