@kynver-app/runtime 0.1.32 → 0.1.37

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
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import { mkdirSync as mkdirSync5, realpathSync } from "node:fs";
5
- import { fileURLToPath as fileURLToPath2 } from "node:url";
4
+ import { mkdirSync as mkdirSync7, realpathSync } from "node:fs";
5
+ import { fileURLToPath as fileURLToPath4 } from "node:url";
6
6
 
7
7
  // src/config.ts
8
8
  import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
@@ -362,7 +362,7 @@ async function runLogin(args) {
362
362
  }
363
363
 
364
364
  // src/dispatch.ts
365
- import path12 from "node:path";
365
+ import path16 from "node:path";
366
366
 
367
367
  // src/callback-headers.ts
368
368
  function buildHarnessCallbackHeaders(secret) {
@@ -424,12 +424,12 @@ var DEFAULT_CRITICAL_FREE_BYTES = 15 * 1024 * 1024 * 1024;
424
424
  var DEFAULT_MAX_USED_PERCENT = 80;
425
425
  var DEFAULT_HARD_MAX_USED_PERCENT = 90;
426
426
  function observeRunnerDiskGate(input = {}) {
427
- const path25 = input.diskPath?.trim() || "/";
427
+ const path32 = input.diskPath?.trim() || "/";
428
428
  const warnBelowBytes = input.diskFreeWarnBytes ?? DEFAULT_WARN_FREE_BYTES;
429
429
  const criticalBelowBytes = input.diskFreeCriticalBytes ?? DEFAULT_CRITICAL_FREE_BYTES;
430
430
  const maxUsedPercent = input.diskMaxUsedPercent ?? DEFAULT_MAX_USED_PERCENT;
431
431
  const hardMaxUsedPercent = input.diskHardMaxUsedPercent ?? DEFAULT_HARD_MAX_USED_PERCENT;
432
- const stats = statfsSync(path25);
432
+ const stats = statfsSync(path32);
433
433
  const freeBytes = Number(stats.bavail) * Number(stats.bsize);
434
434
  const totalBytes = Number(stats.blocks) * Number(stats.bsize);
435
435
  const usedPercent = totalBytes > 0 ? (totalBytes - freeBytes) / totalBytes * 100 : 100;
@@ -449,7 +449,7 @@ function observeRunnerDiskGate(input = {}) {
449
449
  }
450
450
  return {
451
451
  ok,
452
- path: path25,
452
+ path: path32,
453
453
  freeBytes,
454
454
  totalBytes,
455
455
  usedPercent,
@@ -538,6 +538,7 @@ function runDirectory(id) {
538
538
 
539
539
  // src/heartbeat.ts
540
540
  import { existsSync as existsSync5, readFileSync as readFileSync3 } from "node:fs";
541
+ var HEARTBEAT_FUTURE_SKEW_MS = 6e4;
541
542
  function isTerminalHeartbeatPhase(phase) {
542
543
  return phase === "complete";
543
544
  }
@@ -552,16 +553,31 @@ function parseHeartbeat(file) {
552
553
  lastHeartbeatAt: null,
553
554
  lastHeartbeatPhase: null,
554
555
  lastHeartbeatSummary: null,
555
- heartbeatBlocker: null
556
+ heartbeatBlocker: null,
557
+ timestampAnomalies: []
556
558
  };
557
559
  if (!existsSync5(file)) return result;
560
+ const maxFutureMs = Date.now() + HEARTBEAT_FUTURE_SKEW_MS;
561
+ const clampedTo = new Date(maxFutureMs).toISOString();
558
562
  const lines = readFileSync3(file, "utf8").split("\n").filter(Boolean);
559
563
  for (const line of lines) {
560
564
  const entry = safeJson(line);
561
565
  if (!entry || typeof entry !== "object" || Array.isArray(entry)) continue;
562
566
  const row = entry;
563
567
  result.heartbeatCount++;
564
- if (row.ts) result.lastHeartbeatAt = String(row.ts);
568
+ if (row.ts) {
569
+ const ts = String(row.ts);
570
+ const tsMs = Date.parse(ts);
571
+ if (Number.isFinite(tsMs) && tsMs > maxFutureMs) {
572
+ result.timestampAnomalies.push({
573
+ kind: "future_heartbeat_timestamp",
574
+ observedAt: ts,
575
+ clampedTo
576
+ });
577
+ } else {
578
+ result.lastHeartbeatAt = ts;
579
+ }
580
+ }
565
581
  if (row.phase !== void 0 && row.phase !== null) result.lastHeartbeatPhase = String(row.phase);
566
582
  if (row.summary !== void 0 && row.summary !== null) result.lastHeartbeatSummary = String(row.summary);
567
583
  result.heartbeatBlocker = row.blocker ? String(row.blocker) : null;
@@ -945,6 +961,112 @@ function landingAttentionReason(verdict) {
945
961
  return verdict.detail ?? verdict.reason ?? "dirty_worktree_no_pr";
946
962
  }
947
963
 
964
+ // src/landing-contract-gate.ts
965
+ function trimOrNull3(value) {
966
+ if (typeof value !== "string") return null;
967
+ const t = value.trim();
968
+ return t.length ? t : null;
969
+ }
970
+ function hasFinalResult3(value) {
971
+ if (value === void 0 || value === null) return false;
972
+ if (typeof value === "string") return value.trim().length > 0;
973
+ if (typeof value === "object") return Object.keys(value).length > 0;
974
+ return true;
975
+ }
976
+ function normalizePrUrl(url) {
977
+ const m = url.trim().match(/github\.com\/([^/]+\/[^/]+)\/(?:pull|pulls)\/(\d+)/i);
978
+ if (!m) return trimOrNull3(url);
979
+ return `https://github.com/${m[1]}/pull/${m[2]}`;
980
+ }
981
+ function parseReconciliation(finalResult) {
982
+ if (!finalResult || typeof finalResult !== "object" || Array.isArray(finalResult)) return [];
983
+ const record = finalResult;
984
+ const raw = record.targetPrReconciliation ?? record.target_pr_reconciliation;
985
+ if (!Array.isArray(raw)) return [];
986
+ const out = [];
987
+ for (const item of raw) {
988
+ if (!item || typeof item !== "object" || Array.isArray(item)) continue;
989
+ const row = item;
990
+ const prUrl = normalizePrUrl(String(row.prUrl ?? row.pr_url ?? ""));
991
+ const outcome = trimOrNull3(row.outcome);
992
+ if (!prUrl || outcome !== "merged" && outcome !== "skipped" && outcome !== "blocked") continue;
993
+ out.push({
994
+ prUrl,
995
+ outcome,
996
+ mergeCommit: trimOrNull3(row.mergeCommit ?? row.merge_commit),
997
+ reason: trimOrNull3(row.reason)
998
+ });
999
+ }
1000
+ return out;
1001
+ }
1002
+ function workerPrUrls(snapshot, finalResult) {
1003
+ const urls = [];
1004
+ const fromSnapshot = normalizePrUrl(trimOrNull3(snapshot.prUrl) ?? "");
1005
+ if (fromSnapshot) urls.push(fromSnapshot);
1006
+ if (finalResult && typeof finalResult === "object" && !Array.isArray(finalResult)) {
1007
+ const pr = normalizePrUrl(String(finalResult.prUrl ?? ""));
1008
+ if (pr) urls.push(pr);
1009
+ }
1010
+ return [...new Set(urls)];
1011
+ }
1012
+ function assessWorkerLandingContract(input) {
1013
+ const { contract, snapshot } = input;
1014
+ const finalResult = input.finalResult ?? snapshot.finalResult;
1015
+ if (!contract.landingOnly && contract.targetPrUrls.length === 0) {
1016
+ return { blocked: false };
1017
+ }
1018
+ if (!hasFinalResult3(finalResult)) return { blocked: false };
1019
+ const reconciliation = parseReconciliation(finalResult);
1020
+ const byUrl = new Map(reconciliation.map((r) => [r.prUrl, r]));
1021
+ const targetSet = new Set(
1022
+ contract.targetPrUrls.map((u) => normalizePrUrl(u) ?? u).filter(Boolean)
1023
+ );
1024
+ const workerPrs = workerPrUrls(snapshot, finalResult);
1025
+ if (contract.landingOnly) {
1026
+ for (const pr of workerPrs) {
1027
+ if (targetSet.size > 0 && !targetSet.has(pr)) {
1028
+ return {
1029
+ blocked: true,
1030
+ reason: "unrelated_implementation_pr",
1031
+ detail: `Landing-only worker attached unrelated PR ${pr}`
1032
+ };
1033
+ }
1034
+ if (targetSet.size === 0) {
1035
+ return {
1036
+ blocked: true,
1037
+ reason: "unrelated_implementation_pr",
1038
+ detail: "Landing-only worker must not open new implementation PRs"
1039
+ };
1040
+ }
1041
+ }
1042
+ }
1043
+ if (contract.targetPrUrls.length === 0) return { blocked: false };
1044
+ const missing = [];
1045
+ for (const target of contract.targetPrUrls) {
1046
+ const key = normalizePrUrl(target) ?? target;
1047
+ const entry = byUrl.get(key);
1048
+ if (!entry) {
1049
+ missing.push(key);
1050
+ continue;
1051
+ }
1052
+ if (entry.outcome !== "merged" && !entry.reason?.trim()) {
1053
+ missing.push(key);
1054
+ }
1055
+ }
1056
+ if (missing.length > 0) {
1057
+ return {
1058
+ blocked: true,
1059
+ reason: missing.every((u) => byUrl.has(u)) ? "incomplete_target_pr_landing" : "missing_target_pr_reconciliation",
1060
+ detail: `Target PR reconciliation incomplete: ${missing.join(", ")}`
1061
+ };
1062
+ }
1063
+ return { blocked: false };
1064
+ }
1065
+ function landingContractAttentionReason(verdict) {
1066
+ if (!verdict.blocked) return void 0;
1067
+ return verdict.detail ?? verdict.reason;
1068
+ }
1069
+
948
1070
  // src/status.ts
949
1071
  var NO_START_MS = 18e4;
950
1072
  var STALE_MS = 6e5;
@@ -954,11 +1076,13 @@ function computeAttention(input) {
954
1076
  return { state: "blocked", reason: input.completionBlocker };
955
1077
  }
956
1078
  if (input.finalResult) {
957
- const landing = assessWorkerLanding({
1079
+ const landingSnapshot = {
958
1080
  finalResult: input.finalResult,
959
1081
  changedFiles: input.changedFiles ?? [],
960
- gitAncestry: input.gitAncestry ?? null
961
- });
1082
+ gitAncestry: input.gitAncestry ?? null,
1083
+ prUrl: input.prUrl ?? null
1084
+ };
1085
+ const landing = assessWorkerLanding(landingSnapshot);
962
1086
  if (landing.blocked) {
963
1087
  const detail = landingAttentionReason(landing);
964
1088
  return {
@@ -966,6 +1090,20 @@ function computeAttention(input) {
966
1090
  reason: landing.reason ? `landing blocked (${landing.reason}): ${detail}` : `landing blocked: ${detail}`
967
1091
  };
968
1092
  }
1093
+ if (input.landingContract) {
1094
+ const contractVerdict = assessWorkerLandingContract({
1095
+ contract: input.landingContract,
1096
+ snapshot: landingSnapshot,
1097
+ finalResult: input.finalResult
1098
+ });
1099
+ const contractDetail = landingContractAttentionReason(contractVerdict);
1100
+ if (contractDetail) {
1101
+ return {
1102
+ state: "needs_attention",
1103
+ reason: contractVerdict.reason ? `landing contract (${contractVerdict.reason}): ${contractDetail}` : `landing contract: ${contractDetail}`
1104
+ };
1105
+ }
1106
+ }
969
1107
  return { state: "done", reason: "final result recorded" };
970
1108
  }
971
1109
  if (!input.alive) {
@@ -1003,11 +1141,18 @@ function computeAttention(input) {
1003
1141
  }
1004
1142
  return { state: "ok", reason: "recent activity" };
1005
1143
  }
1144
+ function resolveFinalResult(worker, parsedFinalResult, heartbeat) {
1145
+ if (parsedFinalResult) return parsedFinalResult;
1146
+ const ackSnapshot = worker.completionSnapshot?.finalResult;
1147
+ if (ackSnapshot !== void 0 && ackSnapshot !== null) return ackSnapshot;
1148
+ return terminalFinalResultFromHeartbeat(heartbeat);
1149
+ }
1006
1150
  function computeWorkerStatus(worker, options = {}) {
1007
1151
  const parsed = parseHarnessStream(worker.stdoutPath);
1008
1152
  const heartbeat = parseHeartbeat(worker.heartbeatPath);
1009
- const finalResult = parsed.finalResult ?? terminalFinalResultFromHeartbeat(heartbeat);
1010
- const alive = isPidAlive(worker.pid);
1153
+ const completionAcknowledged = typeof worker.completionReportedAt === "string" && worker.completionReportedAt.trim().length > 0;
1154
+ const finalResult = resolveFinalResult(worker, parsed.finalResult, heartbeat);
1155
+ const alive = completionAcknowledged ? false : isPidAlive(worker.pid);
1011
1156
  const stdoutBytes = fileSize(worker.stdoutPath);
1012
1157
  const stderrBytes = fileSize(worker.stderrPath);
1013
1158
  const heartbeatBytes = fileSize(worker.heartbeatPath);
@@ -1039,7 +1184,7 @@ function computeWorkerStatus(worker, options = {}) {
1039
1184
  gitAncestry,
1040
1185
  completionBlocker
1041
1186
  });
1042
- const workerStatusLabel = completionBlocker || attention.state === "blocked" ? "blocked" : attention.state === "done" ? "done" : finalResult ? "exited" : alive ? "running" : "exited";
1187
+ const workerStatusLabel = completionBlocker || attention.state === "blocked" ? "blocked" : completionAcknowledged || attention.state === "done" ? "done" : finalResult ? "exited" : alive ? "running" : "exited";
1043
1188
  return {
1044
1189
  runId: worker.runId,
1045
1190
  worker: worker.name,
@@ -1056,12 +1201,13 @@ function computeWorkerStatus(worker, options = {}) {
1056
1201
  firstEventAt: parsed.firstEventAt,
1057
1202
  lastEventAt: parsed.lastEventAt,
1058
1203
  lastActivityAt,
1059
- currentTool: parsed.currentTool,
1204
+ currentTool: completionAcknowledged ? null : parsed.currentTool,
1060
1205
  heartbeatCount: heartbeat.heartbeatCount,
1061
1206
  lastHeartbeatAt: heartbeat.lastHeartbeatAt,
1062
1207
  lastHeartbeatPhase: heartbeat.lastHeartbeatPhase,
1063
1208
  lastHeartbeatSummary: heartbeat.lastHeartbeatSummary,
1064
1209
  heartbeatBlocker: heartbeat.heartbeatBlocker,
1210
+ timestampAnomalies: heartbeat.timestampAnomalies,
1065
1211
  finalResult,
1066
1212
  error,
1067
1213
  changedFiles,
@@ -1201,6 +1347,16 @@ function normalize(value) {
1201
1347
  return value.toLowerCase();
1202
1348
  }
1203
1349
  var PERSONA_DEFAULT_LANE = {
1350
+ ghost: "system",
1351
+ astra: "plan_author",
1352
+ rhea: "implementer",
1353
+ mnemo: "implementer",
1354
+ sentinel: "deep_reviewer",
1355
+ pixel: "implementer",
1356
+ schema: "implementer",
1357
+ atlas: "runtime_verifier",
1358
+ bridge: "implementer",
1359
+ catalyst: "implementer",
1204
1360
  dalton: "implementer",
1205
1361
  lorentz: "report_reviewer"
1206
1362
  };
@@ -1580,7 +1736,7 @@ function hasLiveWorkerForTask(runId, taskId) {
1580
1736
 
1581
1737
  // src/supervisor.ts
1582
1738
  import { existsSync as existsSync10, mkdirSync as mkdirSync3 } from "node:fs";
1583
- import path11 from "node:path";
1739
+ import path12 from "node:path";
1584
1740
 
1585
1741
  // src/prompt.ts
1586
1742
  function buildPrompt(input) {
@@ -1602,6 +1758,7 @@ function buildPrompt(input) {
1602
1758
  const planArtifactLines = compact ? [
1603
1759
  "Plan artifacts: when authoring/revising docs/superpowers/plans/, open a GitHub PR early and iterate from that PR branch; do not leave the canonical plan only in the harness worktree."
1604
1760
  ] : [
1761
+ "Plan persistence: use AgentOS API/MCP first; on approval/auth/network/server/interruption failures run `kynver plan persist` (queues under ~/.kynver/state/plan-outbox) then `kynver plan outbox drain` when connectivity returns. Never treat /tmp-only files as persisted plans.",
1605
1762
  "PR-first plan artifacts (when authoring or revising docs/superpowers/plans/):",
1606
1763
  "- Before substantial plan drafting: create a feature branch, open a GitHub PR (draft OK), commit and push the plan file \u2014 do not leave the canonical plan only in this harness worktree.",
1607
1764
  "- Iterate review on that PR branch; link prUrl on the AgentOS task and plan progress evidence (`--evidence pr:<url>`).",
@@ -1615,6 +1772,7 @@ function buildPrompt(input) {
1615
1772
  `Progress heartbeat file: ${input.heartbeatPath}`,
1616
1773
  "After each major step, append one JSON line to the heartbeat file with fields: ts, phase, summary, changedFiles, blocker.",
1617
1774
  "Final response must include files changed, verification commands, and unresolved risks.",
1775
+ "Structured final result (recommended): record completion as JSON with summary, laneExpertise { whatChanged, why, files, prUrls, verification, risks, blockers, lessonsLearned, laneGuidance }, and targetPrReconciliation [{ prUrl, outcome: merged|skipped|blocked, mergeCommit?, reason? }] for every target PR on landing-only tasks.",
1618
1776
  "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
1777
  "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
1778
  "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.",
@@ -1801,15 +1959,15 @@ function resolveWorkerProvider(name) {
1801
1959
  // src/auto-complete.ts
1802
1960
  import { spawn as spawn3 } from "node:child_process";
1803
1961
  import { existsSync as existsSync9, openSync as openSync3, closeSync as closeSync3 } from "node:fs";
1804
- import path10 from "node:path";
1962
+ import path11 from "node:path";
1805
1963
  import { fileURLToPath } from "node:url";
1806
1964
 
1807
1965
  // src/worker-ops.ts
1808
- import path9 from "node:path";
1966
+ import path10 from "node:path";
1809
1967
 
1810
1968
  // src/pr-handoff/pr-handoff-assess.ts
1811
1969
  var REVIEW_LANE_RULE = /^(lane:)?(review|deep_review|planning|landing)(:|$)/i;
1812
- function trimOrNull3(value) {
1970
+ function trimOrNull4(value) {
1813
1971
  if (typeof value !== "string") return null;
1814
1972
  const trimmed = value.trim();
1815
1973
  return trimmed.length ? trimmed : null;
@@ -1817,7 +1975,7 @@ function trimOrNull3(value) {
1817
1975
  function committedHead(ancestry) {
1818
1976
  if (!ancestry?.checked) return null;
1819
1977
  if (ancestry.headIsAncestorOfBase !== false) return null;
1820
- return trimOrNull3(ancestry.head);
1978
+ return trimOrNull4(ancestry.head);
1821
1979
  }
1822
1980
  function extractPrUrlFromText(value) {
1823
1981
  if (value === void 0 || value === null) return null;
@@ -1825,11 +1983,11 @@ function extractPrUrlFromText(value) {
1825
1983
  const m = text.match(
1826
1984
  /https?:\/\/[^\s)>"]+\/(?:pull|pulls|merge_requests|pull-requests)\/\d+/i
1827
1985
  );
1828
- return m ? trimOrNull3(m[0]) : null;
1986
+ return m ? trimOrNull4(m[0]) : null;
1829
1987
  }
1830
1988
  function hasWorkProduct(snapshot) {
1831
1989
  if (snapshot.changedFiles.length > 0) return true;
1832
- if (trimOrNull3(snapshot.headCommit)) return true;
1990
+ if (trimOrNull4(snapshot.headCommit)) return true;
1833
1991
  if (committedHead(snapshot.gitAncestry)) return true;
1834
1992
  return false;
1835
1993
  }
@@ -1837,14 +1995,14 @@ function assessPrHandoffRequirement(input) {
1837
1995
  if (!input.dispatched) {
1838
1996
  return { required: false, reason: "not_dispatched" };
1839
1997
  }
1840
- const rule = trimOrNull3(input.routingRule) ?? "";
1998
+ const rule = trimOrNull4(input.routingRule) ?? "";
1841
1999
  if (rule && REVIEW_LANE_RULE.test(rule)) {
1842
2000
  return { required: false, reason: "review_lane" };
1843
2001
  }
1844
- if (trimOrNull3(input.patchPath) || trimOrNull3(input.artifactBundlePath)) {
2002
+ if (trimOrNull4(input.patchPath) || trimOrNull4(input.artifactBundlePath)) {
1845
2003
  return { required: false, reason: "patch_or_bundle" };
1846
2004
  }
1847
- const prUrl = trimOrNull3(input.prUrl) ?? trimOrNull3(input.snapshot.prUrl);
2005
+ const prUrl = trimOrNull4(input.prUrl) ?? trimOrNull4(input.snapshot.prUrl);
1848
2006
  if (prUrl) {
1849
2007
  return { required: false, reason: "already_has_pr" };
1850
2008
  }
@@ -1860,8 +2018,8 @@ function buildPrHandoffSnapshotFromStatus(status, extras) {
1860
2018
  worktreePath: status.worktreePath,
1861
2019
  gitAncestry: status.gitAncestry,
1862
2020
  finalResult: status.finalResult,
1863
- headCommit: trimOrNull3(extras?.headCommit) ?? committedHead(status.gitAncestry),
1864
- prUrl: trimOrNull3(extras?.prUrl) ?? null
2021
+ headCommit: trimOrNull4(extras?.headCommit) ?? committedHead(status.gitAncestry),
2022
+ prUrl: trimOrNull4(extras?.prUrl) ?? null
1865
2023
  };
1866
2024
  }
1867
2025
 
@@ -2154,6 +2312,80 @@ function persistCompletionAck(worker, runId, fields) {
2154
2312
  saveWorker(runId, worker);
2155
2313
  }
2156
2314
 
2315
+ // src/worker-lifecycle.ts
2316
+ import path9 from "node:path";
2317
+ var TASK_LEFT_RUNNING = /* @__PURE__ */ new Set([
2318
+ "awaiting_review",
2319
+ "blocked",
2320
+ "done",
2321
+ "needs_input",
2322
+ "waiting",
2323
+ "scheduled",
2324
+ "ready",
2325
+ "cancelled",
2326
+ "failed"
2327
+ ]);
2328
+ function isCompletionAcknowledged(worker) {
2329
+ return Boolean(
2330
+ typeof worker.completionReportedAt === "string" && worker.completionReportedAt.trim()
2331
+ );
2332
+ }
2333
+ function completionSnapshotFromStatus(status) {
2334
+ const summary = typeof status.lastHeartbeatSummary === "string" ? status.lastHeartbeatSummary : null;
2335
+ return {
2336
+ finalResult: status.finalResult ?? summary ?? "completed",
2337
+ summary
2338
+ };
2339
+ }
2340
+ function persistCompletionAcknowledged(worker, status, opts) {
2341
+ if (isCompletionAcknowledged(worker) && worker.status === "done" && worker.completionSnapshot != null) {
2342
+ return;
2343
+ }
2344
+ const at = (/* @__PURE__ */ new Date()).toISOString();
2345
+ const snapshot = completionSnapshotFromStatus(status);
2346
+ const source = opts?.source?.trim();
2347
+ worker.completionReportedAt = at;
2348
+ worker.completionSnapshot = snapshot;
2349
+ worker.status = "done";
2350
+ if (source) worker.completionAckSource = source;
2351
+ saveWorker(worker.runId, worker);
2352
+ }
2353
+ function taskStatusByIdFromOperatorTick(operatorTick) {
2354
+ const map = /* @__PURE__ */ new Map();
2355
+ const body = operatorTick;
2356
+ const items = body.response?.tick?.filteredItems;
2357
+ if (!Array.isArray(items)) return map;
2358
+ for (const item of items) {
2359
+ if (item.kind !== "task" || typeof item.id !== "string") continue;
2360
+ const status = typeof item.taskStatus === "string" ? item.taskStatus.trim() : "";
2361
+ if (status) map.set(item.id, status);
2362
+ }
2363
+ return map;
2364
+ }
2365
+ function syncCompletionAcknowledgedFromOperatorTick(runId, operatorTick) {
2366
+ const taskStatusById = taskStatusByIdFromOperatorTick(operatorTick);
2367
+ if (taskStatusById.size === 0) return [];
2368
+ const run = loadRun(runId);
2369
+ const synced = [];
2370
+ for (const name of Object.keys(run.workers || {})) {
2371
+ const worker = readJson(
2372
+ path9.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
2373
+ void 0
2374
+ );
2375
+ if (!worker?.taskId || isCompletionAcknowledged(worker)) continue;
2376
+ const taskStatus = taskStatusById.get(worker.taskId);
2377
+ if (!taskStatus || taskStatus === "running" || !TASK_LEFT_RUNNING.has(taskStatus)) {
2378
+ continue;
2379
+ }
2380
+ const status = computeWorkerStatus(worker, { base: run.base, baseCommit: run.baseCommit });
2381
+ persistCompletionAcknowledged(worker, status, {
2382
+ source: `board-task-${taskStatus}`
2383
+ });
2384
+ synced.push({ worker: name, taskId: worker.taskId, taskStatus });
2385
+ }
2386
+ return synced;
2387
+ }
2388
+
2157
2389
  // src/worker-ops.ts
2158
2390
  async function postCompletion(url, secret, body) {
2159
2391
  const res = await fetch(url, {
@@ -2285,6 +2517,7 @@ async function tryCompleteWorker(args) {
2285
2517
  workerInjection: {
2286
2518
  instructionPolicyFingerprint: worker.instructionPolicyFingerprint ?? null,
2287
2519
  instructionPolicyEvidence: worker.instructionPolicyEvidence ?? null,
2520
+ policyAt: worker.instructionPolicyEvidence && typeof worker.instructionPolicyEvidence === "object" && "policyAt" in worker.instructionPolicyEvidence && typeof worker.instructionPolicyEvidence.policyAt === "string" ? worker.instructionPolicyEvidence.policyAt : null,
2288
2521
  personaSlug: worker.personaSlug ?? null,
2289
2522
  personaEvidence: worker.personaEvidence ?? null
2290
2523
  }
@@ -2305,6 +2538,7 @@ async function tryCompleteWorker(args) {
2305
2538
  completionResponse: result.parsed
2306
2539
  };
2307
2540
  persistCompletionAck(worker, worker.runId, ack);
2541
+ persistCompletionAcknowledged(worker, status, { source: "harness-completion" });
2308
2542
  const prUrl = status.prUrl;
2309
2543
  return {
2310
2544
  ok: true,
@@ -2372,7 +2606,7 @@ function workerStatus(args) {
2372
2606
  const worker = loadWorker(String(args.run), String(args.name));
2373
2607
  const run = loadRun(worker.runId);
2374
2608
  const status = computeWorkerStatus(worker, workerStatusOptions(run));
2375
- writeJson(path9.join(worker.workerDir, "last-status.json"), status);
2609
+ writeJson(path10.join(worker.workerDir, "last-status.json"), status);
2376
2610
  console.log(JSON.stringify(status, null, 2));
2377
2611
  }
2378
2612
  function buildRunBoard(runId) {
@@ -2380,7 +2614,7 @@ function buildRunBoard(runId) {
2380
2614
  const names = Object.keys(run.workers || {});
2381
2615
  const workers = names.map((name) => {
2382
2616
  const worker = readJson(
2383
- path9.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
2617
+ path10.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
2384
2618
  void 0
2385
2619
  );
2386
2620
  if (!worker) {
@@ -2490,7 +2724,7 @@ function buildRunBoard(runId) {
2490
2724
  needsAttention: workers.filter((w) => w.attention && w.attention !== "ok" && w.attention !== "done").map((w) => w.worker),
2491
2725
  workers
2492
2726
  };
2493
- writeJson(path9.join(runDirectory(run.id), "last-board.json"), board);
2727
+ writeJson(path10.join(runDirectory(run.id), "last-board.json"), board);
2494
2728
  return board;
2495
2729
  }
2496
2730
  async function publishHarnessBoardSnapshot(args, source) {
@@ -2657,12 +2891,12 @@ async function autoCompleteWorkerCli(raw) {
2657
2891
  }
2658
2892
  }
2659
2893
  function resolveDefaultCliPath() {
2660
- return path10.join(fileURLToPath(new URL(".", import.meta.url)), "cli.js");
2894
+ return path11.join(fileURLToPath(new URL(".", import.meta.url)), "cli.js");
2661
2895
  }
2662
2896
  function spawnCompletionSidecar(opts) {
2663
2897
  const cliPath = opts.cliPath ?? resolveDefaultCliPath();
2664
2898
  if (!existsSync9(cliPath)) return void 0;
2665
- const logPath = path10.join(opts.workerDir, "auto-complete.log");
2899
+ const logPath = path11.join(opts.workerDir, "auto-complete.log");
2666
2900
  let logFd;
2667
2901
  try {
2668
2902
  logFd = openSync3(logPath, "a");
@@ -2744,16 +2978,16 @@ function spawnWorkerProcess(run, opts) {
2744
2978
  launchModel = preflight.model;
2745
2979
  }
2746
2980
  const { worktreesDir } = getPaths();
2747
- const workerDir = path11.join(runDirectory(run.id), "workers", name);
2981
+ const workerDir = path12.join(runDirectory(run.id), "workers", name);
2748
2982
  mkdirSync3(workerDir, { recursive: true });
2749
- const worktreePath = path11.join(worktreesDir, run.id, name);
2983
+ const worktreePath = path12.join(worktreesDir, run.id, name);
2750
2984
  const branch = opts.branch || `agent/${run.id}/${name}`;
2751
2985
  if (existsSync10(worktreePath)) throw new Error(`worktree path already exists: ${worktreePath}`);
2752
2986
  git(run.repo, ["fetch", "origin", "--prune"], { allowFailure: true });
2753
2987
  git(run.repo, ["worktree", "add", "-b", branch, worktreePath, run.baseCommit], { throwError: true });
2754
- const stdoutPath = path11.join(workerDir, "stdout.jsonl");
2755
- const stderrPath = path11.join(workerDir, "stderr.log");
2756
- const heartbeatPath = path11.join(workerDir, "heartbeat.jsonl");
2988
+ const stdoutPath = path12.join(workerDir, "stdout.jsonl");
2989
+ const stderrPath = path12.join(workerDir, "stderr.log");
2990
+ const heartbeatPath = path12.join(workerDir, "heartbeat.jsonl");
2757
2991
  const prompt = buildPrompt({
2758
2992
  task: opts.task,
2759
2993
  ownedPaths: opts.ownedPaths || [],
@@ -2814,7 +3048,7 @@ function spawnWorkerProcess(run, opts) {
2814
3048
  startedAt: (/* @__PURE__ */ new Date()).toISOString()
2815
3049
  };
2816
3050
  saveWorker(run.id, worker);
2817
- run.workers = { ...run.workers || {}, [name]: { workerDir, statusPath: path11.join(workerDir, "worker.json") } };
3051
+ run.workers = { ...run.workers || {}, [name]: { workerDir, statusPath: path12.join(workerDir, "worker.json") } };
2818
3052
  run.status = "running";
2819
3053
  saveRun(run);
2820
3054
  if (worker.agentOsId && worker.taskId) {
@@ -2908,6 +3142,579 @@ async function startWorker(args) {
2908
3142
  }
2909
3143
  }
2910
3144
 
3145
+ // src/plan-persist/body-hash.ts
3146
+ import { createHash } from "node:crypto";
3147
+ function hashPlanBody(body) {
3148
+ const normalized = body.replace(/\r\n/g, "\n").trimEnd();
3149
+ return createHash("sha256").update(normalized, "utf8").digest("hex");
3150
+ }
3151
+ function hashSummary(summary) {
3152
+ if (summary == null) return null;
3153
+ const trimmed = summary.trim();
3154
+ if (!trimmed) return null;
3155
+ return createHash("sha256").update(trimmed, "utf8").digest("hex");
3156
+ }
3157
+
3158
+ // src/plan-persist/errors.ts
3159
+ var PlanPersistError = class extends Error {
3160
+ kind;
3161
+ httpStatus;
3162
+ constructor(kind, message, httpStatus) {
3163
+ super(message);
3164
+ this.name = "PlanPersistError";
3165
+ this.kind = kind;
3166
+ this.httpStatus = httpStatus;
3167
+ }
3168
+ };
3169
+ function classifyHttpFailure(status, message) {
3170
+ if (status === 401 || status === 403) {
3171
+ return new PlanPersistError("auth", message, status);
3172
+ }
3173
+ if (status >= 500) {
3174
+ return new PlanPersistError("server", message, status);
3175
+ }
3176
+ return new PlanPersistError("permanent", message, status);
3177
+ }
3178
+ function classifyFetchFailure(err) {
3179
+ const message = err instanceof Error ? err.message : String(err);
3180
+ if (/ECONNREFUSED|ENOTFOUND|ETIMEDOUT|fetch failed|network/i.test(message)) {
3181
+ return new PlanPersistError("network", message);
3182
+ }
3183
+ return new PlanPersistError("tool_interruption", message);
3184
+ }
3185
+ function isRetryableFailure(kind) {
3186
+ return kind !== "permanent";
3187
+ }
3188
+
3189
+ // src/plan-persist/agentos-api.ts
3190
+ function authHeaders(apiKey) {
3191
+ const headers = { "Content-Type": "application/json" };
3192
+ if (apiKey) headers.Authorization = `Bearer ${apiKey}`;
3193
+ return headers;
3194
+ }
3195
+ async function parseJsonResponse(res) {
3196
+ const text = await res.text();
3197
+ try {
3198
+ return JSON.parse(text);
3199
+ } catch {
3200
+ return text;
3201
+ }
3202
+ }
3203
+ async function agentOsGetPlan(slug, planId, deps = {}) {
3204
+ const base = resolveBaseUrl(deps.baseUrl);
3205
+ const apiKey = deps.apiKey ?? loadApiKey();
3206
+ const fetchFn = deps.fetchFn ?? fetch;
3207
+ const url = `${base}/api/agent-os/${encodeURIComponent(slug)}/plans/${encodeURIComponent(planId)}`;
3208
+ try {
3209
+ const res = await fetchFn(url, { method: "GET", headers: authHeaders(apiKey) });
3210
+ const parsed = await parseJsonResponse(res);
3211
+ if (!res.ok) {
3212
+ const msg = typeof parsed === "object" && parsed && "error" in parsed ? String(parsed.error) : `GET plan failed (${res.status})`;
3213
+ throw classifyHttpFailure(res.status, msg);
3214
+ }
3215
+ return parsed;
3216
+ } catch (err) {
3217
+ if (err instanceof PlanPersistError) throw err;
3218
+ throw classifyFetchFailure(err);
3219
+ }
3220
+ }
3221
+ async function agentOsWritePlan(input, deps = {}) {
3222
+ const base = resolveBaseUrl(deps.baseUrl);
3223
+ const apiKey = deps.apiKey ?? loadApiKey();
3224
+ const fetchFn = deps.fetchFn ?? fetch;
3225
+ const slug = input.agentOsSlug;
3226
+ try {
3227
+ if (input.operation === "create") {
3228
+ const url2 = `${base}/api/agent-os/${encodeURIComponent(slug)}/plans`;
3229
+ const body2 = {
3230
+ title: input.title,
3231
+ summary: input.summary ?? null,
3232
+ slug: input.planSlug ?? null,
3233
+ sourceRefs: mergeSourceRefs(input),
3234
+ initialVersion: {
3235
+ title: input.title,
3236
+ body: input.body,
3237
+ summary: input.summary ?? null,
3238
+ changeSummary: input.changeSummary ?? null,
3239
+ author: input.author ?? null,
3240
+ sourceRefs: mergeSourceRefs(input)
3241
+ }
3242
+ };
3243
+ const res2 = await fetchFn(url2, {
3244
+ method: "POST",
3245
+ headers: authHeaders(apiKey),
3246
+ body: JSON.stringify(body2)
3247
+ });
3248
+ const parsed2 = await parseJsonResponse(res2);
3249
+ if (!res2.ok) {
3250
+ const msg = typeof parsed2 === "object" && parsed2 && "error" in parsed2 ? String(parsed2.error) : `create plan failed (${res2.status})`;
3251
+ throw classifyHttpFailure(res2.status, msg);
3252
+ }
3253
+ const row2 = parsed2;
3254
+ return {
3255
+ planId: row2.plan.id,
3256
+ versionId: row2.version.id,
3257
+ versionNumber: row2.version.versionNumber
3258
+ };
3259
+ }
3260
+ const planId = input.planId;
3261
+ if (!planId) throw new PlanPersistError("permanent", "planId is required for this operation");
3262
+ if (input.operation === "update_metadata") {
3263
+ const url2 = `${base}/api/agent-os/${encodeURIComponent(slug)}/plans/${encodeURIComponent(planId)}`;
3264
+ const body2 = {
3265
+ title: input.title,
3266
+ summary: input.summary ?? null,
3267
+ sourceRefs: mergeSourceRefs(input)
3268
+ };
3269
+ const res2 = await fetchFn(url2, {
3270
+ method: "PATCH",
3271
+ headers: authHeaders(apiKey),
3272
+ body: JSON.stringify(body2)
3273
+ });
3274
+ const parsed2 = await parseJsonResponse(res2);
3275
+ if (!res2.ok) {
3276
+ const msg = typeof parsed2 === "object" && parsed2 && "error" in parsed2 ? String(parsed2.error) : `update plan failed (${res2.status})`;
3277
+ throw classifyHttpFailure(res2.status, msg);
3278
+ }
3279
+ const row2 = parsed2;
3280
+ return {
3281
+ planId: row2.id,
3282
+ versionId: row2.currentVersionId,
3283
+ versionNumber: null
3284
+ };
3285
+ }
3286
+ const url = `${base}/api/agent-os/${encodeURIComponent(slug)}/plans/${encodeURIComponent(planId)}/versions`;
3287
+ const body = {
3288
+ title: input.title,
3289
+ body: input.body,
3290
+ summary: input.summary ?? null,
3291
+ changeSummary: input.changeSummary ?? null,
3292
+ author: input.author ?? null,
3293
+ sourceRefs: mergeSourceRefs(input),
3294
+ markCurrent: input.markCurrent !== false
3295
+ };
3296
+ const res = await fetchFn(url, {
3297
+ method: "POST",
3298
+ headers: authHeaders(apiKey),
3299
+ body: JSON.stringify(body)
3300
+ });
3301
+ const parsed = await parseJsonResponse(res);
3302
+ if (!res.ok) {
3303
+ const msg = typeof parsed === "object" && parsed && "error" in parsed ? String(parsed.error) : `add version failed (${res.status})`;
3304
+ throw classifyHttpFailure(res.status, msg);
3305
+ }
3306
+ const row = parsed;
3307
+ return {
3308
+ planId: row.version.planId,
3309
+ versionId: row.version.id,
3310
+ versionNumber: row.version.versionNumber
3311
+ };
3312
+ } catch (err) {
3313
+ if (err instanceof PlanPersistError) throw err;
3314
+ throw classifyFetchFailure(err);
3315
+ }
3316
+ }
3317
+ function mergeSourceRefs(input) {
3318
+ const refs = { ...input.sourceRefs ?? {} };
3319
+ if (input.model) refs.model = input.model;
3320
+ if (!Object.keys(refs).length) return input.sourceRefs ?? null;
3321
+ return refs;
3322
+ }
3323
+
3324
+ // src/plan-persist/idempotency.ts
3325
+ import { createHash as createHash2 } from "node:crypto";
3326
+ function buildPlanPersistIdempotencyKey(input) {
3327
+ const payload = {
3328
+ operation: input.operation,
3329
+ agentOsSlug: input.agentOsSlug,
3330
+ planId: input.planId ?? null,
3331
+ planSlug: input.planSlug ?? null,
3332
+ title: input.title.trim(),
3333
+ summaryHash: hashSummary(input.summary),
3334
+ bodyHash: hashPlanBody(input.body),
3335
+ changeSummary: input.changeSummary?.trim() ?? null,
3336
+ markCurrent: input.markCurrent ?? true
3337
+ };
3338
+ return createHash2("sha256").update(JSON.stringify(payload), "utf8").digest("hex");
3339
+ }
3340
+
3341
+ // src/plan-persist/paths.ts
3342
+ import { mkdirSync as mkdirSync4 } from "node:fs";
3343
+ import { homedir as homedir3 } from "node:os";
3344
+ import path13 from "node:path";
3345
+ function resolveKynverStateRoot() {
3346
+ const env = process.env.KYNVER_STATE_ROOT;
3347
+ if (env) return path13.resolve(env);
3348
+ return path13.join(homedir3(), ".kynver", "state");
3349
+ }
3350
+ function planOutboxDir() {
3351
+ return path13.join(resolveKynverStateRoot(), "plan-outbox");
3352
+ }
3353
+ function planOutboxArchiveDir() {
3354
+ return path13.join(resolveKynverStateRoot(), "plan-outbox-archive");
3355
+ }
3356
+ function ensurePlanOutboxDirs() {
3357
+ const outboxDir = planOutboxDir();
3358
+ const archiveDir = planOutboxArchiveDir();
3359
+ mkdirSync4(outboxDir, { recursive: true });
3360
+ mkdirSync4(archiveDir, { recursive: true });
3361
+ return { outboxDir, archiveDir };
3362
+ }
3363
+ function isTmpOnlyPath(filePath) {
3364
+ const resolved = path13.resolve(filePath);
3365
+ return resolved.startsWith("/tmp/") || resolved.startsWith(path13.join("/var", "folders"));
3366
+ }
3367
+
3368
+ // src/plan-persist/outbox-store.ts
3369
+ import {
3370
+ existsSync as existsSync12,
3371
+ readFileSync as readFileSync6,
3372
+ renameSync,
3373
+ readdirSync as readdirSync4,
3374
+ writeFileSync as writeFileSync3,
3375
+ unlinkSync
3376
+ } from "node:fs";
3377
+ import path14 from "node:path";
3378
+ import { randomUUID } from "node:crypto";
3379
+ var DEFAULT_MAX_RETRIES = 12;
3380
+ function listOutboxItems() {
3381
+ const { outboxDir } = ensurePlanOutboxDirs();
3382
+ const files = readdirSync4(outboxDir).filter((f) => f.endsWith(".json"));
3383
+ const items = [];
3384
+ for (const file of files) {
3385
+ const item = readOutboxItem(path14.join(outboxDir, file));
3386
+ if (item && item.queueStatus === "queued") items.push(item);
3387
+ }
3388
+ return items.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
3389
+ }
3390
+ function findOutboxByIdempotencyKey(key) {
3391
+ for (const item of listOutboxItems()) {
3392
+ if (item.idempotencyKey === key) return item;
3393
+ }
3394
+ return null;
3395
+ }
3396
+ function readOutboxItem(jsonPath) {
3397
+ if (!existsSync12(jsonPath)) return null;
3398
+ try {
3399
+ return JSON.parse(readFileSync6(jsonPath, "utf8"));
3400
+ } catch {
3401
+ return null;
3402
+ }
3403
+ }
3404
+ function readOutboxBody(item) {
3405
+ const { outboxDir } = ensurePlanOutboxDirs();
3406
+ const bodyFile = path14.join(outboxDir, item.bodyPath);
3407
+ return readFileSync6(bodyFile, "utf8");
3408
+ }
3409
+ function writeOutboxItem(input, opts) {
3410
+ const { outboxDir } = ensurePlanOutboxDirs();
3411
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3412
+ const id = opts.existing?.id ?? randomUUID();
3413
+ const bodyPath = opts.existing?.bodyPath ?? `${id}.body.md`;
3414
+ const jsonPath = path14.join(outboxDir, `${id}.json`);
3415
+ const bodyFile = path14.join(outboxDir, bodyPath);
3416
+ if (!opts.existing) {
3417
+ writeFileSync3(bodyFile, input.body, "utf8");
3418
+ }
3419
+ const item = {
3420
+ id,
3421
+ idempotencyKey: buildPlanPersistIdempotencyKey(input),
3422
+ operation: input.operation,
3423
+ agentOsSlug: input.agentOsSlug,
3424
+ planId: input.planId ?? opts.existing?.planId ?? null,
3425
+ planSlug: input.planSlug ?? opts.existing?.planSlug ?? null,
3426
+ title: input.title,
3427
+ summary: input.summary ?? null,
3428
+ bodyPath,
3429
+ bodyHash: hashPlanBody(input.body),
3430
+ author: input.author ?? null,
3431
+ model: input.model ?? null,
3432
+ sourceRefs: input.sourceRefs ?? null,
3433
+ changeSummary: input.changeSummary ?? null,
3434
+ markCurrent: input.markCurrent ?? true,
3435
+ createdAt: opts.existing?.createdAt ?? now,
3436
+ updatedAt: now,
3437
+ retryCount: (opts.existing?.retryCount ?? 0) + (opts.existing ? 1 : 0),
3438
+ maxRetries: input.maxRetries ?? opts.existing?.maxRetries ?? DEFAULT_MAX_RETRIES,
3439
+ lastError: opts.lastError,
3440
+ lastFailureKind: opts.lastFailureKind,
3441
+ queueStatus: "queued",
3442
+ userStatus: "queued for retry",
3443
+ readbackEvidence: null
3444
+ };
3445
+ writeFileSync3(jsonPath, `${JSON.stringify(item, null, 2)}
3446
+ `, { mode: 384 });
3447
+ return item;
3448
+ }
3449
+ function saveOutboxItem(item) {
3450
+ const { outboxDir } = ensurePlanOutboxDirs();
3451
+ const jsonPath = path14.join(outboxDir, `${item.id}.json`);
3452
+ writeFileSync3(jsonPath, `${JSON.stringify(item, null, 2)}
3453
+ `, { mode: 384 });
3454
+ }
3455
+ function archiveOutboxItem(item) {
3456
+ const { outboxDir, archiveDir } = ensurePlanOutboxDirs();
3457
+ const jsonSrc = path14.join(outboxDir, `${item.id}.json`);
3458
+ const bodySrc = path14.join(outboxDir, item.bodyPath);
3459
+ const jsonDst = path14.join(archiveDir, `${item.id}.json`);
3460
+ const bodyDst = path14.join(archiveDir, item.bodyPath);
3461
+ if (existsSync12(jsonSrc)) renameSync(jsonSrc, jsonDst);
3462
+ if (existsSync12(bodySrc)) renameSync(bodySrc, bodyDst);
3463
+ }
3464
+ function outboxItemPaths(item) {
3465
+ const { outboxDir } = ensurePlanOutboxDirs();
3466
+ return {
3467
+ jsonPath: path14.join(outboxDir, `${item.id}.json`),
3468
+ bodyPath: path14.join(outboxDir, item.bodyPath)
3469
+ };
3470
+ }
3471
+ function outboxInputFromItem(item, body) {
3472
+ return {
3473
+ operation: item.operation,
3474
+ agentOsSlug: item.agentOsSlug,
3475
+ planId: item.planId,
3476
+ planSlug: item.planSlug,
3477
+ title: item.title,
3478
+ summary: item.summary,
3479
+ body,
3480
+ changeSummary: item.changeSummary ?? void 0,
3481
+ author: item.author ?? void 0,
3482
+ model: item.model ?? void 0,
3483
+ sourceRefs: item.sourceRefs,
3484
+ markCurrent: item.markCurrent ?? true,
3485
+ maxRetries: item.maxRetries
3486
+ };
3487
+ }
3488
+
3489
+ // src/plan-persist/readback.ts
3490
+ async function verifyPlanReadback(slug, expectation, deps = {}) {
3491
+ const payload = await agentOsGetPlan(slug, expectation.planId, deps);
3492
+ const plan = payload.plan;
3493
+ const current = payload.currentVersion;
3494
+ if (plan.title.trim() !== expectation.title.trim()) {
3495
+ throw new PlanPersistError(
3496
+ "verification_failed",
3497
+ `title mismatch: expected "${expectation.title}", got "${plan.title}"`
3498
+ );
3499
+ }
3500
+ const expectedSummaryHash = hashSummary(expectation.summary);
3501
+ const actualSummaryHash = hashSummary(plan.summary);
3502
+ if (expectedSummaryHash !== actualSummaryHash) {
3503
+ throw new PlanPersistError("verification_failed", "summary mismatch on readback");
3504
+ }
3505
+ if (expectation.versionId && plan.currentVersionId !== expectation.versionId) {
3506
+ throw new PlanPersistError(
3507
+ "verification_failed",
3508
+ `currentVersionId mismatch: expected ${expectation.versionId}, got ${plan.currentVersionId}`
3509
+ );
3510
+ }
3511
+ if (expectation.versionNumber != null) {
3512
+ if (!current || current.versionNumber !== expectation.versionNumber) {
3513
+ throw new PlanPersistError(
3514
+ "verification_failed",
3515
+ `versionNumber mismatch: expected ${expectation.versionNumber}, got ${current?.versionNumber ?? "none"}`
3516
+ );
3517
+ }
3518
+ }
3519
+ const bodyForHash = current?.body ?? "";
3520
+ const actualBodyHash = hashPlanBody(bodyForHash);
3521
+ if (expectation.bodyHash && actualBodyHash !== expectation.bodyHash) {
3522
+ throw new PlanPersistError("verification_failed", "body hash mismatch on readback");
3523
+ }
3524
+ return {
3525
+ planId: plan.id,
3526
+ currentVersionId: plan.currentVersionId,
3527
+ versionNumber: current?.versionNumber ?? null,
3528
+ title: plan.title,
3529
+ summary: plan.summary,
3530
+ bodyHash: expectation.bodyHash || actualBodyHash,
3531
+ readAt: (/* @__PURE__ */ new Date()).toISOString()
3532
+ };
3533
+ }
3534
+ function buildReadbackExpectation(input, write) {
3535
+ return {
3536
+ planId: write.planId,
3537
+ title: input.title,
3538
+ summary: input.summary ?? null,
3539
+ body: input.body,
3540
+ bodyHash: hashPlanBody(input.body),
3541
+ versionId: write.versionId,
3542
+ versionNumber: write.versionNumber
3543
+ };
3544
+ }
3545
+
3546
+ // src/plan-persist/persist.ts
3547
+ var SUCCESS_STATUS = "persisted and read back";
3548
+ var QUEUED_STATUS = "queued for retry";
3549
+ var FAILED_STATUS = "failed and needs action";
3550
+ async function persistPlan(input, deps = {}) {
3551
+ if (input.bodyPathHint && isTmpOnlyPath(input.bodyPathHint)) {
3552
+ }
3553
+ const idempotencyKey = buildPlanPersistIdempotencyKey(input);
3554
+ const existingOutbox = findOutboxByIdempotencyKey(idempotencyKey);
3555
+ if (existingOutbox?.readbackEvidence) {
3556
+ return {
3557
+ userStatus: SUCCESS_STATUS,
3558
+ outboxId: existingOutbox.id,
3559
+ planId: existingOutbox.readbackEvidence.planId,
3560
+ readbackEvidence: existingOutbox.readbackEvidence,
3561
+ idempotencyKey
3562
+ };
3563
+ }
3564
+ if (input.immediateFailure) {
3565
+ return queueForRetry(input, input.immediateFailure.message, input.immediateFailure.kind, existingOutbox);
3566
+ }
3567
+ const writeFn = deps.writePlan ?? agentOsWritePlan;
3568
+ const verifyFn = deps.verifyReadback ?? verifyPlanReadback;
3569
+ try {
3570
+ const write = await writeFn(input, deps);
3571
+ const enriched = { ...input, planId: write.planId };
3572
+ const expectation = buildReadbackExpectation(enriched, write);
3573
+ const readback = await verifyFn(
3574
+ input.agentOsSlug,
3575
+ readbackExpectationForOperation(input, expectation),
3576
+ deps
3577
+ );
3578
+ if (existingOutbox) archiveOutboxItem(existingOutbox);
3579
+ return {
3580
+ userStatus: SUCCESS_STATUS,
3581
+ planId: write.planId,
3582
+ versionId: write.versionId ?? void 0,
3583
+ readbackEvidence: readback,
3584
+ idempotencyKey
3585
+ };
3586
+ } catch (err) {
3587
+ const failure = err instanceof PlanPersistError ? err : new PlanPersistError("tool_interruption", err instanceof Error ? err.message : String(err));
3588
+ if (!isRetryableFailure(failure.kind)) {
3589
+ const item = writeOutboxItem(input, {
3590
+ lastError: failure.message,
3591
+ lastFailureKind: failure.kind,
3592
+ existing: existingOutbox ?? void 0
3593
+ });
3594
+ const paths = outboxItemPaths(item);
3595
+ const failed = markOutboxFailed(item, failure.message);
3596
+ return {
3597
+ userStatus: FAILED_STATUS,
3598
+ outboxId: failed.id,
3599
+ outboxPath: paths.jsonPath,
3600
+ bodyPath: paths.bodyPath,
3601
+ lastError: failure.message,
3602
+ idempotencyKey
3603
+ };
3604
+ }
3605
+ return queueForRetry(input, failure.message, failure.kind, existingOutbox);
3606
+ }
3607
+ }
3608
+ function readbackExpectationForOperation(input, expectation) {
3609
+ if (input.operation === "update_metadata") {
3610
+ return { ...expectation, body: "", bodyHash: "" };
3611
+ }
3612
+ return expectation;
3613
+ }
3614
+ function queueForRetry(input, message, kind, existing) {
3615
+ const item = writeOutboxItem(input, {
3616
+ lastError: message,
3617
+ lastFailureKind: kind,
3618
+ existing: existing ?? void 0
3619
+ });
3620
+ const paths = outboxItemPaths(item);
3621
+ if (item.retryCount >= item.maxRetries) {
3622
+ const failed = markOutboxFailed(item, message);
3623
+ return {
3624
+ userStatus: FAILED_STATUS,
3625
+ outboxId: failed.id,
3626
+ outboxPath: paths.jsonPath,
3627
+ bodyPath: paths.bodyPath,
3628
+ lastError: message,
3629
+ idempotencyKey: item.idempotencyKey
3630
+ };
3631
+ }
3632
+ return {
3633
+ userStatus: QUEUED_STATUS,
3634
+ outboxId: item.id,
3635
+ outboxPath: paths.jsonPath,
3636
+ bodyPath: paths.bodyPath,
3637
+ lastError: message,
3638
+ idempotencyKey: item.idempotencyKey
3639
+ };
3640
+ }
3641
+ function markOutboxFailed(item, message) {
3642
+ const failed = {
3643
+ ...item,
3644
+ queueStatus: "failed",
3645
+ userStatus: FAILED_STATUS,
3646
+ lastError: message,
3647
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
3648
+ };
3649
+ saveOutboxItem(failed);
3650
+ return failed;
3651
+ }
3652
+
3653
+ // src/plan-persist/drain.ts
3654
+ import path15 from "node:path";
3655
+ async function drainPlanOutbox(opts = {}, deps = {}) {
3656
+ const items = listOutboxItems().filter(
3657
+ (item) => opts.outboxId ? item.id === opts.outboxId : true
3658
+ );
3659
+ const slice = opts.max && opts.max > 0 ? items.slice(0, opts.max) : items;
3660
+ const result = {
3661
+ processed: 0,
3662
+ succeeded: 0,
3663
+ stillQueued: 0,
3664
+ failed: 0,
3665
+ results: []
3666
+ };
3667
+ for (const item of slice) {
3668
+ result.processed += 1;
3669
+ const body = readOutboxBody(item);
3670
+ const input = outboxInputFromItem(item, body);
3671
+ const attempt = await persistPlan(input, deps);
3672
+ if (attempt.userStatus === "persisted and read back") {
3673
+ result.succeeded += 1;
3674
+ } else if (attempt.userStatus === "failed and needs action") {
3675
+ result.failed += 1;
3676
+ } else {
3677
+ result.stillQueued += 1;
3678
+ }
3679
+ result.results.push({
3680
+ outboxId: item.id,
3681
+ userStatus: attempt.userStatus,
3682
+ lastError: attempt.lastError
3683
+ });
3684
+ }
3685
+ return result;
3686
+ }
3687
+ function loadOutboxById(outboxId) {
3688
+ const jsonPath = path15.join(planOutboxDir(), `${outboxId}.json`);
3689
+ return readOutboxItem(jsonPath);
3690
+ }
3691
+
3692
+ // src/plan-persist/handoff.ts
3693
+ function formatPlanOutboxHandoffBlock(item) {
3694
+ const paths = outboxItemPaths(item);
3695
+ return [
3696
+ "## Plan persistence risk",
3697
+ "",
3698
+ `AgentOS plan write is **not** confirmed (${item.userStatus}).`,
3699
+ `- outboxId: \`${item.id}\``,
3700
+ item.planId ? `- planId: \`${item.planId}\`` : "- planId: (pending \u2014 create not yet applied)",
3701
+ `- outbox: \`${paths.jsonPath}\``,
3702
+ `- body: \`${paths.bodyPath}\``,
3703
+ "",
3704
+ "Drain when approval/connectivity returns: `kynver plan outbox drain`"
3705
+ ].join("\n");
3706
+ }
3707
+ function extractPlanOutboxFromTask(task) {
3708
+ const meta = task.metadata && typeof task.metadata === "object" ? task.metadata : null;
3709
+ const outboxId = typeof task.planPersistenceOutboxId === "string" && task.planPersistenceOutboxId || (meta && typeof meta.planPersistenceOutboxId === "string" ? meta.planPersistenceOutboxId : void 0);
3710
+ if (!outboxId) return null;
3711
+ return {
3712
+ outboxId,
3713
+ jsonPath: typeof task.planPersistenceOutboxPath === "string" ? task.planPersistenceOutboxPath : meta && typeof meta.planPersistenceOutboxPath === "string" ? meta.planPersistenceOutboxPath : void 0,
3714
+ bodyPath: typeof task.planPersistenceBodyPath === "string" ? task.planPersistenceBodyPath : meta && typeof meta.planPersistenceBodyPath === "string" ? meta.planPersistenceBodyPath : void 0
3715
+ };
3716
+ }
3717
+
2911
3718
  // src/dispatch.ts
2912
3719
  var DEFAULT_DISPATCH_LEASE_MS = 60 * 60 * 1e3;
2913
3720
  function readHarnessWorkerContext(decision) {
@@ -2937,14 +3744,29 @@ function normalizePersonaSlug(value) {
2937
3744
  return trimmed.length ? trimmed : null;
2938
3745
  }
2939
3746
  function buildDispatchTaskText(task, agentOsId) {
2940
- return [
3747
+ const lines = [
2941
3748
  `[AgentOS task ${task.id}] ${task.title}`,
2942
3749
  "",
2943
3750
  task.description ? String(task.description) : "(no description on the board task)",
2944
3751
  "",
2945
3752
  `Board linkage: agentOsId=${agentOsId}, taskId=${task.id}, attempt=${task.attempt}, executor=${task.executor}${task.executorRef ? `, executorRef=${task.executorRef}` : ""}.`,
2946
3753
  "This worker was dispatched from the AgentOS board. The harness reports your completion back to the board when you finish."
2947
- ].join("\n");
3754
+ ];
3755
+ const outboxRef = extractPlanOutboxFromTask(task);
3756
+ if (outboxRef?.outboxId) {
3757
+ const item = loadOutboxById(outboxRef.outboxId);
3758
+ if (item) {
3759
+ lines.push("", formatPlanOutboxHandoffBlock(item));
3760
+ } else {
3761
+ lines.push(
3762
+ "",
3763
+ `## Plan persistence risk`,
3764
+ "",
3765
+ `Unconfirmed AgentOS plan write (outboxId=${outboxRef.outboxId}).`
3766
+ );
3767
+ }
3768
+ }
3769
+ return lines.join("\n");
2948
3770
  }
2949
3771
  async function dispatchRun(args) {
2950
3772
  const pipeline = args.pipeline === true || args.pipeline === "true";
@@ -2963,7 +3785,7 @@ async function dispatchRun(args) {
2963
3785
  const activeHarnessWorkers = [];
2964
3786
  for (const name of Object.keys(run.workers || {})) {
2965
3787
  const worker = readJson(
2966
- path12.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
3788
+ path16.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
2967
3789
  void 0
2968
3790
  );
2969
3791
  if (!worker?.taskId || !isPidAlive(worker.pid)) continue;
@@ -3167,7 +3989,7 @@ async function dispatchRun(args) {
3167
3989
  }
3168
3990
 
3169
3991
  // src/sweep.ts
3170
- import path13 from "node:path";
3992
+ import path17 from "node:path";
3171
3993
  async function sweepRun(args) {
3172
3994
  const pipeline = args.pipeline === true || args.pipeline === "true";
3173
3995
  try {
@@ -3180,7 +4002,7 @@ async function sweepRun(args) {
3180
4002
  const releasedLocalOrphans = [];
3181
4003
  for (const name of Object.keys(run.workers || {})) {
3182
4004
  const worker = readJson(
3183
- path13.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
4005
+ path17.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
3184
4006
  void 0
3185
4007
  );
3186
4008
  if (!worker || !worker.dispatched || !worker.taskId) continue;
@@ -3223,11 +4045,11 @@ async function sweepRun(args) {
3223
4045
  }
3224
4046
 
3225
4047
  // src/worktree.ts
3226
- import { existsSync as existsSync11, mkdirSync as mkdirSync4 } from "node:fs";
3227
- import path15 from "node:path";
4048
+ import { existsSync as existsSync13, mkdirSync as mkdirSync5 } from "node:fs";
4049
+ import path19 from "node:path";
3228
4050
 
3229
4051
  // src/validate.ts
3230
- import path14 from "node:path";
4052
+ import path18 from "node:path";
3231
4053
  var RUN_ID_RE = /^[a-z0-9][a-z0-9._-]{0,127}$/i;
3232
4054
  function validateRunId(runId) {
3233
4055
  const trimmed = runId.trim();
@@ -3235,7 +4057,7 @@ function validateRunId(runId) {
3235
4057
  return trimmed;
3236
4058
  }
3237
4059
  function validateRepo(repo) {
3238
- const resolved = path14.resolve(repo);
4060
+ const resolved = path18.resolve(repo);
3239
4061
  if (resolved.includes("..")) throw new Error("repo path must not contain .. segments");
3240
4062
  return resolved;
3241
4063
  }
@@ -3246,8 +4068,8 @@ function createRun(args) {
3246
4068
  ensureGitRepo(repo);
3247
4069
  const id = args.id ? validateRunId(String(args.id)) : timestampSlug(String(args.name || "run"));
3248
4070
  const dir = runDirectory(id);
3249
- if (existsSync11(dir)) failExists(`run already exists: ${id}`);
3250
- mkdirSync4(dir, { recursive: true });
4071
+ if (existsSync13(dir)) failExists(`run already exists: ${id}`);
4072
+ mkdirSync5(dir, { recursive: true });
3251
4073
  const base = String(args.base || "origin/main");
3252
4074
  const baseCommit = git(repo, ["rev-parse", base]).trim();
3253
4075
  const run = {
@@ -3260,12 +4082,12 @@ function createRun(args) {
3260
4082
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
3261
4083
  workers: {}
3262
4084
  };
3263
- writeJson(path15.join(dir, "run.json"), run);
4085
+ writeJson(path19.join(dir, "run.json"), run);
3264
4086
  console.log(JSON.stringify({ runId: id, runDir: dir, repo, base, baseCommit }, null, 2));
3265
4087
  }
3266
4088
  function listRuns() {
3267
4089
  const { runsDir } = getPaths();
3268
- const rows = listRunIds(runsDir).map((id) => readJson(path15.join(runDirectory(id), "run.json"), void 0)).filter(Boolean).map((run) => ({
4090
+ const rows = listRunIds(runsDir).map((id) => readJson(path19.join(runDirectory(id), "run.json"), void 0)).filter(Boolean).map((run) => ({
3269
4091
  id: run.id,
3270
4092
  name: run.name,
3271
4093
  status: run.status,
@@ -3280,13 +4102,64 @@ function failExists(message) {
3280
4102
  }
3281
4103
 
3282
4104
  // src/pipeline-tick.ts
3283
- import path24 from "node:path";
4105
+ import path28 from "node:path";
4106
+
4107
+ // src/pipeline-dispatch.ts
4108
+ var RESERVED_REVIEW_STARTS = 1;
4109
+ function countDispatchStarts(result) {
4110
+ if (!result || typeof result !== "object") return 0;
4111
+ const startedCount = result.startedCount;
4112
+ if (typeof startedCount === "number") return startedCount;
4113
+ const outcomes = result.outcomes;
4114
+ if (!Array.isArray(outcomes)) return 0;
4115
+ return outcomes.filter((o) => o.started).length;
4116
+ }
4117
+ function stripCliMaxStarts(args) {
4118
+ const { maxStarts: _maxStarts, ...rest } = args;
4119
+ return rest;
4120
+ }
4121
+ async function runPipelineDispatch(args, slots) {
4122
+ if (slots <= 0) {
4123
+ return { ok: true, skipped: true, reason: "no slots", maxStarts: 0, startedCount: 0 };
4124
+ }
4125
+ const base = stripCliMaxStarts(args);
4126
+ const reviewBudget = Math.min(slots, RESERVED_REVIEW_STARTS);
4127
+ const workBudget = Math.max(0, slots - reviewBudget);
4128
+ const review = await dispatchRun({
4129
+ ...base,
4130
+ execute: true,
4131
+ pipeline: true,
4132
+ lane: "review",
4133
+ maxStarts: String(reviewBudget)
4134
+ });
4135
+ const reviewStarted = countDispatchStarts(review);
4136
+ const workSlots = workBudget + (reviewBudget - reviewStarted);
4137
+ if (workSlots <= 0) {
4138
+ return {
4139
+ ...typeof review === "object" && review !== null ? review : {},
4140
+ passes: { review },
4141
+ startedCount: reviewStarted
4142
+ };
4143
+ }
4144
+ const work = await dispatchRun({
4145
+ ...base,
4146
+ execute: true,
4147
+ pipeline: true,
4148
+ maxStarts: String(workSlots)
4149
+ });
4150
+ const workStarted = countDispatchStarts(work);
4151
+ return {
4152
+ passes: { review, work },
4153
+ startedCount: reviewStarted + workStarted,
4154
+ ok: true
4155
+ };
4156
+ }
3284
4157
 
3285
4158
  // src/stale-reconcile.ts
3286
- import path17 from "node:path";
4159
+ import path21 from "node:path";
3287
4160
 
3288
4161
  // src/finalize.ts
3289
- import path16 from "node:path";
4162
+ import path20 from "node:path";
3290
4163
  var ACTIVE_RUN_STATUSES = /* @__PURE__ */ new Set(["running", "dispatching", "pending", "queued"]);
3291
4164
  function terminalStatusFor(run) {
3292
4165
  const names = Object.keys(run.workers || {});
@@ -3297,7 +4170,7 @@ function terminalStatusFor(run) {
3297
4170
  let anyLandingBlocked = false;
3298
4171
  for (const name of names) {
3299
4172
  const worker = readJson(
3300
- path16.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
4173
+ path20.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
3301
4174
  void 0
3302
4175
  );
3303
4176
  if (!worker) continue;
@@ -3349,7 +4222,7 @@ function reconcileStaleWorkers() {
3349
4222
  const now = Date.now();
3350
4223
  for (const run of listRunRecords()) {
3351
4224
  for (const name of Object.keys(run.workers || {})) {
3352
- const workerPath = path17.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json");
4225
+ const workerPath = path21.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json");
3353
4226
  const worker = readJson(workerPath, void 0);
3354
4227
  if (!worker || worker.status !== "running") {
3355
4228
  outcomes.push({
@@ -3425,7 +4298,7 @@ function reconcileStaleWorkers() {
3425
4298
  }
3426
4299
 
3427
4300
  // src/plan-progress-daemon-sync.ts
3428
- import path18 from "node:path";
4301
+ import path22 from "node:path";
3429
4302
 
3430
4303
  // src/plan-progress-sync.ts
3431
4304
  async function syncPlanProgress(args) {
@@ -3449,7 +4322,7 @@ async function syncActiveWorkerPlanProgress(runId, args) {
3449
4322
  const outcomes = [];
3450
4323
  for (const name of Object.keys(run.workers || {})) {
3451
4324
  const worker = readJson(
3452
- path18.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
4325
+ path22.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
3453
4326
  void 0
3454
4327
  );
3455
4328
  if (!worker?.dispatched || !worker.taskId) continue;
@@ -3498,7 +4371,7 @@ async function fetchWorkspaceRuntimePreferences(agentOsId, args) {
3498
4371
  }
3499
4372
 
3500
4373
  // src/cleanup.ts
3501
- import path23 from "node:path";
4374
+ import path27 from "node:path";
3502
4375
 
3503
4376
  // src/cleanup-types.ts
3504
4377
  var DEFAULT_NODE_MODULES_AGE_MS = 6 * 60 * 60 * 1e3;
@@ -3569,14 +4442,14 @@ function skipNodeModulesRemoval(input) {
3569
4442
  }
3570
4443
 
3571
4444
  // src/cleanup-execute.ts
3572
- import { existsSync as existsSync13, rmSync } from "node:fs";
3573
- import path20 from "node:path";
4445
+ import { existsSync as existsSync15, rmSync } from "node:fs";
4446
+ import path24 from "node:path";
3574
4447
 
3575
4448
  // src/cleanup-dir-size.ts
3576
- import { existsSync as existsSync12, readdirSync as readdirSync4, statSync as statSync2 } from "node:fs";
3577
- import path19 from "node:path";
4449
+ import { existsSync as existsSync14, readdirSync as readdirSync5, statSync as statSync2 } from "node:fs";
4450
+ import path23 from "node:path";
3578
4451
  function directorySizeBytes(root, maxEntries = 5e4) {
3579
- if (!existsSync12(root)) return 0;
4452
+ if (!existsSync14(root)) return 0;
3580
4453
  let total = 0;
3581
4454
  let seen = 0;
3582
4455
  const stack = [root];
@@ -3584,13 +4457,13 @@ function directorySizeBytes(root, maxEntries = 5e4) {
3584
4457
  const current = stack.pop();
3585
4458
  let entries;
3586
4459
  try {
3587
- entries = readdirSync4(current);
4460
+ entries = readdirSync5(current);
3588
4461
  } catch {
3589
4462
  continue;
3590
4463
  }
3591
4464
  for (const name of entries) {
3592
4465
  if (seen++ > maxEntries) return null;
3593
- const full = path19.join(current, name);
4466
+ const full = path23.join(current, name);
3594
4467
  let st;
3595
4468
  try {
3596
4469
  st = statSync2(full);
@@ -3606,7 +4479,7 @@ function directorySizeBytes(root, maxEntries = 5e4) {
3606
4479
 
3607
4480
  // src/cleanup-execute.ts
3608
4481
  function removeNodeModules(candidate, execute) {
3609
- if (!existsSync13(candidate.path)) {
4482
+ if (!existsSync15(candidate.path)) {
3610
4483
  return {
3611
4484
  ...candidate,
3612
4485
  executed: false,
@@ -3637,7 +4510,7 @@ function removeNodeModules(candidate, execute) {
3637
4510
  }
3638
4511
  }
3639
4512
  function removeWorktree(candidate, execute) {
3640
- if (!existsSync13(candidate.path)) {
4513
+ if (!existsSync15(candidate.path)) {
3641
4514
  return {
3642
4515
  ...candidate,
3643
4516
  executed: false,
@@ -3654,7 +4527,7 @@ function removeWorktree(candidate, execute) {
3654
4527
  if (repo) {
3655
4528
  git(repo, ["worktree", "remove", "--force", candidate.path], { allowFailure: true });
3656
4529
  }
3657
- if (existsSync13(candidate.path)) {
4530
+ if (existsSync15(candidate.path)) {
3658
4531
  rmSync(candidate.path, { recursive: true, force: true });
3659
4532
  }
3660
4533
  return {
@@ -3674,20 +4547,20 @@ function removeWorktree(candidate, execute) {
3674
4547
  }
3675
4548
  }
3676
4549
  function isHarnessNodeModulesPath(targetPath, harnessRoot, worktreesDir) {
3677
- const resolved = path20.resolve(targetPath);
3678
- const nm = resolved.endsWith(`${path20.sep}node_modules`) ? resolved : null;
4550
+ const resolved = path24.resolve(targetPath);
4551
+ const nm = resolved.endsWith(`${path24.sep}node_modules`) ? resolved : null;
3679
4552
  if (!nm) return "path_outside_harness";
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);
4553
+ const rel = path24.relative(worktreesDir, nm);
4554
+ if (rel.startsWith("..") || path24.isAbsolute(rel)) return "path_outside_harness";
4555
+ const parts = rel.split(path24.sep);
3683
4556
  if (parts.length < 3 || parts[parts.length - 1] !== "node_modules") return "path_outside_harness";
3684
- if (!resolved.startsWith(path20.resolve(harnessRoot))) return "path_outside_harness";
4557
+ if (!resolved.startsWith(path24.resolve(harnessRoot))) return "path_outside_harness";
3685
4558
  return null;
3686
4559
  }
3687
4560
 
3688
4561
  // src/cleanup-scan.ts
3689
- import { existsSync as existsSync14, readdirSync as readdirSync5, statSync as statSync3 } from "node:fs";
3690
- import path21 from "node:path";
4562
+ import { existsSync as existsSync16, readdirSync as readdirSync6, statSync as statSync3 } from "node:fs";
4563
+ import path25 from "node:path";
3691
4564
  function pathAgeMs(target, now) {
3692
4565
  try {
3693
4566
  const mtime = statSync3(target).mtimeMs;
@@ -3697,17 +4570,17 @@ function pathAgeMs(target, now) {
3697
4570
  }
3698
4571
  }
3699
4572
  function isPathInside(child, parent) {
3700
- const rel = path21.relative(parent, child);
3701
- return rel === "" || !rel.startsWith("..") && !path21.isAbsolute(rel);
4573
+ const rel = path25.relative(parent, child);
4574
+ return rel === "" || !rel.startsWith("..") && !path25.isAbsolute(rel);
3702
4575
  }
3703
4576
  function scanNodeModulesCandidates(opts) {
3704
4577
  const candidates = [];
3705
4578
  const seen = /* @__PURE__ */ new Set();
3706
4579
  for (const entry of opts.index.values()) {
3707
4580
  if (opts.runIdFilter && entry.runId !== opts.runIdFilter) continue;
3708
- const nm = path21.join(entry.worktreePath, "node_modules");
3709
- if (!existsSync14(nm)) continue;
3710
- const resolved = path21.resolve(nm);
4581
+ const nm = path25.join(entry.worktreePath, "node_modules");
4582
+ if (!existsSync16(nm)) continue;
4583
+ const resolved = path25.resolve(nm);
3711
4584
  if (seen.has(resolved)) continue;
3712
4585
  seen.add(resolved);
3713
4586
  candidates.push({
@@ -3720,16 +4593,16 @@ function scanNodeModulesCandidates(opts) {
3720
4593
  ageMs: pathAgeMs(resolved, opts.now)
3721
4594
  });
3722
4595
  }
3723
- if (!opts.includeOrphans || !existsSync14(opts.worktreesDir)) return candidates;
3724
- for (const runEntry of readdirSync5(opts.worktreesDir, { withFileTypes: true })) {
4596
+ if (!opts.includeOrphans || !existsSync16(opts.worktreesDir)) return candidates;
4597
+ for (const runEntry of readdirSync6(opts.worktreesDir, { withFileTypes: true })) {
3725
4598
  if (!runEntry.isDirectory()) continue;
3726
- const runPath = path21.join(opts.worktreesDir, runEntry.name);
3727
- for (const workerEntry of readdirSync5(runPath, { withFileTypes: true })) {
4599
+ const runPath = path25.join(opts.worktreesDir, runEntry.name);
4600
+ for (const workerEntry of readdirSync6(runPath, { withFileTypes: true })) {
3728
4601
  if (!workerEntry.isDirectory()) continue;
3729
- const worktreePath = path21.join(runPath, workerEntry.name);
3730
- const nm = path21.join(worktreePath, "node_modules");
3731
- if (!existsSync14(nm)) continue;
3732
- const resolved = path21.resolve(nm);
4602
+ const worktreePath = path25.join(runPath, workerEntry.name);
4603
+ const nm = path25.join(worktreePath, "node_modules");
4604
+ if (!existsSync16(nm)) continue;
4605
+ const resolved = path25.resolve(nm);
3733
4606
  if (seen.has(resolved)) continue;
3734
4607
  if (!isPathInside(resolved, opts.harnessRoot)) continue;
3735
4608
  seen.add(resolved);
@@ -3752,7 +4625,7 @@ function scanWorktreeCandidates(opts) {
3752
4625
  for (const entry of opts.index.values()) {
3753
4626
  if (opts.runIdFilter && entry.runId !== opts.runIdFilter) continue;
3754
4627
  const resolved = entry.worktreePath;
3755
- if (!existsSync14(resolved)) continue;
4628
+ if (!existsSync16(resolved)) continue;
3756
4629
  if (seen.has(resolved)) continue;
3757
4630
  seen.add(resolved);
3758
4631
  candidates.push({
@@ -3769,17 +4642,17 @@ function scanWorktreeCandidates(opts) {
3769
4642
  }
3770
4643
 
3771
4644
  // src/cleanup-worktree-index.ts
3772
- import path22 from "node:path";
4645
+ import path26 from "node:path";
3773
4646
  function buildWorktreeIndex() {
3774
4647
  const index = /* @__PURE__ */ new Map();
3775
4648
  for (const run of listRunRecords()) {
3776
4649
  for (const name of Object.keys(run.workers || {})) {
3777
- const workerPath = path22.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json");
4650
+ const workerPath = path26.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json");
3778
4651
  const worker = readJson(workerPath, void 0);
3779
4652
  if (!worker?.worktreePath) continue;
3780
4653
  const status = computeWorkerStatus(worker, { base: run.base, baseCommit: run.baseCommit });
3781
- index.set(path22.resolve(worker.worktreePath), {
3782
- worktreePath: path22.resolve(worker.worktreePath),
4654
+ index.set(path26.resolve(worker.worktreePath), {
4655
+ worktreePath: path26.resolve(worker.worktreePath),
3783
4656
  runId: run.id,
3784
4657
  workerName: name,
3785
4658
  run,
@@ -3793,8 +4666,8 @@ function buildWorktreeIndex() {
3793
4666
 
3794
4667
  // src/cleanup.ts
3795
4668
  function resolveOptions(options = {}) {
3796
- const harnessRoot = options.harnessRoot ? path23.resolve(options.harnessRoot) : resolveHarnessRoot();
3797
- const { worktreesDir } = options.harnessRoot ? { worktreesDir: path23.join(harnessRoot, "worktrees") } : getHarnessPaths();
4669
+ const harnessRoot = options.harnessRoot ? path27.resolve(options.harnessRoot) : resolveHarnessRoot();
4670
+ const { worktreesDir } = options.harnessRoot ? { worktreesDir: path27.join(harnessRoot, "worktrees") } : getHarnessPaths();
3798
4671
  const execute = options.execute === true;
3799
4672
  const nodeModulesAgeMs = options.nodeModulesAgeMs ?? DEFAULT_NODE_MODULES_AGE_MS;
3800
4673
  const worktreesAgeMs = options.worktreesAgeMs ?? 0;
@@ -3838,7 +4711,7 @@ function runHarnessCleanup(options = {}) {
3838
4711
  actions.push({ ...candidate, executed: false, skipped: true, skipReason: pathSkip });
3839
4712
  continue;
3840
4713
  }
3841
- const worktreePath = path23.resolve(candidate.path, "..");
4714
+ const worktreePath = path27.resolve(candidate.path, "..");
3842
4715
  const indexed = index.get(worktreePath) ?? null;
3843
4716
  const guardReason = skipNodeModulesRemoval({
3844
4717
  indexed,
@@ -3854,7 +4727,7 @@ function runHarnessCleanup(options = {}) {
3854
4727
  actions.push(removeNodeModules(candidate, resolved.execute));
3855
4728
  }
3856
4729
  for (const candidate of scanWorktreeCandidates(scanOpts)) {
3857
- const indexed = index.get(path23.resolve(candidate.path)) ?? null;
4730
+ const indexed = index.get(path27.resolve(candidate.path)) ?? null;
3858
4731
  const guardReason = skipWorktreeRemoval({
3859
4732
  indexed,
3860
4733
  includeOrphans: resolved.includeOrphans,
@@ -3923,7 +4796,7 @@ async function completeFinishedWorkers(runId, args) {
3923
4796
  const outcomes = [];
3924
4797
  for (const name of Object.keys(run.workers || {})) {
3925
4798
  const worker = readJson(
3926
- path24.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
4799
+ path28.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
3927
4800
  void 0
3928
4801
  );
3929
4802
  if (!worker?.taskId || worker.localOnly) continue;
@@ -3974,6 +4847,7 @@ async function runPipelineTick(args) {
3974
4847
  configuredMaxWorkersOverride: workspacePrefs?.maxConcurrentWorkers
3975
4848
  });
3976
4849
  const operatorTick = await postOperatorTick(agentOsId, runId, resourceGate, args);
4850
+ const completionAckSync = syncCompletionAcknowledgedFromOperatorTick(runId, operatorTick);
3977
4851
  const leaseRenewal = await renewActiveTaskLeases(runId, args);
3978
4852
  const completedWorkers = await completeFinishedWorkers(runId, args);
3979
4853
  const staleReconcile = reconcileStaleWorkers();
@@ -3988,14 +4862,14 @@ async function runPipelineTick(args) {
3988
4862
  const sweep = await sweepRun({ run: runId, agentOsId, pipeline: true, ...args });
3989
4863
  let dispatch = null;
3990
4864
  if (execute && maxStarts > 0) {
3991
- dispatch = await dispatchRun({
3992
- run: runId,
3993
- agentOsId,
3994
- execute: true,
3995
- maxStarts: String(maxStarts),
3996
- pipeline: true,
3997
- ...args
3998
- });
4865
+ dispatch = await runPipelineDispatch(
4866
+ {
4867
+ ...args,
4868
+ run: runId,
4869
+ agentOsId
4870
+ },
4871
+ maxStarts
4872
+ );
3999
4873
  } else {
4000
4874
  dispatch = {
4001
4875
  ok: true,
@@ -4016,6 +4890,7 @@ async function runPipelineTick(args) {
4016
4890
  staleReconcile,
4017
4891
  harnessCleanup,
4018
4892
  planProgressSync,
4893
+ completionAckSync,
4019
4894
  operatorTick,
4020
4895
  sweep,
4021
4896
  dispatch,
@@ -4087,6 +4962,8 @@ async function emitPlanProgress(args) {
4087
4962
  const url = `${base}/api/agent-os/by-id/${encodeURIComponent(agentOsId)}/plans/${encodeURIComponent(planId)}/progress-events`;
4088
4963
  const cfg = loadUserConfig();
4089
4964
  const provider = cfg.workerProvider ? `provider:${cfg.workerProvider}` : void 0;
4965
+ const explicitProposed = args.proposed === true || args.proposed === "true" ? true : args.proposed === false || args.proposed === "false" ? false : void 0;
4966
+ const proposed = explicitProposed ?? (status !== "done" && (roleLane === "implementer" || roleLane === "repair_implementer"));
4090
4967
  const body = {
4091
4968
  rowKey: args.row ? String(args.row) : void 0,
4092
4969
  rowId: args.rowId ? String(args.rowId) : void 0,
@@ -4097,9 +4974,9 @@ async function emitPlanProgress(args) {
4097
4974
  note: args.note ? String(args.note) : void 0,
4098
4975
  remainingWork: args.remaining ? String(args.remaining) : void 0,
4099
4976
  evidence: evidence.length ? evidence : void 0,
4100
- proposed: args.proposed === true || args.proposed === "true",
4101
4977
  executorRef: args.executorRef ? String(args.executorRef) : provider
4102
4978
  };
4979
+ if (proposed !== void 0) body.proposed = proposed;
4103
4980
  const res = await fetch(url, {
4104
4981
  method: "POST",
4105
4982
  headers: buildHarnessCallbackHeaders(secret),
@@ -4153,6 +5030,86 @@ async function verifyPlan(args) {
4153
5030
  console.log(JSON.stringify(parsed, null, 2));
4154
5031
  }
4155
5032
 
5033
+ // src/plan-persist-cli.ts
5034
+ import { readFileSync as readFileSync7 } from "node:fs";
5035
+ var OPERATIONS = ["create", "add_version", "update_metadata"];
5036
+ var FAILURE_KINDS = [
5037
+ "approval_guard",
5038
+ "auth",
5039
+ "network",
5040
+ "server",
5041
+ "tool_interruption"
5042
+ ];
5043
+ function readBodyArg(args) {
5044
+ const bodyFile = args.bodyFile ? String(args.bodyFile) : void 0;
5045
+ if (bodyFile) {
5046
+ return { body: readFileSync7(bodyFile, "utf8"), bodyPathHint: bodyFile };
5047
+ }
5048
+ const inline = args.body ? String(args.body) : void 0;
5049
+ if (inline) return { body: inline };
5050
+ throw new Error("requires --body-file PATH or --body TEXT");
5051
+ }
5052
+ async function runPlanPersist(args) {
5053
+ const operationRaw = required(args.operation ? String(args.operation) : void 0, "operation");
5054
+ if (!OPERATIONS.includes(operationRaw)) {
5055
+ throw new Error(`invalid --operation ${operationRaw}`);
5056
+ }
5057
+ const operation = operationRaw;
5058
+ const cfg = loadUserConfig();
5059
+ const agentOsSlug = required(
5060
+ args.slug ? String(args.slug) : cfg.agentOsSlug,
5061
+ "slug (or agentOsSlug in ~/.kynver/config.json)"
5062
+ );
5063
+ const title = required(args.title ? String(args.title) : void 0, "title");
5064
+ const { body, bodyPathHint } = readBodyArg(args);
5065
+ if (bodyPathHint && isTmpOnlyPath(bodyPathHint)) {
5066
+ console.warn(
5067
+ JSON.stringify({
5068
+ warning: "/tmp-only body path is not durable; AgentOS persistence requires outbox or successful API write",
5069
+ bodyPathHint
5070
+ })
5071
+ );
5072
+ }
5073
+ const input = {
5074
+ operation,
5075
+ agentOsSlug,
5076
+ title,
5077
+ body,
5078
+ bodyPathHint,
5079
+ summary: args.summary ? String(args.summary) : void 0,
5080
+ planId: args.plan ? String(args.plan) : void 0,
5081
+ planSlug: args.planSlug ? String(args.planSlug) : void 0,
5082
+ changeSummary: args.changeSummary ? String(args.changeSummary) : void 0,
5083
+ author: args.author ? String(args.author) : void 0,
5084
+ model: args.model ? String(args.model) : void 0,
5085
+ maxRetries: args.maxRetries ? Number(args.maxRetries) : void 0,
5086
+ immediateFailure: parseImmediateFailure(args)
5087
+ };
5088
+ const result = await persistPlan(input);
5089
+ console.log(JSON.stringify(result, null, 2));
5090
+ if (result.userStatus === "failed and needs action") process.exit(1);
5091
+ }
5092
+ function parseImmediateFailure(args) {
5093
+ const kind = args.failureKind ? String(args.failureKind) : void 0;
5094
+ if (!kind) return void 0;
5095
+ if (!FAILURE_KINDS.includes(kind)) {
5096
+ throw new Error(`invalid --failure-kind ${kind}`);
5097
+ }
5098
+ const message = args.failureMessage ? String(args.failureMessage) : `immediate failure (${kind})`;
5099
+ return { kind, message };
5100
+ }
5101
+ async function runPlanOutboxList() {
5102
+ const items = listOutboxItems();
5103
+ console.log(JSON.stringify({ count: items.length, items }, null, 2));
5104
+ }
5105
+ async function runPlanOutboxDrain(args) {
5106
+ const max = args.max ? Number(args.max) : void 0;
5107
+ const outboxId = args.id ? String(args.id) : void 0;
5108
+ const result = await drainPlanOutbox({ max, outboxId });
5109
+ console.log(JSON.stringify(result, null, 2));
5110
+ if (result.failed > 0) process.exit(1);
5111
+ }
5112
+
4156
5113
  // src/cleanup-cli.ts
4157
5114
  function runCleanupCli(args) {
4158
5115
  const execute = args.execute === true || args.execute === "true";
@@ -4173,6 +5130,555 @@ function runCleanupCli(args) {
4173
5130
  }
4174
5131
  }
4175
5132
 
5133
+ // src/monitor/monitor.service.ts
5134
+ import path30 from "node:path";
5135
+
5136
+ // src/monitor/monitor.classify.ts
5137
+ function expectedLeaseOwner(runId) {
5138
+ return `kynver-harness:${runId}`;
5139
+ }
5140
+ function classifyWorkerHealth(input) {
5141
+ const { worker, status, taskLease } = input;
5142
+ const leaseOwner = taskLease?.leaseOwner ?? null;
5143
+ const expectedOwner = expectedLeaseOwner(worker.runId);
5144
+ if (worker.dispatched && taskLease) {
5145
+ if (taskLease.status === "running" && leaseOwner && leaseOwner !== expectedOwner) {
5146
+ return {
5147
+ health: "orphaned",
5148
+ reason: `task lease held by ${leaseOwner}, expected ${expectedOwner}`
5149
+ };
5150
+ }
5151
+ if (taskLease.status === "running" && !status.alive && !status.finalResult) {
5152
+ return {
5153
+ health: "orphaned",
5154
+ reason: "board task running but worker process is not alive"
5155
+ };
5156
+ }
5157
+ }
5158
+ if (worker.status === "running" && !status.alive && !status.finalResult) {
5159
+ return {
5160
+ health: "orphaned",
5161
+ reason: "worker.json still running but process is dead"
5162
+ };
5163
+ }
5164
+ if (status.attention.state === "stale") {
5165
+ return { health: "stale", reason: status.attention.reason };
5166
+ }
5167
+ const hbMs = status.lastHeartbeatAt ? Date.parse(status.lastHeartbeatAt) : NaN;
5168
+ if (status.alive && Number.isFinite(hbMs) && Date.now() - hbMs > STALE_MS) {
5169
+ return {
5170
+ health: "stale",
5171
+ reason: `heartbeat older than ${Math.floor(STALE_MS / 1e3)}s`
5172
+ };
5173
+ }
5174
+ if (status.alive && worker.pid && !isPidAlive(worker.pid)) {
5175
+ return { health: "orphaned", reason: "pid recorded but process is not alive" };
5176
+ }
5177
+ if (taskLease?.status === "running" && !status.alive && status.finalResult) {
5178
+ return {
5179
+ health: "healthy",
5180
+ reason: "finished worker awaiting completion replay"
5181
+ };
5182
+ }
5183
+ return {
5184
+ health: "healthy",
5185
+ reason: status.attention.reason || "worker within expected lifecycle bounds"
5186
+ };
5187
+ }
5188
+
5189
+ // src/monitor/monitor.store.ts
5190
+ import { existsSync as existsSync17, mkdirSync as mkdirSync6, readdirSync as readdirSync7, unlinkSync as unlinkSync2 } from "node:fs";
5191
+ import path29 from "node:path";
5192
+ function monitorsDir() {
5193
+ const { harnessRoot } = getHarnessPaths();
5194
+ const dir = path29.join(harnessRoot, "monitors");
5195
+ mkdirSync6(dir, { recursive: true });
5196
+ return dir;
5197
+ }
5198
+ function monitorIdFor(runId, workerName) {
5199
+ return workerName ? `${safeSlug(runId)}--${safeSlug(workerName)}` : safeSlug(runId);
5200
+ }
5201
+ function monitorPath(monitorId) {
5202
+ return path29.join(monitorsDir(), `${monitorId}.json`);
5203
+ }
5204
+ function loadMonitorSession(monitorId) {
5205
+ return readJson(monitorPath(monitorId), void 0);
5206
+ }
5207
+ function saveMonitorSession(session) {
5208
+ writeJson(monitorPath(session.monitorId), session);
5209
+ }
5210
+ function deleteMonitorSession(monitorId) {
5211
+ const file = monitorPath(monitorId);
5212
+ if (!existsSync17(file)) return false;
5213
+ unlinkSync2(file);
5214
+ return true;
5215
+ }
5216
+ function listMonitorSessions() {
5217
+ const dir = monitorsDir();
5218
+ if (!existsSync17(dir)) return [];
5219
+ const entries = [];
5220
+ for (const name of readdirSync7(dir)) {
5221
+ if (!name.endsWith(".json")) continue;
5222
+ const session = readJson(
5223
+ path29.join(dir, name),
5224
+ void 0
5225
+ );
5226
+ if (!session?.monitorId) continue;
5227
+ entries.push({
5228
+ monitorId: session.monitorId,
5229
+ runId: session.runId,
5230
+ workerName: session.workerName,
5231
+ agentOsId: session.agentOsId,
5232
+ pid: session.pid,
5233
+ alive: session.pid ? isPidAlive(session.pid) : false,
5234
+ startedAt: session.startedAt,
5235
+ pollMs: session.pollMs,
5236
+ logPath: session.logPath
5237
+ });
5238
+ }
5239
+ return entries.sort((a, b) => a.startedAt.localeCompare(b.startedAt));
5240
+ }
5241
+
5242
+ // src/monitor/monitor.terminal.ts
5243
+ function assessAutoCompleteEligibility(input) {
5244
+ const { worker, status } = input;
5245
+ const blockers = [];
5246
+ if (worker.localOnly) {
5247
+ blockers.push("local-only worker (no board linkage)");
5248
+ }
5249
+ if (!worker.agentOsId || !worker.taskId) {
5250
+ blockers.push("missing agentOsId/taskId linkage");
5251
+ }
5252
+ if (hasCompletionAck(worker)) {
5253
+ blockers.push("completion already acknowledged");
5254
+ }
5255
+ if (worker.completionBlocker) {
5256
+ blockers.push(worker.completionBlocker);
5257
+ }
5258
+ if (status.heartbeatBlocker && status.alive) {
5259
+ blockers.push(`worker heartbeat blocker: ${status.heartbeatBlocker}`);
5260
+ }
5261
+ if (status.attention.state === "blocked") {
5262
+ blockers.push(status.attention.reason || "worker attention blocked");
5263
+ }
5264
+ if (isLandingBlockedWorkerStatus(status)) {
5265
+ blockers.push(status.attention.reason || "landing gate blocked");
5266
+ }
5267
+ const terminalVerified = isFinishedWorkerStatus(status);
5268
+ let terminalReason;
5269
+ if (terminalVerified) {
5270
+ if (status.finalResult) terminalReason = "final_result";
5271
+ else if (!status.alive) terminalReason = "process_exited";
5272
+ else terminalReason = "terminal_status";
5273
+ } else {
5274
+ blockers.push("worker has not reached a terminal condition");
5275
+ }
5276
+ const eligible = terminalVerified && blockers.length === 0;
5277
+ return {
5278
+ eligible,
5279
+ terminalVerified,
5280
+ terminalReason,
5281
+ blockers
5282
+ };
5283
+ }
5284
+
5285
+ // src/monitor/monitor.task-lease.ts
5286
+ async function fetchTaskLeasesForWorkers(input) {
5287
+ const out = /* @__PURE__ */ new Map();
5288
+ const agentOsId = input.agentOsId?.trim();
5289
+ if (!agentOsId || input.taskIds.length === 0) return out;
5290
+ const base = resolveBaseUrl(input.baseUrl);
5291
+ try {
5292
+ const secret = await resolveCallbackSecretWithMint(input.secret, agentOsId, { baseUrl: base });
5293
+ const url = `${base}/api/agent-os/by-id/${encodeURIComponent(agentOsId)}/harness/monitor/task-leases`;
5294
+ const res = await postJsonWithCredentialRefresh(
5295
+ url,
5296
+ secret,
5297
+ { taskIds: [...new Set(input.taskIds)] },
5298
+ { agentOsId, baseUrl: base }
5299
+ );
5300
+ if (!res.ok || !res.response || typeof res.response !== "object") return out;
5301
+ const rows = res.response.tasks;
5302
+ if (!Array.isArray(rows)) return out;
5303
+ for (const row of rows) {
5304
+ if (row?.taskId) out.set(row.taskId, row);
5305
+ }
5306
+ } catch {
5307
+ }
5308
+ return out;
5309
+ }
5310
+
5311
+ // src/monitor/monitor.service.ts
5312
+ function workerRecord2(runId, name) {
5313
+ return readJson(
5314
+ path30.join(runDirectory(runId), "workers", safeSlug(name), "worker.json"),
5315
+ void 0
5316
+ );
5317
+ }
5318
+ function workerNamesForRun(runId, scope) {
5319
+ const run = loadRun(runId);
5320
+ const names = Object.keys(run.workers || {});
5321
+ if (!scope) return names;
5322
+ const wanted = safeSlug(scope);
5323
+ return names.filter((n) => safeSlug(n) === wanted);
5324
+ }
5325
+ function buildWorkerView(worker, taskLeases) {
5326
+ const run = loadRun(worker.runId);
5327
+ const status = computeWorkerStatus(worker, { base: run.base, baseCommit: run.baseCommit });
5328
+ const taskLease = worker.taskId ? taskLeases.get(worker.taskId) ?? null : null;
5329
+ const health = classifyWorkerHealth({ worker, status, taskLease });
5330
+ const autoComplete = assessAutoCompleteEligibility({ worker, status });
5331
+ return {
5332
+ runId: worker.runId,
5333
+ worker: worker.name,
5334
+ health: health.health,
5335
+ healthReason: health.reason,
5336
+ workerStatus: status.status,
5337
+ attentionState: status.attention.state,
5338
+ attentionReason: status.attention.reason,
5339
+ alive: status.alive,
5340
+ taskId: worker.taskId,
5341
+ leaseOwner: taskLease?.leaseOwner ?? void 0,
5342
+ taskStatus: taskLease?.status,
5343
+ autoComplete,
5344
+ status
5345
+ };
5346
+ }
5347
+ async function runMonitorTick(args) {
5348
+ const runId = String(args.run || "");
5349
+ required(runId, "--run");
5350
+ const scope = args.name ? String(args.name) : void 0;
5351
+ const agentOsId = args.agentOsId ? String(args.agentOsId) : void 0;
5352
+ const run = loadRun(runId);
5353
+ const names = workerNamesForRun(runId, scope);
5354
+ const workers = [];
5355
+ for (const name of names) {
5356
+ const worker = workerRecord2(runId, name);
5357
+ if (worker) workers.push(worker);
5358
+ }
5359
+ const resolvedAgentOsId = agentOsId || workers.map((w) => w.agentOsId).find((id) => typeof id === "string" && id.trim()) || void 0;
5360
+ const taskIds = workers.map((w) => w.taskId).filter((id) => Boolean(id));
5361
+ const taskLeases = await fetchTaskLeasesForWorkers({
5362
+ agentOsId: resolvedAgentOsId,
5363
+ taskIds,
5364
+ baseUrl: args.baseUrl ? String(args.baseUrl) : void 0,
5365
+ secret: args.secret ? String(args.secret) : void 0
5366
+ });
5367
+ const views = workers.map((w) => buildWorkerView(w, taskLeases));
5368
+ let leaseRenewal;
5369
+ if (resolvedAgentOsId && args.renewLeases !== false && args.renewLeases !== "false") {
5370
+ leaseRenewal = await renewActiveTaskLeases(runId, {
5371
+ ...args,
5372
+ agentOsId: resolvedAgentOsId
5373
+ });
5374
+ }
5375
+ const autoCompleted = [];
5376
+ const shouldAutoComplete = args.autoComplete === true || args.autoComplete === "true";
5377
+ if (shouldAutoComplete) {
5378
+ for (const view of views) {
5379
+ if (!view.autoComplete.eligible) {
5380
+ autoCompleted.push({
5381
+ worker: view.worker,
5382
+ outcome: "skipped",
5383
+ ok: false,
5384
+ reason: view.autoComplete.blockers.join("; ") || "not eligible"
5385
+ });
5386
+ continue;
5387
+ }
5388
+ const outcome = await autoCompleteWorker({
5389
+ run: runId,
5390
+ name: view.worker,
5391
+ ...resolvedAgentOsId ? { agentOsId: resolvedAgentOsId } : {},
5392
+ ...args.baseUrl ? { baseUrl: String(args.baseUrl) } : {},
5393
+ ...args.secret ? { secret: String(args.secret) } : {}
5394
+ });
5395
+ autoCompleted.push({
5396
+ worker: view.worker,
5397
+ outcome: outcome.outcome,
5398
+ ok: outcome.outcome === "completed",
5399
+ reason: outcome.reason
5400
+ });
5401
+ }
5402
+ }
5403
+ return {
5404
+ runId,
5405
+ agentOsId: resolvedAgentOsId,
5406
+ workers: views,
5407
+ leaseRenewal,
5408
+ autoCompleted
5409
+ };
5410
+ }
5411
+ function getMonitorStatus(args) {
5412
+ const runId = String(args.run || "");
5413
+ required(runId, "--run");
5414
+ const scope = args.name ? String(args.name) : void 0;
5415
+ const names = workerNamesForRun(runId, scope);
5416
+ const workers = [];
5417
+ for (const name of names) {
5418
+ const worker = workerRecord2(runId, name);
5419
+ if (!worker) continue;
5420
+ workers.push(buildWorkerView(worker, /* @__PURE__ */ new Map()));
5421
+ }
5422
+ return { runId, workers, autoCompleted: [] };
5423
+ }
5424
+ function listMonitors() {
5425
+ return listMonitorSessions();
5426
+ }
5427
+ function stopMonitor(args) {
5428
+ const runId = String(args.run || "");
5429
+ required(runId, "--run");
5430
+ const monitorId = monitorIdFor(runId, args.name ? String(args.name) : void 0);
5431
+ const session = loadMonitorSession(monitorId);
5432
+ if (!session) {
5433
+ return { monitorId, stopped: false };
5434
+ }
5435
+ if (session.pid && isPidAlive(session.pid)) {
5436
+ try {
5437
+ process.kill(session.pid, "SIGTERM");
5438
+ } catch {
5439
+ }
5440
+ }
5441
+ session.stoppedAt = (/* @__PURE__ */ new Date()).toISOString();
5442
+ saveMonitorSession(session);
5443
+ deleteMonitorSession(monitorId);
5444
+ return { monitorId, stopped: true, pid: session.pid };
5445
+ }
5446
+ async function monitorAutoCompleteCli(args) {
5447
+ const runId = String(args.run || "");
5448
+ const name = String(args.name || "");
5449
+ required(runId, "--run");
5450
+ required(name, "--name");
5451
+ const worker = loadWorker(runId, name);
5452
+ const run = loadRun(runId);
5453
+ const status = computeWorkerStatus(worker, { base: run.base, baseCommit: run.baseCommit });
5454
+ const assessment = assessAutoCompleteEligibility({ worker, status });
5455
+ if (!assessment.eligible) {
5456
+ console.log(
5457
+ JSON.stringify(
5458
+ {
5459
+ runId,
5460
+ worker: name,
5461
+ outcome: "blocked",
5462
+ blockers: assessment.blockers,
5463
+ terminalVerified: assessment.terminalVerified
5464
+ },
5465
+ null,
5466
+ 2
5467
+ )
5468
+ );
5469
+ process.exitCode = 1;
5470
+ return;
5471
+ }
5472
+ const outcome = await autoCompleteWorker({
5473
+ ...args,
5474
+ run: runId,
5475
+ name
5476
+ });
5477
+ console.log(JSON.stringify(outcome, null, 2));
5478
+ if (outcome.outcome !== "completed" && outcome.outcome !== "blocked") {
5479
+ process.exitCode = 1;
5480
+ }
5481
+ }
5482
+
5483
+ // src/monitor/monitor-loop.ts
5484
+ var DEFAULT_POLL_MS2 = 5e3;
5485
+ var DEFAULT_MAX_TOTAL_MS2 = 6 * 60 * 60 * 1e3;
5486
+ async function runMonitorLoop(args) {
5487
+ const monitorId = String(args.monitorId || "");
5488
+ const pollMs = Number(args.pollMs) > 0 ? Math.floor(Number(args.pollMs)) : DEFAULT_POLL_MS2;
5489
+ const maxTotalMs = Number(args.maxTotalMs) > 0 ? Math.floor(Number(args.maxTotalMs)) : DEFAULT_MAX_TOTAL_MS2;
5490
+ const startMs = Date.now();
5491
+ while (Date.now() - startMs <= maxTotalMs) {
5492
+ const session = monitorId ? loadMonitorSession(monitorId) : void 0;
5493
+ if (session?.stoppedAt) break;
5494
+ const tick = await runMonitorTick({
5495
+ ...args,
5496
+ autoComplete: args.autoComplete ?? true,
5497
+ renewLeases: args.renewLeases ?? true
5498
+ });
5499
+ console.log(JSON.stringify({ monitorId, phase: "tick", ...tick }));
5500
+ const allTerminal = tick.workers.length > 0 && tick.workers.every(
5501
+ (w) => w.autoComplete.terminalVerified && (w.autoComplete.eligible || w.autoComplete.blockers.some((b) => b.includes("already acknowledged")))
5502
+ );
5503
+ if (allTerminal && tick.autoCompleted.every((a) => a.ok || a.outcome === "skipped")) {
5504
+ if (monitorId && session) {
5505
+ session.stoppedAt = (/* @__PURE__ */ new Date()).toISOString();
5506
+ saveMonitorSession(session);
5507
+ }
5508
+ break;
5509
+ }
5510
+ sleepMs(pollMs);
5511
+ }
5512
+ }
5513
+
5514
+ // src/monitor/monitor-spawn.ts
5515
+ import { spawn as spawn4 } from "node:child_process";
5516
+ import { closeSync as closeSync4, existsSync as existsSync18, openSync as openSync4 } from "node:fs";
5517
+ import path31 from "node:path";
5518
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
5519
+ function resolveDefaultCliPath2() {
5520
+ return path31.join(fileURLToPath2(new URL(".", import.meta.url)), "..", "cli.js");
5521
+ }
5522
+ function spawnMonitorSidecar(opts) {
5523
+ const cliPath = opts.cliPath ?? resolveDefaultCliPath2();
5524
+ if (!existsSync18(cliPath)) return void 0;
5525
+ const monitorId = monitorIdFor(opts.runId, opts.workerName);
5526
+ const { harnessRoot } = getHarnessPaths();
5527
+ const logPath = path31.join(harnessRoot, "monitors", `${monitorId}.log`);
5528
+ let logFd;
5529
+ try {
5530
+ logFd = openSync4(logPath, "a");
5531
+ } catch {
5532
+ logFd = void 0;
5533
+ }
5534
+ const nodeExecutable = opts.nodeExecutable ?? process.execPath;
5535
+ const pollMs = opts.pollMs ?? 5e3;
5536
+ const args = [
5537
+ cliPath,
5538
+ "monitor",
5539
+ "run-loop",
5540
+ "--run",
5541
+ opts.runId,
5542
+ "--monitor-id",
5543
+ monitorId,
5544
+ "--poll-ms",
5545
+ String(pollMs),
5546
+ "--auto-complete",
5547
+ "true",
5548
+ "--renew-leases",
5549
+ "true"
5550
+ ];
5551
+ if (opts.workerName) args.push("--name", opts.workerName);
5552
+ if (opts.agentOsId) args.push("--agent-os-id", opts.agentOsId);
5553
+ if (opts.baseUrl) args.push("--base-url", opts.baseUrl);
5554
+ if (opts.secret) args.push("--secret", opts.secret);
5555
+ const stdio = [
5556
+ "ignore",
5557
+ logFd ?? "ignore",
5558
+ logFd ?? "ignore"
5559
+ ];
5560
+ try {
5561
+ const child = spawn4(
5562
+ nodeExecutable,
5563
+ args,
5564
+ hiddenSpawnOptions({
5565
+ detached: true,
5566
+ stdio,
5567
+ env: process.env
5568
+ })
5569
+ );
5570
+ if (logFd !== void 0) closeSync4(logFd);
5571
+ child.unref();
5572
+ const session = {
5573
+ monitorId,
5574
+ runId: opts.runId,
5575
+ workerName: opts.workerName,
5576
+ agentOsId: opts.agentOsId,
5577
+ pid: child.pid,
5578
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
5579
+ pollMs,
5580
+ logPath
5581
+ };
5582
+ saveMonitorSession(session);
5583
+ return { monitorId, pid: child.pid, logPath, session };
5584
+ } catch {
5585
+ if (logFd !== void 0) {
5586
+ try {
5587
+ closeSync4(logFd);
5588
+ } catch {
5589
+ }
5590
+ }
5591
+ return void 0;
5592
+ }
5593
+ }
5594
+
5595
+ // src/monitor/monitor-cli.ts
5596
+ async function startMonitorCli(args) {
5597
+ const runId = String(args.run || "");
5598
+ required(runId, "--run");
5599
+ const workerName = args.name ? String(args.name) : void 0;
5600
+ const monitorId = monitorIdFor(runId, workerName);
5601
+ const existing = loadMonitorSession(monitorId);
5602
+ if (existing?.pid && !existing.stoppedAt) {
5603
+ return { monitorId, session: existing, spawned: false, pid: existing.pid };
5604
+ }
5605
+ const spawned = spawnMonitorSidecar({
5606
+ runId,
5607
+ workerName,
5608
+ agentOsId: args.agentOsId ? String(args.agentOsId) : void 0,
5609
+ pollMs: Number(args.pollMs) > 0 ? Math.floor(Number(args.pollMs)) : void 0,
5610
+ baseUrl: args.baseUrl ? String(args.baseUrl) : void 0,
5611
+ secret: args.secret ? String(args.secret) : void 0
5612
+ });
5613
+ if (!spawned) {
5614
+ throw new Error("failed to spawn monitor sidecar (cli.js missing or spawn error)");
5615
+ }
5616
+ return {
5617
+ monitorId,
5618
+ session: spawned.session,
5619
+ spawned: true,
5620
+ pid: spawned.pid
5621
+ };
5622
+ }
5623
+ async function monitorStatusCli(args) {
5624
+ const runId = String(args.run || "");
5625
+ if (runId) {
5626
+ const tick = args.tick === true || args.tick === "true" ? await runMonitorTick({ ...args, autoComplete: false }) : getMonitorStatus(args);
5627
+ console.log(JSON.stringify(tick, null, 2));
5628
+ return;
5629
+ }
5630
+ console.log(JSON.stringify({ monitors: listMonitors() }, null, 2));
5631
+ }
5632
+ function monitorStopCli(args) {
5633
+ console.log(JSON.stringify(stopMonitor(args), null, 2));
5634
+ }
5635
+ function monitorListCli() {
5636
+ console.log(JSON.stringify({ monitors: listMonitors() }, null, 2));
5637
+ }
5638
+ async function monitorRunLoopCli(args) {
5639
+ await runMonitorLoop(args);
5640
+ }
5641
+ async function monitorTickCli(args) {
5642
+ const tick = await runMonitorTick(args);
5643
+ console.log(JSON.stringify(tick, null, 2));
5644
+ }
5645
+
5646
+ // src/package-version.ts
5647
+ import { existsSync as existsSync19, readFileSync as readFileSync8 } from "node:fs";
5648
+ import { dirname, join } from "node:path";
5649
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
5650
+ function resolvePackageRoot(moduleUrl) {
5651
+ let dir = dirname(fileURLToPath3(moduleUrl));
5652
+ for (let depth = 0; depth < 6; depth += 1) {
5653
+ if (existsSync19(join(dir, "package.json"))) return dir;
5654
+ const parent = dirname(dir);
5655
+ if (parent === dir) break;
5656
+ dir = parent;
5657
+ }
5658
+ throw new Error(`package.json not found above ${dirname(fileURLToPath3(moduleUrl))}`);
5659
+ }
5660
+ function readOwnPackageVersion(moduleUrl = import.meta.url) {
5661
+ const pkgPath = join(resolvePackageRoot(moduleUrl), "package.json");
5662
+ const pkg = JSON.parse(readFileSync8(pkgPath, "utf8"));
5663
+ if (typeof pkg.version !== "string" || !pkg.version.trim()) {
5664
+ throw new Error(`Missing package.json version at ${pkgPath}`);
5665
+ }
5666
+ return pkg.version;
5667
+ }
5668
+ var PACKAGE_VERSION = readOwnPackageVersion();
5669
+ function wantsCliVersion(argv) {
5670
+ return argv.some((arg) => arg === "--version" || arg === "-v");
5671
+ }
5672
+ function printCliVersionAndExit(version, binName) {
5673
+ console.log(binName ? `${binName} ${version}` : version);
5674
+ process.exit(0);
5675
+ }
5676
+ function handleCliVersionFlag(argv, moduleUrl = import.meta.url, binName) {
5677
+ if (!wantsCliVersion(argv)) return false;
5678
+ printCliVersionAndExit(readOwnPackageVersion(moduleUrl), binName);
5679
+ return true;
5680
+ }
5681
+
4176
5682
  // src/cli.ts
4177
5683
  function isHelpFlag(arg) {
4178
5684
  return arg === "help" || arg === "--help" || arg === "-h";
@@ -4204,17 +5710,28 @@ function usage(code = 0) {
4204
5710
  " kynver worker auto-complete --run RUN_ID --name worker [--agent-os-id AOS_ID] [--poll-ms 5000] [--max-total-ms 21600000] [--complete-attempts 3] [--complete-backoff-ms 5000] [--base-url URL] [--secret SECRET]",
4205
5711
  " kynver plan progress --plan PLAN_ID --row ROW_KEY --role ROLE --status STATUS [--task TASK_ID] [--note NOTE] [--evidence type:value] [--agent-os-id AOS_ID]",
4206
5712
  " kynver plan verify --plan PLAN_ID [--worktree PATH] [--task TASK_ID] [--human-override]",
4207
- " kynver cleanup [--execute] [--node-modules-age-ms MS] [--worktrees-age-ms MS] [--harness-root PATH] [--include-orphans]"
5713
+ " kynver plan persist --operation create|add_version|update_metadata --title TITLE (--body-file PATH | --body TEXT) [--slug SLUG] [--plan PLAN_ID] [--summary TEXT] [--failure-kind approval_guard|auth|network|server|tool_interruption]",
5714
+ " kynver plan outbox list",
5715
+ " kynver plan outbox drain [--max N] [--id OUTBOX_ID]",
5716
+ " kynver cleanup [--execute] [--node-modules-age-ms MS] [--worktrees-age-ms MS] [--harness-root PATH] [--include-orphans]",
5717
+ " kynver monitor start --run RUN_ID [--name worker] [--agent-os-id AOS_ID] [--poll-ms MS]",
5718
+ " kynver monitor status [--run RUN_ID] [--name worker] [--tick]",
5719
+ " kynver monitor stop --run RUN_ID [--name worker]",
5720
+ " kynver monitor list",
5721
+ " kynver monitor tick --run RUN_ID [--name worker] [--agent-os-id AOS_ID] [--auto-complete] [--renew-leases]",
5722
+ " kynver monitor auto-complete --run RUN_ID --name worker [--agent-os-id AOS_ID] [--base-url URL] [--secret SECRET]",
5723
+ " kynver monitor run-loop --run RUN_ID --monitor-id ID [--name worker] [--agent-os-id AOS_ID] [--poll-ms MS] [--auto-complete] [--renew-leases]"
4208
5724
  ].join("\n")
4209
5725
  );
4210
5726
  process.exit(code);
4211
5727
  }
4212
5728
  async function main(argv = process.argv.slice(2)) {
5729
+ if (handleCliVersionFlag(argv, import.meta.url, "kynver")) return;
4213
5730
  if (argv.length === 0 || isHelpFlag(argv[0])) return usage(0);
4214
5731
  const scope = argv.shift();
4215
5732
  let action;
4216
5733
  let rest;
4217
- if (scope === "run" || scope === "worker" || scope === "plan" || scope === "runner") {
5734
+ if (scope === "run" || scope === "worker" || scope === "plan" || scope === "runner" || scope === "monitor") {
4218
5735
  action = argv.shift();
4219
5736
  rest = argv;
4220
5737
  } else {
@@ -4223,14 +5740,21 @@ async function main(argv = process.argv.slice(2)) {
4223
5740
  if (action && isHelpFlag(action) || rest.some(isHelpFlag)) return usage(0);
4224
5741
  const args = parseArgs(rest);
4225
5742
  const { runsDir, worktreesDir } = getPaths();
4226
- mkdirSync5(runsDir, { recursive: true });
4227
- mkdirSync5(worktreesDir, { recursive: true });
5743
+ mkdirSync7(runsDir, { recursive: true });
5744
+ mkdirSync7(worktreesDir, { recursive: true });
4228
5745
  if (scope === "login") return void await runLogin(args);
4229
5746
  if (scope === "runner" && action === "credential") return void await mintRunnerCredential(args);
4230
5747
  if (scope === "setup") return void await runSetup(args);
4231
5748
  if (scope === "daemon") return void await runDaemon(args);
4232
5749
  if (scope === "plan" && action === "progress") return void await emitPlanProgress(args);
4233
5750
  if (scope === "plan" && action === "verify") return void await verifyPlan(args);
5751
+ if (scope === "plan" && action === "persist") return void await runPlanPersist(args);
5752
+ if (scope === "plan" && action === "outbox") {
5753
+ const outboxAction = rest.shift();
5754
+ if (outboxAction === "list") return void await runPlanOutboxList();
5755
+ if (outboxAction === "drain") return void await runPlanOutboxDrain(parseArgs(rest));
5756
+ unknownCommand("plan", `outbox ${outboxAction ?? ""}`.trim());
5757
+ }
4234
5758
  if (scope === "cleanup") return runCleanupCli(args);
4235
5759
  if (scope === "run" && action === "create") return createRun(args);
4236
5760
  if (scope === "run" && action === "list") return listRuns();
@@ -4243,9 +5767,20 @@ async function main(argv = process.argv.slice(2)) {
4243
5767
  if (scope === "worker" && action === "stop") return stopWorker(args);
4244
5768
  if (scope === "worker" && action === "complete") return void await completeWorker(args);
4245
5769
  if (scope === "worker" && action === "auto-complete") return void await autoCompleteWorkerCli(args);
5770
+ if (scope === "monitor" && action === "start") {
5771
+ const result = await startMonitorCli(args);
5772
+ console.log(JSON.stringify(result, null, 2));
5773
+ return;
5774
+ }
5775
+ if (scope === "monitor" && action === "status") return void await monitorStatusCli(args);
5776
+ if (scope === "monitor" && action === "stop") return monitorStopCli(args);
5777
+ if (scope === "monitor" && action === "list") return monitorListCli();
5778
+ if (scope === "monitor" && action === "tick") return void await monitorTickCli(args);
5779
+ if (scope === "monitor" && action === "auto-complete") return void await monitorAutoCompleteCli(args);
5780
+ if (scope === "monitor" && action === "run-loop") return void await monitorRunLoopCli(args);
4246
5781
  unknownCommand(scope, action);
4247
5782
  }
4248
- var isCliEntry = process.argv[1] && realpathSync.native(process.argv[1]) === realpathSync.native(fileURLToPath2(import.meta.url));
5783
+ var isCliEntry = process.argv[1] && realpathSync.native(process.argv[1]) === realpathSync.native(fileURLToPath4(import.meta.url));
4249
5784
  if (isCliEntry) {
4250
5785
  void main().catch((error) => {
4251
5786
  console.error(error);