@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/index.js CHANGED
@@ -1,13 +1,49 @@
1
+ // src/package-version.ts
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ function resolvePackageRoot(moduleUrl) {
6
+ let dir = dirname(fileURLToPath(moduleUrl));
7
+ for (let depth = 0; depth < 6; depth += 1) {
8
+ if (existsSync(join(dir, "package.json"))) return dir;
9
+ const parent = dirname(dir);
10
+ if (parent === dir) break;
11
+ dir = parent;
12
+ }
13
+ throw new Error(`package.json not found above ${dirname(fileURLToPath(moduleUrl))}`);
14
+ }
15
+ function readOwnPackageVersion(moduleUrl = import.meta.url) {
16
+ const pkgPath = join(resolvePackageRoot(moduleUrl), "package.json");
17
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
18
+ if (typeof pkg.version !== "string" || !pkg.version.trim()) {
19
+ throw new Error(`Missing package.json version at ${pkgPath}`);
20
+ }
21
+ return pkg.version;
22
+ }
23
+ var PACKAGE_VERSION = readOwnPackageVersion();
24
+ function wantsCliVersion(argv) {
25
+ return argv.some((arg) => arg === "--version" || arg === "-v");
26
+ }
27
+ function printCliVersionAndExit(version, binName) {
28
+ console.log(binName ? `${binName} ${version}` : version);
29
+ process.exit(0);
30
+ }
31
+ function handleCliVersionFlag(argv, moduleUrl = import.meta.url, binName) {
32
+ if (!wantsCliVersion(argv)) return false;
33
+ printCliVersionAndExit(readOwnPackageVersion(moduleUrl), binName);
34
+ return true;
35
+ }
36
+
1
37
  // src/dispatch.ts
2
- import path12 from "node:path";
38
+ import path16 from "node:path";
3
39
 
4
40
  // src/config.ts
5
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
41
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "node:fs";
6
42
  import { homedir } from "node:os";
7
43
  import path2 from "node:path";
8
44
 
9
45
  // src/util.ts
10
- import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
46
+ import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, readdirSync, statSync, writeFileSync } from "node:fs";
11
47
  import path from "node:path";
