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