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