12
48
  function fail(message) {
13
49
  console.error(message);
@@ -30,7 +66,7 @@ function safeJson(line) {
30
66
  }
31
67
  function readJson(file, fallback) {
32
68
  try {
33
- return JSON.parse(readFileSync(file, "utf8"));
69
+ return JSON.parse(readFileSync2(file, "utf8"));
34
70
  } catch (error) {
35
71
  if (arguments.length > 1) return fallback;
36
72
  fail(`failed to read ${file}: ${error.message}`);
@@ -68,15 +104,15 @@ function fileMtime(file) {
68
104
  }
69
105
  }
70
106
  function tailFile(file, lines) {
71
- if (!existsSync(file)) return "";
72
- const data = readFileSync(file, "utf8");
107
+ if (!existsSync2(file)) return "";
108
+ const data = readFileSync2(file, "utf8");
73
109
  return data.split("\n").slice(-lines).join("\n");
74
110
  }
75
111
  function readMaybeFile(file) {
76
- return file ? readFileSync(path.resolve(file), "utf8") : "";
112
+ return file ? readFileSync2(path.resolve(file), "utf8") : "";
77
113
  }
78
114
  function listRunIds(runsDir) {
79
- if (!existsSync(runsDir)) return [];
115
+ if (!existsSync2(runsDir)) return [];
80
116
  return readdirSync(runsDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
81
117
  }
82
118
  function sleepMs(ms) {
@@ -120,9 +156,9 @@ var CONFIG_DIR = path2.join(homedir(), ".kynver");
120
156
  var CONFIG_FILE = path2.join(CONFIG_DIR, "config.json");
121
157
  var CREDENTIALS_FILE = path2.join(CONFIG_DIR, "credentials");
122
158
  function loadUserConfig() {
123
- if (!existsSync2(CONFIG_FILE)) return {};
159
+ if (!existsSync3(CONFIG_FILE)) return {};
124
160
  try {
125
- return JSON.parse(readFileSync2(CONFIG_FILE, "utf8"));
161
+ return JSON.parse(readFileSync3(CONFIG_FILE, "utf8"));
126
162
  } catch {
127
163
  return {};
128
164
  }
@@ -133,9 +169,9 @@ function saveUserConfig(config) {
133
169
  `, { mode: 384 });
134
170
  }
135
171
  function loadCredentialsFile() {
136
- if (!existsSync2(CREDENTIALS_FILE)) return {};
172
+ if (!existsSync3(CREDENTIALS_FILE)) return {};
137
173
  try {
138
- return JSON.parse(readFileSync2(CREDENTIALS_FILE, "utf8"));
174
+ return JSON.parse(readFileSync3(CREDENTIALS_FILE, "utf8"));
139
175
  } catch {
140
176
  return {};
141
177
  }
@@ -425,12 +461,12 @@ var DEFAULT_CRITICAL_FREE_BYTES = 15 * 1024 * 1024 * 1024;
425
461
  var DEFAULT_MAX_USED_PERCENT = 80;
426
462
  var DEFAULT_HARD_MAX_USED_PERCENT = 90;
427
463
  function observeRunnerDiskGate(input = {}) {
428
- const path25 = input.diskPath?.trim() || "/";
464
+ const path32 = input.diskPath?.trim() || "/";
429
465
  const warnBelowBytes = input.diskFreeWarnBytes ?? DEFAULT_WARN_FREE_BYTES;
430
466
  const criticalBelowBytes = input.diskFreeCriticalBytes ?? DEFAULT_CRITICAL_FREE_BYTES;
431
467
  const maxUsedPercent = input.diskMaxUsedPercent ?? DEFAULT_MAX_USED_PERCENT;
432
468
  const hardMaxUsedPercent = input.diskHardMaxUsedPercent ?? DEFAULT_HARD_MAX_USED_PERCENT;
433
- const stats = statfsSync(path25);
469
+ const stats = statfsSync(path32);
434
470
  const freeBytes = Number(stats.bavail) * Number(stats.bsize);
435
471
  const totalBytes = Number(stats.blocks) * Number(stats.bsize);
436
472
  const usedPercent = totalBytes > 0 ? (totalBytes - freeBytes) / totalBytes * 100 : 100;
@@ -450,7 +486,7 @@ function observeRunnerDiskGate(input = {}) {
450
486
  }
451
487
  return {
452
488
  ok,
453
- path: path25,
489
+ path: path32,
454
490
  freeBytes,
455
491
  totalBytes,
456
492
  usedPercent,
@@ -463,16 +499,16 @@ function observeRunnerDiskGate(input = {}) {
463
499
  }
464
500
 
465
501
  // src/resource-gate.ts
466
- import { readFileSync as readFileSync5 } from "node:fs";
502
+ import { readFileSync as readFileSync6 } from "node:fs";
467
503
  import os from "node:os";
468
504
  import path5 from "node:path";
469
505
 
470
506
  // src/run-store.ts
471
- import { existsSync as existsSync4, readdirSync as readdirSync2 } from "node:fs";
507
+ import { existsSync as existsSync5, readdirSync as readdirSync2 } from "node:fs";
472
508
  import path4 from "node:path";
473
509
 
474
510
  // src/paths.ts
475
- import { existsSync as existsSync3 } from "node:fs";
511
+ import { existsSync as existsSync4 } from "node:fs";
476
512
  import { homedir as homedir2 } from "node:os";
477
513
  import path3 from "node:path";
478
514
  var LEGACY_ROOT = path3.join(homedir2(), ".openclaw", "harness");
@@ -480,8 +516,8 @@ function resolveHarnessRoot() {
480
516
  const env = process.env.KYNVER_HARNESS_ROOT || process.env.OPUS_HARNESS_ROOT;
481
517
  if (env) return path3.resolve(env);
482
518
  const kynverRoot = path3.join(homedir2(), ".kynver", "harness");
483
- if (existsSync3(kynverRoot)) return kynverRoot;
484
- if (existsSync3(LEGACY_ROOT)) return LEGACY_ROOT;
519
+ if (existsSync4(kynverRoot)) return kynverRoot;
520
+ if (existsSync4(LEGACY_ROOT)) return LEGACY_ROOT;
485
521
  return kynverRoot;
486
522
  }
487
523
  function getHarnessPaths() {
@@ -506,7 +542,7 @@ function loadRun(id) {
506
542
  }
507
543
  function listRunRecords() {
508
544
  const { runsDir } = getPaths();
509
- if (!existsSync4(runsDir)) return [];
545
+ if (!existsSync5(runsDir)) return [];
510
546
  const runs = [];
511
547
  for (const entry of readdirSync2(runsDir, { withFileTypes: true })) {
512
548
  if (!entry.isDirectory()) continue;
@@ -538,7 +574,8 @@ function runDirectory(id) {
538
574
  }
539
575
 
540
576
  // src/heartbeat.ts
541
- import { existsSync as existsSync5, readFileSync as readFileSync3 } from "node:fs";
577
+ import { existsSync as existsSync6, readFileSync as readFileSync4 } from "node:fs";
578
+ var HEARTBEAT_FUTURE_SKEW_MS = 6e4;
542
579
  function isTerminalHeartbeatPhase(phase) {
543
580
  return phase === "complete";
544
581
  }
@@ -553,16 +590,31 @@ function parseHeartbeat(file) {
553
590
  lastHeartbeatAt: null,
554
591
  lastHeartbeatPhase: null,
555
592
  lastHeartbeatSummary: null,
556
- heartbeatBlocker: null
593
+ heartbeatBlocker: null,
594
+ timestampAnomalies: []
557
595
  };
558
- if (!existsSync5(file)) return result;
559
- const lines = readFileSync3(file, "utf8").split("\n").filter(Boolean);
596
+ if (!existsSync6(file)) return result;
597
+ const maxFutureMs = Date.now() + HEARTBEAT_FUTURE_SKEW_MS;
598
+ const clampedTo = new Date(maxFutureMs).toISOString();
599
+ const lines = readFileSync4(file, "utf8").split("\n").filter(Boolean);
560
600
  for (const line of lines) {
561
601
  const entry = safeJson(line);
562
602
  if (!entry || typeof entry !== "object" || Array.isArray(entry)) continue;
563
603
  const row = entry;
564
604
  result.heartbeatCount++;
565
- if (row.ts) result.lastHeartbeatAt = String(row.ts);
605
+ if (row.ts) {
606
+ const ts = String(row.ts);
607
+ const tsMs = Date.parse(ts);
608
+ if (Number.isFinite(tsMs) && tsMs > maxFutureMs) {
609
+ result.timestampAnomalies.push({
610
+ kind: "future_heartbeat_timestamp",
611
+ observedAt: ts,
612
+ clampedTo
613
+ });
614
+ } else {
615
+ result.lastHeartbeatAt = ts;
616
+ }
617
+ }
566
618
  if (row.phase !== void 0 && row.phase !== null) result.lastHeartbeatPhase = String(row.phase);
567
619
  if (row.summary !== void 0 && row.summary !== null) result.lastHeartbeatSummary = String(row.summary);
568
620
  result.heartbeatBlocker = row.blocker ? String(row.blocker) : null;
@@ -571,7 +623,7 @@ function parseHeartbeat(file) {
571
623
  }
572
624
 
573
625
  // src/stream.ts
574
- import { existsSync as existsSync6, readFileSync as readFileSync4 } from "node:fs";
626
+ import { existsSync as existsSync7, readFileSync as readFileSync5 } from "node:fs";
575
627
  function eventTimestampIso(event) {
576
628
  const tsMs = event.timestamp_ms;
577
629
  return event.timestamp || event.ts || (tsMs ? new Date(tsMs).toISOString() : void 0);
@@ -600,8 +652,8 @@ function parseHarnessStream(file) {
600
652
  finalResult: null,
601
653
  error: null
602
654
  };
603
- if (!existsSync6(file)) return result;
604
- const lines = readFileSync4(file, "utf8").split("\n").filter(Boolean);
655
+ if (!existsSync7(file)) return result;
656
+ const lines = readFileSync5(file, "utf8").split("\n").filter(Boolean);
605
657
  for (const line of lines) {
606
658
  const event = safeJson(line);
607
659
  if (!event) continue;
@@ -949,6 +1001,112 @@ function landingAttentionReason(verdict) {
949
1001
  return verdict.detail ?? verdict.reason ?? "dirty_worktree_no_pr";
950
1002
  }
951
1003
 
1004
+ // src/landing-contract-gate.ts
1005
+ function trimOrNull3(value) {
1006
+ if (typeof value !== "string") return null;
1007
+ const t = value.trim();
1008
+ return t.length ? t : null;
1009
+ }
1010
+ function hasFinalResult3(value) {
1011
+ if (value === void 0 || value === null) return false;
1012
+ if (typeof value === "string") return value.trim().length > 0;
1013
+ if (typeof value === "object") return Object.keys(value).length > 0;
1014
+ return true;
1015
+ }
1016
+ function normalizePrUrl(url) {
1017
+ const m = url.trim().match(/github\.com\/([^/]+\/[^/]+)\/(?:pull|pulls)\/(\d+)/i);
1018
+ if (!m) return trimOrNull3(url);
1019
+ return `https://github.com/${m[1]}/pull/${m[2]}`;
1020
+ }
1021
+ function parseReconciliation(finalResult) {
1022
+ if (!finalResult || typeof finalResult !== "object" || Array.isArray(finalResult)) return [];
1023
+ const record = finalResult;
1024
+ const raw = record.targetPrReconciliation ?? record.target_pr_reconciliation;
1025
+ if (!Array.isArray(raw)) return [];
1026
+ const out = [];
1027
+ for (const item of raw) {
1028
+ if (!item || typeof item !== "object" || Array.isArray(item)) continue;
1029
+ const row = item;
1030
+ const prUrl = normalizePrUrl(String(row.prUrl ?? row.pr_url ?? ""));
1031
+ const outcome = trimOrNull3(row.outcome);
1032
+ if (!prUrl || outcome !== "merged" && outcome !== "skipped" && outcome !== "blocked") continue;
1033
+ out.push({
1034
+ prUrl,
1035
+ outcome,
1036
+ mergeCommit: trimOrNull3(row.mergeCommit ?? row.merge_commit),
1037
+ reason: trimOrNull3(row.reason)
1038
+ });
1039
+ }
1040
+ return out;
1041
+ }
1042
+ function workerPrUrls(snapshot, finalResult) {
1043
+ const urls = [];
1044
+ const fromSnapshot = normalizePrUrl(trimOrNull3(snapshot.prUrl) ?? "");
1045
+ if (fromSnapshot) urls.push(fromSnapshot);
1046
+ if (finalResult && typeof finalResult === "object" && !Array.isArray(finalResult)) {
1047
+ const pr = normalizePrUrl(String(finalResult.prUrl ?? ""));
1048
+ if (pr) urls.push(pr);
1049
+ }
1050
+ return [...new Set(urls)];
1051
+ }
1052
+ function assessWorkerLandingContract(input) {
1053
+ const { contract, snapshot } = input;
1054
+ const finalResult = input.finalResult ?? snapshot.finalResult;
1055
+ if (!contract.landingOnly && contract.targetPrUrls.length === 0) {
1056
+ return { blocked: false };
1057
+ }
1058
+ if (!hasFinalResult3(finalResult)) return { blocked: false };
1059
+ const reconciliation = parseReconciliation(finalResult);
1060
+ const byUrl = new Map(reconciliation.map((r) => [r.prUrl, r]));
1061
+ const targetSet = new Set(
1062
+ contract.targetPrUrls.map((u) => normalizePrUrl(u) ?? u).filter(Boolean)
1063
+ );
1064
+ const workerPrs = workerPrUrls(snapshot, finalResult);
1065
+ if (contract.landingOnly) {
1066
+ for (const pr of workerPrs) {
1067
+ if (targetSet.size > 0 && !targetSet.has(pr)) {
1068
+ return {
1069
+ blocked: true,
1070
+ reason: "unrelated_implementation_pr",
1071
+ detail: `Landing-only worker attached unrelated PR ${pr}`
1072
+ };
1073
+ }
1074
+ if (targetSet.size === 0) {
1075
+ return {
1076
+ blocked: true,
1077
+ reason: "unrelated_implementation_pr",
1078
+ detail: "Landing-only worker must not open new implementation PRs"
1079
+ };
1080
+ }
1081
+ }
1082
+ }
1083
+ if (contract.targetPrUrls.length === 0) return { blocked: false };
1084
+ const missing = [];
1085
+ for (const target of contract.targetPrUrls) {
1086
+ const key = normalizePrUrl(target) ?? target;
1087
+ const entry = byUrl.get(key);
1088
+ if (!entry) {
1089
+ missing.push(key);
1090
+ continue;
1091
+ }
1092
+ if (entry.outcome !== "merged" && !entry.reason?.trim()) {
1093
+ missing.push(key);
1094
+ }
1095
+ }
1096
+ if (missing.length > 0) {
1097
+ return {
1098
+ blocked: true,
1099
+ reason: missing.every((u) => byUrl.has(u)) ? "incomplete_target_pr_landing" : "missing_target_pr_reconciliation",
1100
+ detail: `Target PR reconciliation incomplete: ${missing.join(", ")}`
1101
+ };
1102
+ }
1103
+ return { blocked: false };
1104
+ }
1105
+ function landingContractAttentionReason(verdict) {
1106
+ if (!verdict.blocked) return void 0;
1107
+ return verdict.detail ?? verdict.reason;
1108
+ }
1109
+
952
1110
  // src/status.ts
953
1111
  var NO_START_MS = 18e4;
954
1112
  var STALE_MS = 6e5;
@@ -958,11 +1116,13 @@ function computeAttention(input) {
958
1116
  return { state: "blocked", reason: input.completionBlocker };
959
1117
  }
960
1118
  if (input.finalResult) {
961
- const landing = assessWorkerLanding({
1119
+ const landingSnapshot = {
962
1120
  finalResult: input.finalResult,
963
1121
  changedFiles: input.changedFiles ?? [],
964
- gitAncestry: input.gitAncestry ?? null
965
- });
1122
+ gitAncestry: input.gitAncestry ?? null,
1123
+ prUrl: input.prUrl ?? null
1124
+ };
1125
+ const landing = assessWorkerLanding(landingSnapshot);
966
1126
  if (landing.blocked) {
967
1127
  const detail = landingAttentionReason(landing);
968
1128
  return {
@@ -970,6 +1130,20 @@ function computeAttention(input) {
970
1130
  reason: landing.reason ? `landing blocked (${landing.reason}): ${detail}` : `landing blocked: ${detail}`
971
1131
  };
972
1132
  }
1133
+ if (input.landingContract) {
1134
+ const contractVerdict = assessWorkerLandingContract({
1135
+ contract: input.landingContract,
1136
+ snapshot: landingSnapshot,
1137
+ finalResult: input.finalResult
1138
+ });
1139
+ const contractDetail = landingContractAttentionReason(contractVerdict);
1140
+ if (contractDetail) {
1141
+ return {
1142
+ state: "needs_attention",
1143
+ reason: contractVerdict.reason ? `landing contract (${contractVerdict.reason}): ${contractDetail}` : `landing contract: ${contractDetail}`
1144
+ };
1145
+ }
1146
+ }
973
1147
  return { state: "done", reason: "final result recorded" };
974
1148
  }
975
1149
  if (!input.alive) {
@@ -1007,11 +1181,18 @@ function computeAttention(input) {
1007
1181
  }
1008
1182
  return { state: "ok", reason: "recent activity" };
1009
1183
  }
1184
+ function resolveFinalResult(worker, parsedFinalResult, heartbeat) {
1185
+ if (parsedFinalResult) return parsedFinalResult;
1186
+ const ackSnapshot = worker.completionSnapshot?.finalResult;
1187
+ if (ackSnapshot !== void 0 && ackSnapshot !== null) return ackSnapshot;
1188
+ return terminalFinalResultFromHeartbeat(heartbeat);
1189
+ }
1010
1190
  function computeWorkerStatus(worker, options = {}) {
1011
1191
  const parsed = parseHarnessStream(worker.stdoutPath);
1012
1192
  const heartbeat = parseHeartbeat(worker.heartbeatPath);
1013
- const finalResult = parsed.finalResult ?? terminalFinalResultFromHeartbeat(heartbeat);
1014
- const alive = isPidAlive(worker.pid);
1193
+ const completionAcknowledged = typeof worker.completionReportedAt === "string" && worker.completionReportedAt.trim().length > 0;
1194
+ const finalResult = resolveFinalResult(worker, parsed.finalResult, heartbeat);
1195
+ const alive = completionAcknowledged ? false : isPidAlive(worker.pid);
1015
1196
  const stdoutBytes = fileSize(worker.stdoutPath);
1016
1197
  const stderrBytes = fileSize(worker.stderrPath);
1017
1198
  const heartbeatBytes = fileSize(worker.heartbeatPath);
@@ -1043,7 +1224,7 @@ function computeWorkerStatus(worker, options = {}) {
1043
1224
  gitAncestry,
1044
1225
  completionBlocker
1045
1226
  });
1046
- const workerStatusLabel = completionBlocker || attention.state === "blocked" ? "blocked" : attention.state === "done" ? "done" : finalResult ? "exited" : alive ? "running" : "exited";
1227
+ const workerStatusLabel = completionBlocker || attention.state === "blocked" ? "blocked" : completionAcknowledged || attention.state === "done" ? "done" : finalResult ? "exited" : alive ? "running" : "exited";
1047
1228
  return {
1048
1229
  runId: worker.runId,
1049
1230
  worker: worker.name,
@@ -1060,12 +1241,13 @@ function computeWorkerStatus(worker, options = {}) {
1060
1241
  firstEventAt: parsed.firstEventAt,
1061
1242
  lastEventAt: parsed.lastEventAt,
1062
1243
  lastActivityAt,
1063
- currentTool: parsed.currentTool,
1244
+ currentTool: completionAcknowledged ? null : parsed.currentTool,
1064
1245
  heartbeatCount: heartbeat.heartbeatCount,
1065
1246
  lastHeartbeatAt: heartbeat.lastHeartbeatAt,
1066
1247
  lastHeartbeatPhase: heartbeat.lastHeartbeatPhase,
1067
1248
  lastHeartbeatSummary: heartbeat.lastHeartbeatSummary,
1068
1249
  heartbeatBlocker: heartbeat.heartbeatBlocker,
1250
+ timestampAnomalies: heartbeat.timestampAnomalies,
1069
1251
  finalResult,
1070
1252
  error,
1071
1253
  changedFiles,
@@ -1124,7 +1306,7 @@ function computeAutoMaxWorkers(totalMemBytes, opts = {}) {
1124
1306
  function readAvailableMemBytes() {
1125
1307
  if (process.platform === "linux") {
1126
1308
  try {
1127
- const meminfo = readFileSync5("/proc/meminfo", "utf8");
1309
+ const meminfo = readFileSync6("/proc/meminfo", "utf8");
1128
1310
  const match = meminfo.match(/^MemAvailable:\s+(\d+)\s*kB/m);
1129
1311
  if (match) return Number(match[1]) * 1024;
1130
1312
  } catch {
@@ -1205,6 +1387,16 @@ function normalize(value) {
1205
1387
  return value.toLowerCase();
1206
1388
  }
1207
1389
  var PERSONA_DEFAULT_LANE = {
1390
+ ghost: "system",
1391
+ astra: "plan_author",
1392
+ rhea: "implementer",
1393
+ mnemo: "implementer",
1394
+ sentinel: "deep_reviewer",
1395
+ pixel: "implementer",
1396
+ schema: "implementer",
1397
+ atlas: "runtime_verifier",
1398
+ bridge: "implementer",
1399
+ catalyst: "implementer",
1208
1400
  dalton: "implementer",
1209
1401
  lorentz: "report_reviewer"
1210
1402
  };
@@ -1583,8 +1775,8 @@ function hasLiveWorkerForTask(runId, taskId) {
1583
1775
  }
1584
1776
 
1585
1777
  // src/supervisor.ts
1586
- import { existsSync as existsSync10, mkdirSync as mkdirSync3 } from "node:fs";
1587
- import path11 from "node:path";
1778
+ import { existsSync as existsSync11, mkdirSync as mkdirSync3 } from "node:fs";
1779
+ import path12 from "node:path";
1588
1780
 
1589
1781
  // src/prompt.ts
1590
1782
  function buildPrompt(input) {
@@ -1606,6 +1798,7 @@ function buildPrompt(input) {
1606
1798
  const planArtifactLines = compact ? [
1607
1799
  "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."
1608
1800
  ] : [
1801
+ "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.",
1609
1802
  "PR-first plan artifacts (when authoring or revising docs/superpowers/plans/):",
1610
1803
  "- 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.",
1611
1804
  "- Iterate review on that PR branch; link prUrl on the AgentOS task and plan progress evidence (`--evidence pr:<url>`).",
@@ -1619,6 +1812,7 @@ function buildPrompt(input) {
1619
1812
  `Progress heartbeat file: ${input.heartbeatPath}`,
1620
1813
  "After each major step, append one JSON line to the heartbeat file with fields: ts, phase, summary, changedFiles, blocker.",
1621
1814
  "Final response must include files changed, verification commands, and unresolved risks.",
1815
+ "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.",
1622
1816
  "Completion handoff (required): before you stop, ensure the harness records a final result \u2014 summarize outcome in your last message and append a heartbeat line with phase `complete`. If you leave uncommitted changes or committed work without a PR, the orchestrator blocks completion until a GitHub PR exists (or you discard/commit cleanly). Exiting with only dirty files and no PR routes to salvage review, not production review.",
1623
1817
  "PR-ready handoff: for substantial implementation work, commit, push, and open a GitHub PR (draft OK) on your branch before finishing \u2014 or rely on the harness to run `gh pr create` at completion when `gh` is authenticated.",
1624
1818
  "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.",
@@ -1636,12 +1830,12 @@ function buildPrompt(input) {
1636
1830
  }
1637
1831
 
1638
1832
  // src/providers/cursor.ts
1639
- import { closeSync as closeSync2, existsSync as existsSync8, openSync as openSync2 } from "node:fs";
1833
+ import { closeSync as closeSync2, existsSync as existsSync9, openSync as openSync2 } from "node:fs";
1640
1834
  import { spawn as spawn2 } from "node:child_process";
1641
1835
  import path8 from "node:path";
1642
1836
 
1643
1837
  // src/providers/cursor-windows.ts
1644
- import { existsSync as existsSync7, readdirSync as readdirSync3 } from "node:fs";
1838
+ import { existsSync as existsSync8, readdirSync as readdirSync3 } from "node:fs";
1645
1839
  import path7 from "node:path";
1646
1840
  var CURSOR_VERSION_DIR = /^\d{4}\.\d{1,2}\.\d{1,2}-[a-f0-9]+$/i;
1647
1841
  function parseCursorVersionSortKey(versionName) {
@@ -1654,7 +1848,7 @@ function parseCursorVersionSortKey(versionName) {
1654
1848
  }
1655
1849
  function pickLatestCursorVersionDir(agentRoot) {
1656
1850
  const versionsRoot = path7.join(agentRoot, "versions");
1657
- if (!existsSync7(versionsRoot)) return null;
1851
+ if (!existsSync8(versionsRoot)) return null;
1658
1852
  let bestDir = null;
1659
1853
  let bestKey = -1;
1660
1854
  for (const entry of readdirSync3(versionsRoot, { withFileTypes: true })) {
@@ -1670,14 +1864,14 @@ function resolveWindowsCursorBundled(agentRoot) {
1670
1864
  const root = agentRoot?.trim() || path7.join(process.env.LOCALAPPDATA || "", "cursor-agent");
1671
1865
  const directNode = path7.join(root, "node.exe");
1672
1866
  const directIndex = path7.join(root, "index.js");
1673
- if (existsSync7(directNode) && existsSync7(directIndex)) {
1867
+ if (existsSync8(directNode) && existsSync8(directIndex)) {
1674
1868
  return { nodeExe: directNode, indexJs: directIndex, versionDir: root };
1675
1869
  }
1676
1870
  const versionDir = pickLatestCursorVersionDir(root);
1677
1871
  if (!versionDir) return null;
1678
1872
  const nodeExe = path7.join(versionDir, "node.exe");
1679
1873
  const indexJs = path7.join(versionDir, "index.js");
1680
- if (!existsSync7(nodeExe) || !existsSync7(indexJs)) return null;
1874
+ if (!existsSync8(nodeExe) || !existsSync8(indexJs)) return null;
1681
1875
  return { nodeExe, indexJs, versionDir };
1682
1876
  }
1683
1877
 
@@ -1695,7 +1889,7 @@ function bundledSpawnTarget(nodeExe, indexJs, versionDir) {
1695
1889
  function resolveCursorSpawn(agentBin) {
1696
1890
  if (process.platform === "win32") {
1697
1891
  const isCursorWrapper = /\.(cmd|bat)$/i.test(agentBin);
1698
- const isBundledNode = /node\.exe$/i.test(agentBin) && existsSync8(path8.join(path8.dirname(agentBin), "index.js"));
1892
+ const isBundledNode = /node\.exe$/i.test(agentBin) && existsSync9(path8.join(path8.dirname(agentBin), "index.js"));
1699
1893
  const isDefaultShim = agentBin === "agent";
1700
1894
  if (isCursorWrapper || isBundledNode || isDefaultShim) {
1701
1895
  const bundled = isCursorWrapper ? resolveWindowsCursorBundled(path8.dirname(agentBin)) : isBundledNode ? {
@@ -1722,7 +1916,7 @@ function resolveAgentBin() {
1722
1916
  );
1723
1917
  if (bundled) return bundled.nodeExe;
1724
1918
  const localAgent = path8.join(process.env.LOCALAPPDATA || "", "cursor-agent", "agent.cmd");
1725
- if (existsSync8(localAgent)) return localAgent;
1919
+ if (existsSync9(localAgent)) return localAgent;
1726
1920
  }
1727
1921
  return "agent";
1728
1922
  }
@@ -1804,16 +1998,16 @@ function resolveWorkerProvider(name) {
1804
1998
 
1805
1999
  // src/auto-complete.ts
1806
2000
  import { spawn as spawn3 } from "node:child_process";
1807
- import { existsSync as existsSync9, openSync as openSync3, closeSync as closeSync3 } from "node:fs";
1808
- import path10 from "node:path";
1809
- import { fileURLToPath } from "node:url";
2001
+ import { existsSync as existsSync10, openSync as openSync3, closeSync as closeSync3 } from "node:fs";
2002
+ import path11 from "node:path";
2003
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
1810
2004
 
1811
2005
  // src/worker-ops.ts
1812
- import path9 from "node:path";
2006
+ import path10 from "node:path";
1813
2007
 
1814
2008
  // src/pr-handoff/pr-handoff-assess.ts
1815
2009
  var REVIEW_LANE_RULE = /^(lane:)?(review|deep_review|planning|landing)(:|$)/i;
1816
- function trimOrNull3(value) {
2010
+ function trimOrNull4(value) {
1817
2011
  if (typeof value !== "string") return null;
1818
2012
  const trimmed = value.trim();
1819
2013
  return trimmed.length ? trimmed : null;
@@ -1821,7 +2015,7 @@ function trimOrNull3(value) {
1821
2015
  function committedHead(ancestry) {
1822
2016
  if (!ancestry?.checked) return null;
1823
2017
  if (ancestry.headIsAncestorOfBase !== false) return null;
1824
- return trimOrNull3(ancestry.head);
2018
+ return trimOrNull4(ancestry.head);
1825
2019
  }
1826
2020
  function extractPrUrlFromText(value) {
1827
2021
  if (value === void 0 || value === null) return null;
@@ -1829,11 +2023,11 @@ function extractPrUrlFromText(value) {
1829
2023
  const m = text.match(
1830
2024
  /https?:\/\/[^\s)>"]+\/(?:pull|pulls|merge_requests|pull-requests)\/\d+/i
1831
2025
  );
1832
- return m ? trimOrNull3(m[0]) : null;
2026
+ return m ? trimOrNull4(m[0]) : null;
1833
2027
  }
1834
2028
  function hasWorkProduct(snapshot) {
1835
2029
  if (snapshot.changedFiles.length > 0) return true;
1836
- if (trimOrNull3(snapshot.headCommit)) return true;
2030
+ if (trimOrNull4(snapshot.headCommit)) return true;
1837
2031
  if (committedHead(snapshot.gitAncestry)) return true;
1838
2032
  return false;
1839
2033
  }
@@ -1841,14 +2035,14 @@ function assessPrHandoffRequirement(input) {
1841
2035
  if (!input.dispatched) {
1842
2036
  return { required: false, reason: "not_dispatched" };
1843
2037
  }
1844
- const rule = trimOrNull3(input.routingRule) ?? "";
2038
+ const rule = trimOrNull4(input.routingRule) ?? "";
1845
2039
  if (rule && REVIEW_LANE_RULE.test(rule)) {
1846
2040
  return { required: false, reason: "review_lane" };
1847
2041
  }
1848
- if (trimOrNull3(input.patchPath) || trimOrNull3(input.artifactBundlePath)) {
2042
+ if (trimOrNull4(input.patchPath) || trimOrNull4(input.artifactBundlePath)) {
1849
2043
  return { required: false, reason: "patch_or_bundle" };
1850
2044
  }
1851
- const prUrl = trimOrNull3(input.prUrl) ?? trimOrNull3(input.snapshot.prUrl);
2045
+ const prUrl = trimOrNull4(input.prUrl) ?? trimOrNull4(input.snapshot.prUrl);
1852
2046
  if (prUrl) {
1853
2047
  return { required: false, reason: "already_has_pr" };
1854
2048
  }
@@ -1864,8 +2058,8 @@ function buildPrHandoffSnapshotFromStatus(status, extras) {
1864
2058
  worktreePath: status.worktreePath,
1865
2059
  gitAncestry: status.gitAncestry,
1866
2060
  finalResult: status.finalResult,
1867
- headCommit: trimOrNull3(extras?.headCommit) ?? committedHead(status.gitAncestry),
1868
- prUrl: trimOrNull3(extras?.prUrl) ?? null
2061
+ headCommit: trimOrNull4(extras?.headCommit) ?? committedHead(status.gitAncestry),
2062
+ prUrl: trimOrNull4(extras?.prUrl) ?? null
1869
2063
  };
1870
2064
  }
1871
2065
 
@@ -2158,6 +2352,80 @@ function persistCompletionAck(worker, runId, fields) {
2158
2352
  saveWorker(runId, worker);
2159
2353
  }
2160
2354
 
2355
+ // src/worker-lifecycle.ts
2356
+ import path9 from "node:path";
2357
+ var TASK_LEFT_RUNNING = /* @__PURE__ */ new Set([
2358
+ "awaiting_review",
2359
+ "blocked",
2360
+ "done",
2361
+ "needs_input",
2362
+ "waiting",
2363
+ "scheduled",
2364
+ "ready",
2365
+ "cancelled",
2366
+ "failed"
2367
+ ]);
2368
+ function isCompletionAcknowledged(worker) {
2369
+ return Boolean(
2370
+ typeof worker.completionReportedAt === "string" && worker.completionReportedAt.trim()
2371
+ );
2372
+ }
2373
+ function completionSnapshotFromStatus(status) {
2374
+ const summary = typeof status.lastHeartbeatSummary === "string" ? status.lastHeartbeatSummary : null;
2375
+ return {
2376
+ finalResult: status.finalResult ?? summary ?? "completed",
2377
+ summary
2378
+ };
2379
+ }
2380
+ function persistCompletionAcknowledged(worker, status, opts) {
2381
+ if (isCompletionAcknowledged(worker) && worker.status === "done" && worker.completionSnapshot != null) {
2382
+ return;
2383
+ }
2384
+ const at = (/* @__PURE__ */ new Date()).toISOString();
2385
+ const snapshot = completionSnapshotFromStatus(status);
2386
+ const source = opts?.source?.trim();
2387
+ worker.completionReportedAt = at;
2388
+ worker.completionSnapshot = snapshot;
2389
+ worker.status = "done";
2390
+ if (source) worker.completionAckSource = source;
2391
+ saveWorker(worker.runId, worker);
2392
+ }
2393
+ function taskStatusByIdFromOperatorTick(operatorTick) {
2394
+ const map = /* @__PURE__ */ new Map();
2395
+ const body = operatorTick;
2396
+ const items = body.response?.tick?.filteredItems;
2397
+ if (!Array.isArray(items)) return map;
2398
+ for (const item of items) {
2399
+ if (item.kind !== "task" || typeof item.id !== "string") continue;
2400
+ const status = typeof item.taskStatus === "string" ? item.taskStatus.trim() : "";
2401
+ if (status) map.set(item.id, status);
2402
+ }
2403
+ return map;
2404
+ }
2405
+ function syncCompletionAcknowledgedFromOperatorTick(runId, operatorTick) {
2406
+ const taskStatusById = taskStatusByIdFromOperatorTick(operatorTick);
2407
+ if (taskStatusById.size === 0) return [];
2408
+ const run = loadRun(runId);
2409
+ const synced = [];
2410
+ for (const name of Object.keys(run.workers || {})) {
2411
+ const worker = readJson(
2412
+ path9.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
2413
+ void 0
2414
+ );
2415
+ if (!worker?.taskId || isCompletionAcknowledged(worker)) continue;
2416
+ const taskStatus = taskStatusById.get(worker.taskId);
2417
+ if (!taskStatus || taskStatus === "running" || !TASK_LEFT_RUNNING.has(taskStatus)) {
2418
+ continue;
2419
+ }
2420
+ const status = computeWorkerStatus(worker, { base: run.base, baseCommit: run.baseCommit });
2421
+ persistCompletionAcknowledged(worker, status, {
2422
+ source: `board-task-${taskStatus}`
2423
+ });
2424
+ synced.push({ worker: name, taskId: worker.taskId, taskStatus });
2425
+ }
2426
+ return synced;
2427
+ }
2428
+
2161
2429
  // src/worker-ops.ts
2162
2430
  async function postCompletion(url, secret, body) {
2163
2431
  const res = await fetch(url, {
@@ -2289,6 +2557,7 @@ async function tryCompleteWorker(args) {
2289
2557
  workerInjection: {
2290
2558
  instructionPolicyFingerprint: worker.instructionPolicyFingerprint ?? null,
2291
2559
  instructionPolicyEvidence: worker.instructionPolicyEvidence ?? null,
2560
+ policyAt: worker.instructionPolicyEvidence && typeof worker.instructionPolicyEvidence === "object" && "policyAt" in worker.instructionPolicyEvidence && typeof worker.instructionPolicyEvidence.policyAt === "string" ? worker.instructionPolicyEvidence.policyAt : null,
2292
2561
  personaSlug: worker.personaSlug ?? null,
2293
2562
  personaEvidence: worker.personaEvidence ?? null
2294
2563
  }
@@ -2309,6 +2578,7 @@ async function tryCompleteWorker(args) {
2309
2578
  completionResponse: result.parsed
2310
2579
  };
2311
2580
  persistCompletionAck(worker, worker.runId, ack);
2581
+ persistCompletionAcknowledged(worker, status, { source: "harness-completion" });
2312
2582
  const prUrl = status.prUrl;
2313
2583
  return {
2314
2584
  ok: true,
@@ -2376,7 +2646,7 @@ function workerStatus(args) {
2376
2646
  const worker = loadWorker(String(args.run), String(args.name));
2377
2647
  const run = loadRun(worker.runId);
2378
2648
  const status = computeWorkerStatus(worker, workerStatusOptions(run));
2379
- writeJson(path9.join(worker.workerDir, "last-status.json"), status);
2649
+ writeJson(path10.join(worker.workerDir, "last-status.json"), status);
2380
2650
  console.log(JSON.stringify(status, null, 2));
2381
2651
  }
2382
2652
  function buildRunBoard(runId) {
@@ -2384,7 +2654,7 @@ function buildRunBoard(runId) {
2384
2654
  const names = Object.keys(run.workers || {});
2385
2655
  const workers = names.map((name) => {
2386
2656
  const worker = readJson(
2387
- path9.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
2657
+ path10.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
2388
2658
  void 0
2389
2659
  );
2390
2660
  if (!worker) {
@@ -2494,7 +2764,7 @@ function buildRunBoard(runId) {
2494
2764
  needsAttention: workers.filter((w) => w.attention && w.attention !== "ok" && w.attention !== "done").map((w) => w.worker),
2495
2765
  workers
2496
2766
  };
2497
- writeJson(path9.join(runDirectory(run.id), "last-board.json"), board);
2767
+ writeJson(path10.join(runDirectory(run.id), "last-board.json"), board);
2498
2768
  return board;
2499
2769
  }
2500
2770
  async function publishHarnessBoardSnapshot(args, source) {
@@ -2661,12 +2931,12 @@ async function autoCompleteWorkerCli(raw) {
2661
2931
  }
2662
2932
  }
2663
2933
  function resolveDefaultCliPath() {
2664
- return path10.join(fileURLToPath(new URL(".", import.meta.url)), "cli.js");
2934
+ return path11.join(fileURLToPath2(new URL(".", import.meta.url)), "cli.js");
2665
2935
  }
2666
2936
  function spawnCompletionSidecar(opts) {
2667
2937
  const cliPath = opts.cliPath ?? resolveDefaultCliPath();
2668
- if (!existsSync9(cliPath)) return void 0;
2669
- const logPath = path10.join(opts.workerDir, "auto-complete.log");
2938
+ if (!existsSync10(cliPath)) return void 0;
2939
+ const logPath = path11.join(opts.workerDir, "auto-complete.log");
2670
2940
  let logFd;
2671
2941
  try {
2672
2942
  logFd = openSync3(logPath, "a");
@@ -2748,16 +3018,16 @@ function spawnWorkerProcess(run, opts) {
2748
3018
  launchModel = preflight.model;
2749
3019
  }
2750
3020
  const { worktreesDir } = getPaths();
2751
- const workerDir = path11.join(runDirectory(run.id), "workers", name);
3021
+ const workerDir = path12.join(runDirectory(run.id), "workers", name);
2752
3022
  mkdirSync3(workerDir, { recursive: true });
2753
- const worktreePath = path11.join(worktreesDir, run.id, name);
3023
+ const worktreePath = path12.join(worktreesDir, run.id, name);
2754
3024
  const branch = opts.branch || `agent/${run.id}/${name}`;
2755
- if (existsSync10(worktreePath)) throw new Error(`worktree path already exists: ${worktreePath}`);
3025
+ if (existsSync11(worktreePath)) throw new Error(`worktree path already exists: ${worktreePath}`);
2756
3026
  git(run.repo, ["fetch", "origin", "--prune"], { allowFailure: true });
2757
3027
  git(run.repo, ["worktree", "add", "-b", branch, worktreePath, run.baseCommit], { throwError: true });
2758
- const stdoutPath = path11.join(workerDir, "stdout.jsonl");
2759
- const stderrPath = path11.join(workerDir, "stderr.log");
2760
- const heartbeatPath = path11.join(workerDir, "heartbeat.jsonl");
3028
+ const stdoutPath = path12.join(workerDir, "stdout.jsonl");
3029
+ const stderrPath = path12.join(workerDir, "stderr.log");
3030
+ const heartbeatPath = path12.join(workerDir, "heartbeat.jsonl");
2761
3031
  const prompt = buildPrompt({
2762
3032
  task: opts.task,
2763
3033
  ownedPaths: opts.ownedPaths || [],
@@ -2818,7 +3088,7 @@ function spawnWorkerProcess(run, opts) {
2818
3088
  startedAt: (/* @__PURE__ */ new Date()).toISOString()
2819
3089
  };
2820
3090
  saveWorker(run.id, worker);
2821
- run.workers = { ...run.workers || {}, [name]: { workerDir, statusPath: path11.join(workerDir, "worker.json") } };
3091
+ run.workers = { ...run.workers || {}, [name]: { workerDir, statusPath: path12.join(workerDir, "worker.json") } };
2822
3092
  run.status = "running";
2823
3093
  saveRun(run);
2824
3094
  if (worker.agentOsId && worker.taskId) {
@@ -2912,6 +3182,579 @@ async function startWorker(args) {
2912
3182
  }
2913
3183
  }
2914
3184
 
3185
+ // src/plan-persist/body-hash.ts
3186
+ import { createHash } from "node:crypto";
3187
+ function hashPlanBody(body) {
3188
+ const normalized = body.replace(/\r\n/g, "\n").trimEnd();
3189
+ return createHash("sha256").update(normalized, "utf8").digest("hex");
3190
+ }
3191
+ function hashSummary(summary) {
3192
+ if (summary == null) return null;
3193
+ const trimmed = summary.trim();
3194
+ if (!trimmed) return null;
3195
+ return createHash("sha256").update(trimmed, "utf8").digest("hex");
3196
+ }
3197
+
3198
+ // src/plan-persist/errors.ts
3199
+ var PlanPersistError = class extends Error {
3200
+ kind;
3201
+ httpStatus;
3202
+ constructor(kind, message, httpStatus) {
3203
+ super(message);
3204
+ this.name = "PlanPersistError";
3205
+ this.kind = kind;
3206
+ this.httpStatus = httpStatus;
3207
+ }
3208
+ };
3209
+ function classifyHttpFailure(status, message) {
3210
+ if (status === 401 || status === 403) {
3211
+ return new PlanPersistError("auth", message, status);
3212
+ }
3213
+ if (status >= 500) {
3214
+ return new PlanPersistError("server", message, status);
3215
+ }
3216
+ return new PlanPersistError("permanent", message, status);
3217
+ }
3218
+ function classifyFetchFailure(err) {
3219
+ const message = err instanceof Error ? err.message : String(err);
3220
+ if (/ECONNREFUSED|ENOTFOUND|ETIMEDOUT|fetch failed|network/i.test(message)) {
3221
+ return new PlanPersistError("network", message);
3222
+ }
3223
+ return new PlanPersistError("tool_interruption", message);
3224
+ }
3225
+ function isRetryableFailure(kind) {
3226
+ return kind !== "permanent";
3227
+ }
3228
+
3229
+ // src/plan-persist/agentos-api.ts
3230
+ function authHeaders(apiKey) {
3231
+ const headers = { "Content-Type": "application/json" };
3232
+ if (apiKey) headers.Authorization = `Bearer ${apiKey}`;
3233
+ return headers;
3234
+ }
3235
+ async function parseJsonResponse(res) {
3236
+ const text = await res.text();
3237
+ try {
3238
+ return JSON.parse(text);
3239
+ } catch {
3240
+ return text;
3241
+ }
3242
+ }
3243
+ async function agentOsGetPlan(slug, planId, deps = {}) {
3244
+ const base = resolveBaseUrl(deps.baseUrl);
3245
+ const apiKey = deps.apiKey ?? loadApiKey();
3246
+ const fetchFn = deps.fetchFn ?? fetch;
3247
+ const url = `${base}/api/agent-os/${encodeURIComponent(slug)}/plans/${encodeURIComponent(planId)}`;
3248
+ try {
3249
+ const res = await fetchFn(url, { method: "GET", headers: authHeaders(apiKey) });
3250
+ const parsed = await parseJsonResponse(res);
3251
+ if (!res.ok) {
3252
+ const msg = typeof parsed === "object" && parsed && "error" in parsed ? String(parsed.error) : `GET plan failed (${res.status})`;
3253
+ throw classifyHttpFailure(res.status, msg);
3254
+ }
3255
+ return parsed;
3256
+ } catch (err) {
3257
+ if (err instanceof PlanPersistError) throw err;
3258
+ throw classifyFetchFailure(err);
3259
+ }
3260
+ }
3261
+ async function agentOsWritePlan(input, deps = {}) {
3262
+ const base = resolveBaseUrl(deps.baseUrl);
3263
+ const apiKey = deps.apiKey ?? loadApiKey();
3264
+ const fetchFn = deps.fetchFn ?? fetch;
3265
+ const slug = input.agentOsSlug;
3266
+ try {
3267
+ if (input.operation === "create") {
3268
+ const url2 = `${base}/api/agent-os/${encodeURIComponent(slug)}/plans`;
3269
+ const body2 = {
3270
+ title: input.title,
3271
+ summary: input.summary ?? null,
3272
+ slug: input.planSlug ?? null,
3273
+ sourceRefs: mergeSourceRefs(input),
3274
+ initialVersion: {
3275
+ title: input.title,
3276
+ body: input.body,
3277
+ summary: input.summary ?? null,
3278
+ changeSummary: input.changeSummary ?? null,
3279
+ author: input.author ?? null,
3280
+ sourceRefs: mergeSourceRefs(input)
3281
+ }
3282
+ };
3283
+ const res2 = await fetchFn(url2, {
3284
+ method: "POST",
3285
+ headers: authHeaders(apiKey),
3286
+ body: JSON.stringify(body2)
3287
+ });
3288
+ const parsed2 = await parseJsonResponse(res2);
3289
+ if (!res2.ok) {
3290
+ const msg = typeof parsed2 === "object" && parsed2 && "error" in parsed2 ? String(parsed2.error) : `create plan failed (${res2.status})`;
3291
+ throw classifyHttpFailure(res2.status, msg);
3292
+ }
3293
+ const row2 = parsed2;
3294
+ return {
3295
+ planId: row2.plan.id,
3296
+ versionId: row2.version.id,
3297
+ versionNumber: row2.version.versionNumber
3298
+ };
3299
+ }
3300
+ const planId = input.planId;
3301
+ if (!planId) throw new PlanPersistError("permanent", "planId is required for this operation");
3302
+ if (input.operation === "update_metadata") {
3303
+ const url2 = `${base}/api/agent-os/${encodeURIComponent(slug)}/plans/${encodeURIComponent(planId)}`;
3304
+ const body2 = {
3305
+ title: input.title,
3306
+ summary: input.summary ?? null,
3307
+ sourceRefs: mergeSourceRefs(input)
3308
+ };
3309
+ const res2 = await fetchFn(url2, {
3310
+ method: "PATCH",
3311
+ headers: authHeaders(apiKey),
3312
+ body: JSON.stringify(body2)
3313
+ });
3314
+ const parsed2 = await parseJsonResponse(res2);
3315
+ if (!res2.ok) {
3316
+ const msg = typeof parsed2 === "object" && parsed2 && "error" in parsed2 ? String(parsed2.error) : `update plan failed (${res2.status})`;
3317
+ throw classifyHttpFailure(res2.status, msg);
3318
+ }
3319
+ const row2 = parsed2;
3320
+ return {
3321
+ planId: row2.id,
3322
+ versionId: row2.currentVersionId,
3323
+ versionNumber: null
3324
+ };
3325
+ }
3326
+ const url = `${base}/api/agent-os/${encodeURIComponent(slug)}/plans/${encodeURIComponent(planId)}/versions`;
3327
+ const body = {
3328
+ title: input.title,
3329
+ body: input.body,
3330
+ summary: input.summary ?? null,
3331
+ changeSummary: input.changeSummary ?? null,
3332
+ author: input.author ?? null,
3333
+ sourceRefs: mergeSourceRefs(input),
3334
+ markCurrent: input.markCurrent !== false
3335
+ };
3336
+ const res = await fetchFn(url, {
3337
+ method: "POST",
3338
+ headers: authHeaders(apiKey),
3339
+ body: JSON.stringify(body)
3340
+ });
3341
+ const parsed = await parseJsonResponse(res);
3342
+ if (!res.ok) {
3343
+ const msg = typeof parsed === "object" && parsed && "error" in parsed ? String(parsed.error) : `add version failed (${res.status})`;
3344
+ throw classifyHttpFailure(res.status, msg);
3345
+ }
3346
+ const row = parsed;
3347
+ return {
3348
+ planId: row.version.planId,
3349
+ versionId: row.version.id,
3350
+ versionNumber: row.version.versionNumber
3351
+ };
3352
+ } catch (err) {
3353
+ if (err instanceof PlanPersistError) throw err;
3354
+ throw classifyFetchFailure(err);
3355
+ }
3356
+ }
3357
+ function mergeSourceRefs(input) {
3358
+ const refs = { ...input.sourceRefs ?? {} };
3359
+ if (input.model) refs.model = input.model;
3360
+ if (!Object.keys(refs).length) return input.sourceRefs ?? null;
3361
+ return refs;
3362
+ }
3363
+
3364
+ // src/plan-persist/idempotency.ts
3365
+ import { createHash as createHash2 } from "node:crypto";
3366
+ function buildPlanPersistIdempotencyKey(input) {
3367
+ const payload = {
3368
+ operation: input.operation,
3369
+ agentOsSlug: input.agentOsSlug,
3370
+ planId: input.planId ?? null,
3371
+ planSlug: input.planSlug ?? null,
3372
+ title: input.title.trim(),
3373
+ summaryHash: hashSummary(input.summary),
3374
+ bodyHash: hashPlanBody(input.body),
3375
+ changeSummary: input.changeSummary?.trim() ?? null,
3376
+ markCurrent: input.markCurrent ?? true
3377
+ };
3378
+ return createHash2("sha256").update(JSON.stringify(payload), "utf8").digest("hex");
3379
+ }
3380
+
3381
+ // src/plan-persist/paths.ts
3382
+ import { mkdirSync as mkdirSync4 } from "node:fs";
3383
+ import { homedir as homedir3 } from "node:os";
3384
+ import path13 from "node:path";
3385
+ function resolveKynverStateRoot() {
3386
+ const env = process.env.KYNVER_STATE_ROOT;
3387
+ if (env) return path13.resolve(env);
3388
+ return path13.join(homedir3(), ".kynver", "state");
3389
+ }
3390
+ function planOutboxDir() {
3391
+ return path13.join(resolveKynverStateRoot(), "plan-outbox");
3392
+ }
3393
+ function planOutboxArchiveDir() {
3394
+ return path13.join(resolveKynverStateRoot(), "plan-outbox-archive");
3395
+ }
3396
+ function ensurePlanOutboxDirs() {
3397
+ const outboxDir = planOutboxDir();
3398
+ const archiveDir = planOutboxArchiveDir();
3399
+ mkdirSync4(outboxDir, { recursive: true });
3400
+ mkdirSync4(archiveDir, { recursive: true });
3401
+ return { outboxDir, archiveDir };
3402
+ }
3403
+ function isTmpOnlyPath(filePath) {
3404
+ const resolved = path13.resolve(filePath);
3405
+ return resolved.startsWith("/tmp/") || resolved.startsWith(path13.join("/var", "folders"));
3406
+ }
3407
+
3408
+ // src/plan-persist/outbox-store.ts
3409
+ import {
3410
+ existsSync as existsSync13,
3411
+ readFileSync as readFileSync7,
3412
+ renameSync,
3413
+ readdirSync as readdirSync4,
3414
+ writeFileSync as writeFileSync3,
3415
+ unlinkSync
3416
+ } from "node:fs";
3417
+ import path14 from "node:path";
3418
+ import { randomUUID } from "node:crypto";
3419
+ var DEFAULT_MAX_RETRIES = 12;
3420
+ function listOutboxItems() {
3421
+ const { outboxDir } = ensurePlanOutboxDirs();
3422
+ const files = readdirSync4(outboxDir).filter((f) => f.endsWith(".json"));
3423
+ const items = [];
3424
+ for (const file of files) {
3425
+ const item = readOutboxItem(path14.join(outboxDir, file));
3426
+ if (item && item.queueStatus === "queued") items.push(item);
3427
+ }
3428
+ return items.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
3429
+ }
3430
+ function findOutboxByIdempotencyKey(key) {
3431
+ for (const item of listOutboxItems()) {
3432
+ if (item.idempotencyKey === key) return item;
3433
+ }
3434
+ return null;
3435
+ }
3436
+ function readOutboxItem(jsonPath) {
3437
+ if (!existsSync13(jsonPath)) return null;
3438
+ try {
3439
+ return JSON.parse(readFileSync7(jsonPath, "utf8"));
3440
+ } catch {
3441
+ return null;
3442
+ }
3443
+ }
3444
+ function readOutboxBody(item) {
3445
+ const { outboxDir } = ensurePlanOutboxDirs();
3446
+ const bodyFile = path14.join(outboxDir, item.bodyPath);
3447
+ return readFileSync7(bodyFile, "utf8");
3448
+ }
3449
+ function writeOutboxItem(input, opts) {
3450
+ const { outboxDir } = ensurePlanOutboxDirs();
3451
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3452
+ const id = opts.existing?.id ?? randomUUID();
3453
+ const bodyPath = opts.existing?.bodyPath ?? `${id}.body.md`;
3454
+ const jsonPath = path14.join(outboxDir, `${id}.json`);
3455
+ const bodyFile = path14.join(outboxDir, bodyPath);
3456
+ if (!opts.existing) {
3457
+ writeFileSync3(bodyFile, input.body, "utf8");
3458
+ }
3459
+ const item = {
3460
+ id,
3461
+ idempotencyKey: buildPlanPersistIdempotencyKey(input),
3462
+ operation: input.operation,
3463
+ agentOsSlug: input.agentOsSlug,
3464
+ planId: input.planId ?? opts.existing?.planId ?? null,
3465
+ planSlug: input.planSlug ?? opts.existing?.planSlug ?? null,
3466
+ title: input.title,
3467
+ summary: input.summary ?? null,
3468
+ bodyPath,
3469
+ bodyHash: hashPlanBody(input.body),
3470
+ author: input.author ?? null,
3471
+ model: input.model ?? null,
3472
+ sourceRefs: input.sourceRefs ?? null,
3473
+ changeSummary: input.changeSummary ?? null,
3474
+ markCurrent: input.markCurrent ?? true,
3475
+ createdAt: opts.existing?.createdAt ?? now,
3476
+ updatedAt: now,
3477
+ retryCount: (opts.existing?.retryCount ?? 0) + (opts.existing ? 1 : 0),
3478
+ maxRetries: input.maxRetries ?? opts.existing?.maxRetries ?? DEFAULT_MAX_RETRIES,
3479
+ lastError: opts.lastError,
3480
+ lastFailureKind: opts.lastFailureKind,
3481
+ queueStatus: "queued",
3482
+ userStatus: "queued for retry",
3483
+ readbackEvidence: null
3484
+ };
3485
+ writeFileSync3(jsonPath, `${JSON.stringify(item, null, 2)}
3486
+ `, { mode: 384 });
3487
+ return item;
3488
+ }
3489
+ function saveOutboxItem(item) {
3490
+ const { outboxDir } = ensurePlanOutboxDirs();
3491
+ const jsonPath = path14.join(outboxDir, `${item.id}.json`);
3492
+ writeFileSync3(jsonPath, `${JSON.stringify(item, null, 2)}
3493
+ `, { mode: 384 });
3494
+ }
3495
+ function archiveOutboxItem(item) {
3496
+ const { outboxDir, archiveDir } = ensurePlanOutboxDirs();
3497
+ const jsonSrc = path14.join(outboxDir, `${item.id}.json`);
3498
+ const bodySrc = path14.join(outboxDir, item.bodyPath);
3499
+ const jsonDst = path14.join(archiveDir, `${item.id}.json`);
3500
+ const bodyDst = path14.join(archiveDir, item.bodyPath);
3501
+ if (existsSync13(jsonSrc)) renameSync(jsonSrc, jsonDst);
3502
+ if (existsSync13(bodySrc)) renameSync(bodySrc, bodyDst);
3503
+ }
3504
+ function outboxItemPaths(item) {
3505
+ const { outboxDir } = ensurePlanOutboxDirs();
3506
+ return {
3507
+ jsonPath: path14.join(outboxDir, `${item.id}.json`),
3508
+ bodyPath: path14.join(outboxDir, item.bodyPath)
3509
+ };
3510
+ }
3511
+ function outboxInputFromItem(item, body) {
3512
+ return {
3513
+ operation: item.operation,
3514
+ agentOsSlug: item.agentOsSlug,
3515
+ planId: item.planId,
3516
+ planSlug: item.planSlug,
3517
+ title: item.title,
3518
+ summary: item.summary,
3519
+ body,
3520
+ changeSummary: item.changeSummary ?? void 0,
3521
+ author: item.author ?? void 0,
3522
+ model: item.model ?? void 0,
3523
+ sourceRefs: item.sourceRefs,
3524
+ markCurrent: item.markCurrent ?? true,
3525
+ maxRetries: item.maxRetries
3526
+ };
3527
+ }
3528
+
3529
+ // src/plan-persist/readback.ts
3530
+ async function verifyPlanReadback(slug, expectation, deps = {}) {
3531
+ const payload = await agentOsGetPlan(slug, expectation.planId, deps);
3532
+ const plan = payload.plan;
3533
+ const current = payload.currentVersion;
3534
+ if (plan.title.trim() !== expectation.title.trim()) {
3535
+ throw new PlanPersistError(
3536
+ "verification_failed",
3537
+ `title mismatch: expected "${expectation.title}", got "${plan.title}"`
3538
+ );
3539
+ }
3540
+ const expectedSummaryHash = hashSummary(expectation.summary);
3541
+ const actualSummaryHash = hashSummary(plan.summary);
3542
+ if (expectedSummaryHash !== actualSummaryHash) {
3543
+ throw new PlanPersistError("verification_failed", "summary mismatch on readback");
3544
+ }
3545
+ if (expectation.versionId && plan.currentVersionId !== expectation.versionId) {
3546
+ throw new PlanPersistError(
3547
+ "verification_failed",
3548
+ `currentVersionId mismatch: expected ${expectation.versionId}, got ${plan.currentVersionId}`
3549
+ );
3550
+ }
3551
+ if (expectation.versionNumber != null) {
3552
+ if (!current || current.versionNumber !== expectation.versionNumber) {
3553
+ throw new PlanPersistError(
3554
+ "verification_failed",
3555
+ `versionNumber mismatch: expected ${expectation.versionNumber}, got ${current?.versionNumber ?? "none"}`
3556
+ );
3557
+ }
3558
+ }
3559
+ const bodyForHash = current?.body ?? "";
3560
+ const actualBodyHash = hashPlanBody(bodyForHash);
3561
+ if (expectation.bodyHash && actualBodyHash !== expectation.bodyHash) {
3562
+ throw new PlanPersistError("verification_failed", "body hash mismatch on readback");
3563
+ }
3564
+ return {
3565
+ planId: plan.id,
3566
+ currentVersionId: plan.currentVersionId,
3567
+ versionNumber: current?.versionNumber ?? null,
3568
+ title: plan.title,
3569
+ summary: plan.summary,
3570
+ bodyHash: expectation.bodyHash || actualBodyHash,
3571
+ readAt: (/* @__PURE__ */ new Date()).toISOString()
3572
+ };
3573
+ }
3574
+ function buildReadbackExpectation(input, write) {
3575
+ return {
3576
+ planId: write.planId,
3577
+ title: input.title,
3578
+ summary: input.summary ?? null,
3579
+ body: input.body,
3580
+ bodyHash: hashPlanBody(input.body),
3581
+ versionId: write.versionId,
3582
+ versionNumber: write.versionNumber
3583
+ };
3584
+ }
3585
+
3586
+ // src/plan-persist/persist.ts
3587
+ var SUCCESS_STATUS = "persisted and read back";
3588
+ var QUEUED_STATUS = "queued for retry";
3589
+ var FAILED_STATUS = "failed and needs action";
3590
+ async function persistPlan(input, deps = {}) {
3591
+ if (input.bodyPathHint && isTmpOnlyPath(input.bodyPathHint)) {
3592
+ }
3593
+ const idempotencyKey = buildPlanPersistIdempotencyKey(input);
3594
+ const existingOutbox = findOutboxByIdempotencyKey(idempotencyKey);
3595
+ if (existingOutbox?.readbackEvidence) {
3596
+ return {
3597
+ userStatus: SUCCESS_STATUS,
3598
+ outboxId: existingOutbox.id,
3599
+ planId: existingOutbox.readbackEvidence.planId,
3600
+ readbackEvidence: existingOutbox.readbackEvidence,
3601
+ idempotencyKey
3602
+ };
3603
+ }
3604
+ if (input.immediateFailure) {
3605
+ return queueForRetry(input, input.immediateFailure.message, input.immediateFailure.kind, existingOutbox);
3606
+ }
3607
+ const writeFn = deps.writePlan ?? agentOsWritePlan;
3608
+ const verifyFn = deps.verifyReadback ?? verifyPlanReadback;
3609
+ try {
3610
+ const write = await writeFn(input, deps);
3611
+ const enriched = { ...input, planId: write.planId };
3612
+ const expectation = buildReadbackExpectation(enriched, write);
3613
+ const readback = await verifyFn(
3614
+ input.agentOsSlug,
3615
+ readbackExpectationForOperation(input, expectation),
3616
+ deps
3617
+ );
3618
+ if (existingOutbox) archiveOutboxItem(existingOutbox);
3619
+ return {
3620
+ userStatus: SUCCESS_STATUS,
3621
+ planId: write.planId,
3622
+ versionId: write.versionId ?? void 0,
3623
+ readbackEvidence: readback,
3624
+ idempotencyKey
3625
+ };
3626
+ } catch (err) {
3627
+ const failure = err instanceof PlanPersistError ? err : new PlanPersistError("tool_interruption", err instanceof Error ? err.message : String(err));
3628
+ if (!isRetryableFailure(failure.kind)) {
3629
+ const item = writeOutboxItem(input, {
3630
+ lastError: failure.message,
3631
+ lastFailureKind: failure.kind,
3632
+ existing: existingOutbox ?? void 0
3633
+ });
3634
+ const paths = outboxItemPaths(item);
3635
+ const failed = markOutboxFailed(item, failure.message);
3636
+ return {
3637
+ userStatus: FAILED_STATUS,
3638
+ outboxId: failed.id,
3639
+ outboxPath: paths.jsonPath,
3640
+ bodyPath: paths.bodyPath,
3641
+ lastError: failure.message,
3642
+ idempotencyKey
3643
+ };
3644
+ }
3645
+ return queueForRetry(input, failure.message, failure.kind, existingOutbox);
3646
+ }
3647
+ }
3648
+ function readbackExpectationForOperation(input, expectation) {
3649
+ if (input.operation === "update_metadata") {
3650
+ return { ...expectation, body: "", bodyHash: "" };
3651
+ }
3652
+ return expectation;
3653
+ }
3654
+ function queueForRetry(input, message, kind, existing) {
3655
+ const item = writeOutboxItem(input, {
3656
+ lastError: message,
3657
+ lastFailureKind: kind,
3658
+ existing: existing ?? void 0
3659
+ });
3660
+ const paths = outboxItemPaths(item);
3661
+ if (item.retryCount >= item.maxRetries) {
3662
+ const failed = markOutboxFailed(item, message);
3663
+ return {
3664
+ userStatus: FAILED_STATUS,
3665
+ outboxId: failed.id,
3666
+ outboxPath: paths.jsonPath,
3667
+ bodyPath: paths.bodyPath,
3668
+ lastError: message,
3669
+ idempotencyKey: item.idempotencyKey
3670
+ };
3671
+ }
3672
+ return {
3673
+ userStatus: QUEUED_STATUS,
3674
+ outboxId: item.id,
3675
+ outboxPath: paths.jsonPath,
3676
+ bodyPath: paths.bodyPath,
3677
+ lastError: message,
3678
+ idempotencyKey: item.idempotencyKey
3679
+ };
3680
+ }
3681
+ function markOutboxFailed(item, message) {
3682
+ const failed = {
3683
+ ...item,
3684
+ queueStatus: "failed",
3685
+ userStatus: FAILED_STATUS,
3686
+ lastError: message,
3687
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
3688
+ };
3689
+ saveOutboxItem(failed);
3690
+ return failed;
3691
+ }
3692
+
3693
+ // src/plan-persist/drain.ts
3694
+ import path15 from "node:path";
3695
+ async function drainPlanOutbox(opts = {}, deps = {}) {
3696
+ const items = listOutboxItems().filter(
3697
+ (item) => opts.outboxId ? item.id === opts.outboxId : true
3698
+ );
3699
+ const slice = opts.max && opts.max > 0 ? items.slice(0, opts.max) : items;
3700
+ const result = {
3701
+ processed: 0,
3702
+ succeeded: 0,
3703
+ stillQueued: 0,
3704
+ failed: 0,
3705
+ results: []
3706
+ };
3707
+ for (const item of slice) {
3708
+ result.processed += 1;
3709
+ const body = readOutboxBody(item);
3710
+ const input = outboxInputFromItem(item, body);
3711
+ const attempt = await persistPlan(input, deps);
3712
+ if (attempt.userStatus === "persisted and read back") {
3713
+ result.succeeded += 1;
3714
+ } else if (attempt.userStatus === "failed and needs action") {
3715
+ result.failed += 1;
3716
+ } else {
3717
+ result.stillQueued += 1;
3718
+ }
3719
+ result.results.push({
3720
+ outboxId: item.id,
3721
+ userStatus: attempt.userStatus,
3722
+ lastError: attempt.lastError
3723
+ });
3724
+ }
3725
+ return result;
3726
+ }
3727
+ function loadOutboxById(outboxId) {
3728
+ const jsonPath = path15.join(planOutboxDir(), `${outboxId}.json`);
3729
+ return readOutboxItem(jsonPath);
3730
+ }
3731
+
3732
+ // src/plan-persist/handoff.ts
3733
+ function formatPlanOutboxHandoffBlock(item) {
3734
+ const paths = outboxItemPaths(item);
3735
+ return [
3736
+ "## Plan persistence risk",
3737
+ "",
3738
+ `AgentOS plan write is **not** confirmed (${item.userStatus}).`,
3739
+ `- outboxId: \`${item.id}\``,
3740
+ item.planId ? `- planId: \`${item.planId}\`` : "- planId: (pending \u2014 create not yet applied)",
3741
+ `- outbox: \`${paths.jsonPath}\``,
3742
+ `- body: \`${paths.bodyPath}\``,
3743
+ "",
3744
+ "Drain when approval/connectivity returns: `kynver plan outbox drain`"
3745
+ ].join("\n");
3746
+ }
3747
+ function extractPlanOutboxFromTask(task) {
3748
+ const meta = task.metadata && typeof task.metadata === "object" ? task.metadata : null;
3749
+ const outboxId = typeof task.planPersistenceOutboxId === "string" && task.planPersistenceOutboxId || (meta && typeof meta.planPersistenceOutboxId === "string" ? meta.planPersistenceOutboxId : void 0);
3750
+ if (!outboxId) return null;
3751
+ return {
3752
+ outboxId,
3753
+ jsonPath: typeof task.planPersistenceOutboxPath === "string" ? task.planPersistenceOutboxPath : meta && typeof meta.planPersistenceOutboxPath === "string" ? meta.planPersistenceOutboxPath : void 0,
3754
+ bodyPath: typeof task.planPersistenceBodyPath === "string" ? task.planPersistenceBodyPath : meta && typeof meta.planPersistenceBodyPath === "string" ? meta.planPersistenceBodyPath : void 0
3755
+ };
3756
+ }
3757
+
2915
3758
  // src/dispatch.ts
2916
3759
  var DEFAULT_DISPATCH_LEASE_MS = 60 * 60 * 1e3;
2917
3760
  function readHarnessWorkerContext(decision) {
@@ -2941,14 +3784,29 @@ function normalizePersonaSlug(value) {
2941
3784
  return trimmed.length ? trimmed : null;
2942
3785
  }
2943
3786
  function buildDispatchTaskText(task, agentOsId) {
2944
- return [
3787
+ const lines = [
2945
3788
  `[AgentOS task ${task.id}] ${task.title}`,
2946
3789
  "",
2947
3790
  task.description ? String(task.description) : "(no description on the board task)",
2948
3791
  "",
2949
3792
  `Board linkage: agentOsId=${agentOsId}, taskId=${task.id}, attempt=${task.attempt}, executor=${task.executor}${task.executorRef ? `, executorRef=${task.executorRef}` : ""}.`,
2950
3793
  "This worker was dispatched from the AgentOS board. The harness reports your completion back to the board when you finish."
2951
- ].join("\n");
3794
+ ];
3795
+ const outboxRef = extractPlanOutboxFromTask(task);
3796
+ if (outboxRef?.outboxId) {
3797
+ const item = loadOutboxById(outboxRef.outboxId);
3798
+ if (item) {
3799
+ lines.push("", formatPlanOutboxHandoffBlock(item));
3800
+ } else {
3801
+ lines.push(
3802
+ "",
3803
+ `## Plan persistence risk`,
3804
+ "",
3805
+ `Unconfirmed AgentOS plan write (outboxId=${outboxRef.outboxId}).`
3806
+ );
3807
+ }
3808
+ }
3809
+ return lines.join("\n");
2952
3810
  }
2953
3811
  async function dispatchRun(args) {
2954
3812
  const pipeline = args.pipeline === true || args.pipeline === "true";
@@ -2967,7 +3825,7 @@ async function dispatchRun(args) {
2967
3825
  const activeHarnessWorkers = [];
2968
3826
  for (const name of Object.keys(run.workers || {})) {
2969
3827
  const worker = readJson(
2970
- path12.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
3828
+ path16.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
2971
3829
  void 0
2972
3830
  );
2973
3831
  if (!worker?.taskId || !isPidAlive(worker.pid)) continue;
@@ -3180,7 +4038,7 @@ function redactHarness(text, secret) {
3180
4038
  }
3181
4039
 
3182
4040
  // src/validate.ts
3183
- import path13 from "node:path";
4041
+ import path17 from "node:path";
3184
4042
  var RUN_ID_RE = /^[a-z0-9][a-z0-9._-]{0,127}$/i;
3185
4043
  var WORKER_NAME_RE = /^[a-z0-9][a-z0-9._-]{0,63}$/i;
3186
4044
  function validateRunId(runId) {
@@ -3194,15 +4052,15 @@ function validateWorkerName(name) {
3194
4052
  return trimmed;
3195
4053
  }
3196
4054
  function validateRepo(repo) {
3197
- const resolved = path13.resolve(repo);
4055
+ const resolved = path17.resolve(repo);
3198
4056
  if (resolved.includes("..")) throw new Error("repo path must not contain .. segments");
3199
4057
  return resolved;
3200
4058
  }
3201
4059
  function validateOwnedPaths(repoRoot, ownedPaths) {
3202
4060
  return ownedPaths.map((owned) => {
3203
- const resolved = path13.resolve(repoRoot, owned);
3204
- const rel = path13.relative(repoRoot, resolved);
3205
- if (rel.startsWith("..") || path13.isAbsolute(rel)) {
4061
+ const resolved = path17.resolve(repoRoot, owned);
4062
+ const rel = path17.relative(repoRoot, resolved);
4063
+ if (rel.startsWith("..") || path17.isAbsolute(rel)) {
3206
4064
  throw new Error(`owned path escapes repo: ${owned}`);
3207
4065
  }
3208
4066
  return resolved;
@@ -3214,15 +4072,15 @@ function validateTailLines(lines) {
3214
4072
  }
3215
4073
 
3216
4074
  // src/worktree.ts
3217
- import { existsSync as existsSync11, mkdirSync as mkdirSync4 } from "node:fs";
3218
- import path14 from "node:path";
4075
+ import { existsSync as existsSync14, mkdirSync as mkdirSync5 } from "node:fs";
4076
+ import path18 from "node:path";
3219
4077
  function createRun(args) {
3220
4078
  const repo = validateRepo(required(String(args.repo || ""), "--repo"));
3221
4079
  ensureGitRepo(repo);
3222
4080
  const id = args.id ? validateRunId(String(args.id)) : timestampSlug(String(args.name || "run"));
3223
4081
  const dir = runDirectory(id);
3224
- if (existsSync11(dir)) failExists(`run already exists: ${id}`);
3225
- mkdirSync4(dir, { recursive: true });
4082
+ if (existsSync14(dir)) failExists(`run already exists: ${id}`);
4083
+ mkdirSync5(dir, { recursive: true });
3226
4084
  const base = String(args.base || "origin/main");
3227
4085
  const baseCommit = git(repo, ["rev-parse", base]).trim();
3228
4086
  const run = {
@@ -3235,12 +4093,12 @@ function createRun(args) {
3235
4093
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
3236
4094
  workers: {}
3237
4095
  };
3238
- writeJson(path14.join(dir, "run.json"), run);
4096
+ writeJson(path18.join(dir, "run.json"), run);
3239
4097
  console.log(JSON.stringify({ runId: id, runDir: dir, repo, base, baseCommit }, null, 2));
3240
4098
  }
3241
4099
  function listRuns() {
3242
4100
  const { runsDir } = getPaths();
3243
- const rows = listRunIds(runsDir).map((id) => readJson(path14.join(runDirectory(id), "run.json"), void 0)).filter(Boolean).map((run) => ({
4101
+ const rows = listRunIds(runsDir).map((id) => readJson(path18.join(runDirectory(id), "run.json"), void 0)).filter(Boolean).map((run) => ({
3244
4102
  id: run.id,
3245
4103
  name: run.name,
3246
4104
  status: run.status,
@@ -3255,7 +4113,7 @@ function failExists(message) {
3255
4113
  }
3256
4114
 
3257
4115
  // src/sweep.ts
3258
- import path15 from "node:path";
4116
+ import path19 from "node:path";
3259
4117
  async function sweepRun(args) {
3260
4118
  const pipeline = args.pipeline === true || args.pipeline === "true";
3261
4119
  try {
@@ -3268,7 +4126,7 @@ async function sweepRun(args) {
3268
4126
  const releasedLocalOrphans = [];
3269
4127
  for (const name of Object.keys(run.workers || {})) {
3270
4128
  const worker = readJson(
3271
- path15.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
4129
+ path19.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
3272
4130
  void 0
3273
4131
  );
3274
4132
  if (!worker || !worker.dispatched || !worker.taskId) continue;
@@ -3311,17 +4169,68 @@ async function sweepRun(args) {
3311
4169
  }
3312
4170
 
3313
4171
  // src/cli.ts
3314
- import { mkdirSync as mkdirSync5, realpathSync } from "node:fs";
3315
- import { fileURLToPath as fileURLToPath2 } from "node:url";
4172
+ import { mkdirSync as mkdirSync7, realpathSync } from "node:fs";
4173
+ import { fileURLToPath as fileURLToPath4 } from "node:url";
3316
4174
 
3317
4175
  // src/pipeline-tick.ts
3318
- import path24 from "node:path";
4176
+ import path28 from "node:path";
4177
+
4178
+ // src/pipeline-dispatch.ts
4179
+ var RESERVED_REVIEW_STARTS = 1;
4180
+ function countDispatchStarts(result) {
4181
+ if (!result || typeof result !== "object") return 0;
4182
+ const startedCount = result.startedCount;
4183
+ if (typeof startedCount === "number") return startedCount;
4184
+ const outcomes = result.outcomes;
4185
+ if (!Array.isArray(outcomes)) return 0;
4186
+ return outcomes.filter((o) => o.started).length;
4187
+ }
4188
+ function stripCliMaxStarts(args) {
4189
+ const { maxStarts: _maxStarts, ...rest } = args;
4190
+ return rest;
4191
+ }
4192
+ async function runPipelineDispatch(args, slots) {
4193
+ if (slots <= 0) {
4194
+ return { ok: true, skipped: true, reason: "no slots", maxStarts: 0, startedCount: 0 };
4195
+ }
4196
+ const base = stripCliMaxStarts(args);
4197
+ const reviewBudget = Math.min(slots, RESERVED_REVIEW_STARTS);
4198
+ const workBudget = Math.max(0, slots - reviewBudget);
4199
+ const review = await dispatchRun({
4200
+ ...base,
4201
+ execute: true,
4202
+ pipeline: true,
4203
+ lane: "review",
4204
+ maxStarts: String(reviewBudget)
4205
+ });
4206
+ const reviewStarted = countDispatchStarts(review);
4207
+ const workSlots = workBudget + (reviewBudget - reviewStarted);
4208
+ if (workSlots <= 0) {
4209
+ return {
4210
+ ...typeof review === "object" && review !== null ? review : {},
4211
+ passes: { review },
4212
+ startedCount: reviewStarted
4213
+ };
4214
+ }
4215
+ const work = await dispatchRun({
4216
+ ...base,
4217
+ execute: true,
4218
+ pipeline: true,
4219
+ maxStarts: String(workSlots)
4220
+ });
4221
+ const workStarted = countDispatchStarts(work);
4222
+ return {
4223
+ passes: { review, work },
4224
+ startedCount: reviewStarted + workStarted,
4225
+ ok: true
4226
+ };
4227
+ }
3319
4228
 
3320
4229
  // src/stale-reconcile.ts
3321
- import path17 from "node:path";
4230
+ import path21 from "node:path";
3322
4231
 
3323
4232
  // src/finalize.ts
3324
- import path16 from "node:path";
4233
+ import path20 from "node:path";
3325
4234
  var ACTIVE_RUN_STATUSES = /* @__PURE__ */ new Set(["running", "dispatching", "pending", "queued"]);
3326
4235
  function terminalStatusFor(run) {
3327
4236
  const names = Object.keys(run.workers || {});
@@ -3332,7 +4241,7 @@ function terminalStatusFor(run) {
3332
4241
  let anyLandingBlocked = false;
3333
4242
  for (const name of names) {
3334
4243
  const worker = readJson(
3335
- path16.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
4244
+ path20.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
3336
4245
  void 0
3337
4246
  );
3338
4247
  if (!worker) continue;
@@ -3384,7 +4293,7 @@ function reconcileStaleWorkers() {
3384
4293
  const now = Date.now();
3385
4294
  for (const run of listRunRecords()) {
3386
4295
  for (const name of Object.keys(run.workers || {})) {
3387
- const workerPath = path17.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json");
4296
+ const workerPath = path21.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json");
3388
4297
  const worker = readJson(workerPath, void 0);
3389
4298
  if (!worker || worker.status !== "running") {
3390
4299
  outcomes.push({
@@ -3460,7 +4369,7 @@ function reconcileStaleWorkers() {
3460
4369
  }
3461
4370
 
3462
4371
  // src/plan-progress-daemon-sync.ts
3463
- import path18 from "node:path";
4372
+ import path22 from "node:path";
3464
4373
 
3465
4374
  // src/plan-progress-sync.ts
3466
4375
  async function syncPlanProgress(args) {
@@ -3484,7 +4393,7 @@ async function syncActiveWorkerPlanProgress(runId, args) {
3484
4393
  const outcomes = [];
3485
4394
  for (const name of Object.keys(run.workers || {})) {
3486
4395
  const worker = readJson(
3487
- path18.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
4396
+ path22.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
3488
4397
  void 0
3489
4398
  );
3490
4399
  if (!worker?.dispatched || !worker.taskId) continue;
@@ -3533,7 +4442,7 @@ async function fetchWorkspaceRuntimePreferences(agentOsId, args) {
3533
4442
  }
3534
4443
 
3535
4444
  // src/cleanup.ts
3536
- import path23 from "node:path";
4445
+ import path27 from "node:path";
3537
4446
 
3538
4447
  // src/cleanup-types.ts
3539
4448
  var DEFAULT_NODE_MODULES_AGE_MS = 6 * 60 * 60 * 1e3;
@@ -3604,14 +4513,14 @@ function skipNodeModulesRemoval(input) {
3604
4513
  }
3605
4514
 
3606
4515
  // src/cleanup-execute.ts
3607
- import { existsSync as existsSync13, rmSync } from "node:fs";
3608
- import path20 from "node:path";
4516
+ import { existsSync as existsSync16, rmSync } from "node:fs";
4517
+ import path24 from "node:path";
3609
4518
 
3610
4519
  // src/cleanup-dir-size.ts
3611
- import { existsSync as existsSync12, readdirSync as readdirSync4, statSync as statSync2 } from "node:fs";
3612
- import path19 from "node:path";
4520
+ import { existsSync as existsSync15, readdirSync as readdirSync5, statSync as statSync2 } from "node:fs";
4521
+ import path23 from "node:path";
3613
4522
  function directorySizeBytes(root, maxEntries = 5e4) {
3614
- if (!existsSync12(root)) return 0;
4523
+ if (!existsSync15(root)) return 0;
3615
4524
  let total = 0;
3616
4525
  let seen = 0;
3617
4526
  const stack = [root];
@@ -3619,13 +4528,13 @@ function directorySizeBytes(root, maxEntries = 5e4) {
3619
4528
  const current = stack.pop();
3620
4529
  let entries;
3621
4530
  try {
3622
- entries = readdirSync4(current);
4531
+ entries = readdirSync5(current);
3623
4532
  } catch {
3624
4533
  continue;
3625
4534
  }
3626
4535
  for (const name of entries) {
3627
4536
  if (seen++ > maxEntries) return null;
3628
- const full = path19.join(current, name);
4537
+ const full = path23.join(current, name);
3629
4538
  let st;
3630
4539
  try {
3631
4540
  st = statSync2(full);
@@ -3641,7 +4550,7 @@ function directorySizeBytes(root, maxEntries = 5e4) {
3641
4550
 
3642
4551
  // src/cleanup-execute.ts
3643
4552
  function removeNodeModules(candidate, execute) {
3644
- if (!existsSync13(candidate.path)) {
4553
+ if (!existsSync16(candidate.path)) {
3645
4554
  return {
3646
4555
  ...candidate,
3647
4556
  executed: false,
@@ -3672,7 +4581,7 @@ function removeNodeModules(candidate, execute) {
3672
4581
  }
3673
4582
  }
3674
4583
  function removeWorktree(candidate, execute) {
3675
- if (!existsSync13(candidate.path)) {
4584
+ if (!existsSync16(candidate.path)) {
3676
4585
  return {
3677
4586
  ...candidate,
3678
4587
  executed: false,
@@ -3689,7 +4598,7 @@ function removeWorktree(candidate, execute) {
3689
4598
  if (repo) {
3690
4599
  git(repo, ["worktree", "remove", "--force", candidate.path], { allowFailure: true });
3691
4600
  }
3692
- if (existsSync13(candidate.path)) {
4601
+ if (existsSync16(candidate.path)) {
3693
4602
  rmSync(candidate.path, { recursive: true, force: true });
3694
4603
  }
3695
4604
  return {
@@ -3709,20 +4618,20 @@ function removeWorktree(candidate, execute) {
3709
4618
  }
3710
4619
  }
3711
4620
  function isHarnessNodeModulesPath(targetPath, harnessRoot, worktreesDir) {
3712
- const resolved = path20.resolve(targetPath);
3713
- const nm = resolved.endsWith(`${path20.sep}node_modules`) ? resolved : null;
4621
+ const resolved = path24.resolve(targetPath);
4622
+ const nm = resolved.endsWith(`${path24.sep}node_modules`) ? resolved : null;
3714
4623
  if (!nm) return "path_outside_harness";
3715
- const rel = path20.relative(worktreesDir, nm);
3716
- if (rel.startsWith("..") || path20.isAbsolute(rel)) return "path_outside_harness";
3717
- const parts = rel.split(path20.sep);
4624
+ const rel = path24.relative(worktreesDir, nm);
4625
+ if (rel.startsWith("..") || path24.isAbsolute(rel)) return "path_outside_harness";
4626
+ const parts = rel.split(path24.sep);
3718
4627
  if (parts.length < 3 || parts[parts.length - 1] !== "node_modules") return "path_outside_harness";
3719
- if (!resolved.startsWith(path20.resolve(harnessRoot))) return "path_outside_harness";
4628
+ if (!resolved.startsWith(path24.resolve(harnessRoot))) return "path_outside_harness";
3720
4629
  return null;
3721
4630
  }
3722
4631
 
3723
4632
  // src/cleanup-scan.ts
3724
- import { existsSync as existsSync14, readdirSync as readdirSync5, statSync as statSync3 } from "node:fs";
3725
- import path21 from "node:path";
4633
+ import { existsSync as existsSync17, readdirSync as readdirSync6, statSync as statSync3 } from "node:fs";
4634
+ import path25 from "node:path";
3726
4635
  function pathAgeMs(target, now) {
3727
4636
  try {
3728
4637
  const mtime = statSync3(target).mtimeMs;
@@ -3732,17 +4641,17 @@ function pathAgeMs(target, now) {
3732
4641
  }
3733
4642
  }
3734
4643
  function isPathInside(child, parent) {
3735
- const rel = path21.relative(parent, child);
3736
- return rel === "" || !rel.startsWith("..") && !path21.isAbsolute(rel);
4644
+ const rel = path25.relative(parent, child);
4645
+ return rel === "" || !rel.startsWith("..") && !path25.isAbsolute(rel);
3737
4646
  }
3738
4647
  function scanNodeModulesCandidates(opts) {
3739
4648
  const candidates = [];
3740
4649
  const seen = /* @__PURE__ */ new Set();
3741
4650
  for (const entry of opts.index.values()) {
3742
4651
  if (opts.runIdFilter && entry.runId !== opts.runIdFilter) continue;
3743
- const nm = path21.join(entry.worktreePath, "node_modules");
3744
- if (!existsSync14(nm)) continue;
3745
- const resolved = path21.resolve(nm);
4652
+ const nm = path25.join(entry.worktreePath, "node_modules");
4653
+ if (!existsSync17(nm)) continue;
4654
+ const resolved = path25.resolve(nm);
3746
4655
  if (seen.has(resolved)) continue;
3747
4656
  seen.add(resolved);
3748
4657
  candidates.push({
@@ -3755,16 +4664,16 @@ function scanNodeModulesCandidates(opts) {
3755
4664
  ageMs: pathAgeMs(resolved, opts.now)
3756
4665
  });
3757
4666
  }
3758
- if (!opts.includeOrphans || !existsSync14(opts.worktreesDir)) return candidates;
3759
- for (const runEntry of readdirSync5(opts.worktreesDir, { withFileTypes: true })) {
4667
+ if (!opts.includeOrphans || !existsSync17(opts.worktreesDir)) return candidates;
4668
+ for (const runEntry of readdirSync6(opts.worktreesDir, { withFileTypes: true })) {
3760
4669
  if (!runEntry.isDirectory()) continue;
3761
- const runPath = path21.join(opts.worktreesDir, runEntry.name);
3762
- for (const workerEntry of readdirSync5(runPath, { withFileTypes: true })) {
4670
+ const runPath = path25.join(opts.worktreesDir, runEntry.name);
4671
+ for (const workerEntry of readdirSync6(runPath, { withFileTypes: true })) {
3763
4672
  if (!workerEntry.isDirectory()) continue;
3764
- const worktreePath = path21.join(runPath, workerEntry.name);
3765
- const nm = path21.join(worktreePath, "node_modules");
3766
- if (!existsSync14(nm)) continue;
3767
- const resolved = path21.resolve(nm);
4673
+ const worktreePath = path25.join(runPath, workerEntry.name);
4674
+ const nm = path25.join(worktreePath, "node_modules");
4675
+ if (!existsSync17(nm)) continue;
4676
+ const resolved = path25.resolve(nm);
3768
4677
  if (seen.has(resolved)) continue;
3769
4678
  if (!isPathInside(resolved, opts.harnessRoot)) continue;
3770
4679
  seen.add(resolved);
@@ -3787,7 +4696,7 @@ function scanWorktreeCandidates(opts) {
3787
4696
  for (const entry of opts.index.values()) {
3788
4697
  if (opts.runIdFilter && entry.runId !== opts.runIdFilter) continue;
3789
4698
  const resolved = entry.worktreePath;
3790
- if (!existsSync14(resolved)) continue;
4699
+ if (!existsSync17(resolved)) continue;
3791
4700
  if (seen.has(resolved)) continue;
3792
4701
  seen.add(resolved);
3793
4702
  candidates.push({
@@ -3804,17 +4713,17 @@ function scanWorktreeCandidates(opts) {
3804
4713
  }
3805
4714
 
3806
4715
  // src/cleanup-worktree-index.ts
3807
- import path22 from "node:path";
4716
+ import path26 from "node:path";
3808
4717
  function buildWorktreeIndex() {
3809
4718
  const index = /* @__PURE__ */ new Map();
3810
4719
  for (const run of listRunRecords()) {
3811
4720
  for (const name of Object.keys(run.workers || {})) {
3812
- const workerPath = path22.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json");
4721
+ const workerPath = path26.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json");
3813
4722
  const worker = readJson(workerPath, void 0);
3814
4723
  if (!worker?.worktreePath) continue;
3815
4724
  const status = computeWorkerStatus(worker, { base: run.base, baseCommit: run.baseCommit });
3816
- index.set(path22.resolve(worker.worktreePath), {
3817
- worktreePath: path22.resolve(worker.worktreePath),
4725
+ index.set(path26.resolve(worker.worktreePath), {
4726
+ worktreePath: path26.resolve(worker.worktreePath),
3818
4727
  runId: run.id,
3819
4728
  workerName: name,
3820
4729
  run,
@@ -3828,8 +4737,8 @@ function buildWorktreeIndex() {
3828
4737
 
3829
4738
  // src/cleanup.ts
3830
4739
  function resolveOptions(options = {}) {
3831
- const harnessRoot = options.harnessRoot ? path23.resolve(options.harnessRoot) : resolveHarnessRoot();
3832
- const { worktreesDir } = options.harnessRoot ? { worktreesDir: path23.join(harnessRoot, "worktrees") } : getHarnessPaths();
4740
+ const harnessRoot = options.harnessRoot ? path27.resolve(options.harnessRoot) : resolveHarnessRoot();
4741
+ const { worktreesDir } = options.harnessRoot ? { worktreesDir: path27.join(harnessRoot, "worktrees") } : getHarnessPaths();
3833
4742
  const execute = options.execute === true;
3834
4743
  const nodeModulesAgeMs = options.nodeModulesAgeMs ?? DEFAULT_NODE_MODULES_AGE_MS;
3835
4744
  const worktreesAgeMs = options.worktreesAgeMs ?? 0;
@@ -3873,7 +4782,7 @@ function runHarnessCleanup(options = {}) {
3873
4782
  actions.push({ ...candidate, executed: false, skipped: true, skipReason: pathSkip });
3874
4783
  continue;
3875
4784
  }
3876
- const worktreePath = path23.resolve(candidate.path, "..");
4785
+ const worktreePath = path27.resolve(candidate.path, "..");
3877
4786
  const indexed = index.get(worktreePath) ?? null;
3878
4787
  const guardReason = skipNodeModulesRemoval({
3879
4788
  indexed,
@@ -3889,7 +4798,7 @@ function runHarnessCleanup(options = {}) {
3889
4798
  actions.push(removeNodeModules(candidate, resolved.execute));
3890
4799
  }
3891
4800
  for (const candidate of scanWorktreeCandidates(scanOpts)) {
3892
- const indexed = index.get(path23.resolve(candidate.path)) ?? null;
4801
+ const indexed = index.get(path27.resolve(candidate.path)) ?? null;
3893
4802
  const guardReason = skipWorktreeRemoval({
3894
4803
  indexed,
3895
4804
  includeOrphans: resolved.includeOrphans,
@@ -3958,7 +4867,7 @@ async function completeFinishedWorkers(runId, args) {
3958
4867
  const outcomes = [];
3959
4868
  for (const name of Object.keys(run.workers || {})) {
3960
4869
  const worker = readJson(
3961
- path24.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
4870
+ path28.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
3962
4871
  void 0
3963
4872
  );
3964
4873
  if (!worker?.taskId || worker.localOnly) continue;
@@ -4009,6 +4918,7 @@ async function runPipelineTick(args) {
4009
4918
  configuredMaxWorkersOverride: workspacePrefs?.maxConcurrentWorkers
4010
4919
  });
4011
4920
  const operatorTick = await postOperatorTick(agentOsId, runId, resourceGate, args);
4921
+ const completionAckSync = syncCompletionAcknowledgedFromOperatorTick(runId, operatorTick);
4012
4922
  const leaseRenewal = await renewActiveTaskLeases(runId, args);
4013
4923
  const completedWorkers = await completeFinishedWorkers(runId, args);
4014
4924
  const staleReconcile = reconcileStaleWorkers();
@@ -4023,14 +4933,14 @@ async function runPipelineTick(args) {
4023
4933
  const sweep = await sweepRun({ run: runId, agentOsId, pipeline: true, ...args });
4024
4934
  let dispatch = null;
4025
4935
  if (execute && maxStarts > 0) {
4026
- dispatch = await dispatchRun({
4027
- run: runId,
4028
- agentOsId,
4029
- execute: true,
4030
- maxStarts: String(maxStarts),
4031
- pipeline: true,
4032
- ...args
4033
- });
4936
+ dispatch = await runPipelineDispatch(
4937
+ {
4938
+ ...args,
4939
+ run: runId,
4940
+ agentOsId
4941
+ },
4942
+ maxStarts
4943
+ );
4034
4944
  } else {
4035
4945
  dispatch = {
4036
4946
  ok: true,
@@ -4051,6 +4961,7 @@ async function runPipelineTick(args) {
4051
4961
  staleReconcile,
4052
4962
  harnessCleanup,
4053
4963
  planProgressSync,
4964
+ completionAckSync,
4054
4965
  operatorTick,
4055
4966
  sweep,
4056
4967
  dispatch,
@@ -4122,6 +5033,8 @@ async function emitPlanProgress(args) {
4122
5033
  const url = `${base}/api/agent-os/by-id/${encodeURIComponent(agentOsId)}/plans/${encodeURIComponent(planId)}/progress-events`;
4123
5034
  const cfg = loadUserConfig();
4124
5035
  const provider = cfg.workerProvider ? `provider:${cfg.workerProvider}` : void 0;
5036
+ const explicitProposed = args.proposed === true || args.proposed === "true" ? true : args.proposed === false || args.proposed === "false" ? false : void 0;
5037
+ const proposed = explicitProposed ?? (status !== "done" && (roleLane === "implementer" || roleLane === "repair_implementer"));
4125
5038
  const body = {
4126
5039
  rowKey: args.row ? String(args.row) : void 0,
4127
5040
  rowId: args.rowId ? String(args.rowId) : void 0,
@@ -4132,9 +5045,9 @@ async function emitPlanProgress(args) {
4132
5045
  note: args.note ? String(args.note) : void 0,
4133
5046
  remainingWork: args.remaining ? String(args.remaining) : void 0,
4134
5047
  evidence: evidence.length ? evidence : void 0,
4135
- proposed: args.proposed === true || args.proposed === "true",
4136
5048
  executorRef: args.executorRef ? String(args.executorRef) : provider
4137
5049
  };
5050
+ if (proposed !== void 0) body.proposed = proposed;
4138
5051
  const res = await fetch(url, {
4139
5052
  method: "POST",
4140
5053
  headers: buildHarnessCallbackHeaders(secret),
@@ -4188,6 +5101,86 @@ async function verifyPlan(args) {
4188
5101
  console.log(JSON.stringify(parsed, null, 2));
4189
5102
  }
4190
5103
 
5104
+ // src/plan-persist-cli.ts
5105
+ import { readFileSync as readFileSync8 } from "node:fs";
5106
+ var OPERATIONS = ["create", "add_version", "update_metadata"];
5107
+ var FAILURE_KINDS = [
5108
+ "approval_guard",
5109
+ "auth",
5110
+ "network",
5111
+ "server",
5112
+ "tool_interruption"
5113
+ ];
5114
+ function readBodyArg(args) {
5115
+ const bodyFile = args.bodyFile ? String(args.bodyFile) : void 0;
5116
+ if (bodyFile) {
5117
+ return { body: readFileSync8(bodyFile, "utf8"), bodyPathHint: bodyFile };
5118
+ }
5119
+ const inline = args.body ? String(args.body) : void 0;
5120
+ if (inline) return { body: inline };
5121
+ throw new Error("requires --body-file PATH or --body TEXT");
5122
+ }
5123
+ async function runPlanPersist(args) {
5124
+ const operationRaw = required(args.operation ? String(args.operation) : void 0, "operation");
5125
+ if (!OPERATIONS.includes(operationRaw)) {
5126
+ throw new Error(`invalid --operation ${operationRaw}`);
5127
+ }
5128
+ const operation = operationRaw;
5129
+ const cfg = loadUserConfig();
5130
+ const agentOsSlug = required(
5131
+ args.slug ? String(args.slug) : cfg.agentOsSlug,
5132
+ "slug (or agentOsSlug in ~/.kynver/config.json)"
5133
+ );
5134
+ const title = required(args.title ? String(args.title) : void 0, "title");
5135
+ const { body, bodyPathHint } = readBodyArg(args);
5136
+ if (bodyPathHint && isTmpOnlyPath(bodyPathHint)) {
5137
+ console.warn(
5138
+ JSON.stringify({
5139
+ warning: "/tmp-only body path is not durable; AgentOS persistence requires outbox or successful API write",
5140
+ bodyPathHint
5141
+ })
5142
+ );
5143
+ }
5144
+ const input = {
5145
+ operation,
5146
+ agentOsSlug,
5147
+ title,
5148
+ body,
5149
+ bodyPathHint,
5150
+ summary: args.summary ? String(args.summary) : void 0,
5151
+ planId: args.plan ? String(args.plan) : void 0,
5152
+ planSlug: args.planSlug ? String(args.planSlug) : void 0,
5153
+ changeSummary: args.changeSummary ? String(args.changeSummary) : void 0,
5154
+ author: args.author ? String(args.author) : void 0,
5155
+ model: args.model ? String(args.model) : void 0,
5156
+ maxRetries: args.maxRetries ? Number(args.maxRetries) : void 0,
5157
+ immediateFailure: parseImmediateFailure(args)
5158
+ };
5159
+ const result = await persistPlan(input);
5160
+ console.log(JSON.stringify(result, null, 2));
5161
+ if (result.userStatus === "failed and needs action") process.exit(1);
5162
+ }
5163
+ function parseImmediateFailure(args) {
5164
+ const kind = args.failureKind ? String(args.failureKind) : void 0;
5165
+ if (!kind) return void 0;
5166
+ if (!FAILURE_KINDS.includes(kind)) {
5167
+ throw new Error(`invalid --failure-kind ${kind}`);
5168
+ }
5169
+ const message = args.failureMessage ? String(args.failureMessage) : `immediate failure (${kind})`;
5170
+ return { kind, message };
5171
+ }
5172
+ async function runPlanOutboxList() {
5173
+ const items = listOutboxItems();
5174
+ console.log(JSON.stringify({ count: items.length, items }, null, 2));
5175
+ }
5176
+ async function runPlanOutboxDrain(args) {
5177
+ const max = args.max ? Number(args.max) : void 0;
5178
+ const outboxId = args.id ? String(args.id) : void 0;
5179
+ const result = await drainPlanOutbox({ max, outboxId });
5180
+ console.log(JSON.stringify(result, null, 2));
5181
+ if (result.failed > 0) process.exit(1);
5182
+ }
5183
+
4191
5184
  // src/cleanup-cli.ts
4192
5185
  function runCleanupCli(args) {
4193
5186
  const execute = args.execute === true || args.execute === "true";
@@ -4208,6 +5201,519 @@ function runCleanupCli(args) {
4208
5201
  }
4209
5202
  }
4210
5203
 
5204
+ // src/monitor/monitor.service.ts
5205
+ import path30 from "node:path";
5206
+
5207
+ // src/monitor/monitor.classify.ts
5208
+ function expectedLeaseOwner(runId) {
5209
+ return `kynver-harness:${runId}`;
5210
+ }
5211
+ function classifyWorkerHealth(input) {
5212
+ const { worker, status, taskLease } = input;
5213
+ const leaseOwner = taskLease?.leaseOwner ?? null;
5214
+ const expectedOwner = expectedLeaseOwner(worker.runId);
5215
+ if (worker.dispatched && taskLease) {
5216
+ if (taskLease.status === "running" && leaseOwner && leaseOwner !== expectedOwner) {
5217
+ return {
5218
+ health: "orphaned",
5219
+ reason: `task lease held by ${leaseOwner}, expected ${expectedOwner}`
5220
+ };
5221
+ }
5222
+ if (taskLease.status === "running" && !status.alive && !status.finalResult) {
5223
+ return {
5224
+ health: "orphaned",
5225
+ reason: "board task running but worker process is not alive"
5226
+ };
5227
+ }
5228
+ }
5229
+ if (worker.status === "running" && !status.alive && !status.finalResult) {
5230
+ return {
5231
+ health: "orphaned",
5232
+ reason: "worker.json still running but process is dead"
5233
+ };
5234
+ }
5235
+ if (status.attention.state === "stale") {
5236
+ return { health: "stale", reason: status.attention.reason };
5237
+ }
5238
+ const hbMs = status.lastHeartbeatAt ? Date.parse(status.lastHeartbeatAt) : NaN;
5239
+ if (status.alive && Number.isFinite(hbMs) && Date.now() - hbMs > STALE_MS) {
5240
+ return {
5241
+ health: "stale",
5242
+ reason: `heartbeat older than ${Math.floor(STALE_MS / 1e3)}s`
5243
+ };
5244
+ }
5245
+ if (status.alive && worker.pid && !isPidAlive(worker.pid)) {
5246
+ return { health: "orphaned", reason: "pid recorded but process is not alive" };
5247
+ }
5248
+ if (taskLease?.status === "running" && !status.alive && status.finalResult) {
5249
+ return {
5250
+ health: "healthy",
5251
+ reason: "finished worker awaiting completion replay"
5252
+ };
5253
+ }
5254
+ return {
5255
+ health: "healthy",
5256
+ reason: status.attention.reason || "worker within expected lifecycle bounds"
5257
+ };
5258
+ }
5259
+
5260
+ // src/monitor/monitor.store.ts
5261
+ import { existsSync as existsSync18, mkdirSync as mkdirSync6, readdirSync as readdirSync7, unlinkSync as unlinkSync2 } from "node:fs";
5262
+ import path29 from "node:path";
5263
+ function monitorsDir() {
5264
+ const { harnessRoot } = getHarnessPaths();
5265
+ const dir = path29.join(harnessRoot, "monitors");
5266
+ mkdirSync6(dir, { recursive: true });
5267
+ return dir;
5268
+ }
5269
+ function monitorIdFor(runId, workerName) {
5270
+ return workerName ? `${safeSlug(runId)}--${safeSlug(workerName)}` : safeSlug(runId);
5271
+ }
5272
+ function monitorPath(monitorId) {
5273
+ return path29.join(monitorsDir(), `${monitorId}.json`);
5274
+ }
5275
+ function loadMonitorSession(monitorId) {
5276
+ return readJson(monitorPath(monitorId), void 0);
5277
+ }
5278
+ function saveMonitorSession(session) {
5279
+ writeJson(monitorPath(session.monitorId), session);
5280
+ }
5281
+ function deleteMonitorSession(monitorId) {
5282
+ const file = monitorPath(monitorId);
5283
+ if (!existsSync18(file)) return false;
5284
+ unlinkSync2(file);
5285
+ return true;
5286
+ }
5287
+ function listMonitorSessions() {
5288
+ const dir = monitorsDir();
5289
+ if (!existsSync18(dir)) return [];
5290
+ const entries = [];
5291
+ for (const name of readdirSync7(dir)) {
5292
+ if (!name.endsWith(".json")) continue;
5293
+ const session = readJson(
5294
+ path29.join(dir, name),
5295
+ void 0
5296
+ );
5297
+ if (!session?.monitorId) continue;
5298
+ entries.push({
5299
+ monitorId: session.monitorId,
5300
+ runId: session.runId,
5301
+ workerName: session.workerName,
5302
+ agentOsId: session.agentOsId,
5303
+ pid: session.pid,
5304
+ alive: session.pid ? isPidAlive(session.pid) : false,
5305
+ startedAt: session.startedAt,
5306
+ pollMs: session.pollMs,
5307
+ logPath: session.logPath
5308
+ });
5309
+ }
5310
+ return entries.sort((a, b) => a.startedAt.localeCompare(b.startedAt));
5311
+ }
5312
+
5313
+ // src/monitor/monitor.terminal.ts
5314
+ function assessAutoCompleteEligibility(input) {
5315
+ const { worker, status } = input;
5316
+ const blockers = [];
5317
+ if (worker.localOnly) {
5318
+ blockers.push("local-only worker (no board linkage)");
5319
+ }
5320
+ if (!worker.agentOsId || !worker.taskId) {
5321
+ blockers.push("missing agentOsId/taskId linkage");
5322
+ }
5323
+ if (hasCompletionAck(worker)) {
5324
+ blockers.push("completion already acknowledged");
5325
+ }
5326
+ if (worker.completionBlocker) {
5327
+ blockers.push(worker.completionBlocker);
5328
+ }
5329
+ if (status.heartbeatBlocker && status.alive) {
5330
+ blockers.push(`worker heartbeat blocker: ${status.heartbeatBlocker}`);
5331
+ }
5332
+ if (status.attention.state === "blocked") {
5333
+ blockers.push(status.attention.reason || "worker attention blocked");
5334
+ }
5335
+ if (isLandingBlockedWorkerStatus(status)) {
5336
+ blockers.push(status.attention.reason || "landing gate blocked");
5337
+ }
5338
+ const terminalVerified = isFinishedWorkerStatus(status);
5339
+ let terminalReason;
5340
+ if (terminalVerified) {
5341
+ if (status.finalResult) terminalReason = "final_result";
5342
+ else if (!status.alive) terminalReason = "process_exited";
5343
+ else terminalReason = "terminal_status";
5344
+ } else {
5345
+ blockers.push("worker has not reached a terminal condition");
5346
+ }
5347
+ const eligible = terminalVerified && blockers.length === 0;
5348
+ return {
5349
+ eligible,
5350
+ terminalVerified,
5351
+ terminalReason,
5352
+ blockers
5353
+ };
5354
+ }
5355
+
5356
+ // src/monitor/monitor.task-lease.ts
5357
+ async function fetchTaskLeasesForWorkers(input) {
5358
+ const out = /* @__PURE__ */ new Map();
5359
+ const agentOsId = input.agentOsId?.trim();
5360
+ if (!agentOsId || input.taskIds.length === 0) return out;
5361
+ const base = resolveBaseUrl(input.baseUrl);
5362
+ try {
5363
+ const secret = await resolveCallbackSecretWithMint(input.secret, agentOsId, { baseUrl: base });
5364
+ const url = `${base}/api/agent-os/by-id/${encodeURIComponent(agentOsId)}/harness/monitor/task-leases`;
5365
+ const res = await postJsonWithCredentialRefresh(
5366
+ url,
5367
+ secret,
5368
+ { taskIds: [...new Set(input.taskIds)] },
5369
+ { agentOsId, baseUrl: base }
5370
+ );
5371
+ if (!res.ok || !res.response || typeof res.response !== "object") return out;
5372
+ const rows = res.response.tasks;
5373
+ if (!Array.isArray(rows)) return out;
5374
+ for (const row of rows) {
5375
+ if (row?.taskId) out.set(row.taskId, row);
5376
+ }
5377
+ } catch {
5378
+ }
5379
+ return out;
5380
+ }
5381
+
5382
+ // src/monitor/monitor.service.ts
5383
+ function workerRecord2(runId, name) {
5384
+ return readJson(
5385
+ path30.join(runDirectory(runId), "workers", safeSlug(name), "worker.json"),
5386
+ void 0
5387
+ );
5388
+ }
5389
+ function workerNamesForRun(runId, scope) {
5390
+ const run = loadRun(runId);
5391
+ const names = Object.keys(run.workers || {});
5392
+ if (!scope) return names;
5393
+ const wanted = safeSlug(scope);
5394
+ return names.filter((n) => safeSlug(n) === wanted);
5395
+ }
5396
+ function buildWorkerView(worker, taskLeases) {
5397
+ const run = loadRun(worker.runId);
5398
+ const status = computeWorkerStatus(worker, { base: run.base, baseCommit: run.baseCommit });
5399
+ const taskLease = worker.taskId ? taskLeases.get(worker.taskId) ?? null : null;
5400
+ const health = classifyWorkerHealth({ worker, status, taskLease });
5401
+ const autoComplete = assessAutoCompleteEligibility({ worker, status });
5402
+ return {
5403
+ runId: worker.runId,
5404
+ worker: worker.name,
5405
+ health: health.health,
5406
+ healthReason: health.reason,
5407
+ workerStatus: status.status,
5408
+ attentionState: status.attention.state,
5409
+ attentionReason: status.attention.reason,
5410
+ alive: status.alive,
5411
+ taskId: worker.taskId,
5412
+ leaseOwner: taskLease?.leaseOwner ?? void 0,
5413
+ taskStatus: taskLease?.status,
5414
+ autoComplete,
5415
+ status
5416
+ };
5417
+ }
5418
+ async function runMonitorTick(args) {
5419
+ const runId = String(args.run || "");
5420
+ required(runId, "--run");
5421
+ const scope = args.name ? String(args.name) : void 0;
5422
+ const agentOsId = args.agentOsId ? String(args.agentOsId) : void 0;
5423
+ const run = loadRun(runId);
5424
+ const names = workerNamesForRun(runId, scope);
5425
+ const workers = [];
5426
+ for (const name of names) {
5427
+ const worker = workerRecord2(runId, name);
5428
+ if (worker) workers.push(worker);
5429
+ }
5430
+ const resolvedAgentOsId = agentOsId || workers.map((w) => w.agentOsId).find((id) => typeof id === "string" && id.trim()) || void 0;
5431
+ const taskIds = workers.map((w) => w.taskId).filter((id) => Boolean(id));
5432
+ const taskLeases = await fetchTaskLeasesForWorkers({
5433
+ agentOsId: resolvedAgentOsId,
5434
+ taskIds,
5435
+ baseUrl: args.baseUrl ? String(args.baseUrl) : void 0,
5436
+ secret: args.secret ? String(args.secret) : void 0
5437
+ });
5438
+ const views = workers.map((w) => buildWorkerView(w, taskLeases));
5439
+ let leaseRenewal;
5440
+ if (resolvedAgentOsId && args.renewLeases !== false && args.renewLeases !== "false") {
5441
+ leaseRenewal = await renewActiveTaskLeases(runId, {
5442
+ ...args,
5443
+ agentOsId: resolvedAgentOsId
5444
+ });
5445
+ }
5446
+ const autoCompleted = [];
5447
+ const shouldAutoComplete = args.autoComplete === true || args.autoComplete === "true";
5448
+ if (shouldAutoComplete) {
5449
+ for (const view of views) {
5450
+ if (!view.autoComplete.eligible) {
5451
+ autoCompleted.push({
5452
+ worker: view.worker,
5453
+ outcome: "skipped",
5454
+ ok: false,
5455
+ reason: view.autoComplete.blockers.join("; ") || "not eligible"
5456
+ });
5457
+ continue;
5458
+ }
5459
+ const outcome = await autoCompleteWorker({
5460
+ run: runId,
5461
+ name: view.worker,
5462
+ ...resolvedAgentOsId ? { agentOsId: resolvedAgentOsId } : {},
5463
+ ...args.baseUrl ? { baseUrl: String(args.baseUrl) } : {},
5464
+ ...args.secret ? { secret: String(args.secret) } : {}
5465
+ });
5466
+ autoCompleted.push({
5467
+ worker: view.worker,
5468
+ outcome: outcome.outcome,
5469
+ ok: outcome.outcome === "completed",
5470
+ reason: outcome.reason
5471
+ });
5472
+ }
5473
+ }
5474
+ return {
5475
+ runId,
5476
+ agentOsId: resolvedAgentOsId,
5477
+ workers: views,
5478
+ leaseRenewal,
5479
+ autoCompleted
5480
+ };
5481
+ }
5482
+ function getMonitorStatus(args) {
5483
+ const runId = String(args.run || "");
5484
+ required(runId, "--run");
5485
+ const scope = args.name ? String(args.name) : void 0;
5486
+ const names = workerNamesForRun(runId, scope);
5487
+ const workers = [];
5488
+ for (const name of names) {
5489
+ const worker = workerRecord2(runId, name);
5490
+ if (!worker) continue;
5491
+ workers.push(buildWorkerView(worker, /* @__PURE__ */ new Map()));
5492
+ }
5493
+ return { runId, workers, autoCompleted: [] };
5494
+ }
5495
+ function listMonitors() {
5496
+ return listMonitorSessions();
5497
+ }
5498
+ function stopMonitor(args) {
5499
+ const runId = String(args.run || "");
5500
+ required(runId, "--run");
5501
+ const monitorId = monitorIdFor(runId, args.name ? String(args.name) : void 0);
5502
+ const session = loadMonitorSession(monitorId);
5503
+ if (!session) {
5504
+ return { monitorId, stopped: false };
5505
+ }
5506
+ if (session.pid && isPidAlive(session.pid)) {
5507
+ try {
5508
+ process.kill(session.pid, "SIGTERM");
5509
+ } catch {
5510
+ }
5511
+ }
5512
+ session.stoppedAt = (/* @__PURE__ */ new Date()).toISOString();
5513
+ saveMonitorSession(session);
5514
+ deleteMonitorSession(monitorId);
5515
+ return { monitorId, stopped: true, pid: session.pid };
5516
+ }
5517
+ async function monitorAutoCompleteCli(args) {
5518
+ const runId = String(args.run || "");
5519
+ const name = String(args.name || "");
5520
+ required(runId, "--run");
5521
+ required(name, "--name");
5522
+ const worker = loadWorker(runId, name);
5523
+ const run = loadRun(runId);
5524
+ const status = computeWorkerStatus(worker, { base: run.base, baseCommit: run.baseCommit });
5525
+ const assessment = assessAutoCompleteEligibility({ worker, status });
5526
+ if (!assessment.eligible) {
5527
+ console.log(
5528
+ JSON.stringify(
5529
+ {
5530
+ runId,
5531
+ worker: name,
5532
+ outcome: "blocked",
5533
+ blockers: assessment.blockers,
5534
+ terminalVerified: assessment.terminalVerified
5535
+ },
5536
+ null,
5537
+ 2
5538
+ )
5539
+ );
5540
+ process.exitCode = 1;
5541
+ return;
5542
+ }
5543
+ const outcome = await autoCompleteWorker({
5544
+ ...args,
5545
+ run: runId,
5546
+ name
5547
+ });
5548
+ console.log(JSON.stringify(outcome, null, 2));
5549
+ if (outcome.outcome !== "completed" && outcome.outcome !== "blocked") {
5550
+ process.exitCode = 1;
5551
+ }
5552
+ }
5553
+
5554
+ // src/monitor/monitor-loop.ts
5555
+ var DEFAULT_POLL_MS2 = 5e3;
5556
+ var DEFAULT_MAX_TOTAL_MS2 = 6 * 60 * 60 * 1e3;
5557
+ async function runMonitorLoop(args) {
5558
+ const monitorId = String(args.monitorId || "");
5559
+ const pollMs = Number(args.pollMs) > 0 ? Math.floor(Number(args.pollMs)) : DEFAULT_POLL_MS2;
5560
+ const maxTotalMs = Number(args.maxTotalMs) > 0 ? Math.floor(Number(args.maxTotalMs)) : DEFAULT_MAX_TOTAL_MS2;
5561
+ const startMs = Date.now();
5562
+ while (Date.now() - startMs <= maxTotalMs) {
5563
+ const session = monitorId ? loadMonitorSession(monitorId) : void 0;
5564
+ if (session?.stoppedAt) break;
5565
+ const tick = await runMonitorTick({
5566
+ ...args,
5567
+ autoComplete: args.autoComplete ?? true,
5568
+ renewLeases: args.renewLeases ?? true
5569
+ });
5570
+ console.log(JSON.stringify({ monitorId, phase: "tick", ...tick }));
5571
+ const allTerminal = tick.workers.length > 0 && tick.workers.every(
5572
+ (w) => w.autoComplete.terminalVerified && (w.autoComplete.eligible || w.autoComplete.blockers.some((b) => b.includes("already acknowledged")))
5573
+ );
5574
+ if (allTerminal && tick.autoCompleted.every((a) => a.ok || a.outcome === "skipped")) {
5575
+ if (monitorId && session) {
5576
+ session.stoppedAt = (/* @__PURE__ */ new Date()).toISOString();
5577
+ saveMonitorSession(session);
5578
+ }
5579
+ break;
5580
+ }
5581
+ sleepMs(pollMs);
5582
+ }
5583
+ }
5584
+
5585
+ // src/monitor/monitor-spawn.ts
5586
+ import { spawn as spawn4 } from "node:child_process";
5587
+ import { closeSync as closeSync4, existsSync as existsSync19, openSync as openSync4 } from "node:fs";
5588
+ import path31 from "node:path";
5589
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
5590
+ function resolveDefaultCliPath2() {
5591
+ return path31.join(fileURLToPath3(new URL(".", import.meta.url)), "..", "cli.js");
5592
+ }
5593
+ function spawnMonitorSidecar(opts) {
5594
+ const cliPath = opts.cliPath ?? resolveDefaultCliPath2();
5595
+ if (!existsSync19(cliPath)) return void 0;
5596
+ const monitorId = monitorIdFor(opts.runId, opts.workerName);
5597
+ const { harnessRoot } = getHarnessPaths();
5598
+ const logPath = path31.join(harnessRoot, "monitors", `${monitorId}.log`);
5599
+ let logFd;
5600
+ try {
5601
+ logFd = openSync4(logPath, "a");
5602
+ } catch {
5603
+ logFd = void 0;
5604
+ }
5605
+ const nodeExecutable = opts.nodeExecutable ?? process.execPath;
5606
+ const pollMs = opts.pollMs ?? 5e3;
5607
+ const args = [
5608
+ cliPath,
5609
+ "monitor",
5610
+ "run-loop",
5611
+ "--run",
5612
+ opts.runId,
5613
+ "--monitor-id",
5614
+ monitorId,
5615
+ "--poll-ms",
5616
+ String(pollMs),
5617
+ "--auto-complete",
5618
+ "true",
5619
+ "--renew-leases",
5620
+ "true"
5621
+ ];
5622
+ if (opts.workerName) args.push("--name", opts.workerName);
5623
+ if (opts.agentOsId) args.push("--agent-os-id", opts.agentOsId);
5624
+ if (opts.baseUrl) args.push("--base-url", opts.baseUrl);
5625
+ if (opts.secret) args.push("--secret", opts.secret);
5626
+ const stdio = [
5627
+ "ignore",
5628
+ logFd ?? "ignore",
5629
+ logFd ?? "ignore"
5630
+ ];
5631
+ try {
5632
+ const child = spawn4(
5633
+ nodeExecutable,
5634
+ args,
5635
+ hiddenSpawnOptions({
5636
+ detached: true,
5637
+ stdio,
5638
+ env: process.env
5639
+ })
5640
+ );
5641
+ if (logFd !== void 0) closeSync4(logFd);
5642
+ child.unref();
5643
+ const session = {
5644
+ monitorId,
5645
+ runId: opts.runId,
5646
+ workerName: opts.workerName,
5647
+ agentOsId: opts.agentOsId,
5648
+ pid: child.pid,
5649
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
5650
+ pollMs,
5651
+ logPath
5652
+ };
5653
+ saveMonitorSession(session);
5654
+ return { monitorId, pid: child.pid, logPath, session };
5655
+ } catch {
5656
+ if (logFd !== void 0) {
5657
+ try {
5658
+ closeSync4(logFd);
5659
+ } catch {
5660
+ }
5661
+ }
5662
+ return void 0;
5663
+ }
5664
+ }
5665
+
5666
+ // src/monitor/monitor-cli.ts
5667
+ async function startMonitorCli(args) {
5668
+ const runId = String(args.run || "");
5669
+ required(runId, "--run");
5670
+ const workerName = args.name ? String(args.name) : void 0;
5671
+ const monitorId = monitorIdFor(runId, workerName);
5672
+ const existing = loadMonitorSession(monitorId);
5673
+ if (existing?.pid && !existing.stoppedAt) {
5674
+ return { monitorId, session: existing, spawned: false, pid: existing.pid };
5675
+ }
5676
+ const spawned = spawnMonitorSidecar({
5677
+ runId,
5678
+ workerName,
5679
+ agentOsId: args.agentOsId ? String(args.agentOsId) : void 0,
5680
+ pollMs: Number(args.pollMs) > 0 ? Math.floor(Number(args.pollMs)) : void 0,
5681
+ baseUrl: args.baseUrl ? String(args.baseUrl) : void 0,
5682
+ secret: args.secret ? String(args.secret) : void 0
5683
+ });
5684
+ if (!spawned) {
5685
+ throw new Error("failed to spawn monitor sidecar (cli.js missing or spawn error)");
5686
+ }
5687
+ return {
5688
+ monitorId,
5689
+ session: spawned.session,
5690
+ spawned: true,
5691
+ pid: spawned.pid
5692
+ };
5693
+ }
5694
+ async function monitorStatusCli(args) {
5695
+ const runId = String(args.run || "");
5696
+ if (runId) {
5697
+ const tick = args.tick === true || args.tick === "true" ? await runMonitorTick({ ...args, autoComplete: false }) : getMonitorStatus(args);
5698
+ console.log(JSON.stringify(tick, null, 2));
5699
+ return;
5700
+ }
5701
+ console.log(JSON.stringify({ monitors: listMonitors() }, null, 2));
5702
+ }
5703
+ function monitorStopCli(args) {
5704
+ console.log(JSON.stringify(stopMonitor(args), null, 2));
5705
+ }
5706
+ function monitorListCli() {
5707
+ console.log(JSON.stringify({ monitors: listMonitors() }, null, 2));
5708
+ }
5709
+ async function monitorRunLoopCli(args) {
5710
+ await runMonitorLoop(args);
5711
+ }
5712
+ async function monitorTickCli(args) {
5713
+ const tick = await runMonitorTick(args);
5714
+ console.log(JSON.stringify(tick, null, 2));
5715
+ }
5716
+
4211
5717
  // src/cli.ts
4212
5718
  function isHelpFlag(arg) {
4213
5719
  return arg === "help" || arg === "--help" || arg === "-h";
@@ -4239,17 +5745,28 @@ function usage(code = 0) {
4239
5745
  " 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]",
4240
5746
  " 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]",
4241
5747
  " kynver plan verify --plan PLAN_ID [--worktree PATH] [--task TASK_ID] [--human-override]",
4242
- " kynver cleanup [--execute] [--node-modules-age-ms MS] [--worktrees-age-ms MS] [--harness-root PATH] [--include-orphans]"
5748
+ " 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]",
5749
+ " kynver plan outbox list",
5750
+ " kynver plan outbox drain [--max N] [--id OUTBOX_ID]",
5751
+ " kynver cleanup [--execute] [--node-modules-age-ms MS] [--worktrees-age-ms MS] [--harness-root PATH] [--include-orphans]",
5752
+ " kynver monitor start --run RUN_ID [--name worker] [--agent-os-id AOS_ID] [--poll-ms MS]",
5753
+ " kynver monitor status [--run RUN_ID] [--name worker] [--tick]",
5754
+ " kynver monitor stop --run RUN_ID [--name worker]",
5755
+ " kynver monitor list",
5756
+ " kynver monitor tick --run RUN_ID [--name worker] [--agent-os-id AOS_ID] [--auto-complete] [--renew-leases]",
5757
+ " kynver monitor auto-complete --run RUN_ID --name worker [--agent-os-id AOS_ID] [--base-url URL] [--secret SECRET]",
5758
+ " kynver monitor run-loop --run RUN_ID --monitor-id ID [--name worker] [--agent-os-id AOS_ID] [--poll-ms MS] [--auto-complete] [--renew-leases]"
4243
5759
  ].join("\n")
4244
5760
  );
4245
5761
  process.exit(code);
4246
5762
  }
4247
5763
  async function main(argv = process.argv.slice(2)) {
5764
+ if (handleCliVersionFlag(argv, import.meta.url, "kynver")) return;
4248
5765
  if (argv.length === 0 || isHelpFlag(argv[0])) return usage(0);
4249
5766
  const scope = argv.shift();
4250
5767
  let action;
4251
5768
  let rest;
4252
- if (scope === "run" || scope === "worker" || scope === "plan" || scope === "runner") {
5769
+ if (scope === "run" || scope === "worker" || scope === "plan" || scope === "runner" || scope === "monitor") {
4253
5770
  action = argv.shift();
4254
5771
  rest = argv;
4255
5772
  } else {
@@ -4258,14 +5775,21 @@ async function main(argv = process.argv.slice(2)) {
4258
5775
  if (action && isHelpFlag(action) || rest.some(isHelpFlag)) return usage(0);
4259
5776
  const args = parseArgs(rest);
4260
5777
  const { runsDir, worktreesDir } = getPaths();
4261
- mkdirSync5(runsDir, { recursive: true });
4262
- mkdirSync5(worktreesDir, { recursive: true });
5778
+ mkdirSync7(runsDir, { recursive: true });
5779
+ mkdirSync7(worktreesDir, { recursive: true });
4263
5780
  if (scope === "login") return void await runLogin(args);
4264
5781
  if (scope === "runner" && action === "credential") return void await mintRunnerCredential(args);
4265
5782
  if (scope === "setup") return void await runSetup(args);
4266
5783
  if (scope === "daemon") return void await runDaemon(args);
4267
5784
  if (scope === "plan" && action === "progress") return void await emitPlanProgress(args);
4268
5785
  if (scope === "plan" && action === "verify") return void await verifyPlan(args);
5786
+ if (scope === "plan" && action === "persist") return void await runPlanPersist(args);
5787
+ if (scope === "plan" && action === "outbox") {
5788
+ const outboxAction = rest.shift();
5789
+ if (outboxAction === "list") return void await runPlanOutboxList();
5790
+ if (outboxAction === "drain") return void await runPlanOutboxDrain(parseArgs(rest));
5791
+ unknownCommand("plan", `outbox ${outboxAction ?? ""}`.trim());
5792
+ }
4269
5793
  if (scope === "cleanup") return runCleanupCli(args);
4270
5794
  if (scope === "run" && action === "create") return createRun(args);
4271
5795
  if (scope === "run" && action === "list") return listRuns();
@@ -4278,9 +5802,20 @@ async function main(argv = process.argv.slice(2)) {
4278
5802
  if (scope === "worker" && action === "stop") return stopWorker(args);
4279
5803
  if (scope === "worker" && action === "complete") return void await completeWorker(args);
4280
5804
  if (scope === "worker" && action === "auto-complete") return void await autoCompleteWorkerCli(args);
5805
+ if (scope === "monitor" && action === "start") {
5806
+ const result = await startMonitorCli(args);
5807
+ console.log(JSON.stringify(result, null, 2));
5808
+ return;
5809
+ }
5810
+ if (scope === "monitor" && action === "status") return void await monitorStatusCli(args);
5811
+ if (scope === "monitor" && action === "stop") return monitorStopCli(args);
5812
+ if (scope === "monitor" && action === "list") return monitorListCli();
5813
+ if (scope === "monitor" && action === "tick") return void await monitorTickCli(args);
5814
+ if (scope === "monitor" && action === "auto-complete") return void await monitorAutoCompleteCli(args);
5815
+ if (scope === "monitor" && action === "run-loop") return void await monitorRunLoopCli(args);
4281
5816
  unknownCommand(scope, action);
4282
5817
  }
4283
- var isCliEntry = process.argv[1] && realpathSync.native(process.argv[1]) === realpathSync.native(fileURLToPath2(import.meta.url));
5818
+ var isCliEntry = process.argv[1] && realpathSync.native(process.argv[1]) === realpathSync.native(fileURLToPath4(import.meta.url));
4284
5819
  if (isCliEntry) {
4285
5820
  void main().catch((error) => {
4286
5821
  console.error(error);
@@ -4289,25 +5824,37 @@ if (isCliEntry) {
4289
5824
  }
4290
5825
  export {
4291
5826
  DEFAULT_DISPATCH_LEASE_MS,
5827
+ PACKAGE_VERSION,
5828
+ assessAutoCompleteEligibility,
4292
5829
  assessExitedWorkerSalvage,
4293
5830
  assessPrHandoffRequirement,
4294
5831
  assessWorkerLanding,
5832
+ assessWorkerLandingContract,
4295
5833
  autoCompleteWorker,
4296
5834
  autoCompleteWorkerCli,
4297
5835
  buildDispatchTaskText,
4298
5836
  buildPrompt,
5837
+ classifyWorkerHealth,
4299
5838
  completeWorker,
4300
5839
  computeAttention,
4301
5840
  computeWorkerStatus,
4302
5841
  createRun,
4303
5842
  deriveRunStatus,
4304
5843
  dispatchRun,
5844
+ drainPlanOutbox,
4305
5845
  ensurePrReadyHandoff,
5846
+ extractPlanOutboxFromTask,
4306
5847
  extractPrUrlFromText,
5848
+ formatPlanOutboxHandoffBlock,
4307
5849
  getHarnessPaths,
5850
+ getMonitorStatus,
5851
+ hashPlanBody,
4308
5852
  isFinishedWorkerStatus,
4309
5853
  isLandingBlockedWorkerStatus,
4310
5854
  isTerminalHeartbeatPhase,
5855
+ landingContractAttentionReason,
5856
+ listMonitors,
5857
+ listOutboxItems,
4311
5858
  listRuns,
4312
5859
  loadUserConfig,
4313
5860
  main,
@@ -4317,6 +5864,7 @@ export {
4317
5864
  parseClaudeStream,
4318
5865
  parseHarnessStream,
4319
5866
  parseHeartbeat,
5867
+ persistPlan,
4320
5868
  postJson,
4321
5869
  preflightCursorModel,
4322
5870
  redactHarness,
@@ -4325,9 +5873,11 @@ export {
4325
5873
  resolveCallbackSecretWithMint,
4326
5874
  resolveHarnessRoot,
4327
5875
  runDaemon,
5876
+ runMonitorTick,
4328
5877
  runStatus,
4329
5878
  saveUserConfig,
4330
5879
  spawnCompletionSidecar,
5880
+ spawnMonitorSidecar,
4331
5881
  spawnWorkerProcess,
4332
5882
  startWorker,
4333
5883
  stopWorker,