@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/cli.js
CHANGED
|
@@ -361,6 +361,9 @@ async function runLogin(args) {
|
|
|
361
361
|
console.log(JSON.stringify({ ok: true, credentialsPath: CREDENTIALS_FILE }, null, 2));
|
|
362
362
|
}
|
|
363
363
|
|
|
364
|
+
// src/dispatch.ts
|
|
365
|
+
import path12 from "node:path";
|
|
366
|
+
|
|
364
367
|
// src/callback-headers.ts
|
|
365
368
|
function buildHarnessCallbackHeaders(secret) {
|
|
366
369
|
const trimmed = String(secret).trim();
|
|
@@ -421,12 +424,12 @@ var DEFAULT_CRITICAL_FREE_BYTES = 15 * 1024 * 1024 * 1024;
|
|
|
421
424
|
var DEFAULT_MAX_USED_PERCENT = 80;
|
|
422
425
|
var DEFAULT_HARD_MAX_USED_PERCENT = 90;
|
|
423
426
|
function observeRunnerDiskGate(input = {}) {
|
|
424
|
-
const
|
|
427
|
+
const path25 = input.diskPath?.trim() || "/";
|
|
425
428
|
const warnBelowBytes = input.diskFreeWarnBytes ?? DEFAULT_WARN_FREE_BYTES;
|
|
426
429
|
const criticalBelowBytes = input.diskFreeCriticalBytes ?? DEFAULT_CRITICAL_FREE_BYTES;
|
|
427
430
|
const maxUsedPercent = input.diskMaxUsedPercent ?? DEFAULT_MAX_USED_PERCENT;
|
|
428
431
|
const hardMaxUsedPercent = input.diskHardMaxUsedPercent ?? DEFAULT_HARD_MAX_USED_PERCENT;
|
|
429
|
-
const stats = statfsSync(
|
|
432
|
+
const stats = statfsSync(path25);
|
|
430
433
|
const freeBytes = Number(stats.bavail) * Number(stats.bsize);
|
|
431
434
|
const totalBytes = Number(stats.blocks) * Number(stats.bsize);
|
|
432
435
|
const usedPercent = totalBytes > 0 ? (totalBytes - freeBytes) / totalBytes * 100 : 100;
|
|
@@ -446,7 +449,7 @@ function observeRunnerDiskGate(input = {}) {
|
|
|
446
449
|
}
|
|
447
450
|
return {
|
|
448
451
|
ok,
|
|
449
|
-
path:
|
|
452
|
+
path: path25,
|
|
450
453
|
freeBytes,
|
|
451
454
|
totalBytes,
|
|
452
455
|
usedPercent,
|
|
@@ -535,6 +538,14 @@ function runDirectory(id) {
|
|
|
535
538
|
|
|
536
539
|
// src/heartbeat.ts
|
|
537
540
|
import { existsSync as existsSync5, readFileSync as readFileSync3 } from "node:fs";
|
|
541
|
+
function isTerminalHeartbeatPhase(phase) {
|
|
542
|
+
return phase === "complete";
|
|
543
|
+
}
|
|
544
|
+
function terminalFinalResultFromHeartbeat(heartbeat) {
|
|
545
|
+
if (!isTerminalHeartbeatPhase(heartbeat.lastHeartbeatPhase)) return null;
|
|
546
|
+
const summary = heartbeat.lastHeartbeatSummary?.trim();
|
|
547
|
+
return summary || "completed";
|
|
548
|
+
}
|
|
538
549
|
function parseHeartbeat(file) {
|
|
539
550
|
const result = {
|
|
540
551
|
heartbeatCount: 0,
|
|
@@ -560,7 +571,27 @@ function parseHeartbeat(file) {
|
|
|
560
571
|
|
|
561
572
|
// src/stream.ts
|
|
562
573
|
import { existsSync as existsSync6, readFileSync as readFileSync4 } from "node:fs";
|
|
563
|
-
function
|
|
574
|
+
function eventTimestampIso(event) {
|
|
575
|
+
const tsMs = event.timestamp_ms;
|
|
576
|
+
return event.timestamp || event.ts || (tsMs ? new Date(tsMs).toISOString() : void 0);
|
|
577
|
+
}
|
|
578
|
+
function cursorToolNameFromCall(toolCall) {
|
|
579
|
+
if (!toolCall) return null;
|
|
580
|
+
for (const key of Object.keys(toolCall)) {
|
|
581
|
+
if (key.endsWith("ToolCall")) {
|
|
582
|
+
const stem = key.slice(0, -"ToolCall".length);
|
|
583
|
+
return stem.length ? stem : key;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
return null;
|
|
587
|
+
}
|
|
588
|
+
function recordStreamResult(result, event) {
|
|
589
|
+
result.finalResult = event.result || event.subtype || event.terminal_reason || "completed";
|
|
590
|
+
if (event.is_error) {
|
|
591
|
+
result.error = String(event.result || event.api_error_status || "stream result error");
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
function parseHarnessStream(file) {
|
|
564
595
|
const result = {
|
|
565
596
|
firstEventAt: null,
|
|
566
597
|
lastEventAt: null,
|
|
@@ -573,8 +604,7 @@ function parseClaudeStream(file) {
|
|
|
573
604
|
for (const line of lines) {
|
|
574
605
|
const event = safeJson(line);
|
|
575
606
|
if (!event) continue;
|
|
576
|
-
const
|
|
577
|
-
const ts = event.timestamp || event.ts || (tsMs ? new Date(tsMs).toISOString() : void 0);
|
|
607
|
+
const ts = eventTimestampIso(event);
|
|
578
608
|
if (ts) {
|
|
579
609
|
result.firstEventAt ||= ts;
|
|
580
610
|
result.lastEventAt = ts;
|
|
@@ -590,9 +620,13 @@ function parseClaudeStream(file) {
|
|
|
590
620
|
if (tool) result.currentTool = String(tool.name || result.currentTool);
|
|
591
621
|
}
|
|
592
622
|
}
|
|
623
|
+
if (event.type === "tool_call" && event.subtype === "started") {
|
|
624
|
+
const toolCall = event.tool_call && typeof event.tool_call === "object" && !Array.isArray(event.tool_call) ? event.tool_call : void 0;
|
|
625
|
+
const name = cursorToolNameFromCall(toolCall);
|
|
626
|
+
if (name) result.currentTool = name;
|
|
627
|
+
}
|
|
593
628
|
if (event.type === "result") {
|
|
594
|
-
result
|
|
595
|
-
if (event.is_error) result.error = String(event.result || event.api_error_status || "Claude result error");
|
|
629
|
+
recordStreamResult(result, event);
|
|
596
630
|
}
|
|
597
631
|
}
|
|
598
632
|
return result;
|
|
@@ -627,6 +661,12 @@ function summarizeEvent(event) {
|
|
|
627
661
|
const result = event.tool_use_result;
|
|
628
662
|
return `[tool:result] stdout=${JSON.stringify(result.stdout || "")} stderr=${JSON.stringify(result.stderr || "")}`;
|
|
629
663
|
}
|
|
664
|
+
if (event.type === "tool_call") {
|
|
665
|
+
const subtype = String(event.subtype || "");
|
|
666
|
+
const toolCall = event.tool_call && typeof event.tool_call === "object" && !Array.isArray(event.tool_call) ? event.tool_call : void 0;
|
|
667
|
+
const name = cursorToolNameFromCall(toolCall) ?? "tool";
|
|
668
|
+
return `[tool:${subtype}] ${name}`;
|
|
669
|
+
}
|
|
630
670
|
if (event.type === "result") {
|
|
631
671
|
return `[result] ${event.subtype || ""} ${oneLine(String(event.result || ""))}`.trim();
|
|
632
672
|
}
|
|
@@ -964,8 +1004,9 @@ function computeAttention(input) {
|
|
|
964
1004
|
return { state: "ok", reason: "recent activity" };
|
|
965
1005
|
}
|
|
966
1006
|
function computeWorkerStatus(worker, options = {}) {
|
|
967
|
-
const parsed =
|
|
1007
|
+
const parsed = parseHarnessStream(worker.stdoutPath);
|
|
968
1008
|
const heartbeat = parseHeartbeat(worker.heartbeatPath);
|
|
1009
|
+
const finalResult = parsed.finalResult ?? terminalFinalResultFromHeartbeat(heartbeat);
|
|
969
1010
|
const alive = isPidAlive(worker.pid);
|
|
970
1011
|
const stdoutBytes = fileSize(worker.stdoutPath);
|
|
971
1012
|
const stderrBytes = fileSize(worker.stderrPath);
|
|
@@ -982,11 +1023,11 @@ function computeWorkerStatus(worker, options = {}) {
|
|
|
982
1023
|
fileMtime(worker.stderrPath),
|
|
983
1024
|
fileMtime(worker.heartbeatPath)
|
|
984
1025
|
]);
|
|
985
|
-
const error = parsed.error || (!alive && !
|
|
1026
|
+
const error = parsed.error || (!alive && !finalResult ? tailFile(worker.stderrPath, 10).trim() || void 0 : void 0);
|
|
986
1027
|
const completionBlocker = typeof worker.completionBlocker === "string" && worker.completionBlocker.trim() ? worker.completionBlocker.trim() : null;
|
|
987
1028
|
const attention = computeAttention({
|
|
988
1029
|
alive,
|
|
989
|
-
finalResult
|
|
1030
|
+
finalResult,
|
|
990
1031
|
firstEventAt: parsed.firstEventAt,
|
|
991
1032
|
stdoutBytes,
|
|
992
1033
|
heartbeatBytes,
|
|
@@ -998,7 +1039,7 @@ function computeWorkerStatus(worker, options = {}) {
|
|
|
998
1039
|
gitAncestry,
|
|
999
1040
|
completionBlocker
|
|
1000
1041
|
});
|
|
1001
|
-
const workerStatusLabel = completionBlocker || attention.state === "blocked" ? "blocked" : attention.state === "done" ? "done" :
|
|
1042
|
+
const workerStatusLabel = completionBlocker || attention.state === "blocked" ? "blocked" : attention.state === "done" ? "done" : finalResult ? "exited" : alive ? "running" : "exited";
|
|
1002
1043
|
return {
|
|
1003
1044
|
runId: worker.runId,
|
|
1004
1045
|
worker: worker.name,
|
|
@@ -1021,7 +1062,7 @@ function computeWorkerStatus(worker, options = {}) {
|
|
|
1021
1062
|
lastHeartbeatPhase: heartbeat.lastHeartbeatPhase,
|
|
1022
1063
|
lastHeartbeatSummary: heartbeat.lastHeartbeatSummary,
|
|
1023
1064
|
heartbeatBlocker: heartbeat.heartbeatBlocker,
|
|
1024
|
-
finalResult
|
|
1065
|
+
finalResult,
|
|
1025
1066
|
error,
|
|
1026
1067
|
changedFiles,
|
|
1027
1068
|
gitAncestry
|
|
@@ -1217,6 +1258,12 @@ var FOREIGN_MODEL_RE = /^(?:gpt-|gpt5|o1|o3|o4|gemini-|grok-|composer|deepseek|l
|
|
|
1217
1258
|
function looksLikeClaudeModel(model) {
|
|
1218
1259
|
return /^claude[-_]/i.test(model) || /^(?:opus|sonnet|haiku)\b/i.test(model);
|
|
1219
1260
|
}
|
|
1261
|
+
var CURSOR_MODEL_ALIASES = /* @__PURE__ */ new Set(["cursor"]);
|
|
1262
|
+
function normalizeCursorModelAlias(model) {
|
|
1263
|
+
const key = model.trim().toLowerCase();
|
|
1264
|
+
if (CURSOR_MODEL_ALIASES.has(key)) return "auto";
|
|
1265
|
+
return null;
|
|
1266
|
+
}
|
|
1220
1267
|
function preflightClaudeModel(model, defaultModel) {
|
|
1221
1268
|
const requested = (model ?? "").trim();
|
|
1222
1269
|
if (!requested) {
|
|
@@ -1249,6 +1296,16 @@ function preflightCursorModel(model, defaultModel) {
|
|
|
1249
1296
|
if (!requested) {
|
|
1250
1297
|
return { ok: true, model: defaultModel, normalized: false };
|
|
1251
1298
|
}
|
|
1299
|
+
const aliasLaunch = normalizeCursorModelAlias(requested);
|
|
1300
|
+
if (aliasLaunch) {
|
|
1301
|
+
return {
|
|
1302
|
+
ok: true,
|
|
1303
|
+
model: aliasLaunch,
|
|
1304
|
+
normalized: true,
|
|
1305
|
+
requested,
|
|
1306
|
+
note: `normalized model "${requested}" \u2192 "${aliasLaunch}" (Cursor provider alias \u2014 use "auto" or a composer id, not "cursor")`
|
|
1307
|
+
};
|
|
1308
|
+
}
|
|
1252
1309
|
if (looksLikeClaudeModel(requested)) {
|
|
1253
1310
|
return {
|
|
1254
1311
|
ok: false,
|
|
@@ -1310,6 +1367,7 @@ var claudeProvider = {
|
|
|
1310
1367
|
|
|
1311
1368
|
// src/model-routing.ts
|
|
1312
1369
|
var GLOBAL_DEFAULT_MODEL = "claude-sonnet-4-6";
|
|
1370
|
+
var CURSOR_DEFAULT_MODEL = "composer-2.5";
|
|
1313
1371
|
function taskString2(task, key) {
|
|
1314
1372
|
const v = task[key];
|
|
1315
1373
|
return typeof v === "string" ? v.trim() : "";
|
|
@@ -1332,6 +1390,27 @@ function inferProviderFromModel(model) {
|
|
|
1332
1390
|
}
|
|
1333
1391
|
return "claude";
|
|
1334
1392
|
}
|
|
1393
|
+
function normalizeProviderAliasModel(model, explicitProvider) {
|
|
1394
|
+
const alias = model.trim().toLowerCase();
|
|
1395
|
+
const provider = explicitProvider?.trim();
|
|
1396
|
+
if (alias === "cursor") {
|
|
1397
|
+
return {
|
|
1398
|
+
model: CURSOR_DEFAULT_MODEL,
|
|
1399
|
+
provider: "cursor",
|
|
1400
|
+
rule: provider && provider !== "cursor" ? "explicit:model_provider_alias_overrode_provider" : "explicit:model_provider_alias",
|
|
1401
|
+
requestedModel: model
|
|
1402
|
+
};
|
|
1403
|
+
}
|
|
1404
|
+
if (alias === "claude" || alias === "anthropic") {
|
|
1405
|
+
return {
|
|
1406
|
+
model: CLAUDE_DEFAULT_MODEL,
|
|
1407
|
+
provider: "claude",
|
|
1408
|
+
rule: provider && provider !== "claude" ? "explicit:model_provider_alias_overrode_provider" : "explicit:model_provider_alias",
|
|
1409
|
+
requestedModel: model
|
|
1410
|
+
};
|
|
1411
|
+
}
|
|
1412
|
+
return null;
|
|
1413
|
+
}
|
|
1335
1414
|
function isOpusLane(ref, title) {
|
|
1336
1415
|
if (ref.includes("deep") && ref.includes("review")) return true;
|
|
1337
1416
|
if (ref.includes("security")) return true;
|
|
@@ -1379,15 +1458,18 @@ function inferModelRoutingFromTask(task) {
|
|
|
1379
1458
|
rule: "priority:low"
|
|
1380
1459
|
};
|
|
1381
1460
|
}
|
|
1461
|
+
const model = resolveGlobalDefaultModel();
|
|
1382
1462
|
return {
|
|
1383
|
-
model
|
|
1384
|
-
provider:
|
|
1385
|
-
rule: "default:
|
|
1463
|
+
model,
|
|
1464
|
+
provider: inferProviderFromModel(model),
|
|
1465
|
+
rule: "default:global"
|
|
1386
1466
|
};
|
|
1387
1467
|
}
|
|
1388
1468
|
function resolveWorkerLaunch(input) {
|
|
1389
1469
|
if (input.explicitModel?.trim()) {
|
|
1390
1470
|
const model2 = input.explicitModel.trim();
|
|
1471
|
+
const providerAlias = normalizeProviderAliasModel(model2, input.explicitProvider);
|
|
1472
|
+
if (providerAlias) return providerAlias;
|
|
1391
1473
|
return {
|
|
1392
1474
|
model: model2,
|
|
1393
1475
|
provider: input.explicitProvider?.trim() || inferProviderFromModel(model2),
|
|
@@ -1427,20 +1509,89 @@ function readHarnessRetryLimits() {
|
|
|
1427
1509
|
};
|
|
1428
1510
|
}
|
|
1429
1511
|
|
|
1512
|
+
// src/lease-renewal.ts
|
|
1513
|
+
import path6 from "node:path";
|
|
1514
|
+
function workerRecord(runId, name) {
|
|
1515
|
+
return readJson(
|
|
1516
|
+
path6.join(runDirectory(runId), "workers", safeSlug(name), "worker.json"),
|
|
1517
|
+
void 0
|
|
1518
|
+
);
|
|
1519
|
+
}
|
|
1520
|
+
async function renewActiveTaskLeases(runId, args) {
|
|
1521
|
+
const run = loadRun(runId);
|
|
1522
|
+
const agentOsId = String(args.agentOsId || "");
|
|
1523
|
+
if (!agentOsId) {
|
|
1524
|
+
return { renewed: [], failed: [], skipped: [] };
|
|
1525
|
+
}
|
|
1526
|
+
const base = resolveBaseUrl(args.baseUrl ? String(args.baseUrl) : void 0);
|
|
1527
|
+
const secret = await resolveCallbackSecretWithMint(
|
|
1528
|
+
args.secret ? String(args.secret) : void 0,
|
|
1529
|
+
agentOsId,
|
|
1530
|
+
{ baseUrl: base }
|
|
1531
|
+
);
|
|
1532
|
+
const leaseDurationMs = Number(args.leaseMs) > 0 ? Math.floor(Number(args.leaseMs)) : DEFAULT_DISPATCH_LEASE_MS;
|
|
1533
|
+
const leaseOwner = `kynver-harness:${runId}`;
|
|
1534
|
+
const renewed = [];
|
|
1535
|
+
const failed = [];
|
|
1536
|
+
const skipped = [];
|
|
1537
|
+
for (const name of Object.keys(run.workers || {})) {
|
|
1538
|
+
const worker = workerRecord(runId, name);
|
|
1539
|
+
if (!worker?.taskId || !worker.agentOsId) {
|
|
1540
|
+
skipped.push(name);
|
|
1541
|
+
continue;
|
|
1542
|
+
}
|
|
1543
|
+
if (!isPidAlive(worker.pid)) {
|
|
1544
|
+
skipped.push(name);
|
|
1545
|
+
continue;
|
|
1546
|
+
}
|
|
1547
|
+
const status = computeWorkerStatus(worker);
|
|
1548
|
+
if (status.status === "done") {
|
|
1549
|
+
skipped.push(name);
|
|
1550
|
+
continue;
|
|
1551
|
+
}
|
|
1552
|
+
const url = `${base}/api/agent-os/by-id/${encodeURIComponent(agentOsId)}/tasks/${encodeURIComponent(worker.taskId)}/renew-lease`;
|
|
1553
|
+
const res = await postJsonWithCredentialRefresh(
|
|
1554
|
+
url,
|
|
1555
|
+
secret,
|
|
1556
|
+
{ leaseOwner, leaseDurationMs },
|
|
1557
|
+
{ agentOsId, baseUrl: base }
|
|
1558
|
+
);
|
|
1559
|
+
if (res.ok) {
|
|
1560
|
+
renewed.push(name);
|
|
1561
|
+
continue;
|
|
1562
|
+
}
|
|
1563
|
+
const reason = res.response && typeof res.response === "object" && "reason" in res.response ? String(res.response.reason ?? `http ${res.status}`) : `http ${res.status}`;
|
|
1564
|
+
failed.push({ worker: name, reason });
|
|
1565
|
+
}
|
|
1566
|
+
return { renewed, failed, skipped };
|
|
1567
|
+
}
|
|
1568
|
+
function hasLiveWorkerForTask(runId, taskId) {
|
|
1569
|
+
const run = loadRun(runId);
|
|
1570
|
+
for (const name of Object.keys(run.workers || {})) {
|
|
1571
|
+
const worker = workerRecord(runId, name);
|
|
1572
|
+
if (!worker || worker.taskId !== taskId) continue;
|
|
1573
|
+
if (!isPidAlive(worker.pid)) continue;
|
|
1574
|
+
const status = computeWorkerStatus(worker);
|
|
1575
|
+
if (status.status === "done") continue;
|
|
1576
|
+
return true;
|
|
1577
|
+
}
|
|
1578
|
+
return false;
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1430
1581
|
// src/supervisor.ts
|
|
1431
1582
|
import { existsSync as existsSync10, mkdirSync as mkdirSync3 } from "node:fs";
|
|
1432
|
-
import
|
|
1583
|
+
import path11 from "node:path";
|
|
1433
1584
|
|
|
1434
1585
|
// src/prompt.ts
|
|
1435
1586
|
function buildPrompt(input) {
|
|
1436
1587
|
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.";
|
|
1437
1588
|
const compact = Boolean(input.model?.toLowerCase().includes("haiku"));
|
|
1438
1589
|
const progressLines = compact ? [
|
|
1439
|
-
"Plan progress: when planId is set, use `kynver plan progress` for running|partial|blocked
|
|
1590
|
+
"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.",
|
|
1440
1591
|
input.planId ? `Active planId: ${input.planId}` : "No planId on this worker."
|
|
1441
1592
|
] : [
|
|
1442
1593
|
"Structured plan progress (required when planId is set):",
|
|
1443
|
-
"- 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).",
|
|
1594
|
+
"- 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.",
|
|
1444
1595
|
"- 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.",
|
|
1445
1596
|
"- 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`).",
|
|
1446
1597
|
"- 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>`.",
|
|
@@ -1464,13 +1615,17 @@ function buildPrompt(input) {
|
|
|
1464
1615
|
`Progress heartbeat file: ${input.heartbeatPath}`,
|
|
1465
1616
|
"After each major step, append one JSON line to the heartbeat file with fields: ts, phase, summary, changedFiles, blocker.",
|
|
1466
1617
|
"Final response must include files changed, verification commands, and unresolved risks.",
|
|
1467
|
-
"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,
|
|
1468
|
-
"
|
|
1618
|
+
"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.",
|
|
1619
|
+
"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.",
|
|
1620
|
+
"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.",
|
|
1621
|
+
"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.",
|
|
1469
1622
|
"",
|
|
1470
1623
|
...progressLines,
|
|
1471
1624
|
"",
|
|
1472
1625
|
...planArtifactLines,
|
|
1473
1626
|
"",
|
|
1627
|
+
...input.personaMarkdown?.trim() ? [input.personaMarkdown.trim(), ""] : [],
|
|
1628
|
+
...input.instructionPolicyMarkdown?.trim() ? ["Operating rules (Lane A \u2014 from AgentOS memory policy):", input.instructionPolicyMarkdown.trim(), ""] : [],
|
|
1474
1629
|
"Task:",
|
|
1475
1630
|
input.task
|
|
1476
1631
|
].join("\n");
|
|
@@ -1479,11 +1634,11 @@ function buildPrompt(input) {
|
|
|
1479
1634
|
// src/providers/cursor.ts
|
|
1480
1635
|
import { closeSync as closeSync2, existsSync as existsSync8, openSync as openSync2 } from "node:fs";
|
|
1481
1636
|
import { spawn as spawn2 } from "node:child_process";
|
|
1482
|
-
import
|
|
1637
|
+
import path8 from "node:path";
|
|
1483
1638
|
|
|
1484
1639
|
// src/providers/cursor-windows.ts
|
|
1485
1640
|
import { existsSync as existsSync7, readdirSync as readdirSync3 } from "node:fs";
|
|
1486
|
-
import
|
|
1641
|
+
import path7 from "node:path";
|
|
1487
1642
|
var CURSOR_VERSION_DIR = /^\d{4}\.\d{1,2}\.\d{1,2}-[a-f0-9]+$/i;
|
|
1488
1643
|
function parseCursorVersionSortKey(versionName) {
|
|
1489
1644
|
const datePart = versionName.split("-")[0];
|
|
@@ -1494,7 +1649,7 @@ function parseCursorVersionSortKey(versionName) {
|
|
|
1494
1649
|
return Number(`${year}${month.padStart(2, "0")}${day.padStart(2, "0")}`);
|
|
1495
1650
|
}
|
|
1496
1651
|
function pickLatestCursorVersionDir(agentRoot) {
|
|
1497
|
-
const versionsRoot =
|
|
1652
|
+
const versionsRoot = path7.join(agentRoot, "versions");
|
|
1498
1653
|
if (!existsSync7(versionsRoot)) return null;
|
|
1499
1654
|
let bestDir = null;
|
|
1500
1655
|
let bestKey = -1;
|
|
@@ -1503,21 +1658,21 @@ function pickLatestCursorVersionDir(agentRoot) {
|
|
|
1503
1658
|
const key = parseCursorVersionSortKey(entry.name);
|
|
1504
1659
|
if (key == null || key <= bestKey) continue;
|
|
1505
1660
|
bestKey = key;
|
|
1506
|
-
bestDir =
|
|
1661
|
+
bestDir = path7.join(versionsRoot, entry.name);
|
|
1507
1662
|
}
|
|
1508
1663
|
return bestDir;
|
|
1509
1664
|
}
|
|
1510
1665
|
function resolveWindowsCursorBundled(agentRoot) {
|
|
1511
|
-
const root = agentRoot?.trim() ||
|
|
1512
|
-
const directNode =
|
|
1513
|
-
const directIndex =
|
|
1666
|
+
const root = agentRoot?.trim() || path7.join(process.env.LOCALAPPDATA || "", "cursor-agent");
|
|
1667
|
+
const directNode = path7.join(root, "node.exe");
|
|
1668
|
+
const directIndex = path7.join(root, "index.js");
|
|
1514
1669
|
if (existsSync7(directNode) && existsSync7(directIndex)) {
|
|
1515
1670
|
return { nodeExe: directNode, indexJs: directIndex, versionDir: root };
|
|
1516
1671
|
}
|
|
1517
1672
|
const versionDir = pickLatestCursorVersionDir(root);
|
|
1518
1673
|
if (!versionDir) return null;
|
|
1519
|
-
const nodeExe =
|
|
1520
|
-
const indexJs =
|
|
1674
|
+
const nodeExe = path7.join(versionDir, "node.exe");
|
|
1675
|
+
const indexJs = path7.join(versionDir, "index.js");
|
|
1521
1676
|
if (!existsSync7(nodeExe) || !existsSync7(indexJs)) return null;
|
|
1522
1677
|
return { nodeExe, indexJs, versionDir };
|
|
1523
1678
|
}
|
|
@@ -1536,13 +1691,13 @@ function bundledSpawnTarget(nodeExe, indexJs, versionDir) {
|
|
|
1536
1691
|
function resolveCursorSpawn(agentBin) {
|
|
1537
1692
|
if (process.platform === "win32") {
|
|
1538
1693
|
const isCursorWrapper = /\.(cmd|bat)$/i.test(agentBin);
|
|
1539
|
-
const isBundledNode = /node\.exe$/i.test(agentBin) && existsSync8(
|
|
1694
|
+
const isBundledNode = /node\.exe$/i.test(agentBin) && existsSync8(path8.join(path8.dirname(agentBin), "index.js"));
|
|
1540
1695
|
const isDefaultShim = agentBin === "agent";
|
|
1541
1696
|
if (isCursorWrapper || isBundledNode || isDefaultShim) {
|
|
1542
|
-
const bundled = isCursorWrapper ? resolveWindowsCursorBundled(
|
|
1697
|
+
const bundled = isCursorWrapper ? resolveWindowsCursorBundled(path8.dirname(agentBin)) : isBundledNode ? {
|
|
1543
1698
|
nodeExe: agentBin,
|
|
1544
|
-
indexJs:
|
|
1545
|
-
versionDir:
|
|
1699
|
+
indexJs: path8.join(path8.dirname(agentBin), "index.js"),
|
|
1700
|
+
versionDir: path8.dirname(agentBin)
|
|
1546
1701
|
} : resolveWindowsCursorBundled();
|
|
1547
1702
|
if (bundled) {
|
|
1548
1703
|
return bundledSpawnTarget(bundled.nodeExe, bundled.indexJs, bundled.versionDir);
|
|
@@ -1562,7 +1717,7 @@ function resolveAgentBin() {
|
|
|
1562
1717
|
process.env.KYNVER_CURSOR_AGENT_ROOT?.trim() || void 0
|
|
1563
1718
|
);
|
|
1564
1719
|
if (bundled) return bundled.nodeExe;
|
|
1565
|
-
const localAgent =
|
|
1720
|
+
const localAgent = path8.join(process.env.LOCALAPPDATA || "", "cursor-agent", "agent.cmd");
|
|
1566
1721
|
if (existsSync8(localAgent)) return localAgent;
|
|
1567
1722
|
}
|
|
1568
1723
|
return "agent";
|
|
@@ -1572,7 +1727,7 @@ function cursorWorkerEnv(agentBin, spawnTarget) {
|
|
|
1572
1727
|
...process.env,
|
|
1573
1728
|
CI: "1",
|
|
1574
1729
|
NO_COLOR: "1",
|
|
1575
|
-
...spawnTarget.bundledVersionDir ? { CURSOR_INVOKED_AS:
|
|
1730
|
+
...spawnTarget.bundledVersionDir ? { CURSOR_INVOKED_AS: path8.basename(agentBin) || "agent.cmd" } : {}
|
|
1576
1731
|
};
|
|
1577
1732
|
}
|
|
1578
1733
|
var cursorProvider = {
|
|
@@ -1646,11 +1801,360 @@ function resolveWorkerProvider(name) {
|
|
|
1646
1801
|
// src/auto-complete.ts
|
|
1647
1802
|
import { spawn as spawn3 } from "node:child_process";
|
|
1648
1803
|
import { existsSync as existsSync9, openSync as openSync3, closeSync as closeSync3 } from "node:fs";
|
|
1649
|
-
import
|
|
1804
|
+
import path10 from "node:path";
|
|
1650
1805
|
import { fileURLToPath } from "node:url";
|
|
1651
1806
|
|
|
1652
1807
|
// src/worker-ops.ts
|
|
1653
|
-
import
|
|
1808
|
+
import path9 from "node:path";
|
|
1809
|
+
|
|
1810
|
+
// src/pr-handoff/pr-handoff-assess.ts
|
|
1811
|
+
var REVIEW_LANE_RULE = /^(lane:)?(review|deep_review|planning|landing)(:|$)/i;
|
|
1812
|
+
function trimOrNull3(value) {
|
|
1813
|
+
if (typeof value !== "string") return null;
|
|
1814
|
+
const trimmed = value.trim();
|
|
1815
|
+
return trimmed.length ? trimmed : null;
|
|
1816
|
+
}
|
|
1817
|
+
function committedHead(ancestry) {
|
|
1818
|
+
if (!ancestry?.checked) return null;
|
|
1819
|
+
if (ancestry.headIsAncestorOfBase !== false) return null;
|
|
1820
|
+
return trimOrNull3(ancestry.head);
|
|
1821
|
+
}
|
|
1822
|
+
function extractPrUrlFromText(value) {
|
|
1823
|
+
if (value === void 0 || value === null) return null;
|
|
1824
|
+
const text = typeof value === "string" ? value : typeof value === "object" && value !== null && "summary" in value ? String(value.summary ?? "") : JSON.stringify(value);
|
|
1825
|
+
const m = text.match(
|
|
1826
|
+
/https?:\/\/[^\s)>"]+\/(?:pull|pulls|merge_requests|pull-requests)\/\d+/i
|
|
1827
|
+
);
|
|
1828
|
+
return m ? trimOrNull3(m[0]) : null;
|
|
1829
|
+
}
|
|
1830
|
+
function hasWorkProduct(snapshot) {
|
|
1831
|
+
if (snapshot.changedFiles.length > 0) return true;
|
|
1832
|
+
if (trimOrNull3(snapshot.headCommit)) return true;
|
|
1833
|
+
if (committedHead(snapshot.gitAncestry)) return true;
|
|
1834
|
+
return false;
|
|
1835
|
+
}
|
|
1836
|
+
function assessPrHandoffRequirement(input) {
|
|
1837
|
+
if (!input.dispatched) {
|
|
1838
|
+
return { required: false, reason: "not_dispatched" };
|
|
1839
|
+
}
|
|
1840
|
+
const rule = trimOrNull3(input.routingRule) ?? "";
|
|
1841
|
+
if (rule && REVIEW_LANE_RULE.test(rule)) {
|
|
1842
|
+
return { required: false, reason: "review_lane" };
|
|
1843
|
+
}
|
|
1844
|
+
if (trimOrNull3(input.patchPath) || trimOrNull3(input.artifactBundlePath)) {
|
|
1845
|
+
return { required: false, reason: "patch_or_bundle" };
|
|
1846
|
+
}
|
|
1847
|
+
const prUrl = trimOrNull3(input.prUrl) ?? trimOrNull3(input.snapshot.prUrl);
|
|
1848
|
+
if (prUrl) {
|
|
1849
|
+
return { required: false, reason: "already_has_pr" };
|
|
1850
|
+
}
|
|
1851
|
+
if (!hasWorkProduct(input.snapshot)) {
|
|
1852
|
+
return { required: false, reason: "no_work_product" };
|
|
1853
|
+
}
|
|
1854
|
+
return { required: true, snapshot: input.snapshot };
|
|
1855
|
+
}
|
|
1856
|
+
function buildPrHandoffSnapshotFromStatus(status, extras) {
|
|
1857
|
+
return {
|
|
1858
|
+
changedFiles: status.changedFiles,
|
|
1859
|
+
branch: status.branch,
|
|
1860
|
+
worktreePath: status.worktreePath,
|
|
1861
|
+
gitAncestry: status.gitAncestry,
|
|
1862
|
+
finalResult: status.finalResult,
|
|
1863
|
+
headCommit: trimOrNull3(extras?.headCommit) ?? committedHead(status.gitAncestry),
|
|
1864
|
+
prUrl: trimOrNull3(extras?.prUrl) ?? null
|
|
1865
|
+
};
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
// src/pr-handoff/pr-handoff-gh.ts
|
|
1869
|
+
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
1870
|
+
function capture(bin, cwd, args) {
|
|
1871
|
+
try {
|
|
1872
|
+
const res = spawnSync2(bin, args, { cwd, encoding: "utf8" });
|
|
1873
|
+
return {
|
|
1874
|
+
status: res.status,
|
|
1875
|
+
stdout: (res.stdout || "").trim(),
|
|
1876
|
+
stderr: (res.stderr || "").trim(),
|
|
1877
|
+
error: res.error ? res.error.message : null
|
|
1878
|
+
};
|
|
1879
|
+
} catch (error) {
|
|
1880
|
+
return {
|
|
1881
|
+
status: null,
|
|
1882
|
+
stdout: "",
|
|
1883
|
+
stderr: "",
|
|
1884
|
+
error: error.message
|
|
1885
|
+
};
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
var defaultPrHandoffExec = {
|
|
1889
|
+
git: (cwd, args) => gitCapture(cwd, args),
|
|
1890
|
+
gh: (cwd, args) => capture("gh", cwd, args)
|
|
1891
|
+
};
|
|
1892
|
+
function parseGithubRepo(remoteUrl) {
|
|
1893
|
+
const trimmed = remoteUrl.trim();
|
|
1894
|
+
const ssh = trimmed.match(/git@github\.com:([^/]+\/[^/.]+)(?:\.git)?/i);
|
|
1895
|
+
if (ssh) return ssh[1];
|
|
1896
|
+
const https = trimmed.match(/github\.com[/:]([^/]+\/[^/.]+?)(?:\.git)?/i);
|
|
1897
|
+
if (https) return https[1];
|
|
1898
|
+
return null;
|
|
1899
|
+
}
|
|
1900
|
+
function firstLine(text) {
|
|
1901
|
+
return text.split("\n").map((l) => l.trim()).find(Boolean) ?? "";
|
|
1902
|
+
}
|
|
1903
|
+
function resolveGithubRepo(worktreePath, exec) {
|
|
1904
|
+
const remote = exec.git(worktreePath, ["remote", "get-url", "origin"]);
|
|
1905
|
+
if (remote.status !== 0) return null;
|
|
1906
|
+
return parseGithubRepo(remote.stdout);
|
|
1907
|
+
}
|
|
1908
|
+
function resolveHeadCommit(worktreePath, exec) {
|
|
1909
|
+
const head = exec.git(worktreePath, ["rev-parse", "HEAD"]);
|
|
1910
|
+
if (head.status !== 0) return null;
|
|
1911
|
+
return head.stdout.trim() || null;
|
|
1912
|
+
}
|
|
1913
|
+
function findOpenPrUrl(worktreePath, repo, branch, exec) {
|
|
1914
|
+
const listed = exec.gh(worktreePath, [
|
|
1915
|
+
"pr",
|
|
1916
|
+
"list",
|
|
1917
|
+
"--repo",
|
|
1918
|
+
repo,
|
|
1919
|
+
"--head",
|
|
1920
|
+
branch,
|
|
1921
|
+
"--state",
|
|
1922
|
+
"open",
|
|
1923
|
+
"--json",
|
|
1924
|
+
"url",
|
|
1925
|
+
"--limit",
|
|
1926
|
+
"1"
|
|
1927
|
+
]);
|
|
1928
|
+
if (listed.status !== 0) return null;
|
|
1929
|
+
try {
|
|
1930
|
+
const rows = JSON.parse(listed.stdout);
|
|
1931
|
+
const url = rows[0]?.url?.trim();
|
|
1932
|
+
return url || null;
|
|
1933
|
+
} catch {
|
|
1934
|
+
return null;
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
function commitAndPushBranch(input) {
|
|
1938
|
+
const { worktreePath, branch, commitMessage, hasDirtyFiles, exec } = input;
|
|
1939
|
+
if (hasDirtyFiles) {
|
|
1940
|
+
const add = exec.git(worktreePath, ["add", "-A"]);
|
|
1941
|
+
if (add.status !== 0) {
|
|
1942
|
+
return {
|
|
1943
|
+
ok: false,
|
|
1944
|
+
committed: false,
|
|
1945
|
+
pushed: false,
|
|
1946
|
+
detail: add.stderr || add.stdout || add.error || "git add failed"
|
|
1947
|
+
};
|
|
1948
|
+
}
|
|
1949
|
+
const commit = exec.git(worktreePath, ["commit", "-m", commitMessage]);
|
|
1950
|
+
if (commit.status !== 0) {
|
|
1951
|
+
return {
|
|
1952
|
+
ok: false,
|
|
1953
|
+
committed: false,
|
|
1954
|
+
pushed: false,
|
|
1955
|
+
detail: commit.stderr || commit.stdout || commit.error || "git commit failed"
|
|
1956
|
+
};
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
const push = exec.git(worktreePath, ["push", "-u", "origin", branch]);
|
|
1960
|
+
if (push.status !== 0) {
|
|
1961
|
+
return {
|
|
1962
|
+
ok: false,
|
|
1963
|
+
committed: hasDirtyFiles,
|
|
1964
|
+
pushed: false,
|
|
1965
|
+
detail: push.stderr || push.stdout || push.error || "git push failed"
|
|
1966
|
+
};
|
|
1967
|
+
}
|
|
1968
|
+
const headCommit = resolveHeadCommit(worktreePath, exec) ?? void 0;
|
|
1969
|
+
return {
|
|
1970
|
+
ok: true,
|
|
1971
|
+
committed: hasDirtyFiles,
|
|
1972
|
+
pushed: true,
|
|
1973
|
+
headCommit
|
|
1974
|
+
};
|
|
1975
|
+
}
|
|
1976
|
+
function createGithubPr(input) {
|
|
1977
|
+
const existing = findOpenPrUrl(input.worktreePath, input.repo, input.branch, input.exec);
|
|
1978
|
+
if (existing) {
|
|
1979
|
+
return { ok: true, prUrl: existing, created: false };
|
|
1980
|
+
}
|
|
1981
|
+
const created = input.exec.gh(input.worktreePath, [
|
|
1982
|
+
"pr",
|
|
1983
|
+
"create",
|
|
1984
|
+
"--repo",
|
|
1985
|
+
input.repo,
|
|
1986
|
+
"--base",
|
|
1987
|
+
input.base,
|
|
1988
|
+
"--head",
|
|
1989
|
+
input.branch,
|
|
1990
|
+
"--title",
|
|
1991
|
+
input.title,
|
|
1992
|
+
"--body",
|
|
1993
|
+
input.body,
|
|
1994
|
+
"--draft"
|
|
1995
|
+
]);
|
|
1996
|
+
if (created.status !== 0) {
|
|
1997
|
+
return {
|
|
1998
|
+
ok: false,
|
|
1999
|
+
detail: created.stderr || created.stdout || created.error || "gh pr create failed"
|
|
2000
|
+
};
|
|
2001
|
+
}
|
|
2002
|
+
const url = extractPrUrlFromGhOutput(created.stdout) ?? findOpenPrUrl(input.worktreePath, input.repo, input.branch, input.exec);
|
|
2003
|
+
if (!url) {
|
|
2004
|
+
return { ok: false, detail: "gh pr create succeeded but no PR URL was parsed" };
|
|
2005
|
+
}
|
|
2006
|
+
return { ok: true, prUrl: url, created: true };
|
|
2007
|
+
}
|
|
2008
|
+
function extractPrUrlFromGhOutput(stdout) {
|
|
2009
|
+
const line = firstLine(stdout);
|
|
2010
|
+
const m = line.match(/https?:\/\/[^\s]+\/pull\/\d+/i);
|
|
2011
|
+
return m ? m[0] : null;
|
|
2012
|
+
}
|
|
2013
|
+
|
|
2014
|
+
// src/pr-handoff/pr-handoff.ts
|
|
2015
|
+
function ghAvailable(exec) {
|
|
2016
|
+
const probe = exec.gh(process.cwd(), ["--version"]);
|
|
2017
|
+
return probe.status === 0;
|
|
2018
|
+
}
|
|
2019
|
+
function defaultPrTitle(workerName, runId) {
|
|
2020
|
+
return `AgentOS harness: ${workerName} (${runId})`;
|
|
2021
|
+
}
|
|
2022
|
+
function defaultPrBody(taskId, workerName, runId) {
|
|
2023
|
+
return [
|
|
2024
|
+
"Automated PR-ready handoff from the Kynver harness runtime.",
|
|
2025
|
+
"",
|
|
2026
|
+
taskId ? `AgentOS task: \`${taskId}\`` : "",
|
|
2027
|
+
`Harness worker: \`${workerName}\` \xB7 run \`${runId}\``,
|
|
2028
|
+
"",
|
|
2029
|
+
"Opened by orchestrator completion enforcement so production review receives a reviewable artifact."
|
|
2030
|
+
].filter(Boolean).join("\n");
|
|
2031
|
+
}
|
|
2032
|
+
function ensurePrReadyHandoff(input, exec = defaultPrHandoffExec) {
|
|
2033
|
+
const prUrlHint = input.prUrlHint ?? extractPrUrlFromText(input.status.finalResult) ?? null;
|
|
2034
|
+
const snapshot = buildPrHandoffSnapshotFromStatus(input.status, {
|
|
2035
|
+
prUrl: prUrlHint,
|
|
2036
|
+
headCommit: null
|
|
2037
|
+
});
|
|
2038
|
+
const requirement = assessPrHandoffRequirement({
|
|
2039
|
+
dispatched: input.worker.dispatched,
|
|
2040
|
+
routingRule: input.worker.routingRule,
|
|
2041
|
+
prUrl: prUrlHint,
|
|
2042
|
+
snapshot
|
|
2043
|
+
});
|
|
2044
|
+
if (!requirement.required) {
|
|
2045
|
+
return { ok: true, prUrl: prUrlHint ?? void 0 };
|
|
2046
|
+
}
|
|
2047
|
+
if (prUrlHint) {
|
|
2048
|
+
return { ok: true, prUrl: prUrlHint };
|
|
2049
|
+
}
|
|
2050
|
+
if (!ghAvailable(exec)) {
|
|
2051
|
+
const dirty = snapshot.changedFiles.length;
|
|
2052
|
+
const detail = dirty ? `${dirty} uncommitted change(s) with no PR URL` : "committed branch with no PR URL";
|
|
2053
|
+
return {
|
|
2054
|
+
ok: false,
|
|
2055
|
+
reason: `PR-ready handoff blocked: ${detail}`,
|
|
2056
|
+
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)."
|
|
2057
|
+
};
|
|
2058
|
+
}
|
|
2059
|
+
const repo = resolveGithubRepo(snapshot.worktreePath, exec);
|
|
2060
|
+
if (!repo) {
|
|
2061
|
+
return {
|
|
2062
|
+
ok: false,
|
|
2063
|
+
reason: "PR-ready handoff blocked: could not resolve github.com origin for the worktree",
|
|
2064
|
+
nextAction: "Ensure `origin` points at GitHub, push the branch, open a PR, and rerun `kynver worker complete`."
|
|
2065
|
+
};
|
|
2066
|
+
}
|
|
2067
|
+
const existing = findOpenPrUrl(snapshot.worktreePath, repo, snapshot.branch, exec);
|
|
2068
|
+
if (existing) {
|
|
2069
|
+
return {
|
|
2070
|
+
ok: true,
|
|
2071
|
+
prUrl: existing,
|
|
2072
|
+
headCommit: snapshot.headCommit ?? resolveHeadCommit(snapshot.worktreePath, exec) ?? void 0
|
|
2073
|
+
};
|
|
2074
|
+
}
|
|
2075
|
+
const hasDirty = snapshot.changedFiles.length > 0;
|
|
2076
|
+
let committed = false;
|
|
2077
|
+
let pushed = false;
|
|
2078
|
+
let headCommit = snapshot.headCommit ?? void 0;
|
|
2079
|
+
const pushResult = commitAndPushBranch({
|
|
2080
|
+
worktreePath: snapshot.worktreePath,
|
|
2081
|
+
branch: snapshot.branch,
|
|
2082
|
+
commitMessage: `chore(harness): PR-ready handoff for ${input.worker.name}`,
|
|
2083
|
+
hasDirtyFiles: hasDirty,
|
|
2084
|
+
exec
|
|
2085
|
+
});
|
|
2086
|
+
if (hasDirty && !pushResult.ok) {
|
|
2087
|
+
return {
|
|
2088
|
+
ok: false,
|
|
2089
|
+
reason: `PR-ready handoff blocked: ${pushResult.detail ?? "git commit/push failed"}`,
|
|
2090
|
+
nextAction: "Commit and push the branch, run `gh pr create`, then rerun `kynver worker complete`."
|
|
2091
|
+
};
|
|
2092
|
+
}
|
|
2093
|
+
if (!hasDirty) {
|
|
2094
|
+
const pushOnly = exec.git(snapshot.worktreePath, ["push", "-u", "origin", snapshot.branch]);
|
|
2095
|
+
if (pushOnly.status !== 0 && !/already up to date/i.test(pushOnly.stderr || pushOnly.stdout)) {
|
|
2096
|
+
return {
|
|
2097
|
+
ok: false,
|
|
2098
|
+
reason: `PR-ready handoff blocked: ${pushOnly.stderr || pushOnly.stdout || pushOnly.error || "git push failed"}`,
|
|
2099
|
+
nextAction: "Push the branch to origin, run `gh pr create`, then rerun `kynver worker complete`."
|
|
2100
|
+
};
|
|
2101
|
+
}
|
|
2102
|
+
pushed = pushOnly.status === 0;
|
|
2103
|
+
} else {
|
|
2104
|
+
committed = pushResult.committed;
|
|
2105
|
+
pushed = pushResult.pushed;
|
|
2106
|
+
if (!pushResult.ok) {
|
|
2107
|
+
return {
|
|
2108
|
+
ok: false,
|
|
2109
|
+
reason: `PR-ready handoff blocked: ${pushResult.detail ?? "git push failed"}`,
|
|
2110
|
+
nextAction: "Fix git auth or merge conflicts, push the branch, run `gh pr create`, then rerun `kynver worker complete`."
|
|
2111
|
+
};
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
2114
|
+
headCommit = pushResult.headCommit ?? headCommit ?? resolveHeadCommit(snapshot.worktreePath, exec) ?? void 0;
|
|
2115
|
+
const base = input.run.base?.trim() || "main";
|
|
2116
|
+
const pr = createGithubPr({
|
|
2117
|
+
worktreePath: snapshot.worktreePath,
|
|
2118
|
+
repo,
|
|
2119
|
+
branch: snapshot.branch,
|
|
2120
|
+
base: base.replace(/^origin\//, ""),
|
|
2121
|
+
title: defaultPrTitle(input.worker.name, input.worker.runId),
|
|
2122
|
+
body: defaultPrBody(input.worker.taskId, input.worker.name, input.worker.runId),
|
|
2123
|
+
exec
|
|
2124
|
+
});
|
|
2125
|
+
if (!pr.ok || !pr.prUrl) {
|
|
2126
|
+
const dirty = snapshot.changedFiles.length;
|
|
2127
|
+
const detail = dirty ? `${dirty} uncommitted change(s) and no PR URL after handoff attempt` : "no PR URL after handoff attempt";
|
|
2128
|
+
return {
|
|
2129
|
+
ok: false,
|
|
2130
|
+
reason: `PR-ready handoff blocked: ${detail}${pr.detail ? ` (${pr.detail})` : ""}`,
|
|
2131
|
+
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`."
|
|
2132
|
+
};
|
|
2133
|
+
}
|
|
2134
|
+
return {
|
|
2135
|
+
ok: true,
|
|
2136
|
+
prUrl: pr.prUrl,
|
|
2137
|
+
headCommit: headCommit ?? void 0,
|
|
2138
|
+
committed,
|
|
2139
|
+
pushed,
|
|
2140
|
+
created: pr.created
|
|
2141
|
+
};
|
|
2142
|
+
}
|
|
2143
|
+
|
|
2144
|
+
// src/completion-ack.ts
|
|
2145
|
+
function hasCompletionAck(worker) {
|
|
2146
|
+
return Boolean(worker.completionReportedAt?.trim());
|
|
2147
|
+
}
|
|
2148
|
+
function persistCompletionAck(worker, runId, fields) {
|
|
2149
|
+
worker.completionReportedAt = fields.completionReportedAt;
|
|
2150
|
+
worker.completionOutcome = fields.completionOutcome;
|
|
2151
|
+
if (fields.completionResponse !== void 0) {
|
|
2152
|
+
worker.completionResponse = fields.completionResponse;
|
|
2153
|
+
}
|
|
2154
|
+
saveWorker(runId, worker);
|
|
2155
|
+
}
|
|
2156
|
+
|
|
2157
|
+
// src/worker-ops.ts
|
|
1654
2158
|
async function postCompletion(url, secret, body) {
|
|
1655
2159
|
const res = await fetch(url, {
|
|
1656
2160
|
method: "POST",
|
|
@@ -1672,6 +2176,42 @@ function completionErrorText(parsed) {
|
|
|
1672
2176
|
}
|
|
1673
2177
|
return void 0;
|
|
1674
2178
|
}
|
|
2179
|
+
function asRecord(value) {
|
|
2180
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
2181
|
+
}
|
|
2182
|
+
function asString(value) {
|
|
2183
|
+
if (typeof value !== "string") return null;
|
|
2184
|
+
const trimmed = value.trim();
|
|
2185
|
+
return trimmed.length ? trimmed : null;
|
|
2186
|
+
}
|
|
2187
|
+
function deriveLifecycleStage(input) {
|
|
2188
|
+
if (input.completionBlocker) return `blocked:${input.completionBlocker}`;
|
|
2189
|
+
if (input.completionOutcome) return input.completionOutcome;
|
|
2190
|
+
if (input.completionReportedAt) return "completion_acknowledged";
|
|
2191
|
+
if (input.finished) return "worker_finished";
|
|
2192
|
+
return "in_progress";
|
|
2193
|
+
}
|
|
2194
|
+
function deriveNextAction(input) {
|
|
2195
|
+
if (input.completionBlocker) {
|
|
2196
|
+
return "Resolve completion blocker, then rerun `kynver worker complete`.";
|
|
2197
|
+
}
|
|
2198
|
+
if (input.completionOutcome === "review_scheduled" || input.completionOutcome === "review_already_scheduled") {
|
|
2199
|
+
return "Await review lane and landing decision in Command Center.";
|
|
2200
|
+
}
|
|
2201
|
+
if (input.completionOutcome === "needs_attention") {
|
|
2202
|
+
return "Inspect blocker/attention reason in Command Center and dispatch a repair task.";
|
|
2203
|
+
}
|
|
2204
|
+
if (input.finished && !input.completionReportedAt) {
|
|
2205
|
+
return "Post completion acknowledgement to AgentOS (`kynver worker complete`).";
|
|
2206
|
+
}
|
|
2207
|
+
return null;
|
|
2208
|
+
}
|
|
2209
|
+
function deriveHandoffState(input) {
|
|
2210
|
+
if (input.prUrl) return "pr_handoff";
|
|
2211
|
+
if (input.headCommit) return "commit_handoff";
|
|
2212
|
+
if (input.changedFiles.length > 0) return "dirty_worktree";
|
|
2213
|
+
return "none";
|
|
2214
|
+
}
|
|
1675
2215
|
function persistCompletionBlocker(worker, reason) {
|
|
1676
2216
|
const current = worker.completionBlocker;
|
|
1677
2217
|
if ((current ?? void 0) === (reason ?? void 0)) return;
|
|
@@ -1682,10 +2222,17 @@ function persistCompletionBlocker(worker, reason) {
|
|
|
1682
2222
|
function workerStatusOptions(run) {
|
|
1683
2223
|
return run ? { base: run.base, baseCommit: run.baseCommit } : {};
|
|
1684
2224
|
}
|
|
2225
|
+
function applyPrHandoffToStatus(status, handoff) {
|
|
2226
|
+
return {
|
|
2227
|
+
...status,
|
|
2228
|
+
...handoff.prUrl ? { prUrl: handoff.prUrl } : {},
|
|
2229
|
+
...handoff.headCommit ? { headCommit: handoff.headCommit } : {}
|
|
2230
|
+
};
|
|
2231
|
+
}
|
|
1685
2232
|
async function tryCompleteWorker(args) {
|
|
1686
2233
|
const worker = loadWorker(String(args.run), String(args.name));
|
|
1687
2234
|
const run = loadRun(worker.runId);
|
|
1688
|
-
|
|
2235
|
+
let status = computeWorkerStatus(worker, workerStatusOptions(run));
|
|
1689
2236
|
const agentOsId = (args.agentOsId ? String(args.agentOsId) : worker.agentOsId) || "";
|
|
1690
2237
|
const taskId = (args.taskId ? String(args.taskId) : worker.taskId) || null;
|
|
1691
2238
|
if (!agentOsId) {
|
|
@@ -1694,6 +2241,34 @@ async function tryCompleteWorker(args) {
|
|
|
1694
2241
|
if (!isFinishedWorkerStatus(status)) {
|
|
1695
2242
|
return { ok: true, skipped: true, reason: "worker-not-finished" };
|
|
1696
2243
|
}
|
|
2244
|
+
const forceReplay = args.force === true || args.force === "true";
|
|
2245
|
+
if (!forceReplay && hasCompletionAck(worker)) {
|
|
2246
|
+
return {
|
|
2247
|
+
ok: true,
|
|
2248
|
+
skipped: true,
|
|
2249
|
+
reason: "completion-already-acknowledged",
|
|
2250
|
+
httpStatus: 200
|
|
2251
|
+
};
|
|
2252
|
+
}
|
|
2253
|
+
if (worker.localOnly) {
|
|
2254
|
+
return { ok: true, skipped: true, reason: "local-only-worker" };
|
|
2255
|
+
}
|
|
2256
|
+
const skipPrHandoff = args.skipPrHandoff === true || args.skipPrHandoff === "true";
|
|
2257
|
+
if (!skipPrHandoff && worker.dispatched && taskId) {
|
|
2258
|
+
const handoff = ensurePrReadyHandoff({ worker, run, status });
|
|
2259
|
+
if (!handoff.ok) {
|
|
2260
|
+
persistCompletionBlocker(worker, handoff.reason);
|
|
2261
|
+
return {
|
|
2262
|
+
ok: false,
|
|
2263
|
+
reason: handoff.reason,
|
|
2264
|
+
nextAction: handoff.nextAction,
|
|
2265
|
+
completionBlocked: true
|
|
2266
|
+
};
|
|
2267
|
+
}
|
|
2268
|
+
if (handoff.prUrl || handoff.headCommit) {
|
|
2269
|
+
status = applyPrHandoffToStatus(status, handoff);
|
|
2270
|
+
}
|
|
2271
|
+
}
|
|
1697
2272
|
const base = resolveBaseUrl(args.baseUrl ? String(args.baseUrl) : void 0);
|
|
1698
2273
|
const explicitSecret = args.secret ? String(args.secret) : void 0;
|
|
1699
2274
|
let secret = await resolveCallbackSecretWithMint(explicitSecret, agentOsId, { baseUrl: base });
|
|
@@ -1706,7 +2281,13 @@ async function tryCompleteWorker(args) {
|
|
|
1706
2281
|
taskId,
|
|
1707
2282
|
startedAt: worker.startedAt,
|
|
1708
2283
|
finishedAt: status.lastActivityAt || (/* @__PURE__ */ new Date()).toISOString(),
|
|
1709
|
-
status
|
|
2284
|
+
status,
|
|
2285
|
+
workerInjection: {
|
|
2286
|
+
instructionPolicyFingerprint: worker.instructionPolicyFingerprint ?? null,
|
|
2287
|
+
instructionPolicyEvidence: worker.instructionPolicyEvidence ?? null,
|
|
2288
|
+
personaSlug: worker.personaSlug ?? null,
|
|
2289
|
+
personaEvidence: worker.personaEvidence ?? null
|
|
2290
|
+
}
|
|
1710
2291
|
};
|
|
1711
2292
|
let result = await postCompletion(url, secret, body);
|
|
1712
2293
|
if ((result.status === 401 || result.status === 403) && !explicitSecret) {
|
|
@@ -1718,7 +2299,19 @@ async function tryCompleteWorker(args) {
|
|
|
1718
2299
|
}
|
|
1719
2300
|
if (result.ok) {
|
|
1720
2301
|
persistCompletionBlocker(worker, void 0);
|
|
1721
|
-
|
|
2302
|
+
const ack = {
|
|
2303
|
+
completionReportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2304
|
+
completionOutcome: "acknowledged",
|
|
2305
|
+
completionResponse: result.parsed
|
|
2306
|
+
};
|
|
2307
|
+
persistCompletionAck(worker, worker.runId, ack);
|
|
2308
|
+
const prUrl = status.prUrl;
|
|
2309
|
+
return {
|
|
2310
|
+
ok: true,
|
|
2311
|
+
httpStatus: result.status,
|
|
2312
|
+
response: result.parsed,
|
|
2313
|
+
...prUrl ? { prHandoff: { prUrl } } : {}
|
|
2314
|
+
};
|
|
1722
2315
|
}
|
|
1723
2316
|
const authRejected = result.status === 401 || result.status === 403;
|
|
1724
2317
|
const detail = completionErrorText(result.parsed) ?? (authRejected ? "runner token unauthorized" : "non-2xx response");
|
|
@@ -1779,7 +2372,7 @@ function workerStatus(args) {
|
|
|
1779
2372
|
const worker = loadWorker(String(args.run), String(args.name));
|
|
1780
2373
|
const run = loadRun(worker.runId);
|
|
1781
2374
|
const status = computeWorkerStatus(worker, workerStatusOptions(run));
|
|
1782
|
-
writeJson(
|
|
2375
|
+
writeJson(path9.join(worker.workerDir, "last-status.json"), status);
|
|
1783
2376
|
console.log(JSON.stringify(status, null, 2));
|
|
1784
2377
|
}
|
|
1785
2378
|
function buildRunBoard(runId) {
|
|
@@ -1787,7 +2380,7 @@ function buildRunBoard(runId) {
|
|
|
1787
2380
|
const names = Object.keys(run.workers || {});
|
|
1788
2381
|
const workers = names.map((name) => {
|
|
1789
2382
|
const worker = readJson(
|
|
1790
|
-
|
|
2383
|
+
path9.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
1791
2384
|
void 0
|
|
1792
2385
|
);
|
|
1793
2386
|
if (!worker) {
|
|
@@ -1809,6 +2402,29 @@ function buildRunBoard(runId) {
|
|
|
1809
2402
|
const completionBlocker = typeof rawBlocker === "string" && rawBlocker ? rawBlocker : void 0;
|
|
1810
2403
|
const boardStatus = completionBlocker ? "blocked" : status.status;
|
|
1811
2404
|
const boardAttention = completionBlocker ? "blocked" : status.attention.state;
|
|
2405
|
+
const completionResponse = asRecord(worker.completionResponse);
|
|
2406
|
+
const completionTask = asRecord(completionResponse?.task);
|
|
2407
|
+
const completionOutcome = asString(completionResponse?.outcome);
|
|
2408
|
+
const completionRouteStatus = asString(completionResponse?.status);
|
|
2409
|
+
const completionWarnings = Array.isArray(completionResponse?.warnings) ? completionResponse.warnings.filter((w) => typeof w === "string" && w.trim().length > 0) : [];
|
|
2410
|
+
const prUrl = asString(completionTask?.prUrl) ?? asString(completionResponse?.prUrl);
|
|
2411
|
+
const lifecycleStage = deriveLifecycleStage({
|
|
2412
|
+
finished: isFinishedWorkerStatus(status),
|
|
2413
|
+
completionBlocker,
|
|
2414
|
+
completionOutcome,
|
|
2415
|
+
completionReportedAt: worker.completionReportedAt ?? null
|
|
2416
|
+
});
|
|
2417
|
+
const nextAction = deriveNextAction({
|
|
2418
|
+
completionBlocker,
|
|
2419
|
+
completionOutcome,
|
|
2420
|
+
completionReportedAt: worker.completionReportedAt ?? null,
|
|
2421
|
+
finished: isFinishedWorkerStatus(status)
|
|
2422
|
+
});
|
|
2423
|
+
const handoffState = deriveHandoffState({
|
|
2424
|
+
changedFiles: status.changedFiles,
|
|
2425
|
+
headCommit,
|
|
2426
|
+
prUrl: prUrl ?? void 0
|
|
2427
|
+
});
|
|
1812
2428
|
return {
|
|
1813
2429
|
worker: status.worker,
|
|
1814
2430
|
status: boardStatus,
|
|
@@ -1828,12 +2444,39 @@ function buildRunBoard(runId) {
|
|
|
1828
2444
|
changedFileCount: status.changedFiles.length,
|
|
1829
2445
|
changedFiles: status.changedFiles,
|
|
1830
2446
|
branch: status.branch,
|
|
2447
|
+
taskId: worker.taskId ?? null,
|
|
2448
|
+
planId: worker.planId ?? null,
|
|
2449
|
+
instructionPolicyFingerprint: typeof worker.instructionPolicyFingerprint === "string" ? worker.instructionPolicyFingerprint : null,
|
|
2450
|
+
instructionPolicyRuleCount: (() => {
|
|
2451
|
+
const raw = worker.instructionPolicyEvidence;
|
|
2452
|
+
if (!raw || typeof raw !== "object") return null;
|
|
2453
|
+
const slugs = raw.ruleSlugs;
|
|
2454
|
+
return Array.isArray(slugs) ? slugs.length : null;
|
|
2455
|
+
})(),
|
|
2456
|
+
leaseOwner: worker.leaseOwner ?? null,
|
|
1831
2457
|
model: typeof worker.model === "string" ? worker.model : void 0,
|
|
1832
2458
|
routingRule: typeof worker.routingRule === "string" ? worker.routingRule : void 0,
|
|
1833
2459
|
requestedModel: typeof worker.requestedModel === "string" ? worker.requestedModel : void 0,
|
|
1834
2460
|
headCommit,
|
|
2461
|
+
prUrl,
|
|
2462
|
+
handoffState,
|
|
1835
2463
|
gitAncestry: status.gitAncestry,
|
|
1836
2464
|
finalResult: status.finalResult,
|
|
2465
|
+
lifecycleStage,
|
|
2466
|
+
completionReportedAt: worker.completionReportedAt ?? null,
|
|
2467
|
+
completionOutcome: worker.completionOutcome ?? null,
|
|
2468
|
+
completionRouteStatus,
|
|
2469
|
+
completionRouteOutcome: completionOutcome,
|
|
2470
|
+
completionWarnings,
|
|
2471
|
+
completionBlocker: completionBlocker ?? null,
|
|
2472
|
+
checkpoint: {
|
|
2473
|
+
phase: status.lastHeartbeatPhase,
|
|
2474
|
+
summary: status.lastHeartbeatSummary,
|
|
2475
|
+
blocker: status.heartbeatBlocker
|
|
2476
|
+
},
|
|
2477
|
+
lastCommandHint: status.currentTool ?? status.lastHeartbeatSummary,
|
|
2478
|
+
failureReason: completionBlocker ?? status.error ?? null,
|
|
2479
|
+
nextAction,
|
|
1837
2480
|
ancestry: status.gitAncestry.relation,
|
|
1838
2481
|
ancestryChecked: status.gitAncestry.checked
|
|
1839
2482
|
};
|
|
@@ -1847,7 +2490,7 @@ function buildRunBoard(runId) {
|
|
|
1847
2490
|
needsAttention: workers.filter((w) => w.attention && w.attention !== "ok" && w.attention !== "done").map((w) => w.worker),
|
|
1848
2491
|
workers
|
|
1849
2492
|
};
|
|
1850
|
-
writeJson(
|
|
2493
|
+
writeJson(path9.join(runDirectory(run.id), "last-board.json"), board);
|
|
1851
2494
|
return board;
|
|
1852
2495
|
}
|
|
1853
2496
|
async function publishHarnessBoardSnapshot(args, source) {
|
|
@@ -2014,12 +2657,12 @@ async function autoCompleteWorkerCli(raw) {
|
|
|
2014
2657
|
}
|
|
2015
2658
|
}
|
|
2016
2659
|
function resolveDefaultCliPath() {
|
|
2017
|
-
return
|
|
2660
|
+
return path10.join(fileURLToPath(new URL(".", import.meta.url)), "cli.js");
|
|
2018
2661
|
}
|
|
2019
2662
|
function spawnCompletionSidecar(opts) {
|
|
2020
2663
|
const cliPath = opts.cliPath ?? resolveDefaultCliPath();
|
|
2021
2664
|
if (!existsSync9(cliPath)) return void 0;
|
|
2022
|
-
const logPath =
|
|
2665
|
+
const logPath = path10.join(opts.workerDir, "auto-complete.log");
|
|
2023
2666
|
let logFd;
|
|
2024
2667
|
try {
|
|
2025
2668
|
logFd = openSync3(logPath, "a");
|
|
@@ -2101,16 +2744,16 @@ function spawnWorkerProcess(run, opts) {
|
|
|
2101
2744
|
launchModel = preflight.model;
|
|
2102
2745
|
}
|
|
2103
2746
|
const { worktreesDir } = getPaths();
|
|
2104
|
-
const workerDir =
|
|
2747
|
+
const workerDir = path11.join(runDirectory(run.id), "workers", name);
|
|
2105
2748
|
mkdirSync3(workerDir, { recursive: true });
|
|
2106
|
-
const worktreePath =
|
|
2749
|
+
const worktreePath = path11.join(worktreesDir, run.id, name);
|
|
2107
2750
|
const branch = opts.branch || `agent/${run.id}/${name}`;
|
|
2108
2751
|
if (existsSync10(worktreePath)) throw new Error(`worktree path already exists: ${worktreePath}`);
|
|
2109
2752
|
git(run.repo, ["fetch", "origin", "--prune"], { allowFailure: true });
|
|
2110
2753
|
git(run.repo, ["worktree", "add", "-b", branch, worktreePath, run.baseCommit], { throwError: true });
|
|
2111
|
-
const stdoutPath =
|
|
2112
|
-
const stderrPath =
|
|
2113
|
-
const heartbeatPath =
|
|
2754
|
+
const stdoutPath = path11.join(workerDir, "stdout.jsonl");
|
|
2755
|
+
const stderrPath = path11.join(workerDir, "stderr.log");
|
|
2756
|
+
const heartbeatPath = path11.join(workerDir, "heartbeat.jsonl");
|
|
2114
2757
|
const prompt = buildPrompt({
|
|
2115
2758
|
task: opts.task,
|
|
2116
2759
|
ownedPaths: opts.ownedPaths || [],
|
|
@@ -2118,6 +2761,8 @@ function spawnWorkerProcess(run, opts) {
|
|
|
2118
2761
|
heartbeatPath,
|
|
2119
2762
|
planId: opts.planId,
|
|
2120
2763
|
taskId: opts.taskId,
|
|
2764
|
+
instructionPolicyMarkdown: opts.instructionPolicyMarkdown,
|
|
2765
|
+
personaMarkdown: opts.personaMarkdown,
|
|
2121
2766
|
model: launchModel
|
|
2122
2767
|
});
|
|
2123
2768
|
let started;
|
|
@@ -2157,14 +2802,19 @@ function spawnWorkerProcess(run, opts) {
|
|
|
2157
2802
|
...opts.agentOsId ? { agentOsId: String(opts.agentOsId) } : {},
|
|
2158
2803
|
...opts.taskId ? { taskId: String(opts.taskId) } : {},
|
|
2159
2804
|
...opts.planId ? { planId: String(opts.planId) } : {},
|
|
2805
|
+
...opts.instructionPolicyFingerprint ? { instructionPolicyFingerprint: String(opts.instructionPolicyFingerprint) } : {},
|
|
2806
|
+
...opts.instructionPolicyEvidence ? { instructionPolicyEvidence: opts.instructionPolicyEvidence } : {},
|
|
2807
|
+
...opts.personaSlug ? { personaSlug: String(opts.personaSlug) } : {},
|
|
2808
|
+
...opts.personaEvidence ? { personaEvidence: opts.personaEvidence } : {},
|
|
2160
2809
|
...opts.leaseOwner ? { leaseOwner: String(opts.leaseOwner) } : {},
|
|
2161
2810
|
...opts.dispatched ? { dispatched: true } : {},
|
|
2811
|
+
...!opts.agentOsId || !opts.taskId ? { localOnly: true } : {},
|
|
2162
2812
|
routingRule: routing.rule,
|
|
2163
2813
|
...routing.requestedModel ? { requestedModel: routing.requestedModel } : {},
|
|
2164
2814
|
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2165
2815
|
};
|
|
2166
2816
|
saveWorker(run.id, worker);
|
|
2167
|
-
run.workers = { ...run.workers || {}, [name]: { workerDir, statusPath:
|
|
2817
|
+
run.workers = { ...run.workers || {}, [name]: { workerDir, statusPath: path11.join(workerDir, "worker.json") } };
|
|
2168
2818
|
run.status = "running";
|
|
2169
2819
|
saveRun(run);
|
|
2170
2820
|
if (worker.agentOsId && worker.taskId) {
|
|
@@ -2184,6 +2834,10 @@ function spawnWorkerProcess(run, opts) {
|
|
|
2184
2834
|
if (!sidecarSpawned) {
|
|
2185
2835
|
const reason = "completion sidecar failed to spawn (CLI not found or spawn error)";
|
|
2186
2836
|
worker.completionBlocker = reason;
|
|
2837
|
+
worker.completionSidecarSpawnFailedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2838
|
+
saveWorker(run.id, worker);
|
|
2839
|
+
} else if (sidecarSpawned.pid) {
|
|
2840
|
+
worker.completionSidecarPid = sidecarSpawned.pid;
|
|
2187
2841
|
saveWorker(run.id, worker);
|
|
2188
2842
|
}
|
|
2189
2843
|
}
|
|
@@ -2201,6 +2855,14 @@ async function startWorker(args) {
|
|
|
2201
2855
|
console.error("missing --task or --task-file");
|
|
2202
2856
|
process.exit(1);
|
|
2203
2857
|
}
|
|
2858
|
+
const boardLinked = Boolean(args.agentOsId && args.taskId);
|
|
2859
|
+
const explicitLocalOnly = args.localOnly === true || args.localOnly === "true";
|
|
2860
|
+
if (!boardLinked && !explicitLocalOnly && (args.agentOsId || args.taskId)) {
|
|
2861
|
+
console.error(
|
|
2862
|
+
"worker start: board-linked workers require both --agent-os-id and --task-id (or pass --local-only for direct runs)"
|
|
2863
|
+
);
|
|
2864
|
+
process.exit(1);
|
|
2865
|
+
}
|
|
2204
2866
|
const wait = args.wait === true || args.wait === "true";
|
|
2205
2867
|
let worker;
|
|
2206
2868
|
try {
|
|
@@ -2248,6 +2910,32 @@ async function startWorker(args) {
|
|
|
2248
2910
|
|
|
2249
2911
|
// src/dispatch.ts
|
|
2250
2912
|
var DEFAULT_DISPATCH_LEASE_MS = 60 * 60 * 1e3;
|
|
2913
|
+
function readHarnessWorkerContext(decision) {
|
|
2914
|
+
const raw = decision.harnessWorkerContext;
|
|
2915
|
+
if (!raw || typeof raw !== "object") return null;
|
|
2916
|
+
const ctx = raw;
|
|
2917
|
+
const markdown = typeof ctx.instructionPolicyMarkdown === "string" ? ctx.instructionPolicyMarkdown : null;
|
|
2918
|
+
const fingerprint = typeof ctx.instructionPolicyFingerprint === "string" ? ctx.instructionPolicyFingerprint : null;
|
|
2919
|
+
const evidence = ctx.instructionPolicyEvidence && typeof ctx.instructionPolicyEvidence === "object" ? ctx.instructionPolicyEvidence : null;
|
|
2920
|
+
const personaMarkdown = typeof ctx.personaMarkdown === "string" ? ctx.personaMarkdown : null;
|
|
2921
|
+
const personaSlug = typeof ctx.personaEvidence === "object" && ctx.personaEvidence && typeof ctx.personaEvidence.injectedPersonaSlug === "string" ? ctx.personaEvidence.injectedPersonaSlug : null;
|
|
2922
|
+
const personaEvidence = ctx.personaEvidence && typeof ctx.personaEvidence === "object" ? ctx.personaEvidence : null;
|
|
2923
|
+
const personaInjectionReady = ctx.personaInjectionReady === true;
|
|
2924
|
+
return {
|
|
2925
|
+
instructionPolicyMarkdown: markdown,
|
|
2926
|
+
instructionPolicyFingerprint: fingerprint,
|
|
2927
|
+
instructionPolicyEvidence: evidence,
|
|
2928
|
+
personaMarkdown,
|
|
2929
|
+
personaSlug,
|
|
2930
|
+
personaEvidence,
|
|
2931
|
+
personaInjectionReady
|
|
2932
|
+
};
|
|
2933
|
+
}
|
|
2934
|
+
function normalizePersonaSlug(value) {
|
|
2935
|
+
if (typeof value !== "string") return null;
|
|
2936
|
+
const trimmed = value.trim().toLowerCase();
|
|
2937
|
+
return trimmed.length ? trimmed : null;
|
|
2938
|
+
}
|
|
2251
2939
|
function buildDispatchTaskText(task, agentOsId) {
|
|
2252
2940
|
return [
|
|
2253
2941
|
`[AgentOS task ${task.id}] ${task.title}`,
|
|
@@ -2272,6 +2960,20 @@ async function dispatchRun(args) {
|
|
|
2272
2960
|
const runnerResourceGate = observeRunnerResourceGate({ runId: run.id });
|
|
2273
2961
|
const requestedStarts = Number(args.maxStarts) > 0 ? Math.floor(Number(args.maxStarts)) : 1;
|
|
2274
2962
|
const cappedStarts = dryRun ? requestedStarts : Math.min(requestedStarts, runnerResourceGate.slotsAvailable);
|
|
2963
|
+
const activeHarnessWorkers = [];
|
|
2964
|
+
for (const name of Object.keys(run.workers || {})) {
|
|
2965
|
+
const worker = readJson(
|
|
2966
|
+
path12.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
2967
|
+
void 0
|
|
2968
|
+
);
|
|
2969
|
+
if (!worker?.taskId || !isPidAlive(worker.pid)) continue;
|
|
2970
|
+
activeHarnessWorkers.push({
|
|
2971
|
+
runId: run.id,
|
|
2972
|
+
workerName: name,
|
|
2973
|
+
taskId: worker.taskId,
|
|
2974
|
+
pid: worker.pid
|
|
2975
|
+
});
|
|
2976
|
+
}
|
|
2275
2977
|
const dispatchUrl = `${base}/api/agent-os/by-id/${encodeURIComponent(agentOsId)}/tasks/dispatch-next`;
|
|
2276
2978
|
const body = {
|
|
2277
2979
|
agentOsId,
|
|
@@ -2281,6 +2983,8 @@ async function dispatchRun(args) {
|
|
|
2281
2983
|
leaseDurationMs: Number(args.leaseMs) > 0 ? Math.floor(Number(args.leaseMs)) : DEFAULT_DISPATCH_LEASE_MS,
|
|
2282
2984
|
runnerDiskGate,
|
|
2283
2985
|
runnerResourceGate,
|
|
2986
|
+
activeHarnessWorkers,
|
|
2987
|
+
harnessBoardSnapshot: buildRunBoard(run.id),
|
|
2284
2988
|
...args.lane ? { lane: String(args.lane) } : {},
|
|
2285
2989
|
executor: args.executor ? String(args.executor) : "harness",
|
|
2286
2990
|
...args.diskPath ? { diskPath: String(args.diskPath) } : {}
|
|
@@ -2340,6 +3044,25 @@ async function dispatchRun(args) {
|
|
|
2340
3044
|
const outcomes = [];
|
|
2341
3045
|
for (const decision of result.started) {
|
|
2342
3046
|
const task = decision.task;
|
|
3047
|
+
const harnessContext = readHarnessWorkerContext(decision);
|
|
3048
|
+
const taskId = String(task.id);
|
|
3049
|
+
const expectedPersona = normalizePersonaSlug(task.personaSlug);
|
|
3050
|
+
if (expectedPersona && (!harnessContext?.personaInjectionReady || !harnessContext.personaMarkdown)) {
|
|
3051
|
+
outcomes.push({
|
|
3052
|
+
taskId,
|
|
3053
|
+
started: false,
|
|
3054
|
+
error: `persona_injection_required: missing anchored context-envelope persona block for "${expectedPersona}"`
|
|
3055
|
+
});
|
|
3056
|
+
continue;
|
|
3057
|
+
}
|
|
3058
|
+
if (hasLiveWorkerForTask(run.id, taskId)) {
|
|
3059
|
+
outcomes.push({
|
|
3060
|
+
taskId,
|
|
3061
|
+
started: false,
|
|
3062
|
+
error: "duplicate_dispatch_prevented: live local worker already owns this task"
|
|
3063
|
+
});
|
|
3064
|
+
continue;
|
|
3065
|
+
}
|
|
2343
3066
|
const attempt = Number(task.attempt) || 1;
|
|
2344
3067
|
if (attempt > retryLimits.maxTaskAttempts) {
|
|
2345
3068
|
outcomes.push({
|
|
@@ -2367,6 +3090,12 @@ async function dispatchRun(args) {
|
|
|
2367
3090
|
agentOsId,
|
|
2368
3091
|
taskId: String(task.id),
|
|
2369
3092
|
planId,
|
|
3093
|
+
instructionPolicyMarkdown: harnessContext?.instructionPolicyMarkdown ?? null,
|
|
3094
|
+
instructionPolicyFingerprint: harnessContext?.instructionPolicyFingerprint ?? null,
|
|
3095
|
+
instructionPolicyEvidence: harnessContext?.instructionPolicyEvidence ?? null,
|
|
3096
|
+
personaMarkdown: harnessContext?.personaMarkdown ?? null,
|
|
3097
|
+
personaSlug: harnessContext?.personaSlug ?? expectedPersona,
|
|
3098
|
+
personaEvidence: harnessContext?.personaEvidence ?? null,
|
|
2370
3099
|
leaseOwner,
|
|
2371
3100
|
dispatched: true
|
|
2372
3101
|
});
|
|
@@ -2378,8 +3107,22 @@ async function dispatchRun(args) {
|
|
|
2378
3107
|
branch: worker.branch,
|
|
2379
3108
|
model: worker.model,
|
|
2380
3109
|
provider: routing.provider,
|
|
2381
|
-
routingRule: routing.rule
|
|
3110
|
+
routingRule: routing.rule,
|
|
3111
|
+
instructionPolicyFingerprint: harnessContext?.instructionPolicyFingerprint ?? null,
|
|
3112
|
+
instructionPolicyRuleCount: Array.isArray(harnessContext?.instructionPolicyEvidence?.ruleSlugs) ? harnessContext.instructionPolicyEvidence.ruleSlugs.length : null,
|
|
3113
|
+
personaSlug: harnessContext?.personaSlug ?? expectedPersona,
|
|
3114
|
+
personaOperatingRuleCount: harnessContext?.personaEvidence?.operatingRuleCount ?? null
|
|
2382
3115
|
});
|
|
3116
|
+
if (harnessContext?.instructionPolicyFingerprint) {
|
|
3117
|
+
console.error(
|
|
3118
|
+
`[dispatch] task ${taskId}: Lane A instruction policy injected fingerprint=${harnessContext.instructionPolicyFingerprint}`
|
|
3119
|
+
);
|
|
3120
|
+
}
|
|
3121
|
+
if (harnessContext?.personaSlug) {
|
|
3122
|
+
console.error(
|
|
3123
|
+
`[dispatch] task ${taskId}: persona context injected slug=${harnessContext.personaSlug}`
|
|
3124
|
+
);
|
|
3125
|
+
}
|
|
2383
3126
|
} catch (error) {
|
|
2384
3127
|
const releaseUrl = `${base}/api/agent-os/by-id/${encodeURIComponent(agentOsId)}/tasks/${encodeURIComponent(String(task.id))}/release`;
|
|
2385
3128
|
let release;
|
|
@@ -2424,7 +3167,7 @@ async function dispatchRun(args) {
|
|
|
2424
3167
|
}
|
|
2425
3168
|
|
|
2426
3169
|
// src/sweep.ts
|
|
2427
|
-
import
|
|
3170
|
+
import path13 from "node:path";
|
|
2428
3171
|
async function sweepRun(args) {
|
|
2429
3172
|
const pipeline = args.pipeline === true || args.pipeline === "true";
|
|
2430
3173
|
try {
|
|
@@ -2437,7 +3180,7 @@ async function sweepRun(args) {
|
|
|
2437
3180
|
const releasedLocalOrphans = [];
|
|
2438
3181
|
for (const name of Object.keys(run.workers || {})) {
|
|
2439
3182
|
const worker = readJson(
|
|
2440
|
-
|
|
3183
|
+
path13.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
2441
3184
|
void 0
|
|
2442
3185
|
);
|
|
2443
3186
|
if (!worker || !worker.dispatched || !worker.taskId) continue;
|
|
@@ -2481,10 +3224,10 @@ async function sweepRun(args) {
|
|
|
2481
3224
|
|
|
2482
3225
|
// src/worktree.ts
|
|
2483
3226
|
import { existsSync as existsSync11, mkdirSync as mkdirSync4 } from "node:fs";
|
|
2484
|
-
import
|
|
3227
|
+
import path15 from "node:path";
|
|
2485
3228
|
|
|
2486
3229
|
// src/validate.ts
|
|
2487
|
-
import
|
|
3230
|
+
import path14 from "node:path";
|
|
2488
3231
|
var RUN_ID_RE = /^[a-z0-9][a-z0-9._-]{0,127}$/i;
|
|
2489
3232
|
function validateRunId(runId) {
|
|
2490
3233
|
const trimmed = runId.trim();
|
|
@@ -2492,7 +3235,7 @@ function validateRunId(runId) {
|
|
|
2492
3235
|
return trimmed;
|
|
2493
3236
|
}
|
|
2494
3237
|
function validateRepo(repo) {
|
|
2495
|
-
const resolved =
|
|
3238
|
+
const resolved = path14.resolve(repo);
|
|
2496
3239
|
if (resolved.includes("..")) throw new Error("repo path must not contain .. segments");
|
|
2497
3240
|
return resolved;
|
|
2498
3241
|
}
|
|
@@ -2517,12 +3260,12 @@ function createRun(args) {
|
|
|
2517
3260
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2518
3261
|
workers: {}
|
|
2519
3262
|
};
|
|
2520
|
-
writeJson(
|
|
3263
|
+
writeJson(path15.join(dir, "run.json"), run);
|
|
2521
3264
|
console.log(JSON.stringify({ runId: id, runDir: dir, repo, base, baseCommit }, null, 2));
|
|
2522
3265
|
}
|
|
2523
3266
|
function listRuns() {
|
|
2524
3267
|
const { runsDir } = getPaths();
|
|
2525
|
-
const rows = listRunIds(runsDir).map((id) => readJson(
|
|
3268
|
+
const rows = listRunIds(runsDir).map((id) => readJson(path15.join(runDirectory(id), "run.json"), void 0)).filter(Boolean).map((run) => ({
|
|
2526
3269
|
id: run.id,
|
|
2527
3270
|
name: run.name,
|
|
2528
3271
|
status: run.status,
|
|
@@ -2537,13 +3280,13 @@ function failExists(message) {
|
|
|
2537
3280
|
}
|
|
2538
3281
|
|
|
2539
3282
|
// src/pipeline-tick.ts
|
|
2540
|
-
import
|
|
3283
|
+
import path24 from "node:path";
|
|
2541
3284
|
|
|
2542
3285
|
// src/stale-reconcile.ts
|
|
2543
|
-
import
|
|
3286
|
+
import path17 from "node:path";
|
|
2544
3287
|
|
|
2545
3288
|
// src/finalize.ts
|
|
2546
|
-
import
|
|
3289
|
+
import path16 from "node:path";
|
|
2547
3290
|
var ACTIVE_RUN_STATUSES = /* @__PURE__ */ new Set(["running", "dispatching", "pending", "queued"]);
|
|
2548
3291
|
function terminalStatusFor(run) {
|
|
2549
3292
|
const names = Object.keys(run.workers || {});
|
|
@@ -2554,7 +3297,7 @@ function terminalStatusFor(run) {
|
|
|
2554
3297
|
let anyLandingBlocked = false;
|
|
2555
3298
|
for (const name of names) {
|
|
2556
3299
|
const worker = readJson(
|
|
2557
|
-
|
|
3300
|
+
path16.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
2558
3301
|
void 0
|
|
2559
3302
|
);
|
|
2560
3303
|
if (!worker) continue;
|
|
@@ -2606,7 +3349,7 @@ function reconcileStaleWorkers() {
|
|
|
2606
3349
|
const now = Date.now();
|
|
2607
3350
|
for (const run of listRunRecords()) {
|
|
2608
3351
|
for (const name of Object.keys(run.workers || {})) {
|
|
2609
|
-
const workerPath =
|
|
3352
|
+
const workerPath = path17.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json");
|
|
2610
3353
|
const worker = readJson(workerPath, void 0);
|
|
2611
3354
|
if (!worker || worker.status !== "running") {
|
|
2612
3355
|
outcomes.push({
|
|
@@ -2619,7 +3362,21 @@ function reconcileStaleWorkers() {
|
|
|
2619
3362
|
}
|
|
2620
3363
|
const status = computeWorkerStatus(worker, { base: run.base, baseCommit: run.baseCommit });
|
|
2621
3364
|
if (status.finalResult) {
|
|
2622
|
-
|
|
3365
|
+
if (worker.status === "running") {
|
|
3366
|
+
const nextStatus = status.attention.state === "blocked" ? "blocked" : status.attention.state === "done" || status.status === "done" ? "done" : "exited";
|
|
3367
|
+
worker.status = nextStatus;
|
|
3368
|
+
worker.reconciledAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3369
|
+
worker.reconcileReason = "synced finished worker record after terminal stdout/heartbeat";
|
|
3370
|
+
saveWorker(run.id, worker);
|
|
3371
|
+
outcomes.push({
|
|
3372
|
+
runId: run.id,
|
|
3373
|
+
worker: name,
|
|
3374
|
+
action: "marked_exited",
|
|
3375
|
+
reason: worker.reconcileReason
|
|
3376
|
+
});
|
|
3377
|
+
} else {
|
|
3378
|
+
outcomes.push({ runId: run.id, worker: name, action: "skipped", reason: "final result present" });
|
|
3379
|
+
}
|
|
2623
3380
|
continue;
|
|
2624
3381
|
}
|
|
2625
3382
|
if (!status.alive) {
|
|
@@ -2668,7 +3425,7 @@ function reconcileStaleWorkers() {
|
|
|
2668
3425
|
}
|
|
2669
3426
|
|
|
2670
3427
|
// src/plan-progress-daemon-sync.ts
|
|
2671
|
-
import
|
|
3428
|
+
import path18 from "node:path";
|
|
2672
3429
|
|
|
2673
3430
|
// src/plan-progress-sync.ts
|
|
2674
3431
|
async function syncPlanProgress(args) {
|
|
@@ -2692,7 +3449,7 @@ async function syncActiveWorkerPlanProgress(runId, args) {
|
|
|
2692
3449
|
const outcomes = [];
|
|
2693
3450
|
for (const name of Object.keys(run.workers || {})) {
|
|
2694
3451
|
const worker = readJson(
|
|
2695
|
-
|
|
3452
|
+
path18.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
2696
3453
|
void 0
|
|
2697
3454
|
);
|
|
2698
3455
|
if (!worker?.dispatched || !worker.taskId) continue;
|
|
@@ -2741,7 +3498,7 @@ async function fetchWorkspaceRuntimePreferences(agentOsId, args) {
|
|
|
2741
3498
|
}
|
|
2742
3499
|
|
|
2743
3500
|
// src/cleanup.ts
|
|
2744
|
-
import
|
|
3501
|
+
import path23 from "node:path";
|
|
2745
3502
|
|
|
2746
3503
|
// src/cleanup-types.ts
|
|
2747
3504
|
var DEFAULT_NODE_MODULES_AGE_MS = 6 * 60 * 60 * 1e3;
|
|
@@ -2813,11 +3570,11 @@ function skipNodeModulesRemoval(input) {
|
|
|
2813
3570
|
|
|
2814
3571
|
// src/cleanup-execute.ts
|
|
2815
3572
|
import { existsSync as existsSync13, rmSync } from "node:fs";
|
|
2816
|
-
import
|
|
3573
|
+
import path20 from "node:path";
|
|
2817
3574
|
|
|
2818
3575
|
// src/cleanup-dir-size.ts
|
|
2819
3576
|
import { existsSync as existsSync12, readdirSync as readdirSync4, statSync as statSync2 } from "node:fs";
|
|
2820
|
-
import
|
|
3577
|
+
import path19 from "node:path";
|
|
2821
3578
|
function directorySizeBytes(root, maxEntries = 5e4) {
|
|
2822
3579
|
if (!existsSync12(root)) return 0;
|
|
2823
3580
|
let total = 0;
|
|
@@ -2833,7 +3590,7 @@ function directorySizeBytes(root, maxEntries = 5e4) {
|
|
|
2833
3590
|
}
|
|
2834
3591
|
for (const name of entries) {
|
|
2835
3592
|
if (seen++ > maxEntries) return null;
|
|
2836
|
-
const full =
|
|
3593
|
+
const full = path19.join(current, name);
|
|
2837
3594
|
let st;
|
|
2838
3595
|
try {
|
|
2839
3596
|
st = statSync2(full);
|
|
@@ -2917,20 +3674,20 @@ function removeWorktree(candidate, execute) {
|
|
|
2917
3674
|
}
|
|
2918
3675
|
}
|
|
2919
3676
|
function isHarnessNodeModulesPath(targetPath, harnessRoot, worktreesDir) {
|
|
2920
|
-
const resolved =
|
|
2921
|
-
const nm = resolved.endsWith(`${
|
|
3677
|
+
const resolved = path20.resolve(targetPath);
|
|
3678
|
+
const nm = resolved.endsWith(`${path20.sep}node_modules`) ? resolved : null;
|
|
2922
3679
|
if (!nm) return "path_outside_harness";
|
|
2923
|
-
const rel =
|
|
2924
|
-
if (rel.startsWith("..") ||
|
|
2925
|
-
const parts = rel.split(
|
|
3680
|
+
const rel = path20.relative(worktreesDir, nm);
|
|
3681
|
+
if (rel.startsWith("..") || path20.isAbsolute(rel)) return "path_outside_harness";
|
|
3682
|
+
const parts = rel.split(path20.sep);
|
|
2926
3683
|
if (parts.length < 3 || parts[parts.length - 1] !== "node_modules") return "path_outside_harness";
|
|
2927
|
-
if (!resolved.startsWith(
|
|
3684
|
+
if (!resolved.startsWith(path20.resolve(harnessRoot))) return "path_outside_harness";
|
|
2928
3685
|
return null;
|
|
2929
3686
|
}
|
|
2930
3687
|
|
|
2931
3688
|
// src/cleanup-scan.ts
|
|
2932
3689
|
import { existsSync as existsSync14, readdirSync as readdirSync5, statSync as statSync3 } from "node:fs";
|
|
2933
|
-
import
|
|
3690
|
+
import path21 from "node:path";
|
|
2934
3691
|
function pathAgeMs(target, now) {
|
|
2935
3692
|
try {
|
|
2936
3693
|
const mtime = statSync3(target).mtimeMs;
|
|
@@ -2940,17 +3697,17 @@ function pathAgeMs(target, now) {
|
|
|
2940
3697
|
}
|
|
2941
3698
|
}
|
|
2942
3699
|
function isPathInside(child, parent) {
|
|
2943
|
-
const rel =
|
|
2944
|
-
return rel === "" || !rel.startsWith("..") && !
|
|
3700
|
+
const rel = path21.relative(parent, child);
|
|
3701
|
+
return rel === "" || !rel.startsWith("..") && !path21.isAbsolute(rel);
|
|
2945
3702
|
}
|
|
2946
3703
|
function scanNodeModulesCandidates(opts) {
|
|
2947
3704
|
const candidates = [];
|
|
2948
3705
|
const seen = /* @__PURE__ */ new Set();
|
|
2949
3706
|
for (const entry of opts.index.values()) {
|
|
2950
3707
|
if (opts.runIdFilter && entry.runId !== opts.runIdFilter) continue;
|
|
2951
|
-
const nm =
|
|
3708
|
+
const nm = path21.join(entry.worktreePath, "node_modules");
|
|
2952
3709
|
if (!existsSync14(nm)) continue;
|
|
2953
|
-
const resolved =
|
|
3710
|
+
const resolved = path21.resolve(nm);
|
|
2954
3711
|
if (seen.has(resolved)) continue;
|
|
2955
3712
|
seen.add(resolved);
|
|
2956
3713
|
candidates.push({
|
|
@@ -2966,13 +3723,13 @@ function scanNodeModulesCandidates(opts) {
|
|
|
2966
3723
|
if (!opts.includeOrphans || !existsSync14(opts.worktreesDir)) return candidates;
|
|
2967
3724
|
for (const runEntry of readdirSync5(opts.worktreesDir, { withFileTypes: true })) {
|
|
2968
3725
|
if (!runEntry.isDirectory()) continue;
|
|
2969
|
-
const runPath =
|
|
3726
|
+
const runPath = path21.join(opts.worktreesDir, runEntry.name);
|
|
2970
3727
|
for (const workerEntry of readdirSync5(runPath, { withFileTypes: true })) {
|
|
2971
3728
|
if (!workerEntry.isDirectory()) continue;
|
|
2972
|
-
const worktreePath =
|
|
2973
|
-
const nm =
|
|
3729
|
+
const worktreePath = path21.join(runPath, workerEntry.name);
|
|
3730
|
+
const nm = path21.join(worktreePath, "node_modules");
|
|
2974
3731
|
if (!existsSync14(nm)) continue;
|
|
2975
|
-
const resolved =
|
|
3732
|
+
const resolved = path21.resolve(nm);
|
|
2976
3733
|
if (seen.has(resolved)) continue;
|
|
2977
3734
|
if (!isPathInside(resolved, opts.harnessRoot)) continue;
|
|
2978
3735
|
seen.add(resolved);
|
|
@@ -3012,17 +3769,17 @@ function scanWorktreeCandidates(opts) {
|
|
|
3012
3769
|
}
|
|
3013
3770
|
|
|
3014
3771
|
// src/cleanup-worktree-index.ts
|
|
3015
|
-
import
|
|
3772
|
+
import path22 from "node:path";
|
|
3016
3773
|
function buildWorktreeIndex() {
|
|
3017
3774
|
const index = /* @__PURE__ */ new Map();
|
|
3018
3775
|
for (const run of listRunRecords()) {
|
|
3019
3776
|
for (const name of Object.keys(run.workers || {})) {
|
|
3020
|
-
const workerPath =
|
|
3777
|
+
const workerPath = path22.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json");
|
|
3021
3778
|
const worker = readJson(workerPath, void 0);
|
|
3022
3779
|
if (!worker?.worktreePath) continue;
|
|
3023
3780
|
const status = computeWorkerStatus(worker, { base: run.base, baseCommit: run.baseCommit });
|
|
3024
|
-
index.set(
|
|
3025
|
-
worktreePath:
|
|
3781
|
+
index.set(path22.resolve(worker.worktreePath), {
|
|
3782
|
+
worktreePath: path22.resolve(worker.worktreePath),
|
|
3026
3783
|
runId: run.id,
|
|
3027
3784
|
workerName: name,
|
|
3028
3785
|
run,
|
|
@@ -3036,8 +3793,8 @@ function buildWorktreeIndex() {
|
|
|
3036
3793
|
|
|
3037
3794
|
// src/cleanup.ts
|
|
3038
3795
|
function resolveOptions(options = {}) {
|
|
3039
|
-
const harnessRoot = options.harnessRoot ?
|
|
3040
|
-
const { worktreesDir } = options.harnessRoot ? { worktreesDir:
|
|
3796
|
+
const harnessRoot = options.harnessRoot ? path23.resolve(options.harnessRoot) : resolveHarnessRoot();
|
|
3797
|
+
const { worktreesDir } = options.harnessRoot ? { worktreesDir: path23.join(harnessRoot, "worktrees") } : getHarnessPaths();
|
|
3041
3798
|
const execute = options.execute === true;
|
|
3042
3799
|
const nodeModulesAgeMs = options.nodeModulesAgeMs ?? DEFAULT_NODE_MODULES_AGE_MS;
|
|
3043
3800
|
const worktreesAgeMs = options.worktreesAgeMs ?? 0;
|
|
@@ -3081,7 +3838,7 @@ function runHarnessCleanup(options = {}) {
|
|
|
3081
3838
|
actions.push({ ...candidate, executed: false, skipped: true, skipReason: pathSkip });
|
|
3082
3839
|
continue;
|
|
3083
3840
|
}
|
|
3084
|
-
const worktreePath =
|
|
3841
|
+
const worktreePath = path23.resolve(candidate.path, "..");
|
|
3085
3842
|
const indexed = index.get(worktreePath) ?? null;
|
|
3086
3843
|
const guardReason = skipNodeModulesRemoval({
|
|
3087
3844
|
indexed,
|
|
@@ -3097,7 +3854,7 @@ function runHarnessCleanup(options = {}) {
|
|
|
3097
3854
|
actions.push(removeNodeModules(candidate, resolved.execute));
|
|
3098
3855
|
}
|
|
3099
3856
|
for (const candidate of scanWorktreeCandidates(scanOpts)) {
|
|
3100
|
-
const indexed = index.get(
|
|
3857
|
+
const indexed = index.get(path23.resolve(candidate.path)) ?? null;
|
|
3101
3858
|
const guardReason = skipWorktreeRemoval({
|
|
3102
3859
|
indexed,
|
|
3103
3860
|
includeOrphans: resolved.includeOrphans,
|
|
@@ -3166,10 +3923,14 @@ async function completeFinishedWorkers(runId, args) {
|
|
|
3166
3923
|
const outcomes = [];
|
|
3167
3924
|
for (const name of Object.keys(run.workers || {})) {
|
|
3168
3925
|
const worker = readJson(
|
|
3169
|
-
|
|
3926
|
+
path24.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
3170
3927
|
void 0
|
|
3171
3928
|
);
|
|
3172
|
-
if (!worker?.taskId) continue;
|
|
3929
|
+
if (!worker?.taskId || worker.localOnly) continue;
|
|
3930
|
+
if (hasCompletionAck(worker)) {
|
|
3931
|
+
outcomes.push({ worker: name, ok: true, taskId: worker.taskId ?? null, skipped: true });
|
|
3932
|
+
continue;
|
|
3933
|
+
}
|
|
3173
3934
|
const status = computeWorkerStatus(worker);
|
|
3174
3935
|
if (!isFinishedWorkerStatus(status)) continue;
|
|
3175
3936
|
const exitedSalvage = assessExitedWorkerSalvage({
|
|
@@ -3213,6 +3974,7 @@ async function runPipelineTick(args) {
|
|
|
3213
3974
|
configuredMaxWorkersOverride: workspacePrefs?.maxConcurrentWorkers
|
|
3214
3975
|
});
|
|
3215
3976
|
const operatorTick = await postOperatorTick(agentOsId, runId, resourceGate, args);
|
|
3977
|
+
const leaseRenewal = await renewActiveTaskLeases(runId, args);
|
|
3216
3978
|
const completedWorkers = await completeFinishedWorkers(runId, args);
|
|
3217
3979
|
const staleReconcile = reconcileStaleWorkers();
|
|
3218
3980
|
const harnessCleanup = isPipelineCleanupEnabled() ? runPipelineHarnessCleanup(runId) : void 0;
|
|
@@ -3249,6 +4011,7 @@ async function runPipelineTick(args) {
|
|
|
3249
4011
|
agentOsId,
|
|
3250
4012
|
execute,
|
|
3251
4013
|
resourceGate,
|
|
4014
|
+
leaseRenewal,
|
|
3252
4015
|
completedWorkers,
|
|
3253
4016
|
staleReconcile,
|
|
3254
4017
|
harnessCleanup,
|