@kynver-app/runtime 0.1.31 → 0.1.32
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 +860 -97
- package/dist/cli.js.map +4 -4
- package/dist/index.js +874 -100
- package/dist/index.js.map +4 -4
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// src/dispatch.ts
|
|
2
|
+
import path12 from "node:path";
|
|
3
|
+
|
|
1
4
|
// src/config.ts
|
|
2
5
|
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
3
6
|
import { homedir } from "node:os";
|
|
@@ -422,12 +425,12 @@ var DEFAULT_CRITICAL_FREE_BYTES = 15 * 1024 * 1024 * 1024;
|
|
|
422
425
|
var DEFAULT_MAX_USED_PERCENT = 80;
|
|
423
426
|
var DEFAULT_HARD_MAX_USED_PERCENT = 90;
|
|
424
427
|
function observeRunnerDiskGate(input = {}) {
|
|
425
|
-
const
|
|
428
|
+
const path25 = input.diskPath?.trim() || "/";
|
|
426
429
|
const warnBelowBytes = input.diskFreeWarnBytes ?? DEFAULT_WARN_FREE_BYTES;
|
|
427
430
|
const criticalBelowBytes = input.diskFreeCriticalBytes ?? DEFAULT_CRITICAL_FREE_BYTES;
|
|
428
431
|
const maxUsedPercent = input.diskMaxUsedPercent ?? DEFAULT_MAX_USED_PERCENT;
|
|
429
432
|
const hardMaxUsedPercent = input.diskHardMaxUsedPercent ?? DEFAULT_HARD_MAX_USED_PERCENT;
|
|
430
|
-
const stats = statfsSync(
|
|
433
|
+
const stats = statfsSync(path25);
|
|
431
434
|
const freeBytes = Number(stats.bavail) * Number(stats.bsize);
|
|
432
435
|
const totalBytes = Number(stats.blocks) * Number(stats.bsize);
|
|
433
436
|
const usedPercent = totalBytes > 0 ? (totalBytes - freeBytes) / totalBytes * 100 : 100;
|
|
@@ -447,7 +450,7 @@ function observeRunnerDiskGate(input = {}) {
|
|
|
447
450
|
}
|
|
448
451
|
return {
|
|
449
452
|
ok,
|
|
450
|
-
path:
|
|
453
|
+
path: path25,
|
|
451
454
|
freeBytes,
|
|
452
455
|
totalBytes,
|
|
453
456
|
usedPercent,
|
|
@@ -536,6 +539,14 @@ function runDirectory(id) {
|
|
|
536
539
|
|
|
537
540
|
// src/heartbeat.ts
|
|
538
541
|
import { existsSync as existsSync5, readFileSync as readFileSync3 } from "node:fs";
|
|
542
|
+
function isTerminalHeartbeatPhase(phase) {
|
|
543
|
+
return phase === "complete";
|
|
544
|
+
}
|
|
545
|
+
function terminalFinalResultFromHeartbeat(heartbeat) {
|
|
546
|
+
if (!isTerminalHeartbeatPhase(heartbeat.lastHeartbeatPhase)) return null;
|
|
547
|
+
const summary = heartbeat.lastHeartbeatSummary?.trim();
|
|
548
|
+
return summary || "completed";
|
|
549
|
+
}
|
|
539
550
|
function parseHeartbeat(file) {
|
|
540
551
|
const result = {
|
|
541
552
|
heartbeatCount: 0,
|
|
@@ -561,7 +572,27 @@ function parseHeartbeat(file) {
|
|
|
561
572
|
|
|
562
573
|
// src/stream.ts
|
|
563
574
|
import { existsSync as existsSync6, readFileSync as readFileSync4 } from "node:fs";
|
|
564
|
-
function
|
|
575
|
+
function eventTimestampIso(event) {
|
|
576
|
+
const tsMs = event.timestamp_ms;
|
|
577
|
+
return event.timestamp || event.ts || (tsMs ? new Date(tsMs).toISOString() : void 0);
|
|
578
|
+
}
|
|
579
|
+
function cursorToolNameFromCall(toolCall) {
|
|
580
|
+
if (!toolCall) return null;
|
|
581
|
+
for (const key of Object.keys(toolCall)) {
|
|
582
|
+
if (key.endsWith("ToolCall")) {
|
|
583
|
+
const stem = key.slice(0, -"ToolCall".length);
|
|
584
|
+
return stem.length ? stem : key;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
return null;
|
|
588
|
+
}
|
|
589
|
+
function recordStreamResult(result, event) {
|
|
590
|
+
result.finalResult = event.result || event.subtype || event.terminal_reason || "completed";
|
|
591
|
+
if (event.is_error) {
|
|
592
|
+
result.error = String(event.result || event.api_error_status || "stream result error");
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
function parseHarnessStream(file) {
|
|
565
596
|
const result = {
|
|
566
597
|
firstEventAt: null,
|
|
567
598
|
lastEventAt: null,
|
|
@@ -574,8 +605,7 @@ function parseClaudeStream(file) {
|
|
|
574
605
|
for (const line of lines) {
|
|
575
606
|
const event = safeJson(line);
|
|
576
607
|
if (!event) continue;
|
|
577
|
-
const
|
|
578
|
-
const ts = event.timestamp || event.ts || (tsMs ? new Date(tsMs).toISOString() : void 0);
|
|
608
|
+
const ts = eventTimestampIso(event);
|
|
579
609
|
if (ts) {
|
|
580
610
|
result.firstEventAt ||= ts;
|
|
581
611
|
result.lastEventAt = ts;
|
|
@@ -591,13 +621,20 @@ function parseClaudeStream(file) {
|
|
|
591
621
|
if (tool) result.currentTool = String(tool.name || result.currentTool);
|
|
592
622
|
}
|
|
593
623
|
}
|
|
624
|
+
if (event.type === "tool_call" && event.subtype === "started") {
|
|
625
|
+
const toolCall = event.tool_call && typeof event.tool_call === "object" && !Array.isArray(event.tool_call) ? event.tool_call : void 0;
|
|
626
|
+
const name = cursorToolNameFromCall(toolCall);
|
|
627
|
+
if (name) result.currentTool = name;
|
|
628
|
+
}
|
|
594
629
|
if (event.type === "result") {
|
|
595
|
-
result
|
|
596
|
-
if (event.is_error) result.error = String(event.result || event.api_error_status || "Claude result error");
|
|
630
|
+
recordStreamResult(result, event);
|
|
597
631
|
}
|
|
598
632
|
}
|
|
599
633
|
return result;
|
|
600
634
|
}
|
|
635
|
+
function parseClaudeStream(file) {
|
|
636
|
+
return parseHarnessStream(file);
|
|
637
|
+
}
|
|
601
638
|
function summarizeEvent(event) {
|
|
602
639
|
if (event.type === "system" && event.subtype) {
|
|
603
640
|
return `[system:${event.subtype}] ${String(event.status || event.cwd || "")}`.trim();
|
|
@@ -628,6 +665,12 @@ function summarizeEvent(event) {
|
|
|
628
665
|
const result = event.tool_use_result;
|
|
629
666
|
return `[tool:result] stdout=${JSON.stringify(result.stdout || "")} stderr=${JSON.stringify(result.stderr || "")}`;
|
|
630
667
|
}
|
|
668
|
+
if (event.type === "tool_call") {
|
|
669
|
+
const subtype = String(event.subtype || "");
|
|
670
|
+
const toolCall = event.tool_call && typeof event.tool_call === "object" && !Array.isArray(event.tool_call) ? event.tool_call : void 0;
|
|
671
|
+
const name = cursorToolNameFromCall(toolCall) ?? "tool";
|
|
672
|
+
return `[tool:${subtype}] ${name}`;
|
|
673
|
+
}
|
|
631
674
|
if (event.type === "result") {
|
|
632
675
|
return `[result] ${event.subtype || ""} ${oneLine(String(event.result || ""))}`.trim();
|
|
633
676
|
}
|
|
@@ -965,8 +1008,9 @@ function computeAttention(input) {
|
|
|
965
1008
|
return { state: "ok", reason: "recent activity" };
|
|
966
1009
|
}
|
|
967
1010
|
function computeWorkerStatus(worker, options = {}) {
|
|
968
|
-
const parsed =
|
|
1011
|
+
const parsed = parseHarnessStream(worker.stdoutPath);
|
|
969
1012
|
const heartbeat = parseHeartbeat(worker.heartbeatPath);
|
|
1013
|
+
const finalResult = parsed.finalResult ?? terminalFinalResultFromHeartbeat(heartbeat);
|
|
970
1014
|
const alive = isPidAlive(worker.pid);
|
|
971
1015
|
const stdoutBytes = fileSize(worker.stdoutPath);
|
|
972
1016
|
const stderrBytes = fileSize(worker.stderrPath);
|
|
@@ -983,11 +1027,11 @@ function computeWorkerStatus(worker, options = {}) {
|
|
|
983
1027
|
fileMtime(worker.stderrPath),
|
|
984
1028
|
fileMtime(worker.heartbeatPath)
|
|
985
1029
|
]);
|
|
986
|
-
const error = parsed.error || (!alive && !
|
|
1030
|
+
const error = parsed.error || (!alive && !finalResult ? tailFile(worker.stderrPath, 10).trim() || void 0 : void 0);
|
|
987
1031
|
const completionBlocker = typeof worker.completionBlocker === "string" && worker.completionBlocker.trim() ? worker.completionBlocker.trim() : null;
|
|
988
1032
|
const attention = computeAttention({
|
|
989
1033
|
alive,
|
|
990
|
-
finalResult
|
|
1034
|
+
finalResult,
|
|
991
1035
|
firstEventAt: parsed.firstEventAt,
|
|
992
1036
|
stdoutBytes,
|
|
993
1037
|
heartbeatBytes,
|
|
@@ -999,7 +1043,7 @@ function computeWorkerStatus(worker, options = {}) {
|
|
|
999
1043
|
gitAncestry,
|
|
1000
1044
|
completionBlocker
|
|
1001
1045
|
});
|
|
1002
|
-
const workerStatusLabel = completionBlocker || attention.state === "blocked" ? "blocked" : attention.state === "done" ? "done" :
|
|
1046
|
+
const workerStatusLabel = completionBlocker || attention.state === "blocked" ? "blocked" : attention.state === "done" ? "done" : finalResult ? "exited" : alive ? "running" : "exited";
|
|
1003
1047
|
return {
|
|
1004
1048
|
runId: worker.runId,
|
|
1005
1049
|
worker: worker.name,
|
|
@@ -1022,7 +1066,7 @@ function computeWorkerStatus(worker, options = {}) {
|
|
|
1022
1066
|
lastHeartbeatPhase: heartbeat.lastHeartbeatPhase,
|
|
1023
1067
|
lastHeartbeatSummary: heartbeat.lastHeartbeatSummary,
|
|
1024
1068
|
heartbeatBlocker: heartbeat.heartbeatBlocker,
|
|
1025
|
-
finalResult
|
|
1069
|
+
finalResult,
|
|
1026
1070
|
error,
|
|
1027
1071
|
changedFiles,
|
|
1028
1072
|
gitAncestry
|
|
@@ -1218,6 +1262,12 @@ var FOREIGN_MODEL_RE = /^(?:gpt-|gpt5|o1|o3|o4|gemini-|grok-|composer|deepseek|l
|
|
|
1218
1262
|
function looksLikeClaudeModel(model) {
|
|
1219
1263
|
return /^claude[-_]/i.test(model) || /^(?:opus|sonnet|haiku)\b/i.test(model);
|
|
1220
1264
|
}
|
|
1265
|
+
var CURSOR_MODEL_ALIASES = /* @__PURE__ */ new Set(["cursor"]);
|
|
1266
|
+
function normalizeCursorModelAlias(model) {
|
|
1267
|
+
const key = model.trim().toLowerCase();
|
|
1268
|
+
if (CURSOR_MODEL_ALIASES.has(key)) return "auto";
|
|
1269
|
+
return null;
|
|
1270
|
+
}
|
|
1221
1271
|
function preflightClaudeModel(model, defaultModel) {
|
|
1222
1272
|
const requested = (model ?? "").trim();
|
|
1223
1273
|
if (!requested) {
|
|
@@ -1250,6 +1300,16 @@ function preflightCursorModel(model, defaultModel) {
|
|
|
1250
1300
|
if (!requested) {
|
|
1251
1301
|
return { ok: true, model: defaultModel, normalized: false };
|
|
1252
1302
|
}
|
|
1303
|
+
const aliasLaunch = normalizeCursorModelAlias(requested);
|
|
1304
|
+
if (aliasLaunch) {
|
|
1305
|
+
return {
|
|
1306
|
+
ok: true,
|
|
1307
|
+
model: aliasLaunch,
|
|
1308
|
+
normalized: true,
|
|
1309
|
+
requested,
|
|
1310
|
+
note: `normalized model "${requested}" \u2192 "${aliasLaunch}" (Cursor provider alias \u2014 use "auto" or a composer id, not "cursor")`
|
|
1311
|
+
};
|
|
1312
|
+
}
|
|
1253
1313
|
if (looksLikeClaudeModel(requested)) {
|
|
1254
1314
|
return {
|
|
1255
1315
|
ok: false,
|
|
@@ -1311,6 +1371,7 @@ var claudeProvider = {
|
|
|
1311
1371
|
|
|
1312
1372
|
// src/model-routing.ts
|
|
1313
1373
|
var GLOBAL_DEFAULT_MODEL = "claude-sonnet-4-6";
|
|
1374
|
+
var CURSOR_DEFAULT_MODEL = "composer-2.5";
|
|
1314
1375
|
function taskString2(task, key) {
|
|
1315
1376
|
const v = task[key];
|
|
1316
1377
|
return typeof v === "string" ? v.trim() : "";
|
|
@@ -1333,6 +1394,27 @@ function inferProviderFromModel(model) {
|
|
|
1333
1394
|
}
|
|
1334
1395
|
return "claude";
|
|
1335
1396
|
}
|
|
1397
|
+
function normalizeProviderAliasModel(model, explicitProvider) {
|
|
1398
|
+
const alias = model.trim().toLowerCase();
|
|
1399
|
+
const provider = explicitProvider?.trim();
|
|
1400
|
+
if (alias === "cursor") {
|
|
1401
|
+
return {
|
|
1402
|
+
model: CURSOR_DEFAULT_MODEL,
|
|
1403
|
+
provider: "cursor",
|
|
1404
|
+
rule: provider && provider !== "cursor" ? "explicit:model_provider_alias_overrode_provider" : "explicit:model_provider_alias",
|
|
1405
|
+
requestedModel: model
|
|
1406
|
+
};
|
|
1407
|
+
}
|
|
1408
|
+
if (alias === "claude" || alias === "anthropic") {
|
|
1409
|
+
return {
|
|
1410
|
+
model: CLAUDE_DEFAULT_MODEL,
|
|
1411
|
+
provider: "claude",
|
|
1412
|
+
rule: provider && provider !== "claude" ? "explicit:model_provider_alias_overrode_provider" : "explicit:model_provider_alias",
|
|
1413
|
+
requestedModel: model
|
|
1414
|
+
};
|
|
1415
|
+
}
|
|
1416
|
+
return null;
|
|
1417
|
+
}
|
|
1336
1418
|
function isOpusLane(ref, title) {
|
|
1337
1419
|
if (ref.includes("deep") && ref.includes("review")) return true;
|
|
1338
1420
|
if (ref.includes("security")) return true;
|
|
@@ -1380,15 +1462,18 @@ function inferModelRoutingFromTask(task) {
|
|
|
1380
1462
|
rule: "priority:low"
|
|
1381
1463
|
};
|
|
1382
1464
|
}
|
|
1465
|
+
const model = resolveGlobalDefaultModel();
|
|
1383
1466
|
return {
|
|
1384
|
-
model
|
|
1385
|
-
provider:
|
|
1386
|
-
rule: "default:
|
|
1467
|
+
model,
|
|
1468
|
+
provider: inferProviderFromModel(model),
|
|
1469
|
+
rule: "default:global"
|
|
1387
1470
|
};
|
|
1388
1471
|
}
|
|
1389
1472
|
function resolveWorkerLaunch(input) {
|
|
1390
1473
|
if (input.explicitModel?.trim()) {
|
|
1391
1474
|
const model2 = input.explicitModel.trim();
|
|
1475
|
+
const providerAlias = normalizeProviderAliasModel(model2, input.explicitProvider);
|
|
1476
|
+
if (providerAlias) return providerAlias;
|
|
1392
1477
|
return {
|
|
1393
1478
|
model: model2,
|
|
1394
1479
|
provider: input.explicitProvider?.trim() || inferProviderFromModel(model2),
|
|
@@ -1428,20 +1513,89 @@ function readHarnessRetryLimits() {
|
|
|
1428
1513
|
};
|
|
1429
1514
|
}
|
|
1430
1515
|
|
|
1516
|
+
// src/lease-renewal.ts
|
|
1517
|
+
import path6 from "node:path";
|
|
1518
|
+
function workerRecord(runId, name) {
|
|
1519
|
+
return readJson(
|
|
1520
|
+
path6.join(runDirectory(runId), "workers", safeSlug(name), "worker.json"),
|
|
1521
|
+
void 0
|
|
1522
|
+
);
|
|
1523
|
+
}
|
|
1524
|
+
async function renewActiveTaskLeases(runId, args) {
|
|
1525
|
+
const run = loadRun(runId);
|
|
1526
|
+
const agentOsId = String(args.agentOsId || "");
|
|
1527
|
+
if (!agentOsId) {
|
|
1528
|
+
return { renewed: [], failed: [], skipped: [] };
|
|
1529
|
+
}
|
|
1530
|
+
const base = resolveBaseUrl(args.baseUrl ? String(args.baseUrl) : void 0);
|
|
1531
|
+
const secret = await resolveCallbackSecretWithMint(
|
|
1532
|
+
args.secret ? String(args.secret) : void 0,
|
|
1533
|
+
agentOsId,
|
|
1534
|
+
{ baseUrl: base }
|
|
1535
|
+
);
|
|
1536
|
+
const leaseDurationMs = Number(args.leaseMs) > 0 ? Math.floor(Number(args.leaseMs)) : DEFAULT_DISPATCH_LEASE_MS;
|
|
1537
|
+
const leaseOwner = `kynver-harness:${runId}`;
|
|
1538
|
+
const renewed = [];
|
|
1539
|
+
const failed = [];
|
|
1540
|
+
const skipped = [];
|
|
1541
|
+
for (const name of Object.keys(run.workers || {})) {
|
|
1542
|
+
const worker = workerRecord(runId, name);
|
|
1543
|
+
if (!worker?.taskId || !worker.agentOsId) {
|
|
1544
|
+
skipped.push(name);
|
|
1545
|
+
continue;
|
|
1546
|
+
}
|
|
1547
|
+
if (!isPidAlive(worker.pid)) {
|
|
1548
|
+
skipped.push(name);
|
|
1549
|
+
continue;
|
|
1550
|
+
}
|
|
1551
|
+
const status = computeWorkerStatus(worker);
|
|
1552
|
+
if (status.status === "done") {
|
|
1553
|
+
skipped.push(name);
|
|
1554
|
+
continue;
|
|
1555
|
+
}
|
|
1556
|
+
const url = `${base}/api/agent-os/by-id/${encodeURIComponent(agentOsId)}/tasks/${encodeURIComponent(worker.taskId)}/renew-lease`;
|
|
1557
|
+
const res = await postJsonWithCredentialRefresh(
|
|
1558
|
+
url,
|
|
1559
|
+
secret,
|
|
1560
|
+
{ leaseOwner, leaseDurationMs },
|
|
1561
|
+
{ agentOsId, baseUrl: base }
|
|
1562
|
+
);
|
|
1563
|
+
if (res.ok) {
|
|
1564
|
+
renewed.push(name);
|
|
1565
|
+
continue;
|
|
1566
|
+
}
|
|
1567
|
+
const reason = res.response && typeof res.response === "object" && "reason" in res.response ? String(res.response.reason ?? `http ${res.status}`) : `http ${res.status}`;
|
|
1568
|
+
failed.push({ worker: name, reason });
|
|
1569
|
+
}
|
|
1570
|
+
return { renewed, failed, skipped };
|
|
1571
|
+
}
|
|
1572
|
+
function hasLiveWorkerForTask(runId, taskId) {
|
|
1573
|
+
const run = loadRun(runId);
|
|
1574
|
+
for (const name of Object.keys(run.workers || {})) {
|
|
1575
|
+
const worker = workerRecord(runId, name);
|
|
1576
|
+
if (!worker || worker.taskId !== taskId) continue;
|
|
1577
|
+
if (!isPidAlive(worker.pid)) continue;
|
|
1578
|
+
const status = computeWorkerStatus(worker);
|
|
1579
|
+
if (status.status === "done") continue;
|
|
1580
|
+
return true;
|
|
1581
|
+
}
|
|
1582
|
+
return false;
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1431
1585
|
// src/supervisor.ts
|
|
1432
1586
|
import { existsSync as existsSync10, mkdirSync as mkdirSync3 } from "node:fs";
|
|
1433
|
-
import
|
|
1587
|
+
import path11 from "node:path";
|
|
1434
1588
|
|
|
1435
1589
|
// src/prompt.ts
|
|
1436
1590
|
function buildPrompt(input) {
|
|
1437
1591
|
const ownership = input.ownedPaths.length ? `Owned paths: ${input.ownedPaths.join(", ")}. Do not edit outside these paths without stopping and reporting why.` : "Owned paths: unrestricted for this worker, but keep edits tightly scoped.";
|
|
1438
1592
|
const compact = Boolean(input.model?.toLowerCase().includes("haiku"));
|
|
1439
1593
|
const progressLines = compact ? [
|
|
1440
|
-
"Plan progress: when planId is set, use `kynver plan progress` for running|partial|blocked
|
|
1594
|
+
"Plan progress: when planId is set, use `kynver plan progress` for in_progress|running|partial|blocked. Use `in_progress` for agent-loop current focus; use `running` only when an executor holds a lease. Row `done` is MCP/session only.",
|
|
1441
1595
|
input.planId ? `Active planId: ${input.planId}` : "No planId on this worker."
|
|
1442
1596
|
] : [
|
|
1443
1597
|
"Structured plan progress (required when planId is set):",
|
|
1444
|
-
"- Harness checkpoints only: `kynver plan progress --plan <planId> --row <rowKey> --role implementer --status running|partial|blocked` (the by-id harness route rejects `done` and confirm events).",
|
|
1598
|
+
"- Harness checkpoints only: `kynver plan progress --plan <planId> --row <rowKey> --role implementer --status in_progress|running|partial|blocked` (the by-id harness route rejects `done` and confirm events). Prefer `in_progress` at turn start for current focus; daemon sets `running` on dispatch.",
|
|
1445
1599
|
"- When a slice is finished, emit `partial` with evidence (`--evidence pr:<url>`, `--evidence path:<file>`, or `--evidence command:<cmd>`). Do not propose or confirm row `done` from the worker CLI.",
|
|
1446
1600
|
"- Propose/confirm row `done` is MCP/session only: chat agents use `agent_os_plan_progress_event_append` on the slug route (implementer proposes with `proposed: true`; report_reviewer/deep_reviewer confirm with `proposed: false`).",
|
|
1447
1601
|
"- When blocked on operator/Ghost/runtime review, create a linked review task (MCP `agent_os_plan_review_task_create` or API) and pass `--review-task <taskId>`.",
|
|
@@ -1465,13 +1619,17 @@ function buildPrompt(input) {
|
|
|
1465
1619
|
`Progress heartbeat file: ${input.heartbeatPath}`,
|
|
1466
1620
|
"After each major step, append one JSON line to the heartbeat file with fields: ts, phase, summary, changedFiles, blocker.",
|
|
1467
1621
|
"Final response must include files changed, verification commands, and unresolved risks.",
|
|
1468
|
-
"Completion handoff (required): before you stop, ensure the harness records a final result \u2014 summarize outcome in your last message and append a heartbeat line with phase `complete`. If you leave uncommitted changes,
|
|
1469
|
-
"
|
|
1622
|
+
"Completion handoff (required): before you stop, ensure the harness records a final result \u2014 summarize outcome in your last message and append a heartbeat line with phase `complete`. If you leave uncommitted changes or committed work without a PR, the orchestrator blocks completion until a GitHub PR exists (or you discard/commit cleanly). Exiting with only dirty files and no PR routes to salvage review, not production review.",
|
|
1623
|
+
"PR-ready handoff: for substantial implementation work, commit, push, and open a GitHub PR (draft OK) on your branch before finishing \u2014 or rely on the harness to run `gh pr create` at completion when `gh` is authenticated.",
|
|
1624
|
+
"Worker resource guard: do not run full monorepo verification (`npm run typecheck`, `npm run build`, or equivalent) from this worker lane unless an operator explicitly requests it. Use targeted checks for touched paths and rely on CI/operator lanes for heavy gates.",
|
|
1625
|
+
"If verification fails (including OOM), append a heartbeat line immediately with the last command, failure reason, dirty-file status, commit/PR handoff state, and next action so recovery does not require log spelunking.",
|
|
1470
1626
|
"",
|
|
1471
1627
|
...progressLines,
|
|
1472
1628
|
"",
|
|
1473
1629
|
...planArtifactLines,
|
|
1474
1630
|
"",
|
|
1631
|
+
...input.personaMarkdown?.trim() ? [input.personaMarkdown.trim(), ""] : [],
|
|
1632
|
+
...input.instructionPolicyMarkdown?.trim() ? ["Operating rules (Lane A \u2014 from AgentOS memory policy):", input.instructionPolicyMarkdown.trim(), ""] : [],
|
|
1475
1633
|
"Task:",
|
|
1476
1634
|
input.task
|
|
1477
1635
|
].join("\n");
|
|
@@ -1480,11 +1638,11 @@ function buildPrompt(input) {
|
|
|
1480
1638
|
// src/providers/cursor.ts
|
|
1481
1639
|
import { closeSync as closeSync2, existsSync as existsSync8, openSync as openSync2 } from "node:fs";
|
|
1482
1640
|
import { spawn as spawn2 } from "node:child_process";
|
|
1483
|
-
import
|
|
1641
|
+
import path8 from "node:path";
|
|
1484
1642
|
|
|
1485
1643
|
// src/providers/cursor-windows.ts
|
|
1486
1644
|
import { existsSync as existsSync7, readdirSync as readdirSync3 } from "node:fs";
|
|
1487
|
-
import
|
|
1645
|
+
import path7 from "node:path";
|
|
1488
1646
|
var CURSOR_VERSION_DIR = /^\d{4}\.\d{1,2}\.\d{1,2}-[a-f0-9]+$/i;
|
|
1489
1647
|
function parseCursorVersionSortKey(versionName) {
|
|
1490
1648
|
const datePart = versionName.split("-")[0];
|
|
@@ -1495,7 +1653,7 @@ function parseCursorVersionSortKey(versionName) {
|
|
|
1495
1653
|
return Number(`${year}${month.padStart(2, "0")}${day.padStart(2, "0")}`);
|
|
1496
1654
|
}
|
|
1497
1655
|
function pickLatestCursorVersionDir(agentRoot) {
|
|
1498
|
-
const versionsRoot =
|
|
1656
|
+
const versionsRoot = path7.join(agentRoot, "versions");
|
|
1499
1657
|
if (!existsSync7(versionsRoot)) return null;
|
|
1500
1658
|
let bestDir = null;
|
|
1501
1659
|
let bestKey = -1;
|
|
@@ -1504,21 +1662,21 @@ function pickLatestCursorVersionDir(agentRoot) {
|
|
|
1504
1662
|
const key = parseCursorVersionSortKey(entry.name);
|
|
1505
1663
|
if (key == null || key <= bestKey) continue;
|
|
1506
1664
|
bestKey = key;
|
|
1507
|
-
bestDir =
|
|
1665
|
+
bestDir = path7.join(versionsRoot, entry.name);
|
|
1508
1666
|
}
|
|
1509
1667
|
return bestDir;
|
|
1510
1668
|
}
|
|
1511
1669
|
function resolveWindowsCursorBundled(agentRoot) {
|
|
1512
|
-
const root = agentRoot?.trim() ||
|
|
1513
|
-
const directNode =
|
|
1514
|
-
const directIndex =
|
|
1670
|
+
const root = agentRoot?.trim() || path7.join(process.env.LOCALAPPDATA || "", "cursor-agent");
|
|
1671
|
+
const directNode = path7.join(root, "node.exe");
|
|
1672
|
+
const directIndex = path7.join(root, "index.js");
|
|
1515
1673
|
if (existsSync7(directNode) && existsSync7(directIndex)) {
|
|
1516
1674
|
return { nodeExe: directNode, indexJs: directIndex, versionDir: root };
|
|
1517
1675
|
}
|
|
1518
1676
|
const versionDir = pickLatestCursorVersionDir(root);
|
|
1519
1677
|
if (!versionDir) return null;
|
|
1520
|
-
const nodeExe =
|
|
1521
|
-
const indexJs =
|
|
1678
|
+
const nodeExe = path7.join(versionDir, "node.exe");
|
|
1679
|
+
const indexJs = path7.join(versionDir, "index.js");
|
|
1522
1680
|
if (!existsSync7(nodeExe) || !existsSync7(indexJs)) return null;
|
|
1523
1681
|
return { nodeExe, indexJs, versionDir };
|
|
1524
1682
|
}
|
|
@@ -1537,13 +1695,13 @@ function bundledSpawnTarget(nodeExe, indexJs, versionDir) {
|
|
|
1537
1695
|
function resolveCursorSpawn(agentBin) {
|
|
1538
1696
|
if (process.platform === "win32") {
|
|
1539
1697
|
const isCursorWrapper = /\.(cmd|bat)$/i.test(agentBin);
|
|
1540
|
-
const isBundledNode = /node\.exe$/i.test(agentBin) && existsSync8(
|
|
1698
|
+
const isBundledNode = /node\.exe$/i.test(agentBin) && existsSync8(path8.join(path8.dirname(agentBin), "index.js"));
|
|
1541
1699
|
const isDefaultShim = agentBin === "agent";
|
|
1542
1700
|
if (isCursorWrapper || isBundledNode || isDefaultShim) {
|
|
1543
|
-
const bundled = isCursorWrapper ? resolveWindowsCursorBundled(
|
|
1701
|
+
const bundled = isCursorWrapper ? resolveWindowsCursorBundled(path8.dirname(agentBin)) : isBundledNode ? {
|
|
1544
1702
|
nodeExe: agentBin,
|
|
1545
|
-
indexJs:
|
|
1546
|
-
versionDir:
|
|
1703
|
+
indexJs: path8.join(path8.dirname(agentBin), "index.js"),
|
|
1704
|
+
versionDir: path8.dirname(agentBin)
|
|
1547
1705
|
} : resolveWindowsCursorBundled();
|
|
1548
1706
|
if (bundled) {
|
|
1549
1707
|
return bundledSpawnTarget(bundled.nodeExe, bundled.indexJs, bundled.versionDir);
|
|
@@ -1563,7 +1721,7 @@ function resolveAgentBin() {
|
|
|
1563
1721
|
process.env.KYNVER_CURSOR_AGENT_ROOT?.trim() || void 0
|
|
1564
1722
|
);
|
|
1565
1723
|
if (bundled) return bundled.nodeExe;
|
|
1566
|
-
const localAgent =
|
|
1724
|
+
const localAgent = path8.join(process.env.LOCALAPPDATA || "", "cursor-agent", "agent.cmd");
|
|
1567
1725
|
if (existsSync8(localAgent)) return localAgent;
|
|
1568
1726
|
}
|
|
1569
1727
|
return "agent";
|
|
@@ -1573,7 +1731,7 @@ function cursorWorkerEnv(agentBin, spawnTarget) {
|
|
|
1573
1731
|
...process.env,
|
|
1574
1732
|
CI: "1",
|
|
1575
1733
|
NO_COLOR: "1",
|
|
1576
|
-
...spawnTarget.bundledVersionDir ? { CURSOR_INVOKED_AS:
|
|
1734
|
+
...spawnTarget.bundledVersionDir ? { CURSOR_INVOKED_AS: path8.basename(agentBin) || "agent.cmd" } : {}
|
|
1577
1735
|
};
|
|
1578
1736
|
}
|
|
1579
1737
|
var cursorProvider = {
|
|
@@ -1647,11 +1805,360 @@ function resolveWorkerProvider(name) {
|
|
|
1647
1805
|
// src/auto-complete.ts
|
|
1648
1806
|
import { spawn as spawn3 } from "node:child_process";
|
|
1649
1807
|
import { existsSync as existsSync9, openSync as openSync3, closeSync as closeSync3 } from "node:fs";
|
|
1650
|
-
import
|
|
1808
|
+
import path10 from "node:path";
|
|
1651
1809
|
import { fileURLToPath } from "node:url";
|
|
1652
1810
|
|
|
1653
1811
|
// src/worker-ops.ts
|
|
1654
|
-
import
|
|
1812
|
+
import path9 from "node:path";
|
|
1813
|
+
|
|
1814
|
+
// src/pr-handoff/pr-handoff-assess.ts
|
|
1815
|
+
var REVIEW_LANE_RULE = /^(lane:)?(review|deep_review|planning|landing)(:|$)/i;
|
|
1816
|
+
function trimOrNull3(value) {
|
|
1817
|
+
if (typeof value !== "string") return null;
|
|
1818
|
+
const trimmed = value.trim();
|
|
1819
|
+
return trimmed.length ? trimmed : null;
|
|
1820
|
+
}
|
|
1821
|
+
function committedHead(ancestry) {
|
|
1822
|
+
if (!ancestry?.checked) return null;
|
|
1823
|
+
if (ancestry.headIsAncestorOfBase !== false) return null;
|
|
1824
|
+
return trimOrNull3(ancestry.head);
|
|
1825
|
+
}
|
|
1826
|
+
function extractPrUrlFromText(value) {
|
|
1827
|
+
if (value === void 0 || value === null) return null;
|
|
1828
|
+
const text = typeof value === "string" ? value : typeof value === "object" && value !== null && "summary" in value ? String(value.summary ?? "") : JSON.stringify(value);
|
|
1829
|
+
const m = text.match(
|
|
1830
|
+
/https?:\/\/[^\s)>"]+\/(?:pull|pulls|merge_requests|pull-requests)\/\d+/i
|
|
1831
|
+
);
|
|
1832
|
+
return m ? trimOrNull3(m[0]) : null;
|
|
1833
|
+
}
|
|
1834
|
+
function hasWorkProduct(snapshot) {
|
|
1835
|
+
if (snapshot.changedFiles.length > 0) return true;
|
|
1836
|
+
if (trimOrNull3(snapshot.headCommit)) return true;
|
|
1837
|
+
if (committedHead(snapshot.gitAncestry)) return true;
|
|
1838
|
+
return false;
|
|
1839
|
+
}
|
|
1840
|
+
function assessPrHandoffRequirement(input) {
|
|
1841
|
+
if (!input.dispatched) {
|
|
1842
|
+
return { required: false, reason: "not_dispatched" };
|
|
1843
|
+
}
|
|
1844
|
+
const rule = trimOrNull3(input.routingRule) ?? "";
|
|
1845
|
+
if (rule && REVIEW_LANE_RULE.test(rule)) {
|
|
1846
|
+
return { required: false, reason: "review_lane" };
|
|
1847
|
+
}
|
|
1848
|
+
if (trimOrNull3(input.patchPath) || trimOrNull3(input.artifactBundlePath)) {
|
|
1849
|
+
return { required: false, reason: "patch_or_bundle" };
|
|
1850
|
+
}
|
|
1851
|
+
const prUrl = trimOrNull3(input.prUrl) ?? trimOrNull3(input.snapshot.prUrl);
|
|
1852
|
+
if (prUrl) {
|
|
1853
|
+
return { required: false, reason: "already_has_pr" };
|
|
1854
|
+
}
|
|
1855
|
+
if (!hasWorkProduct(input.snapshot)) {
|
|
1856
|
+
return { required: false, reason: "no_work_product" };
|
|
1857
|
+
}
|
|
1858
|
+
return { required: true, snapshot: input.snapshot };
|
|
1859
|
+
}
|
|
1860
|
+
function buildPrHandoffSnapshotFromStatus(status, extras) {
|
|
1861
|
+
return {
|
|
1862
|
+
changedFiles: status.changedFiles,
|
|
1863
|
+
branch: status.branch,
|
|
1864
|
+
worktreePath: status.worktreePath,
|
|
1865
|
+
gitAncestry: status.gitAncestry,
|
|
1866
|
+
finalResult: status.finalResult,
|
|
1867
|
+
headCommit: trimOrNull3(extras?.headCommit) ?? committedHead(status.gitAncestry),
|
|
1868
|
+
prUrl: trimOrNull3(extras?.prUrl) ?? null
|
|
1869
|
+
};
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
// src/pr-handoff/pr-handoff-gh.ts
|
|
1873
|
+
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
1874
|
+
function capture(bin, cwd, args) {
|
|
1875
|
+
try {
|
|
1876
|
+
const res = spawnSync2(bin, args, { cwd, encoding: "utf8" });
|
|
1877
|
+
return {
|
|
1878
|
+
status: res.status,
|
|
1879
|
+
stdout: (res.stdout || "").trim(),
|
|
1880
|
+
stderr: (res.stderr || "").trim(),
|
|
1881
|
+
error: res.error ? res.error.message : null
|
|
1882
|
+
};
|
|
1883
|
+
} catch (error) {
|
|
1884
|
+
return {
|
|
1885
|
+
status: null,
|
|
1886
|
+
stdout: "",
|
|
1887
|
+
stderr: "",
|
|
1888
|
+
error: error.message
|
|
1889
|
+
};
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
var defaultPrHandoffExec = {
|
|
1893
|
+
git: (cwd, args) => gitCapture(cwd, args),
|
|
1894
|
+
gh: (cwd, args) => capture("gh", cwd, args)
|
|
1895
|
+
};
|
|
1896
|
+
function parseGithubRepo(remoteUrl) {
|
|
1897
|
+
const trimmed = remoteUrl.trim();
|
|
1898
|
+
const ssh = trimmed.match(/git@github\.com:([^/]+\/[^/.]+)(?:\.git)?/i);
|
|
1899
|
+
if (ssh) return ssh[1];
|
|
1900
|
+
const https = trimmed.match(/github\.com[/:]([^/]+\/[^/.]+?)(?:\.git)?/i);
|
|
1901
|
+
if (https) return https[1];
|
|
1902
|
+
return null;
|
|
1903
|
+
}
|
|
1904
|
+
function firstLine(text) {
|
|
1905
|
+
return text.split("\n").map((l) => l.trim()).find(Boolean) ?? "";
|
|
1906
|
+
}
|
|
1907
|
+
function resolveGithubRepo(worktreePath, exec) {
|
|
1908
|
+
const remote = exec.git(worktreePath, ["remote", "get-url", "origin"]);
|
|
1909
|
+
if (remote.status !== 0) return null;
|
|
1910
|
+
return parseGithubRepo(remote.stdout);
|
|
1911
|
+
}
|
|
1912
|
+
function resolveHeadCommit(worktreePath, exec) {
|
|
1913
|
+
const head = exec.git(worktreePath, ["rev-parse", "HEAD"]);
|
|
1914
|
+
if (head.status !== 0) return null;
|
|
1915
|
+
return head.stdout.trim() || null;
|
|
1916
|
+
}
|
|
1917
|
+
function findOpenPrUrl(worktreePath, repo, branch, exec) {
|
|
1918
|
+
const listed = exec.gh(worktreePath, [
|
|
1919
|
+
"pr",
|
|
1920
|
+
"list",
|
|
1921
|
+
"--repo",
|
|
1922
|
+
repo,
|
|
1923
|
+
"--head",
|
|
1924
|
+
branch,
|
|
1925
|
+
"--state",
|
|
1926
|
+
"open",
|
|
1927
|
+
"--json",
|
|
1928
|
+
"url",
|
|
1929
|
+
"--limit",
|
|
1930
|
+
"1"
|
|
1931
|
+
]);
|
|
1932
|
+
if (listed.status !== 0) return null;
|
|
1933
|
+
try {
|
|
1934
|
+
const rows = JSON.parse(listed.stdout);
|
|
1935
|
+
const url = rows[0]?.url?.trim();
|
|
1936
|
+
return url || null;
|
|
1937
|
+
} catch {
|
|
1938
|
+
return null;
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
function commitAndPushBranch(input) {
|
|
1942
|
+
const { worktreePath, branch, commitMessage, hasDirtyFiles, exec } = input;
|
|
1943
|
+
if (hasDirtyFiles) {
|
|
1944
|
+
const add = exec.git(worktreePath, ["add", "-A"]);
|
|
1945
|
+
if (add.status !== 0) {
|
|
1946
|
+
return {
|
|
1947
|
+
ok: false,
|
|
1948
|
+
committed: false,
|
|
1949
|
+
pushed: false,
|
|
1950
|
+
detail: add.stderr || add.stdout || add.error || "git add failed"
|
|
1951
|
+
};
|
|
1952
|
+
}
|
|
1953
|
+
const commit = exec.git(worktreePath, ["commit", "-m", commitMessage]);
|
|
1954
|
+
if (commit.status !== 0) {
|
|
1955
|
+
return {
|
|
1956
|
+
ok: false,
|
|
1957
|
+
committed: false,
|
|
1958
|
+
pushed: false,
|
|
1959
|
+
detail: commit.stderr || commit.stdout || commit.error || "git commit failed"
|
|
1960
|
+
};
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
const push = exec.git(worktreePath, ["push", "-u", "origin", branch]);
|
|
1964
|
+
if (push.status !== 0) {
|
|
1965
|
+
return {
|
|
1966
|
+
ok: false,
|
|
1967
|
+
committed: hasDirtyFiles,
|
|
1968
|
+
pushed: false,
|
|
1969
|
+
detail: push.stderr || push.stdout || push.error || "git push failed"
|
|
1970
|
+
};
|
|
1971
|
+
}
|
|
1972
|
+
const headCommit = resolveHeadCommit(worktreePath, exec) ?? void 0;
|
|
1973
|
+
return {
|
|
1974
|
+
ok: true,
|
|
1975
|
+
committed: hasDirtyFiles,
|
|
1976
|
+
pushed: true,
|
|
1977
|
+
headCommit
|
|
1978
|
+
};
|
|
1979
|
+
}
|
|
1980
|
+
function createGithubPr(input) {
|
|
1981
|
+
const existing = findOpenPrUrl(input.worktreePath, input.repo, input.branch, input.exec);
|
|
1982
|
+
if (existing) {
|
|
1983
|
+
return { ok: true, prUrl: existing, created: false };
|
|
1984
|
+
}
|
|
1985
|
+
const created = input.exec.gh(input.worktreePath, [
|
|
1986
|
+
"pr",
|
|
1987
|
+
"create",
|
|
1988
|
+
"--repo",
|
|
1989
|
+
input.repo,
|
|
1990
|
+
"--base",
|
|
1991
|
+
input.base,
|
|
1992
|
+
"--head",
|
|
1993
|
+
input.branch,
|
|
1994
|
+
"--title",
|
|
1995
|
+
input.title,
|
|
1996
|
+
"--body",
|
|
1997
|
+
input.body,
|
|
1998
|
+
"--draft"
|
|
1999
|
+
]);
|
|
2000
|
+
if (created.status !== 0) {
|
|
2001
|
+
return {
|
|
2002
|
+
ok: false,
|
|
2003
|
+
detail: created.stderr || created.stdout || created.error || "gh pr create failed"
|
|
2004
|
+
};
|
|
2005
|
+
}
|
|
2006
|
+
const url = extractPrUrlFromGhOutput(created.stdout) ?? findOpenPrUrl(input.worktreePath, input.repo, input.branch, input.exec);
|
|
2007
|
+
if (!url) {
|
|
2008
|
+
return { ok: false, detail: "gh pr create succeeded but no PR URL was parsed" };
|
|
2009
|
+
}
|
|
2010
|
+
return { ok: true, prUrl: url, created: true };
|
|
2011
|
+
}
|
|
2012
|
+
function extractPrUrlFromGhOutput(stdout) {
|
|
2013
|
+
const line = firstLine(stdout);
|
|
2014
|
+
const m = line.match(/https?:\/\/[^\s]+\/pull\/\d+/i);
|
|
2015
|
+
return m ? m[0] : null;
|
|
2016
|
+
}
|
|
2017
|
+
|
|
2018
|
+
// src/pr-handoff/pr-handoff.ts
|
|
2019
|
+
function ghAvailable(exec) {
|
|
2020
|
+
const probe = exec.gh(process.cwd(), ["--version"]);
|
|
2021
|
+
return probe.status === 0;
|
|
2022
|
+
}
|
|
2023
|
+
function defaultPrTitle(workerName, runId) {
|
|
2024
|
+
return `AgentOS harness: ${workerName} (${runId})`;
|
|
2025
|
+
}
|
|
2026
|
+
function defaultPrBody(taskId, workerName, runId) {
|
|
2027
|
+
return [
|
|
2028
|
+
"Automated PR-ready handoff from the Kynver harness runtime.",
|
|
2029
|
+
"",
|
|
2030
|
+
taskId ? `AgentOS task: \`${taskId}\`` : "",
|
|
2031
|
+
`Harness worker: \`${workerName}\` \xB7 run \`${runId}\``,
|
|
2032
|
+
"",
|
|
2033
|
+
"Opened by orchestrator completion enforcement so production review receives a reviewable artifact."
|
|
2034
|
+
].filter(Boolean).join("\n");
|
|
2035
|
+
}
|
|
2036
|
+
function ensurePrReadyHandoff(input, exec = defaultPrHandoffExec) {
|
|
2037
|
+
const prUrlHint = input.prUrlHint ?? extractPrUrlFromText(input.status.finalResult) ?? null;
|
|
2038
|
+
const snapshot = buildPrHandoffSnapshotFromStatus(input.status, {
|
|
2039
|
+
prUrl: prUrlHint,
|
|
2040
|
+
headCommit: null
|
|
2041
|
+
});
|
|
2042
|
+
const requirement = assessPrHandoffRequirement({
|
|
2043
|
+
dispatched: input.worker.dispatched,
|
|
2044
|
+
routingRule: input.worker.routingRule,
|
|
2045
|
+
prUrl: prUrlHint,
|
|
2046
|
+
snapshot
|
|
2047
|
+
});
|
|
2048
|
+
if (!requirement.required) {
|
|
2049
|
+
return { ok: true, prUrl: prUrlHint ?? void 0 };
|
|
2050
|
+
}
|
|
2051
|
+
if (prUrlHint) {
|
|
2052
|
+
return { ok: true, prUrl: prUrlHint };
|
|
2053
|
+
}
|
|
2054
|
+
if (!ghAvailable(exec)) {
|
|
2055
|
+
const dirty = snapshot.changedFiles.length;
|
|
2056
|
+
const detail = dirty ? `${dirty} uncommitted change(s) with no PR URL` : "committed branch with no PR URL";
|
|
2057
|
+
return {
|
|
2058
|
+
ok: false,
|
|
2059
|
+
reason: `PR-ready handoff blocked: ${detail}`,
|
|
2060
|
+
nextAction: "Install and authenticate GitHub CLI (`gh auth login`), then commit, push, and run `gh pr create` (or rerun `kynver worker complete` after the PR exists)."
|
|
2061
|
+
};
|
|
2062
|
+
}
|
|
2063
|
+
const repo = resolveGithubRepo(snapshot.worktreePath, exec);
|
|
2064
|
+
if (!repo) {
|
|
2065
|
+
return {
|
|
2066
|
+
ok: false,
|
|
2067
|
+
reason: "PR-ready handoff blocked: could not resolve github.com origin for the worktree",
|
|
2068
|
+
nextAction: "Ensure `origin` points at GitHub, push the branch, open a PR, and rerun `kynver worker complete`."
|
|
2069
|
+
};
|
|
2070
|
+
}
|
|
2071
|
+
const existing = findOpenPrUrl(snapshot.worktreePath, repo, snapshot.branch, exec);
|
|
2072
|
+
if (existing) {
|
|
2073
|
+
return {
|
|
2074
|
+
ok: true,
|
|
2075
|
+
prUrl: existing,
|
|
2076
|
+
headCommit: snapshot.headCommit ?? resolveHeadCommit(snapshot.worktreePath, exec) ?? void 0
|
|
2077
|
+
};
|
|
2078
|
+
}
|
|
2079
|
+
const hasDirty = snapshot.changedFiles.length > 0;
|
|
2080
|
+
let committed = false;
|
|
2081
|
+
let pushed = false;
|
|
2082
|
+
let headCommit = snapshot.headCommit ?? void 0;
|
|
2083
|
+
const pushResult = commitAndPushBranch({
|
|
2084
|
+
worktreePath: snapshot.worktreePath,
|
|
2085
|
+
branch: snapshot.branch,
|
|
2086
|
+
commitMessage: `chore(harness): PR-ready handoff for ${input.worker.name}`,
|
|
2087
|
+
hasDirtyFiles: hasDirty,
|
|
2088
|
+
exec
|
|
2089
|
+
});
|
|
2090
|
+
if (hasDirty && !pushResult.ok) {
|
|
2091
|
+
return {
|
|
2092
|
+
ok: false,
|
|
2093
|
+
reason: `PR-ready handoff blocked: ${pushResult.detail ?? "git commit/push failed"}`,
|
|
2094
|
+
nextAction: "Commit and push the branch, run `gh pr create`, then rerun `kynver worker complete`."
|
|
2095
|
+
};
|
|
2096
|
+
}
|
|
2097
|
+
if (!hasDirty) {
|
|
2098
|
+
const pushOnly = exec.git(snapshot.worktreePath, ["push", "-u", "origin", snapshot.branch]);
|
|
2099
|
+
if (pushOnly.status !== 0 && !/already up to date/i.test(pushOnly.stderr || pushOnly.stdout)) {
|
|
2100
|
+
return {
|
|
2101
|
+
ok: false,
|
|
2102
|
+
reason: `PR-ready handoff blocked: ${pushOnly.stderr || pushOnly.stdout || pushOnly.error || "git push failed"}`,
|
|
2103
|
+
nextAction: "Push the branch to origin, run `gh pr create`, then rerun `kynver worker complete`."
|
|
2104
|
+
};
|
|
2105
|
+
}
|
|
2106
|
+
pushed = pushOnly.status === 0;
|
|
2107
|
+
} else {
|
|
2108
|
+
committed = pushResult.committed;
|
|
2109
|
+
pushed = pushResult.pushed;
|
|
2110
|
+
if (!pushResult.ok) {
|
|
2111
|
+
return {
|
|
2112
|
+
ok: false,
|
|
2113
|
+
reason: `PR-ready handoff blocked: ${pushResult.detail ?? "git push failed"}`,
|
|
2114
|
+
nextAction: "Fix git auth or merge conflicts, push the branch, run `gh pr create`, then rerun `kynver worker complete`."
|
|
2115
|
+
};
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
2118
|
+
headCommit = pushResult.headCommit ?? headCommit ?? resolveHeadCommit(snapshot.worktreePath, exec) ?? void 0;
|
|
2119
|
+
const base = input.run.base?.trim() || "main";
|
|
2120
|
+
const pr = createGithubPr({
|
|
2121
|
+
worktreePath: snapshot.worktreePath,
|
|
2122
|
+
repo,
|
|
2123
|
+
branch: snapshot.branch,
|
|
2124
|
+
base: base.replace(/^origin\//, ""),
|
|
2125
|
+
title: defaultPrTitle(input.worker.name, input.worker.runId),
|
|
2126
|
+
body: defaultPrBody(input.worker.taskId, input.worker.name, input.worker.runId),
|
|
2127
|
+
exec
|
|
2128
|
+
});
|
|
2129
|
+
if (!pr.ok || !pr.prUrl) {
|
|
2130
|
+
const dirty = snapshot.changedFiles.length;
|
|
2131
|
+
const detail = dirty ? `${dirty} uncommitted change(s) and no PR URL after handoff attempt` : "no PR URL after handoff attempt";
|
|
2132
|
+
return {
|
|
2133
|
+
ok: false,
|
|
2134
|
+
reason: `PR-ready handoff blocked: ${detail}${pr.detail ? ` (${pr.detail})` : ""}`,
|
|
2135
|
+
nextAction: "Run `gh pr create` on the worker branch (or fix `gh` auth), attach the PR URL to the task, then rerun `kynver worker complete`."
|
|
2136
|
+
};
|
|
2137
|
+
}
|
|
2138
|
+
return {
|
|
2139
|
+
ok: true,
|
|
2140
|
+
prUrl: pr.prUrl,
|
|
2141
|
+
headCommit: headCommit ?? void 0,
|
|
2142
|
+
committed,
|
|
2143
|
+
pushed,
|
|
2144
|
+
created: pr.created
|
|
2145
|
+
};
|
|
2146
|
+
}
|
|
2147
|
+
|
|
2148
|
+
// src/completion-ack.ts
|
|
2149
|
+
function hasCompletionAck(worker) {
|
|
2150
|
+
return Boolean(worker.completionReportedAt?.trim());
|
|
2151
|
+
}
|
|
2152
|
+
function persistCompletionAck(worker, runId, fields) {
|
|
2153
|
+
worker.completionReportedAt = fields.completionReportedAt;
|
|
2154
|
+
worker.completionOutcome = fields.completionOutcome;
|
|
2155
|
+
if (fields.completionResponse !== void 0) {
|
|
2156
|
+
worker.completionResponse = fields.completionResponse;
|
|
2157
|
+
}
|
|
2158
|
+
saveWorker(runId, worker);
|
|
2159
|
+
}
|
|
2160
|
+
|
|
2161
|
+
// src/worker-ops.ts
|
|
1655
2162
|
async function postCompletion(url, secret, body) {
|
|
1656
2163
|
const res = await fetch(url, {
|
|
1657
2164
|
method: "POST",
|
|
@@ -1673,6 +2180,42 @@ function completionErrorText(parsed) {
|
|
|
1673
2180
|
}
|
|
1674
2181
|
return void 0;
|
|
1675
2182
|
}
|
|
2183
|
+
function asRecord(value) {
|
|
2184
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
2185
|
+
}
|
|
2186
|
+
function asString(value) {
|
|
2187
|
+
if (typeof value !== "string") return null;
|
|
2188
|
+
const trimmed = value.trim();
|
|
2189
|
+
return trimmed.length ? trimmed : null;
|
|
2190
|
+
}
|
|
2191
|
+
function deriveLifecycleStage(input) {
|
|
2192
|
+
if (input.completionBlocker) return `blocked:${input.completionBlocker}`;
|
|
2193
|
+
if (input.completionOutcome) return input.completionOutcome;
|
|
2194
|
+
if (input.completionReportedAt) return "completion_acknowledged";
|
|
2195
|
+
if (input.finished) return "worker_finished";
|
|
2196
|
+
return "in_progress";
|
|
2197
|
+
}
|
|
2198
|
+
function deriveNextAction(input) {
|
|
2199
|
+
if (input.completionBlocker) {
|
|
2200
|
+
return "Resolve completion blocker, then rerun `kynver worker complete`.";
|
|
2201
|
+
}
|
|
2202
|
+
if (input.completionOutcome === "review_scheduled" || input.completionOutcome === "review_already_scheduled") {
|
|
2203
|
+
return "Await review lane and landing decision in Command Center.";
|
|
2204
|
+
}
|
|
2205
|
+
if (input.completionOutcome === "needs_attention") {
|
|
2206
|
+
return "Inspect blocker/attention reason in Command Center and dispatch a repair task.";
|
|
2207
|
+
}
|
|
2208
|
+
if (input.finished && !input.completionReportedAt) {
|
|
2209
|
+
return "Post completion acknowledgement to AgentOS (`kynver worker complete`).";
|
|
2210
|
+
}
|
|
2211
|
+
return null;
|
|
2212
|
+
}
|
|
2213
|
+
function deriveHandoffState(input) {
|
|
2214
|
+
if (input.prUrl) return "pr_handoff";
|
|
2215
|
+
if (input.headCommit) return "commit_handoff";
|
|
2216
|
+
if (input.changedFiles.length > 0) return "dirty_worktree";
|
|
2217
|
+
return "none";
|
|
2218
|
+
}
|
|
1676
2219
|
function persistCompletionBlocker(worker, reason) {
|
|
1677
2220
|
const current = worker.completionBlocker;
|
|
1678
2221
|
if ((current ?? void 0) === (reason ?? void 0)) return;
|
|
@@ -1683,10 +2226,17 @@ function persistCompletionBlocker(worker, reason) {
|
|
|
1683
2226
|
function workerStatusOptions(run) {
|
|
1684
2227
|
return run ? { base: run.base, baseCommit: run.baseCommit } : {};
|
|
1685
2228
|
}
|
|
2229
|
+
function applyPrHandoffToStatus(status, handoff) {
|
|
2230
|
+
return {
|
|
2231
|
+
...status,
|
|
2232
|
+
...handoff.prUrl ? { prUrl: handoff.prUrl } : {},
|
|
2233
|
+
...handoff.headCommit ? { headCommit: handoff.headCommit } : {}
|
|
2234
|
+
};
|
|
2235
|
+
}
|
|
1686
2236
|
async function tryCompleteWorker(args) {
|
|
1687
2237
|
const worker = loadWorker(String(args.run), String(args.name));
|
|
1688
2238
|
const run = loadRun(worker.runId);
|
|
1689
|
-
|
|
2239
|
+
let status = computeWorkerStatus(worker, workerStatusOptions(run));
|
|
1690
2240
|
const agentOsId = (args.agentOsId ? String(args.agentOsId) : worker.agentOsId) || "";
|
|
1691
2241
|
const taskId = (args.taskId ? String(args.taskId) : worker.taskId) || null;
|
|
1692
2242
|
if (!agentOsId) {
|
|
@@ -1695,6 +2245,34 @@ async function tryCompleteWorker(args) {
|
|
|
1695
2245
|
if (!isFinishedWorkerStatus(status)) {
|
|
1696
2246
|
return { ok: true, skipped: true, reason: "worker-not-finished" };
|
|
1697
2247
|
}
|
|
2248
|
+
const forceReplay = args.force === true || args.force === "true";
|
|
2249
|
+
if (!forceReplay && hasCompletionAck(worker)) {
|
|
2250
|
+
return {
|
|
2251
|
+
ok: true,
|
|
2252
|
+
skipped: true,
|
|
2253
|
+
reason: "completion-already-acknowledged",
|
|
2254
|
+
httpStatus: 200
|
|
2255
|
+
};
|
|
2256
|
+
}
|
|
2257
|
+
if (worker.localOnly) {
|
|
2258
|
+
return { ok: true, skipped: true, reason: "local-only-worker" };
|
|
2259
|
+
}
|
|
2260
|
+
const skipPrHandoff = args.skipPrHandoff === true || args.skipPrHandoff === "true";
|
|
2261
|
+
if (!skipPrHandoff && worker.dispatched && taskId) {
|
|
2262
|
+
const handoff = ensurePrReadyHandoff({ worker, run, status });
|
|
2263
|
+
if (!handoff.ok) {
|
|
2264
|
+
persistCompletionBlocker(worker, handoff.reason);
|
|
2265
|
+
return {
|
|
2266
|
+
ok: false,
|
|
2267
|
+
reason: handoff.reason,
|
|
2268
|
+
nextAction: handoff.nextAction,
|
|
2269
|
+
completionBlocked: true
|
|
2270
|
+
};
|
|
2271
|
+
}
|
|
2272
|
+
if (handoff.prUrl || handoff.headCommit) {
|
|
2273
|
+
status = applyPrHandoffToStatus(status, handoff);
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
1698
2276
|
const base = resolveBaseUrl(args.baseUrl ? String(args.baseUrl) : void 0);
|
|
1699
2277
|
const explicitSecret = args.secret ? String(args.secret) : void 0;
|
|
1700
2278
|
let secret = await resolveCallbackSecretWithMint(explicitSecret, agentOsId, { baseUrl: base });
|
|
@@ -1707,7 +2285,13 @@ async function tryCompleteWorker(args) {
|
|
|
1707
2285
|
taskId,
|
|
1708
2286
|
startedAt: worker.startedAt,
|
|
1709
2287
|
finishedAt: status.lastActivityAt || (/* @__PURE__ */ new Date()).toISOString(),
|
|
1710
|
-
status
|
|
2288
|
+
status,
|
|
2289
|
+
workerInjection: {
|
|
2290
|
+
instructionPolicyFingerprint: worker.instructionPolicyFingerprint ?? null,
|
|
2291
|
+
instructionPolicyEvidence: worker.instructionPolicyEvidence ?? null,
|
|
2292
|
+
personaSlug: worker.personaSlug ?? null,
|
|
2293
|
+
personaEvidence: worker.personaEvidence ?? null
|
|
2294
|
+
}
|
|
1711
2295
|
};
|
|
1712
2296
|
let result = await postCompletion(url, secret, body);
|
|
1713
2297
|
if ((result.status === 401 || result.status === 403) && !explicitSecret) {
|
|
@@ -1719,7 +2303,19 @@ async function tryCompleteWorker(args) {
|
|
|
1719
2303
|
}
|
|
1720
2304
|
if (result.ok) {
|
|
1721
2305
|
persistCompletionBlocker(worker, void 0);
|
|
1722
|
-
|
|
2306
|
+
const ack = {
|
|
2307
|
+
completionReportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2308
|
+
completionOutcome: "acknowledged",
|
|
2309
|
+
completionResponse: result.parsed
|
|
2310
|
+
};
|
|
2311
|
+
persistCompletionAck(worker, worker.runId, ack);
|
|
2312
|
+
const prUrl = status.prUrl;
|
|
2313
|
+
return {
|
|
2314
|
+
ok: true,
|
|
2315
|
+
httpStatus: result.status,
|
|
2316
|
+
response: result.parsed,
|
|
2317
|
+
...prUrl ? { prHandoff: { prUrl } } : {}
|
|
2318
|
+
};
|
|
1723
2319
|
}
|
|
1724
2320
|
const authRejected = result.status === 401 || result.status === 403;
|
|
1725
2321
|
const detail = completionErrorText(result.parsed) ?? (authRejected ? "runner token unauthorized" : "non-2xx response");
|
|
@@ -1780,7 +2376,7 @@ function workerStatus(args) {
|
|
|
1780
2376
|
const worker = loadWorker(String(args.run), String(args.name));
|
|
1781
2377
|
const run = loadRun(worker.runId);
|
|
1782
2378
|
const status = computeWorkerStatus(worker, workerStatusOptions(run));
|
|
1783
|
-
writeJson(
|
|
2379
|
+
writeJson(path9.join(worker.workerDir, "last-status.json"), status);
|
|
1784
2380
|
console.log(JSON.stringify(status, null, 2));
|
|
1785
2381
|
}
|
|
1786
2382
|
function buildRunBoard(runId) {
|
|
@@ -1788,7 +2384,7 @@ function buildRunBoard(runId) {
|
|
|
1788
2384
|
const names = Object.keys(run.workers || {});
|
|
1789
2385
|
const workers = names.map((name) => {
|
|
1790
2386
|
const worker = readJson(
|
|
1791
|
-
|
|
2387
|
+
path9.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
1792
2388
|
void 0
|
|
1793
2389
|
);
|
|
1794
2390
|
if (!worker) {
|
|
@@ -1810,6 +2406,29 @@ function buildRunBoard(runId) {
|
|
|
1810
2406
|
const completionBlocker = typeof rawBlocker === "string" && rawBlocker ? rawBlocker : void 0;
|
|
1811
2407
|
const boardStatus = completionBlocker ? "blocked" : status.status;
|
|
1812
2408
|
const boardAttention = completionBlocker ? "blocked" : status.attention.state;
|
|
2409
|
+
const completionResponse = asRecord(worker.completionResponse);
|
|
2410
|
+
const completionTask = asRecord(completionResponse?.task);
|
|
2411
|
+
const completionOutcome = asString(completionResponse?.outcome);
|
|
2412
|
+
const completionRouteStatus = asString(completionResponse?.status);
|
|
2413
|
+
const completionWarnings = Array.isArray(completionResponse?.warnings) ? completionResponse.warnings.filter((w) => typeof w === "string" && w.trim().length > 0) : [];
|
|
2414
|
+
const prUrl = asString(completionTask?.prUrl) ?? asString(completionResponse?.prUrl);
|
|
2415
|
+
const lifecycleStage = deriveLifecycleStage({
|
|
2416
|
+
finished: isFinishedWorkerStatus(status),
|
|
2417
|
+
completionBlocker,
|
|
2418
|
+
completionOutcome,
|
|
2419
|
+
completionReportedAt: worker.completionReportedAt ?? null
|
|
2420
|
+
});
|
|
2421
|
+
const nextAction = deriveNextAction({
|
|
2422
|
+
completionBlocker,
|
|
2423
|
+
completionOutcome,
|
|
2424
|
+
completionReportedAt: worker.completionReportedAt ?? null,
|
|
2425
|
+
finished: isFinishedWorkerStatus(status)
|
|
2426
|
+
});
|
|
2427
|
+
const handoffState = deriveHandoffState({
|
|
2428
|
+
changedFiles: status.changedFiles,
|
|
2429
|
+
headCommit,
|
|
2430
|
+
prUrl: prUrl ?? void 0
|
|
2431
|
+
});
|
|
1813
2432
|
return {
|
|
1814
2433
|
worker: status.worker,
|
|
1815
2434
|
status: boardStatus,
|
|
@@ -1829,12 +2448,39 @@ function buildRunBoard(runId) {
|
|
|
1829
2448
|
changedFileCount: status.changedFiles.length,
|
|
1830
2449
|
changedFiles: status.changedFiles,
|
|
1831
2450
|
branch: status.branch,
|
|
2451
|
+
taskId: worker.taskId ?? null,
|
|
2452
|
+
planId: worker.planId ?? null,
|
|
2453
|
+
instructionPolicyFingerprint: typeof worker.instructionPolicyFingerprint === "string" ? worker.instructionPolicyFingerprint : null,
|
|
2454
|
+
instructionPolicyRuleCount: (() => {
|
|
2455
|
+
const raw = worker.instructionPolicyEvidence;
|
|
2456
|
+
if (!raw || typeof raw !== "object") return null;
|
|
2457
|
+
const slugs = raw.ruleSlugs;
|
|
2458
|
+
return Array.isArray(slugs) ? slugs.length : null;
|
|
2459
|
+
})(),
|
|
2460
|
+
leaseOwner: worker.leaseOwner ?? null,
|
|
1832
2461
|
model: typeof worker.model === "string" ? worker.model : void 0,
|
|
1833
2462
|
routingRule: typeof worker.routingRule === "string" ? worker.routingRule : void 0,
|
|
1834
2463
|
requestedModel: typeof worker.requestedModel === "string" ? worker.requestedModel : void 0,
|
|
1835
2464
|
headCommit,
|
|
2465
|
+
prUrl,
|
|
2466
|
+
handoffState,
|
|
1836
2467
|
gitAncestry: status.gitAncestry,
|
|
1837
2468
|
finalResult: status.finalResult,
|
|
2469
|
+
lifecycleStage,
|
|
2470
|
+
completionReportedAt: worker.completionReportedAt ?? null,
|
|
2471
|
+
completionOutcome: worker.completionOutcome ?? null,
|
|
2472
|
+
completionRouteStatus,
|
|
2473
|
+
completionRouteOutcome: completionOutcome,
|
|
2474
|
+
completionWarnings,
|
|
2475
|
+
completionBlocker: completionBlocker ?? null,
|
|
2476
|
+
checkpoint: {
|
|
2477
|
+
phase: status.lastHeartbeatPhase,
|
|
2478
|
+
summary: status.lastHeartbeatSummary,
|
|
2479
|
+
blocker: status.heartbeatBlocker
|
|
2480
|
+
},
|
|
2481
|
+
lastCommandHint: status.currentTool ?? status.lastHeartbeatSummary,
|
|
2482
|
+
failureReason: completionBlocker ?? status.error ?? null,
|
|
2483
|
+
nextAction,
|
|
1838
2484
|
ancestry: status.gitAncestry.relation,
|
|
1839
2485
|
ancestryChecked: status.gitAncestry.checked
|
|
1840
2486
|
};
|
|
@@ -1848,7 +2494,7 @@ function buildRunBoard(runId) {
|
|
|
1848
2494
|
needsAttention: workers.filter((w) => w.attention && w.attention !== "ok" && w.attention !== "done").map((w) => w.worker),
|
|
1849
2495
|
workers
|
|
1850
2496
|
};
|
|
1851
|
-
writeJson(
|
|
2497
|
+
writeJson(path9.join(runDirectory(run.id), "last-board.json"), board);
|
|
1852
2498
|
return board;
|
|
1853
2499
|
}
|
|
1854
2500
|
async function publishHarnessBoardSnapshot(args, source) {
|
|
@@ -2015,12 +2661,12 @@ async function autoCompleteWorkerCli(raw) {
|
|
|
2015
2661
|
}
|
|
2016
2662
|
}
|
|
2017
2663
|
function resolveDefaultCliPath() {
|
|
2018
|
-
return
|
|
2664
|
+
return path10.join(fileURLToPath(new URL(".", import.meta.url)), "cli.js");
|
|
2019
2665
|
}
|
|
2020
2666
|
function spawnCompletionSidecar(opts) {
|
|
2021
2667
|
const cliPath = opts.cliPath ?? resolveDefaultCliPath();
|
|
2022
2668
|
if (!existsSync9(cliPath)) return void 0;
|
|
2023
|
-
const logPath =
|
|
2669
|
+
const logPath = path10.join(opts.workerDir, "auto-complete.log");
|
|
2024
2670
|
let logFd;
|
|
2025
2671
|
try {
|
|
2026
2672
|
logFd = openSync3(logPath, "a");
|
|
@@ -2102,16 +2748,16 @@ function spawnWorkerProcess(run, opts) {
|
|
|
2102
2748
|
launchModel = preflight.model;
|
|
2103
2749
|
}
|
|
2104
2750
|
const { worktreesDir } = getPaths();
|
|
2105
|
-
const workerDir =
|
|
2751
|
+
const workerDir = path11.join(runDirectory(run.id), "workers", name);
|
|
2106
2752
|
mkdirSync3(workerDir, { recursive: true });
|
|
2107
|
-
const worktreePath =
|
|
2753
|
+
const worktreePath = path11.join(worktreesDir, run.id, name);
|
|
2108
2754
|
const branch = opts.branch || `agent/${run.id}/${name}`;
|
|
2109
2755
|
if (existsSync10(worktreePath)) throw new Error(`worktree path already exists: ${worktreePath}`);
|
|
2110
2756
|
git(run.repo, ["fetch", "origin", "--prune"], { allowFailure: true });
|
|
2111
2757
|
git(run.repo, ["worktree", "add", "-b", branch, worktreePath, run.baseCommit], { throwError: true });
|
|
2112
|
-
const stdoutPath =
|
|
2113
|
-
const stderrPath =
|
|
2114
|
-
const heartbeatPath =
|
|
2758
|
+
const stdoutPath = path11.join(workerDir, "stdout.jsonl");
|
|
2759
|
+
const stderrPath = path11.join(workerDir, "stderr.log");
|
|
2760
|
+
const heartbeatPath = path11.join(workerDir, "heartbeat.jsonl");
|
|
2115
2761
|
const prompt = buildPrompt({
|
|
2116
2762
|
task: opts.task,
|
|
2117
2763
|
ownedPaths: opts.ownedPaths || [],
|
|
@@ -2119,6 +2765,8 @@ function spawnWorkerProcess(run, opts) {
|
|
|
2119
2765
|
heartbeatPath,
|
|
2120
2766
|
planId: opts.planId,
|
|
2121
2767
|
taskId: opts.taskId,
|
|
2768
|
+
instructionPolicyMarkdown: opts.instructionPolicyMarkdown,
|
|
2769
|
+
personaMarkdown: opts.personaMarkdown,
|
|
2122
2770
|
model: launchModel
|
|
2123
2771
|
});
|
|
2124
2772
|
let started;
|
|
@@ -2158,14 +2806,19 @@ function spawnWorkerProcess(run, opts) {
|
|
|
2158
2806
|
...opts.agentOsId ? { agentOsId: String(opts.agentOsId) } : {},
|
|
2159
2807
|
...opts.taskId ? { taskId: String(opts.taskId) } : {},
|
|
2160
2808
|
...opts.planId ? { planId: String(opts.planId) } : {},
|
|
2809
|
+
...opts.instructionPolicyFingerprint ? { instructionPolicyFingerprint: String(opts.instructionPolicyFingerprint) } : {},
|
|
2810
|
+
...opts.instructionPolicyEvidence ? { instructionPolicyEvidence: opts.instructionPolicyEvidence } : {},
|
|
2811
|
+
...opts.personaSlug ? { personaSlug: String(opts.personaSlug) } : {},
|
|
2812
|
+
...opts.personaEvidence ? { personaEvidence: opts.personaEvidence } : {},
|
|
2161
2813
|
...opts.leaseOwner ? { leaseOwner: String(opts.leaseOwner) } : {},
|
|
2162
2814
|
...opts.dispatched ? { dispatched: true } : {},
|
|
2815
|
+
...!opts.agentOsId || !opts.taskId ? { localOnly: true } : {},
|
|
2163
2816
|
routingRule: routing.rule,
|
|
2164
2817
|
...routing.requestedModel ? { requestedModel: routing.requestedModel } : {},
|
|
2165
2818
|
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2166
2819
|
};
|
|
2167
2820
|
saveWorker(run.id, worker);
|
|
2168
|
-
run.workers = { ...run.workers || {}, [name]: { workerDir, statusPath:
|
|
2821
|
+
run.workers = { ...run.workers || {}, [name]: { workerDir, statusPath: path11.join(workerDir, "worker.json") } };
|
|
2169
2822
|
run.status = "running";
|
|
2170
2823
|
saveRun(run);
|
|
2171
2824
|
if (worker.agentOsId && worker.taskId) {
|
|
@@ -2185,6 +2838,10 @@ function spawnWorkerProcess(run, opts) {
|
|
|
2185
2838
|
if (!sidecarSpawned) {
|
|
2186
2839
|
const reason = "completion sidecar failed to spawn (CLI not found or spawn error)";
|
|
2187
2840
|
worker.completionBlocker = reason;
|
|
2841
|
+
worker.completionSidecarSpawnFailedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2842
|
+
saveWorker(run.id, worker);
|
|
2843
|
+
} else if (sidecarSpawned.pid) {
|
|
2844
|
+
worker.completionSidecarPid = sidecarSpawned.pid;
|
|
2188
2845
|
saveWorker(run.id, worker);
|
|
2189
2846
|
}
|
|
2190
2847
|
}
|
|
@@ -2202,6 +2859,14 @@ async function startWorker(args) {
|
|
|
2202
2859
|
console.error("missing --task or --task-file");
|
|
2203
2860
|
process.exit(1);
|
|
2204
2861
|
}
|
|
2862
|
+
const boardLinked = Boolean(args.agentOsId && args.taskId);
|
|
2863
|
+
const explicitLocalOnly = args.localOnly === true || args.localOnly === "true";
|
|
2864
|
+
if (!boardLinked && !explicitLocalOnly && (args.agentOsId || args.taskId)) {
|
|
2865
|
+
console.error(
|
|
2866
|
+
"worker start: board-linked workers require both --agent-os-id and --task-id (or pass --local-only for direct runs)"
|
|
2867
|
+
);
|
|
2868
|
+
process.exit(1);
|
|
2869
|
+
}
|
|
2205
2870
|
const wait = args.wait === true || args.wait === "true";
|
|
2206
2871
|
let worker;
|
|
2207
2872
|
try {
|
|
@@ -2249,6 +2914,32 @@ async function startWorker(args) {
|
|
|
2249
2914
|
|
|
2250
2915
|
// src/dispatch.ts
|
|
2251
2916
|
var DEFAULT_DISPATCH_LEASE_MS = 60 * 60 * 1e3;
|
|
2917
|
+
function readHarnessWorkerContext(decision) {
|
|
2918
|
+
const raw = decision.harnessWorkerContext;
|
|
2919
|
+
if (!raw || typeof raw !== "object") return null;
|
|
2920
|
+
const ctx = raw;
|
|
2921
|
+
const markdown = typeof ctx.instructionPolicyMarkdown === "string" ? ctx.instructionPolicyMarkdown : null;
|
|
2922
|
+
const fingerprint = typeof ctx.instructionPolicyFingerprint === "string" ? ctx.instructionPolicyFingerprint : null;
|
|
2923
|
+
const evidence = ctx.instructionPolicyEvidence && typeof ctx.instructionPolicyEvidence === "object" ? ctx.instructionPolicyEvidence : null;
|
|
2924
|
+
const personaMarkdown = typeof ctx.personaMarkdown === "string" ? ctx.personaMarkdown : null;
|
|
2925
|
+
const personaSlug = typeof ctx.personaEvidence === "object" && ctx.personaEvidence && typeof ctx.personaEvidence.injectedPersonaSlug === "string" ? ctx.personaEvidence.injectedPersonaSlug : null;
|
|
2926
|
+
const personaEvidence = ctx.personaEvidence && typeof ctx.personaEvidence === "object" ? ctx.personaEvidence : null;
|
|
2927
|
+
const personaInjectionReady = ctx.personaInjectionReady === true;
|
|
2928
|
+
return {
|
|
2929
|
+
instructionPolicyMarkdown: markdown,
|
|
2930
|
+
instructionPolicyFingerprint: fingerprint,
|
|
2931
|
+
instructionPolicyEvidence: evidence,
|
|
2932
|
+
personaMarkdown,
|
|
2933
|
+
personaSlug,
|
|
2934
|
+
personaEvidence,
|
|
2935
|
+
personaInjectionReady
|
|
2936
|
+
};
|
|
2937
|
+
}
|
|
2938
|
+
function normalizePersonaSlug(value) {
|
|
2939
|
+
if (typeof value !== "string") return null;
|
|
2940
|
+
const trimmed = value.trim().toLowerCase();
|
|
2941
|
+
return trimmed.length ? trimmed : null;
|
|
2942
|
+
}
|
|
2252
2943
|
function buildDispatchTaskText(task, agentOsId) {
|
|
2253
2944
|
return [
|
|
2254
2945
|
`[AgentOS task ${task.id}] ${task.title}`,
|
|
@@ -2273,6 +2964,20 @@ async function dispatchRun(args) {
|
|
|
2273
2964
|
const runnerResourceGate = observeRunnerResourceGate({ runId: run.id });
|
|
2274
2965
|
const requestedStarts = Number(args.maxStarts) > 0 ? Math.floor(Number(args.maxStarts)) : 1;
|
|
2275
2966
|
const cappedStarts = dryRun ? requestedStarts : Math.min(requestedStarts, runnerResourceGate.slotsAvailable);
|
|
2967
|
+
const activeHarnessWorkers = [];
|
|
2968
|
+
for (const name of Object.keys(run.workers || {})) {
|
|
2969
|
+
const worker = readJson(
|
|
2970
|
+
path12.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
2971
|
+
void 0
|
|
2972
|
+
);
|
|
2973
|
+
if (!worker?.taskId || !isPidAlive(worker.pid)) continue;
|
|
2974
|
+
activeHarnessWorkers.push({
|
|
2975
|
+
runId: run.id,
|
|
2976
|
+
workerName: name,
|
|
2977
|
+
taskId: worker.taskId,
|
|
2978
|
+
pid: worker.pid
|
|
2979
|
+
});
|
|
2980
|
+
}
|
|
2276
2981
|
const dispatchUrl = `${base}/api/agent-os/by-id/${encodeURIComponent(agentOsId)}/tasks/dispatch-next`;
|
|
2277
2982
|
const body = {
|
|
2278
2983
|
agentOsId,
|
|
@@ -2282,6 +2987,8 @@ async function dispatchRun(args) {
|
|
|
2282
2987
|
leaseDurationMs: Number(args.leaseMs) > 0 ? Math.floor(Number(args.leaseMs)) : DEFAULT_DISPATCH_LEASE_MS,
|
|
2283
2988
|
runnerDiskGate,
|
|
2284
2989
|
runnerResourceGate,
|
|
2990
|
+
activeHarnessWorkers,
|
|
2991
|
+
harnessBoardSnapshot: buildRunBoard(run.id),
|
|
2285
2992
|
...args.lane ? { lane: String(args.lane) } : {},
|
|
2286
2993
|
executor: args.executor ? String(args.executor) : "harness",
|
|
2287
2994
|
...args.diskPath ? { diskPath: String(args.diskPath) } : {}
|
|
@@ -2341,6 +3048,25 @@ async function dispatchRun(args) {
|
|
|
2341
3048
|
const outcomes = [];
|
|
2342
3049
|
for (const decision of result.started) {
|
|
2343
3050
|
const task = decision.task;
|
|
3051
|
+
const harnessContext = readHarnessWorkerContext(decision);
|
|
3052
|
+
const taskId = String(task.id);
|
|
3053
|
+
const expectedPersona = normalizePersonaSlug(task.personaSlug);
|
|
3054
|
+
if (expectedPersona && (!harnessContext?.personaInjectionReady || !harnessContext.personaMarkdown)) {
|
|
3055
|
+
outcomes.push({
|
|
3056
|
+
taskId,
|
|
3057
|
+
started: false,
|
|
3058
|
+
error: `persona_injection_required: missing anchored context-envelope persona block for "${expectedPersona}"`
|
|
3059
|
+
});
|
|
3060
|
+
continue;
|
|
3061
|
+
}
|
|
3062
|
+
if (hasLiveWorkerForTask(run.id, taskId)) {
|
|
3063
|
+
outcomes.push({
|
|
3064
|
+
taskId,
|
|
3065
|
+
started: false,
|
|
3066
|
+
error: "duplicate_dispatch_prevented: live local worker already owns this task"
|
|
3067
|
+
});
|
|
3068
|
+
continue;
|
|
3069
|
+
}
|
|
2344
3070
|
const attempt = Number(task.attempt) || 1;
|
|
2345
3071
|
if (attempt > retryLimits.maxTaskAttempts) {
|
|
2346
3072
|
outcomes.push({
|
|
@@ -2368,6 +3094,12 @@ async function dispatchRun(args) {
|
|
|
2368
3094
|
agentOsId,
|
|
2369
3095
|
taskId: String(task.id),
|
|
2370
3096
|
planId,
|
|
3097
|
+
instructionPolicyMarkdown: harnessContext?.instructionPolicyMarkdown ?? null,
|
|
3098
|
+
instructionPolicyFingerprint: harnessContext?.instructionPolicyFingerprint ?? null,
|
|
3099
|
+
instructionPolicyEvidence: harnessContext?.instructionPolicyEvidence ?? null,
|
|
3100
|
+
personaMarkdown: harnessContext?.personaMarkdown ?? null,
|
|
3101
|
+
personaSlug: harnessContext?.personaSlug ?? expectedPersona,
|
|
3102
|
+
personaEvidence: harnessContext?.personaEvidence ?? null,
|
|
2371
3103
|
leaseOwner,
|
|
2372
3104
|
dispatched: true
|
|
2373
3105
|
});
|
|
@@ -2379,8 +3111,22 @@ async function dispatchRun(args) {
|
|
|
2379
3111
|
branch: worker.branch,
|
|
2380
3112
|
model: worker.model,
|
|
2381
3113
|
provider: routing.provider,
|
|
2382
|
-
routingRule: routing.rule
|
|
3114
|
+
routingRule: routing.rule,
|
|
3115
|
+
instructionPolicyFingerprint: harnessContext?.instructionPolicyFingerprint ?? null,
|
|
3116
|
+
instructionPolicyRuleCount: Array.isArray(harnessContext?.instructionPolicyEvidence?.ruleSlugs) ? harnessContext.instructionPolicyEvidence.ruleSlugs.length : null,
|
|
3117
|
+
personaSlug: harnessContext?.personaSlug ?? expectedPersona,
|
|
3118
|
+
personaOperatingRuleCount: harnessContext?.personaEvidence?.operatingRuleCount ?? null
|
|
2383
3119
|
});
|
|
3120
|
+
if (harnessContext?.instructionPolicyFingerprint) {
|
|
3121
|
+
console.error(
|
|
3122
|
+
`[dispatch] task ${taskId}: Lane A instruction policy injected fingerprint=${harnessContext.instructionPolicyFingerprint}`
|
|
3123
|
+
);
|
|
3124
|
+
}
|
|
3125
|
+
if (harnessContext?.personaSlug) {
|
|
3126
|
+
console.error(
|
|
3127
|
+
`[dispatch] task ${taskId}: persona context injected slug=${harnessContext.personaSlug}`
|
|
3128
|
+
);
|
|
3129
|
+
}
|
|
2384
3130
|
} catch (error) {
|
|
2385
3131
|
const releaseUrl = `${base}/api/agent-os/by-id/${encodeURIComponent(agentOsId)}/tasks/${encodeURIComponent(String(task.id))}/release`;
|
|
2386
3132
|
let release;
|
|
@@ -2434,7 +3180,7 @@ function redactHarness(text, secret) {
|
|
|
2434
3180
|
}
|
|
2435
3181
|
|
|
2436
3182
|
// src/validate.ts
|
|
2437
|
-
import
|
|
3183
|
+
import path13 from "node:path";
|
|
2438
3184
|
var RUN_ID_RE = /^[a-z0-9][a-z0-9._-]{0,127}$/i;
|
|
2439
3185
|
var WORKER_NAME_RE = /^[a-z0-9][a-z0-9._-]{0,63}$/i;
|
|
2440
3186
|
function validateRunId(runId) {
|
|
@@ -2448,15 +3194,15 @@ function validateWorkerName(name) {
|
|
|
2448
3194
|
return trimmed;
|
|
2449
3195
|
}
|
|
2450
3196
|
function validateRepo(repo) {
|
|
2451
|
-
const resolved =
|
|
3197
|
+
const resolved = path13.resolve(repo);
|
|
2452
3198
|
if (resolved.includes("..")) throw new Error("repo path must not contain .. segments");
|
|
2453
3199
|
return resolved;
|
|
2454
3200
|
}
|
|
2455
3201
|
function validateOwnedPaths(repoRoot, ownedPaths) {
|
|
2456
3202
|
return ownedPaths.map((owned) => {
|
|
2457
|
-
const resolved =
|
|
2458
|
-
const rel =
|
|
2459
|
-
if (rel.startsWith("..") ||
|
|
3203
|
+
const resolved = path13.resolve(repoRoot, owned);
|
|
3204
|
+
const rel = path13.relative(repoRoot, resolved);
|
|
3205
|
+
if (rel.startsWith("..") || path13.isAbsolute(rel)) {
|
|
2460
3206
|
throw new Error(`owned path escapes repo: ${owned}`);
|
|
2461
3207
|
}
|
|
2462
3208
|
return resolved;
|
|
@@ -2469,7 +3215,7 @@ function validateTailLines(lines) {
|
|
|
2469
3215
|
|
|
2470
3216
|
// src/worktree.ts
|
|
2471
3217
|
import { existsSync as existsSync11, mkdirSync as mkdirSync4 } from "node:fs";
|
|
2472
|
-
import
|
|
3218
|
+
import path14 from "node:path";
|
|
2473
3219
|
function createRun(args) {
|
|
2474
3220
|
const repo = validateRepo(required(String(args.repo || ""), "--repo"));
|
|
2475
3221
|
ensureGitRepo(repo);
|
|
@@ -2489,12 +3235,12 @@ function createRun(args) {
|
|
|
2489
3235
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2490
3236
|
workers: {}
|
|
2491
3237
|
};
|
|
2492
|
-
writeJson(
|
|
3238
|
+
writeJson(path14.join(dir, "run.json"), run);
|
|
2493
3239
|
console.log(JSON.stringify({ runId: id, runDir: dir, repo, base, baseCommit }, null, 2));
|
|
2494
3240
|
}
|
|
2495
3241
|
function listRuns() {
|
|
2496
3242
|
const { runsDir } = getPaths();
|
|
2497
|
-
const rows = listRunIds(runsDir).map((id) => readJson(
|
|
3243
|
+
const rows = listRunIds(runsDir).map((id) => readJson(path14.join(runDirectory(id), "run.json"), void 0)).filter(Boolean).map((run) => ({
|
|
2498
3244
|
id: run.id,
|
|
2499
3245
|
name: run.name,
|
|
2500
3246
|
status: run.status,
|
|
@@ -2509,7 +3255,7 @@ function failExists(message) {
|
|
|
2509
3255
|
}
|
|
2510
3256
|
|
|
2511
3257
|
// src/sweep.ts
|
|
2512
|
-
import
|
|
3258
|
+
import path15 from "node:path";
|
|
2513
3259
|
async function sweepRun(args) {
|
|
2514
3260
|
const pipeline = args.pipeline === true || args.pipeline === "true";
|
|
2515
3261
|
try {
|
|
@@ -2522,7 +3268,7 @@ async function sweepRun(args) {
|
|
|
2522
3268
|
const releasedLocalOrphans = [];
|
|
2523
3269
|
for (const name of Object.keys(run.workers || {})) {
|
|
2524
3270
|
const worker = readJson(
|
|
2525
|
-
|
|
3271
|
+
path15.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
2526
3272
|
void 0
|
|
2527
3273
|
);
|
|
2528
3274
|
if (!worker || !worker.dispatched || !worker.taskId) continue;
|
|
@@ -2569,13 +3315,13 @@ import { mkdirSync as mkdirSync5, realpathSync } from "node:fs";
|
|
|
2569
3315
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
2570
3316
|
|
|
2571
3317
|
// src/pipeline-tick.ts
|
|
2572
|
-
import
|
|
3318
|
+
import path24 from "node:path";
|
|
2573
3319
|
|
|
2574
3320
|
// src/stale-reconcile.ts
|
|
2575
|
-
import
|
|
3321
|
+
import path17 from "node:path";
|
|
2576
3322
|
|
|
2577
3323
|
// src/finalize.ts
|
|
2578
|
-
import
|
|
3324
|
+
import path16 from "node:path";
|
|
2579
3325
|
var ACTIVE_RUN_STATUSES = /* @__PURE__ */ new Set(["running", "dispatching", "pending", "queued"]);
|
|
2580
3326
|
function terminalStatusFor(run) {
|
|
2581
3327
|
const names = Object.keys(run.workers || {});
|
|
@@ -2586,7 +3332,7 @@ function terminalStatusFor(run) {
|
|
|
2586
3332
|
let anyLandingBlocked = false;
|
|
2587
3333
|
for (const name of names) {
|
|
2588
3334
|
const worker = readJson(
|
|
2589
|
-
|
|
3335
|
+
path16.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
2590
3336
|
void 0
|
|
2591
3337
|
);
|
|
2592
3338
|
if (!worker) continue;
|
|
@@ -2638,7 +3384,7 @@ function reconcileStaleWorkers() {
|
|
|
2638
3384
|
const now = Date.now();
|
|
2639
3385
|
for (const run of listRunRecords()) {
|
|
2640
3386
|
for (const name of Object.keys(run.workers || {})) {
|
|
2641
|
-
const workerPath =
|
|
3387
|
+
const workerPath = path17.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json");
|
|
2642
3388
|
const worker = readJson(workerPath, void 0);
|
|
2643
3389
|
if (!worker || worker.status !== "running") {
|
|
2644
3390
|
outcomes.push({
|
|
@@ -2651,7 +3397,21 @@ function reconcileStaleWorkers() {
|
|
|
2651
3397
|
}
|
|
2652
3398
|
const status = computeWorkerStatus(worker, { base: run.base, baseCommit: run.baseCommit });
|
|
2653
3399
|
if (status.finalResult) {
|
|
2654
|
-
|
|
3400
|
+
if (worker.status === "running") {
|
|
3401
|
+
const nextStatus = status.attention.state === "blocked" ? "blocked" : status.attention.state === "done" || status.status === "done" ? "done" : "exited";
|
|
3402
|
+
worker.status = nextStatus;
|
|
3403
|
+
worker.reconciledAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3404
|
+
worker.reconcileReason = "synced finished worker record after terminal stdout/heartbeat";
|
|
3405
|
+
saveWorker(run.id, worker);
|
|
3406
|
+
outcomes.push({
|
|
3407
|
+
runId: run.id,
|
|
3408
|
+
worker: name,
|
|
3409
|
+
action: "marked_exited",
|
|
3410
|
+
reason: worker.reconcileReason
|
|
3411
|
+
});
|
|
3412
|
+
} else {
|
|
3413
|
+
outcomes.push({ runId: run.id, worker: name, action: "skipped", reason: "final result present" });
|
|
3414
|
+
}
|
|
2655
3415
|
continue;
|
|
2656
3416
|
}
|
|
2657
3417
|
if (!status.alive) {
|
|
@@ -2700,7 +3460,7 @@ function reconcileStaleWorkers() {
|
|
|
2700
3460
|
}
|
|
2701
3461
|
|
|
2702
3462
|
// src/plan-progress-daemon-sync.ts
|
|
2703
|
-
import
|
|
3463
|
+
import path18 from "node:path";
|
|
2704
3464
|
|
|
2705
3465
|
// src/plan-progress-sync.ts
|
|
2706
3466
|
async function syncPlanProgress(args) {
|
|
@@ -2724,7 +3484,7 @@ async function syncActiveWorkerPlanProgress(runId, args) {
|
|
|
2724
3484
|
const outcomes = [];
|
|
2725
3485
|
for (const name of Object.keys(run.workers || {})) {
|
|
2726
3486
|
const worker = readJson(
|
|
2727
|
-
|
|
3487
|
+
path18.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
2728
3488
|
void 0
|
|
2729
3489
|
);
|
|
2730
3490
|
if (!worker?.dispatched || !worker.taskId) continue;
|
|
@@ -2773,7 +3533,7 @@ async function fetchWorkspaceRuntimePreferences(agentOsId, args) {
|
|
|
2773
3533
|
}
|
|
2774
3534
|
|
|
2775
3535
|
// src/cleanup.ts
|
|
2776
|
-
import
|
|
3536
|
+
import path23 from "node:path";
|
|
2777
3537
|
|
|
2778
3538
|
// src/cleanup-types.ts
|
|
2779
3539
|
var DEFAULT_NODE_MODULES_AGE_MS = 6 * 60 * 60 * 1e3;
|
|
@@ -2845,11 +3605,11 @@ function skipNodeModulesRemoval(input) {
|
|
|
2845
3605
|
|
|
2846
3606
|
// src/cleanup-execute.ts
|
|
2847
3607
|
import { existsSync as existsSync13, rmSync } from "node:fs";
|
|
2848
|
-
import
|
|
3608
|
+
import path20 from "node:path";
|
|
2849
3609
|
|
|
2850
3610
|
// src/cleanup-dir-size.ts
|
|
2851
3611
|
import { existsSync as existsSync12, readdirSync as readdirSync4, statSync as statSync2 } from "node:fs";
|
|
2852
|
-
import
|
|
3612
|
+
import path19 from "node:path";
|
|
2853
3613
|
function directorySizeBytes(root, maxEntries = 5e4) {
|
|
2854
3614
|
if (!existsSync12(root)) return 0;
|
|
2855
3615
|
let total = 0;
|
|
@@ -2865,7 +3625,7 @@ function directorySizeBytes(root, maxEntries = 5e4) {
|
|
|
2865
3625
|
}
|
|
2866
3626
|
for (const name of entries) {
|
|
2867
3627
|
if (seen++ > maxEntries) return null;
|
|
2868
|
-
const full =
|
|
3628
|
+
const full = path19.join(current, name);
|
|
2869
3629
|
let st;
|
|
2870
3630
|
try {
|
|
2871
3631
|
st = statSync2(full);
|
|
@@ -2949,20 +3709,20 @@ function removeWorktree(candidate, execute) {
|
|
|
2949
3709
|
}
|
|
2950
3710
|
}
|
|
2951
3711
|
function isHarnessNodeModulesPath(targetPath, harnessRoot, worktreesDir) {
|
|
2952
|
-
const resolved =
|
|
2953
|
-
const nm = resolved.endsWith(`${
|
|
3712
|
+
const resolved = path20.resolve(targetPath);
|
|
3713
|
+
const nm = resolved.endsWith(`${path20.sep}node_modules`) ? resolved : null;
|
|
2954
3714
|
if (!nm) return "path_outside_harness";
|
|
2955
|
-
const rel =
|
|
2956
|
-
if (rel.startsWith("..") ||
|
|
2957
|
-
const parts = rel.split(
|
|
3715
|
+
const rel = path20.relative(worktreesDir, nm);
|
|
3716
|
+
if (rel.startsWith("..") || path20.isAbsolute(rel)) return "path_outside_harness";
|
|
3717
|
+
const parts = rel.split(path20.sep);
|
|
2958
3718
|
if (parts.length < 3 || parts[parts.length - 1] !== "node_modules") return "path_outside_harness";
|
|
2959
|
-
if (!resolved.startsWith(
|
|
3719
|
+
if (!resolved.startsWith(path20.resolve(harnessRoot))) return "path_outside_harness";
|
|
2960
3720
|
return null;
|
|
2961
3721
|
}
|
|
2962
3722
|
|
|
2963
3723
|
// src/cleanup-scan.ts
|
|
2964
3724
|
import { existsSync as existsSync14, readdirSync as readdirSync5, statSync as statSync3 } from "node:fs";
|
|
2965
|
-
import
|
|
3725
|
+
import path21 from "node:path";
|
|
2966
3726
|
function pathAgeMs(target, now) {
|
|
2967
3727
|
try {
|
|
2968
3728
|
const mtime = statSync3(target).mtimeMs;
|
|
@@ -2972,17 +3732,17 @@ function pathAgeMs(target, now) {
|
|
|
2972
3732
|
}
|
|
2973
3733
|
}
|
|
2974
3734
|
function isPathInside(child, parent) {
|
|
2975
|
-
const rel =
|
|
2976
|
-
return rel === "" || !rel.startsWith("..") && !
|
|
3735
|
+
const rel = path21.relative(parent, child);
|
|
3736
|
+
return rel === "" || !rel.startsWith("..") && !path21.isAbsolute(rel);
|
|
2977
3737
|
}
|
|
2978
3738
|
function scanNodeModulesCandidates(opts) {
|
|
2979
3739
|
const candidates = [];
|
|
2980
3740
|
const seen = /* @__PURE__ */ new Set();
|
|
2981
3741
|
for (const entry of opts.index.values()) {
|
|
2982
3742
|
if (opts.runIdFilter && entry.runId !== opts.runIdFilter) continue;
|
|
2983
|
-
const nm =
|
|
3743
|
+
const nm = path21.join(entry.worktreePath, "node_modules");
|
|
2984
3744
|
if (!existsSync14(nm)) continue;
|
|
2985
|
-
const resolved =
|
|
3745
|
+
const resolved = path21.resolve(nm);
|
|
2986
3746
|
if (seen.has(resolved)) continue;
|
|
2987
3747
|
seen.add(resolved);
|
|
2988
3748
|
candidates.push({
|
|
@@ -2998,13 +3758,13 @@ function scanNodeModulesCandidates(opts) {
|
|
|
2998
3758
|
if (!opts.includeOrphans || !existsSync14(opts.worktreesDir)) return candidates;
|
|
2999
3759
|
for (const runEntry of readdirSync5(opts.worktreesDir, { withFileTypes: true })) {
|
|
3000
3760
|
if (!runEntry.isDirectory()) continue;
|
|
3001
|
-
const runPath =
|
|
3761
|
+
const runPath = path21.join(opts.worktreesDir, runEntry.name);
|
|
3002
3762
|
for (const workerEntry of readdirSync5(runPath, { withFileTypes: true })) {
|
|
3003
3763
|
if (!workerEntry.isDirectory()) continue;
|
|
3004
|
-
const worktreePath =
|
|
3005
|
-
const nm =
|
|
3764
|
+
const worktreePath = path21.join(runPath, workerEntry.name);
|
|
3765
|
+
const nm = path21.join(worktreePath, "node_modules");
|
|
3006
3766
|
if (!existsSync14(nm)) continue;
|
|
3007
|
-
const resolved =
|
|
3767
|
+
const resolved = path21.resolve(nm);
|
|
3008
3768
|
if (seen.has(resolved)) continue;
|
|
3009
3769
|
if (!isPathInside(resolved, opts.harnessRoot)) continue;
|
|
3010
3770
|
seen.add(resolved);
|
|
@@ -3044,17 +3804,17 @@ function scanWorktreeCandidates(opts) {
|
|
|
3044
3804
|
}
|
|
3045
3805
|
|
|
3046
3806
|
// src/cleanup-worktree-index.ts
|
|
3047
|
-
import
|
|
3807
|
+
import path22 from "node:path";
|
|
3048
3808
|
function buildWorktreeIndex() {
|
|
3049
3809
|
const index = /* @__PURE__ */ new Map();
|
|
3050
3810
|
for (const run of listRunRecords()) {
|
|
3051
3811
|
for (const name of Object.keys(run.workers || {})) {
|
|
3052
|
-
const workerPath =
|
|
3812
|
+
const workerPath = path22.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json");
|
|
3053
3813
|
const worker = readJson(workerPath, void 0);
|
|
3054
3814
|
if (!worker?.worktreePath) continue;
|
|
3055
3815
|
const status = computeWorkerStatus(worker, { base: run.base, baseCommit: run.baseCommit });
|
|
3056
|
-
index.set(
|
|
3057
|
-
worktreePath:
|
|
3816
|
+
index.set(path22.resolve(worker.worktreePath), {
|
|
3817
|
+
worktreePath: path22.resolve(worker.worktreePath),
|
|
3058
3818
|
runId: run.id,
|
|
3059
3819
|
workerName: name,
|
|
3060
3820
|
run,
|
|
@@ -3068,8 +3828,8 @@ function buildWorktreeIndex() {
|
|
|
3068
3828
|
|
|
3069
3829
|
// src/cleanup.ts
|
|
3070
3830
|
function resolveOptions(options = {}) {
|
|
3071
|
-
const harnessRoot = options.harnessRoot ?
|
|
3072
|
-
const { worktreesDir } = options.harnessRoot ? { worktreesDir:
|
|
3831
|
+
const harnessRoot = options.harnessRoot ? path23.resolve(options.harnessRoot) : resolveHarnessRoot();
|
|
3832
|
+
const { worktreesDir } = options.harnessRoot ? { worktreesDir: path23.join(harnessRoot, "worktrees") } : getHarnessPaths();
|
|
3073
3833
|
const execute = options.execute === true;
|
|
3074
3834
|
const nodeModulesAgeMs = options.nodeModulesAgeMs ?? DEFAULT_NODE_MODULES_AGE_MS;
|
|
3075
3835
|
const worktreesAgeMs = options.worktreesAgeMs ?? 0;
|
|
@@ -3113,7 +3873,7 @@ function runHarnessCleanup(options = {}) {
|
|
|
3113
3873
|
actions.push({ ...candidate, executed: false, skipped: true, skipReason: pathSkip });
|
|
3114
3874
|
continue;
|
|
3115
3875
|
}
|
|
3116
|
-
const worktreePath =
|
|
3876
|
+
const worktreePath = path23.resolve(candidate.path, "..");
|
|
3117
3877
|
const indexed = index.get(worktreePath) ?? null;
|
|
3118
3878
|
const guardReason = skipNodeModulesRemoval({
|
|
3119
3879
|
indexed,
|
|
@@ -3129,7 +3889,7 @@ function runHarnessCleanup(options = {}) {
|
|
|
3129
3889
|
actions.push(removeNodeModules(candidate, resolved.execute));
|
|
3130
3890
|
}
|
|
3131
3891
|
for (const candidate of scanWorktreeCandidates(scanOpts)) {
|
|
3132
|
-
const indexed = index.get(
|
|
3892
|
+
const indexed = index.get(path23.resolve(candidate.path)) ?? null;
|
|
3133
3893
|
const guardReason = skipWorktreeRemoval({
|
|
3134
3894
|
indexed,
|
|
3135
3895
|
includeOrphans: resolved.includeOrphans,
|
|
@@ -3198,10 +3958,14 @@ async function completeFinishedWorkers(runId, args) {
|
|
|
3198
3958
|
const outcomes = [];
|
|
3199
3959
|
for (const name of Object.keys(run.workers || {})) {
|
|
3200
3960
|
const worker = readJson(
|
|
3201
|
-
|
|
3961
|
+
path24.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
3202
3962
|
void 0
|
|
3203
3963
|
);
|
|
3204
|
-
if (!worker?.taskId) continue;
|
|
3964
|
+
if (!worker?.taskId || worker.localOnly) continue;
|
|
3965
|
+
if (hasCompletionAck(worker)) {
|
|
3966
|
+
outcomes.push({ worker: name, ok: true, taskId: worker.taskId ?? null, skipped: true });
|
|
3967
|
+
continue;
|
|
3968
|
+
}
|
|
3205
3969
|
const status = computeWorkerStatus(worker);
|
|
3206
3970
|
if (!isFinishedWorkerStatus(status)) continue;
|
|
3207
3971
|
const exitedSalvage = assessExitedWorkerSalvage({
|
|
@@ -3245,6 +4009,7 @@ async function runPipelineTick(args) {
|
|
|
3245
4009
|
configuredMaxWorkersOverride: workspacePrefs?.maxConcurrentWorkers
|
|
3246
4010
|
});
|
|
3247
4011
|
const operatorTick = await postOperatorTick(agentOsId, runId, resourceGate, args);
|
|
4012
|
+
const leaseRenewal = await renewActiveTaskLeases(runId, args);
|
|
3248
4013
|
const completedWorkers = await completeFinishedWorkers(runId, args);
|
|
3249
4014
|
const staleReconcile = reconcileStaleWorkers();
|
|
3250
4015
|
const harnessCleanup = isPipelineCleanupEnabled() ? runPipelineHarnessCleanup(runId) : void 0;
|
|
@@ -3281,6 +4046,7 @@ async function runPipelineTick(args) {
|
|
|
3281
4046
|
agentOsId,
|
|
3282
4047
|
execute,
|
|
3283
4048
|
resourceGate,
|
|
4049
|
+
leaseRenewal,
|
|
3284
4050
|
completedWorkers,
|
|
3285
4051
|
staleReconcile,
|
|
3286
4052
|
harnessCleanup,
|
|
@@ -3524,6 +4290,7 @@ if (isCliEntry) {
|
|
|
3524
4290
|
export {
|
|
3525
4291
|
DEFAULT_DISPATCH_LEASE_MS,
|
|
3526
4292
|
assessExitedWorkerSalvage,
|
|
4293
|
+
assessPrHandoffRequirement,
|
|
3527
4294
|
assessWorkerLanding,
|
|
3528
4295
|
autoCompleteWorker,
|
|
3529
4296
|
autoCompleteWorkerCli,
|
|
@@ -3535,17 +4302,23 @@ export {
|
|
|
3535
4302
|
createRun,
|
|
3536
4303
|
deriveRunStatus,
|
|
3537
4304
|
dispatchRun,
|
|
4305
|
+
ensurePrReadyHandoff,
|
|
4306
|
+
extractPrUrlFromText,
|
|
3538
4307
|
getHarnessPaths,
|
|
3539
4308
|
isFinishedWorkerStatus,
|
|
3540
4309
|
isLandingBlockedWorkerStatus,
|
|
4310
|
+
isTerminalHeartbeatPhase,
|
|
3541
4311
|
listRuns,
|
|
3542
4312
|
loadUserConfig,
|
|
3543
4313
|
main,
|
|
4314
|
+
normalizeCursorModelAlias,
|
|
3544
4315
|
observeRunnerDiskGate,
|
|
3545
4316
|
parseArgs,
|
|
3546
4317
|
parseClaudeStream,
|
|
4318
|
+
parseHarnessStream,
|
|
3547
4319
|
parseHeartbeat,
|
|
3548
4320
|
postJson,
|
|
4321
|
+
preflightCursorModel,
|
|
3549
4322
|
redactHarness,
|
|
3550
4323
|
resolveBaseUrl,
|
|
3551
4324
|
resolveCallbackSecret,
|
|
@@ -3561,6 +4334,7 @@ export {
|
|
|
3561
4334
|
summarizeEvent,
|
|
3562
4335
|
sweepRun,
|
|
3563
4336
|
tailWorker,
|
|
4337
|
+
terminalFinalResultFromHeartbeat,
|
|
3564
4338
|
usage,
|
|
3565
4339
|
validateOwnedPaths,
|
|
3566
4340
|
validateRepo,
|