@kynver-app/runtime 0.1.31 → 0.1.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -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 path23 = input.diskPath?.trim() || "/";
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(path23);
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: path23,
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 parseClaudeStream(file) {
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 tsMs = event.timestamp_ms;
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.finalResult = event.result || event.subtype || event.terminal_reason || "completed";
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 = parseClaudeStream(worker.stdoutPath);
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 && !parsed.finalResult ? tailFile(worker.stderrPath, 10).trim() || void 0 : void 0);
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: parsed.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" : parsed.finalResult ? "exited" : alive ? "running" : "exited";
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: parsed.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: resolveGlobalDefaultModel(),
1384
- provider: "claude",
1385
- rule: "default:sonnet"
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 path10 from "node:path";
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 only; row `done` is MCP/session only.",
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, commit or open a PR; exiting with only dirty files and no final result routes to salvage review, not production review.",
1468
- "Long-running commands: prefer bounded verification (targeted tests/typecheck for touched paths). If a full build is required, note it in heartbeat phase `verify` so a silent exit is not mistaken for success.",
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 path7 from "node:path";
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 path6 from "node:path";
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 = path6.join(agentRoot, "versions");
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 = path6.join(versionsRoot, entry.name);
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() || path6.join(process.env.LOCALAPPDATA || "", "cursor-agent");
1512
- const directNode = path6.join(root, "node.exe");
1513
- const directIndex = path6.join(root, "index.js");
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 = path6.join(versionDir, "node.exe");
1520
- const indexJs = path6.join(versionDir, "index.js");
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(path7.join(path7.dirname(agentBin), "index.js"));
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(path7.dirname(agentBin)) : isBundledNode ? {
1697
+ const bundled = isCursorWrapper ? resolveWindowsCursorBundled(path8.dirname(agentBin)) : isBundledNode ? {
1543
1698
  nodeExe: agentBin,
1544
- indexJs: path7.join(path7.dirname(agentBin), "index.js"),
1545
- versionDir: path7.dirname(agentBin)
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 = path7.join(process.env.LOCALAPPDATA || "", "cursor-agent", "agent.cmd");
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: path7.basename(agentBin) || "agent.cmd" } : {}
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 path9 from "node:path";
1804
+ import path10 from "node:path";
1650
1805
  import { fileURLToPath } from "node:url";
1651
1806
 
1652
1807
  // src/worker-ops.ts
1653
- import path8 from "node:path";
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
- const status = computeWorkerStatus(worker, workerStatusOptions(run));
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
- return { ok: true, httpStatus: result.status, response: result.parsed };
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(path8.join(worker.workerDir, "last-status.json"), status);
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
- path8.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
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(path8.join(runDirectory(run.id), "last-board.json"), board);
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 path9.join(fileURLToPath(new URL(".", import.meta.url)), "cli.js");
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 = path9.join(opts.workerDir, "auto-complete.log");
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 = path10.join(runDirectory(run.id), "workers", name);
2747
+ const workerDir = path11.join(runDirectory(run.id), "workers", name);
2105
2748
  mkdirSync3(workerDir, { recursive: true });
2106
- const worktreePath = path10.join(worktreesDir, run.id, name);
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 = path10.join(workerDir, "stdout.jsonl");
2112
- const stderrPath = path10.join(workerDir, "stderr.log");
2113
- const heartbeatPath = path10.join(workerDir, "heartbeat.jsonl");
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: path10.join(workerDir, "worker.json") } };
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 path11 from "node:path";
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
- path11.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
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 path13 from "node:path";
3227
+ import path15 from "node:path";
2485
3228
 
2486
3229
  // src/validate.ts
2487
- import path12 from "node:path";
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 = path12.resolve(repo);
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(path13.join(dir, "run.json"), run);
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(path13.join(runDirectory(id), "run.json"), void 0)).filter(Boolean).map((run) => ({
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 path22 from "node:path";
3283
+ import path24 from "node:path";
2541
3284
 
2542
3285
  // src/stale-reconcile.ts
2543
- import path15 from "node:path";
3286
+ import path17 from "node:path";
2544
3287
 
2545
3288
  // src/finalize.ts
2546
- import path14 from "node:path";
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
- path14.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
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 = path15.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json");
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
- outcomes.push({ runId: run.id, worker: name, action: "skipped", reason: "final result present" });
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 path16 from "node:path";
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
- path16.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
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 path21 from "node:path";
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 path18 from "node:path";
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 path17 from "node:path";
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 = path17.join(current, name);
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 = path18.resolve(targetPath);
2921
- const nm = resolved.endsWith(`${path18.sep}node_modules`) ? resolved : null;
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 = path18.relative(worktreesDir, nm);
2924
- if (rel.startsWith("..") || path18.isAbsolute(rel)) return "path_outside_harness";
2925
- const parts = rel.split(path18.sep);
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(path18.resolve(harnessRoot))) return "path_outside_harness";
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 path19 from "node:path";
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 = path19.relative(parent, child);
2944
- return rel === "" || !rel.startsWith("..") && !path19.isAbsolute(rel);
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 = path19.join(entry.worktreePath, "node_modules");
3708
+ const nm = path21.join(entry.worktreePath, "node_modules");
2952
3709
  if (!existsSync14(nm)) continue;
2953
- const resolved = path19.resolve(nm);
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 = path19.join(opts.worktreesDir, runEntry.name);
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 = path19.join(runPath, workerEntry.name);
2973
- const nm = path19.join(worktreePath, "node_modules");
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 = path19.resolve(nm);
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 path20 from "node:path";
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 = path20.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json");
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(path20.resolve(worker.worktreePath), {
3025
- worktreePath: path20.resolve(worker.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 ? path21.resolve(options.harnessRoot) : resolveHarnessRoot();
3040
- const { worktreesDir } = options.harnessRoot ? { worktreesDir: path21.join(harnessRoot, "worktrees") } : getHarnessPaths();
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 = path21.resolve(candidate.path, "..");
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(path21.resolve(candidate.path)) ?? null;
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
- path22.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
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,