@kynver-app/runtime 0.1.34 → 0.1.38
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 +1650 -114
- package/dist/cli.js.map +4 -4
- package/dist/index.js +1704 -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,580 @@ 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
|
+
if (filePath.startsWith("/tmp/") || filePath.startsWith("/var/folders/")) return true;
|
|
3365
|
+
const resolved = path13.resolve(filePath);
|
|
3366
|
+
return resolved.startsWith("/tmp/") || resolved.startsWith(path13.join("/var", "folders"));
|
|
3367
|
+
}
|
|
3368
|
+
|
|
3369
|
+
// src/plan-persist/outbox-store.ts
|
|
3370
|
+
import {
|
|
3371
|
+
existsSync as existsSync12,
|
|
3372
|
+
readFileSync as readFileSync6,
|
|
3373
|
+
renameSync,
|
|
3374
|
+
readdirSync as readdirSync4,
|
|
3375
|
+
writeFileSync as writeFileSync3,
|
|
3376
|
+
unlinkSync
|
|
3377
|
+
} from "node:fs";
|
|
3378
|
+
import path14 from "node:path";
|
|
3379
|
+
import { randomUUID } from "node:crypto";
|
|
3380
|
+
var DEFAULT_MAX_RETRIES = 12;
|
|
3381
|
+
function listOutboxItems() {
|
|
3382
|
+
const { outboxDir } = ensurePlanOutboxDirs();
|
|
3383
|
+
const files = readdirSync4(outboxDir).filter((f) => f.endsWith(".json"));
|
|
3384
|
+
const items = [];
|
|
3385
|
+
for (const file of files) {
|
|
3386
|
+
const item = readOutboxItem(path14.join(outboxDir, file));
|
|
3387
|
+
if (item && item.queueStatus === "queued") items.push(item);
|
|
3388
|
+
}
|
|
3389
|
+
return items.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
3390
|
+
}
|
|
3391
|
+
function findOutboxByIdempotencyKey(key) {
|
|
3392
|
+
for (const item of listOutboxItems()) {
|
|
3393
|
+
if (item.idempotencyKey === key) return item;
|
|
3394
|
+
}
|
|
3395
|
+
return null;
|
|
3396
|
+
}
|
|
3397
|
+
function readOutboxItem(jsonPath) {
|
|
3398
|
+
if (!existsSync12(jsonPath)) return null;
|
|
3399
|
+
try {
|
|
3400
|
+
return JSON.parse(readFileSync6(jsonPath, "utf8"));
|
|
3401
|
+
} catch {
|
|
3402
|
+
return null;
|
|
3403
|
+
}
|
|
3404
|
+
}
|
|
3405
|
+
function readOutboxBody(item) {
|
|
3406
|
+
const { outboxDir } = ensurePlanOutboxDirs();
|
|
3407
|
+
const bodyFile = path14.join(outboxDir, item.bodyPath);
|
|
3408
|
+
return readFileSync6(bodyFile, "utf8");
|
|
3409
|
+
}
|
|
3410
|
+
function writeOutboxItem(input, opts) {
|
|
3411
|
+
const { outboxDir } = ensurePlanOutboxDirs();
|
|
3412
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3413
|
+
const id = opts.existing?.id ?? randomUUID();
|
|
3414
|
+
const bodyPath = opts.existing?.bodyPath ?? `${id}.body.md`;
|
|
3415
|
+
const jsonPath = path14.join(outboxDir, `${id}.json`);
|
|
3416
|
+
const bodyFile = path14.join(outboxDir, bodyPath);
|
|
3417
|
+
if (!opts.existing) {
|
|
3418
|
+
writeFileSync3(bodyFile, input.body, "utf8");
|
|
3419
|
+
}
|
|
3420
|
+
const item = {
|
|
3421
|
+
id,
|
|
3422
|
+
idempotencyKey: buildPlanPersistIdempotencyKey(input),
|
|
3423
|
+
operation: input.operation,
|
|
3424
|
+
agentOsSlug: input.agentOsSlug,
|
|
3425
|
+
planId: input.planId ?? opts.existing?.planId ?? null,
|
|
3426
|
+
planSlug: input.planSlug ?? opts.existing?.planSlug ?? null,
|
|
3427
|
+
title: input.title,
|
|
3428
|
+
summary: input.summary ?? null,
|
|
3429
|
+
bodyPath,
|
|
3430
|
+
bodyHash: hashPlanBody(input.body),
|
|
3431
|
+
author: input.author ?? null,
|
|
3432
|
+
model: input.model ?? null,
|
|
3433
|
+
sourceRefs: input.sourceRefs ?? null,
|
|
3434
|
+
changeSummary: input.changeSummary ?? null,
|
|
3435
|
+
markCurrent: input.markCurrent ?? true,
|
|
3436
|
+
createdAt: opts.existing?.createdAt ?? now,
|
|
3437
|
+
updatedAt: now,
|
|
3438
|
+
retryCount: (opts.existing?.retryCount ?? 0) + (opts.existing ? 1 : 0),
|
|
3439
|
+
maxRetries: input.maxRetries ?? opts.existing?.maxRetries ?? DEFAULT_MAX_RETRIES,
|
|
3440
|
+
lastError: opts.lastError,
|
|
3441
|
+
lastFailureKind: opts.lastFailureKind,
|
|
3442
|
+
queueStatus: "queued",
|
|
3443
|
+
userStatus: "queued for retry",
|
|
3444
|
+
readbackEvidence: null
|
|
3445
|
+
};
|
|
3446
|
+
writeFileSync3(jsonPath, `${JSON.stringify(item, null, 2)}
|
|
3447
|
+
`, { mode: 384 });
|
|
3448
|
+
return item;
|
|
3449
|
+
}
|
|
3450
|
+
function saveOutboxItem(item) {
|
|
3451
|
+
const { outboxDir } = ensurePlanOutboxDirs();
|
|
3452
|
+
const jsonPath = path14.join(outboxDir, `${item.id}.json`);
|
|
3453
|
+
writeFileSync3(jsonPath, `${JSON.stringify(item, null, 2)}
|
|
3454
|
+
`, { mode: 384 });
|
|
3455
|
+
}
|
|
3456
|
+
function archiveOutboxItem(item) {
|
|
3457
|
+
const { outboxDir, archiveDir } = ensurePlanOutboxDirs();
|
|
3458
|
+
const jsonSrc = path14.join(outboxDir, `${item.id}.json`);
|
|
3459
|
+
const bodySrc = path14.join(outboxDir, item.bodyPath);
|
|
3460
|
+
const jsonDst = path14.join(archiveDir, `${item.id}.json`);
|
|
3461
|
+
const bodyDst = path14.join(archiveDir, item.bodyPath);
|
|
3462
|
+
if (existsSync12(jsonSrc)) renameSync(jsonSrc, jsonDst);
|
|
3463
|
+
if (existsSync12(bodySrc)) renameSync(bodySrc, bodyDst);
|
|
3464
|
+
}
|
|
3465
|
+
function outboxItemPaths(item) {
|
|
3466
|
+
const { outboxDir } = ensurePlanOutboxDirs();
|
|
3467
|
+
return {
|
|
3468
|
+
jsonPath: path14.join(outboxDir, `${item.id}.json`),
|
|
3469
|
+
bodyPath: path14.join(outboxDir, item.bodyPath)
|
|
3470
|
+
};
|
|
3471
|
+
}
|
|
3472
|
+
function outboxInputFromItem(item, body) {
|
|
3473
|
+
return {
|
|
3474
|
+
operation: item.operation,
|
|
3475
|
+
agentOsSlug: item.agentOsSlug,
|
|
3476
|
+
planId: item.planId,
|
|
3477
|
+
planSlug: item.planSlug,
|
|
3478
|
+
title: item.title,
|
|
3479
|
+
summary: item.summary,
|
|
3480
|
+
body,
|
|
3481
|
+
changeSummary: item.changeSummary ?? void 0,
|
|
3482
|
+
author: item.author ?? void 0,
|
|
3483
|
+
model: item.model ?? void 0,
|
|
3484
|
+
sourceRefs: item.sourceRefs,
|
|
3485
|
+
markCurrent: item.markCurrent ?? true,
|
|
3486
|
+
maxRetries: item.maxRetries
|
|
3487
|
+
};
|
|
3488
|
+
}
|
|
3489
|
+
|
|
3490
|
+
// src/plan-persist/readback.ts
|
|
3491
|
+
async function verifyPlanReadback(slug, expectation, deps = {}) {
|
|
3492
|
+
const payload = await agentOsGetPlan(slug, expectation.planId, deps);
|
|
3493
|
+
const plan = payload.plan;
|
|
3494
|
+
const current = payload.currentVersion;
|
|
3495
|
+
if (plan.title.trim() !== expectation.title.trim()) {
|
|
3496
|
+
throw new PlanPersistError(
|
|
3497
|
+
"verification_failed",
|
|
3498
|
+
`title mismatch: expected "${expectation.title}", got "${plan.title}"`
|
|
3499
|
+
);
|
|
3500
|
+
}
|
|
3501
|
+
const expectedSummaryHash = hashSummary(expectation.summary);
|
|
3502
|
+
const actualSummaryHash = hashSummary(plan.summary);
|
|
3503
|
+
if (expectedSummaryHash !== actualSummaryHash) {
|
|
3504
|
+
throw new PlanPersistError("verification_failed", "summary mismatch on readback");
|
|
3505
|
+
}
|
|
3506
|
+
if (expectation.versionId && plan.currentVersionId !== expectation.versionId) {
|
|
3507
|
+
throw new PlanPersistError(
|
|
3508
|
+
"verification_failed",
|
|
3509
|
+
`currentVersionId mismatch: expected ${expectation.versionId}, got ${plan.currentVersionId}`
|
|
3510
|
+
);
|
|
3511
|
+
}
|
|
3512
|
+
if (expectation.versionNumber != null) {
|
|
3513
|
+
if (!current || current.versionNumber !== expectation.versionNumber) {
|
|
3514
|
+
throw new PlanPersistError(
|
|
3515
|
+
"verification_failed",
|
|
3516
|
+
`versionNumber mismatch: expected ${expectation.versionNumber}, got ${current?.versionNumber ?? "none"}`
|
|
3517
|
+
);
|
|
3518
|
+
}
|
|
3519
|
+
}
|
|
3520
|
+
const bodyForHash = current?.body ?? "";
|
|
3521
|
+
const actualBodyHash = hashPlanBody(bodyForHash);
|
|
3522
|
+
if (expectation.bodyHash && actualBodyHash !== expectation.bodyHash) {
|
|
3523
|
+
throw new PlanPersistError("verification_failed", "body hash mismatch on readback");
|
|
3524
|
+
}
|
|
3525
|
+
return {
|
|
3526
|
+
planId: plan.id,
|
|
3527
|
+
currentVersionId: plan.currentVersionId,
|
|
3528
|
+
versionNumber: current?.versionNumber ?? null,
|
|
3529
|
+
title: plan.title,
|
|
3530
|
+
summary: plan.summary,
|
|
3531
|
+
bodyHash: expectation.bodyHash || actualBodyHash,
|
|
3532
|
+
readAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3533
|
+
};
|
|
3534
|
+
}
|
|
3535
|
+
function buildReadbackExpectation(input, write) {
|
|
3536
|
+
return {
|
|
3537
|
+
planId: write.planId,
|
|
3538
|
+
title: input.title,
|
|
3539
|
+
summary: input.summary ?? null,
|
|
3540
|
+
body: input.body,
|
|
3541
|
+
bodyHash: hashPlanBody(input.body),
|
|
3542
|
+
versionId: write.versionId,
|
|
3543
|
+
versionNumber: write.versionNumber
|
|
3544
|
+
};
|
|
3545
|
+
}
|
|
3546
|
+
|
|
3547
|
+
// src/plan-persist/persist.ts
|
|
3548
|
+
var SUCCESS_STATUS = "persisted and read back";
|
|
3549
|
+
var QUEUED_STATUS = "queued for retry";
|
|
3550
|
+
var FAILED_STATUS = "failed and needs action";
|
|
3551
|
+
async function persistPlan(input, deps = {}) {
|
|
3552
|
+
if (input.bodyPathHint && isTmpOnlyPath(input.bodyPathHint)) {
|
|
3553
|
+
}
|
|
3554
|
+
const idempotencyKey = buildPlanPersistIdempotencyKey(input);
|
|
3555
|
+
const existingOutbox = findOutboxByIdempotencyKey(idempotencyKey);
|
|
3556
|
+
if (existingOutbox?.readbackEvidence) {
|
|
3557
|
+
return {
|
|
3558
|
+
userStatus: SUCCESS_STATUS,
|
|
3559
|
+
outboxId: existingOutbox.id,
|
|
3560
|
+
planId: existingOutbox.readbackEvidence.planId,
|
|
3561
|
+
readbackEvidence: existingOutbox.readbackEvidence,
|
|
3562
|
+
idempotencyKey
|
|
3563
|
+
};
|
|
3564
|
+
}
|
|
3565
|
+
if (input.immediateFailure) {
|
|
3566
|
+
return queueForRetry(input, input.immediateFailure.message, input.immediateFailure.kind, existingOutbox);
|
|
3567
|
+
}
|
|
3568
|
+
const writeFn = deps.writePlan ?? agentOsWritePlan;
|
|
3569
|
+
const verifyFn = deps.verifyReadback ?? verifyPlanReadback;
|
|
3570
|
+
try {
|
|
3571
|
+
const write = await writeFn(input, deps);
|
|
3572
|
+
const enriched = { ...input, planId: write.planId };
|
|
3573
|
+
const expectation = buildReadbackExpectation(enriched, write);
|
|
3574
|
+
const readback = await verifyFn(
|
|
3575
|
+
input.agentOsSlug,
|
|
3576
|
+
readbackExpectationForOperation(input, expectation),
|
|
3577
|
+
deps
|
|
3578
|
+
);
|
|
3579
|
+
if (existingOutbox) archiveOutboxItem(existingOutbox);
|
|
3580
|
+
return {
|
|
3581
|
+
userStatus: SUCCESS_STATUS,
|
|
3582
|
+
planId: write.planId,
|
|
3583
|
+
versionId: write.versionId ?? void 0,
|
|
3584
|
+
readbackEvidence: readback,
|
|
3585
|
+
idempotencyKey
|
|
3586
|
+
};
|
|
3587
|
+
} catch (err) {
|
|
3588
|
+
const failure = err instanceof PlanPersistError ? err : new PlanPersistError("tool_interruption", err instanceof Error ? err.message : String(err));
|
|
3589
|
+
if (!isRetryableFailure(failure.kind)) {
|
|
3590
|
+
const item = writeOutboxItem(input, {
|
|
3591
|
+
lastError: failure.message,
|
|
3592
|
+
lastFailureKind: failure.kind,
|
|
3593
|
+
existing: existingOutbox ?? void 0
|
|
3594
|
+
});
|
|
3595
|
+
const paths = outboxItemPaths(item);
|
|
3596
|
+
const failed = markOutboxFailed(item, failure.message);
|
|
3597
|
+
return {
|
|
3598
|
+
userStatus: FAILED_STATUS,
|
|
3599
|
+
outboxId: failed.id,
|
|
3600
|
+
outboxPath: paths.jsonPath,
|
|
3601
|
+
bodyPath: paths.bodyPath,
|
|
3602
|
+
lastError: failure.message,
|
|
3603
|
+
idempotencyKey
|
|
3604
|
+
};
|
|
3605
|
+
}
|
|
3606
|
+
return queueForRetry(input, failure.message, failure.kind, existingOutbox);
|
|
3607
|
+
}
|
|
3608
|
+
}
|
|
3609
|
+
function readbackExpectationForOperation(input, expectation) {
|
|
3610
|
+
if (input.operation === "update_metadata") {
|
|
3611
|
+
return { ...expectation, body: "", bodyHash: "" };
|
|
3612
|
+
}
|
|
3613
|
+
return expectation;
|
|
3614
|
+
}
|
|
3615
|
+
function queueForRetry(input, message, kind, existing) {
|
|
3616
|
+
const item = writeOutboxItem(input, {
|
|
3617
|
+
lastError: message,
|
|
3618
|
+
lastFailureKind: kind,
|
|
3619
|
+
existing: existing ?? void 0
|
|
3620
|
+
});
|
|
3621
|
+
const paths = outboxItemPaths(item);
|
|
3622
|
+
if (item.retryCount >= item.maxRetries) {
|
|
3623
|
+
const failed = markOutboxFailed(item, message);
|
|
3624
|
+
return {
|
|
3625
|
+
userStatus: FAILED_STATUS,
|
|
3626
|
+
outboxId: failed.id,
|
|
3627
|
+
outboxPath: paths.jsonPath,
|
|
3628
|
+
bodyPath: paths.bodyPath,
|
|
3629
|
+
lastError: message,
|
|
3630
|
+
idempotencyKey: item.idempotencyKey
|
|
3631
|
+
};
|
|
3632
|
+
}
|
|
3633
|
+
return {
|
|
3634
|
+
userStatus: QUEUED_STATUS,
|
|
3635
|
+
outboxId: item.id,
|
|
3636
|
+
outboxPath: paths.jsonPath,
|
|
3637
|
+
bodyPath: paths.bodyPath,
|
|
3638
|
+
lastError: message,
|
|
3639
|
+
idempotencyKey: item.idempotencyKey
|
|
3640
|
+
};
|
|
3641
|
+
}
|
|
3642
|
+
function markOutboxFailed(item, message) {
|
|
3643
|
+
const failed = {
|
|
3644
|
+
...item,
|
|
3645
|
+
queueStatus: "failed",
|
|
3646
|
+
userStatus: FAILED_STATUS,
|
|
3647
|
+
lastError: message,
|
|
3648
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3649
|
+
};
|
|
3650
|
+
saveOutboxItem(failed);
|
|
3651
|
+
return failed;
|
|
3652
|
+
}
|
|
3653
|
+
|
|
3654
|
+
// src/plan-persist/drain.ts
|
|
3655
|
+
import path15 from "node:path";
|
|
3656
|
+
async function drainPlanOutbox(opts = {}, deps = {}) {
|
|
3657
|
+
const items = listOutboxItems().filter(
|
|
3658
|
+
(item) => opts.outboxId ? item.id === opts.outboxId : true
|
|
3659
|
+
);
|
|
3660
|
+
const slice = opts.max && opts.max > 0 ? items.slice(0, opts.max) : items;
|
|
3661
|
+
const result = {
|
|
3662
|
+
processed: 0,
|
|
3663
|
+
succeeded: 0,
|
|
3664
|
+
stillQueued: 0,
|
|
3665
|
+
failed: 0,
|
|
3666
|
+
results: []
|
|
3667
|
+
};
|
|
3668
|
+
for (const item of slice) {
|
|
3669
|
+
result.processed += 1;
|
|
3670
|
+
const body = readOutboxBody(item);
|
|
3671
|
+
const input = outboxInputFromItem(item, body);
|
|
3672
|
+
const attempt = await persistPlan(input, deps);
|
|
3673
|
+
if (attempt.userStatus === "persisted and read back") {
|
|
3674
|
+
result.succeeded += 1;
|
|
3675
|
+
} else if (attempt.userStatus === "failed and needs action") {
|
|
3676
|
+
result.failed += 1;
|
|
3677
|
+
} else {
|
|
3678
|
+
result.stillQueued += 1;
|
|
3679
|
+
}
|
|
3680
|
+
result.results.push({
|
|
3681
|
+
outboxId: item.id,
|
|
3682
|
+
userStatus: attempt.userStatus,
|
|
3683
|
+
lastError: attempt.lastError
|
|
3684
|
+
});
|
|
3685
|
+
}
|
|
3686
|
+
return result;
|
|
3687
|
+
}
|
|
3688
|
+
function loadOutboxById(outboxId) {
|
|
3689
|
+
const jsonPath = path15.join(planOutboxDir(), `${outboxId}.json`);
|
|
3690
|
+
return readOutboxItem(jsonPath);
|
|
3691
|
+
}
|
|
3692
|
+
|
|
3693
|
+
// src/plan-persist/handoff.ts
|
|
3694
|
+
function formatPlanOutboxHandoffBlock(item) {
|
|
3695
|
+
const paths = outboxItemPaths(item);
|
|
3696
|
+
return [
|
|
3697
|
+
"## Plan persistence risk",
|
|
3698
|
+
"",
|
|
3699
|
+
`AgentOS plan write is **not** confirmed (${item.userStatus}).`,
|
|
3700
|
+
`- outboxId: \`${item.id}\``,
|
|
3701
|
+
item.planId ? `- planId: \`${item.planId}\`` : "- planId: (pending \u2014 create not yet applied)",
|
|
3702
|
+
`- outbox: \`${paths.jsonPath}\``,
|
|
3703
|
+
`- body: \`${paths.bodyPath}\``,
|
|
3704
|
+
"",
|
|
3705
|
+
"Drain when approval/connectivity returns: `kynver plan outbox drain`"
|
|
3706
|
+
].join("\n");
|
|
3707
|
+
}
|
|
3708
|
+
function extractPlanOutboxFromTask(task) {
|
|
3709
|
+
const meta = task.metadata && typeof task.metadata === "object" ? task.metadata : null;
|
|
3710
|
+
const outboxId = typeof task.planPersistenceOutboxId === "string" && task.planPersistenceOutboxId || (meta && typeof meta.planPersistenceOutboxId === "string" ? meta.planPersistenceOutboxId : void 0);
|
|
3711
|
+
if (!outboxId) return null;
|
|
3712
|
+
return {
|
|
3713
|
+
outboxId,
|
|
3714
|
+
jsonPath: typeof task.planPersistenceOutboxPath === "string" ? task.planPersistenceOutboxPath : meta && typeof meta.planPersistenceOutboxPath === "string" ? meta.planPersistenceOutboxPath : void 0,
|
|
3715
|
+
bodyPath: typeof task.planPersistenceBodyPath === "string" ? task.planPersistenceBodyPath : meta && typeof meta.planPersistenceBodyPath === "string" ? meta.planPersistenceBodyPath : void 0
|
|
3716
|
+
};
|
|
3717
|
+
}
|
|
3718
|
+
|
|
2911
3719
|
// src/dispatch.ts
|
|
2912
3720
|
var DEFAULT_DISPATCH_LEASE_MS = 60 * 60 * 1e3;
|
|
2913
3721
|
function readHarnessWorkerContext(decision) {
|
|
@@ -2937,14 +3745,29 @@ function normalizePersonaSlug(value) {
|
|
|
2937
3745
|
return trimmed.length ? trimmed : null;
|
|
2938
3746
|
}
|
|
2939
3747
|
function buildDispatchTaskText(task, agentOsId) {
|
|
2940
|
-
|
|
3748
|
+
const lines = [
|
|
2941
3749
|
`[AgentOS task ${task.id}] ${task.title}`,
|
|
2942
3750
|
"",
|
|
2943
3751
|
task.description ? String(task.description) : "(no description on the board task)",
|
|
2944
3752
|
"",
|
|
2945
3753
|
`Board linkage: agentOsId=${agentOsId}, taskId=${task.id}, attempt=${task.attempt}, executor=${task.executor}${task.executorRef ? `, executorRef=${task.executorRef}` : ""}.`,
|
|
2946
3754
|
"This worker was dispatched from the AgentOS board. The harness reports your completion back to the board when you finish."
|
|
2947
|
-
]
|
|
3755
|
+
];
|
|
3756
|
+
const outboxRef = extractPlanOutboxFromTask(task);
|
|
3757
|
+
if (outboxRef?.outboxId) {
|
|
3758
|
+
const item = loadOutboxById(outboxRef.outboxId);
|
|
3759
|
+
if (item) {
|
|
3760
|
+
lines.push("", formatPlanOutboxHandoffBlock(item));
|
|
3761
|
+
} else {
|
|
3762
|
+
lines.push(
|
|
3763
|
+
"",
|
|
3764
|
+
`## Plan persistence risk`,
|
|
3765
|
+
"",
|
|
3766
|
+
`Unconfirmed AgentOS plan write (outboxId=${outboxRef.outboxId}).`
|
|
3767
|
+
);
|
|
3768
|
+
}
|
|
3769
|
+
}
|
|
3770
|
+
return lines.join("\n");
|
|
2948
3771
|
}
|
|
2949
3772
|
async function dispatchRun(args) {
|
|
2950
3773
|
const pipeline = args.pipeline === true || args.pipeline === "true";
|
|
@@ -2963,7 +3786,7 @@ async function dispatchRun(args) {
|
|
|
2963
3786
|
const activeHarnessWorkers = [];
|
|
2964
3787
|
for (const name of Object.keys(run.workers || {})) {
|
|
2965
3788
|
const worker = readJson(
|
|
2966
|
-
|
|
3789
|
+
path16.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
2967
3790
|
void 0
|
|
2968
3791
|
);
|
|
2969
3792
|
if (!worker?.taskId || !isPidAlive(worker.pid)) continue;
|
|
@@ -3167,7 +3990,7 @@ async function dispatchRun(args) {
|
|
|
3167
3990
|
}
|
|
3168
3991
|
|
|
3169
3992
|
// src/sweep.ts
|
|
3170
|
-
import
|
|
3993
|
+
import path17 from "node:path";
|
|
3171
3994
|
async function sweepRun(args) {
|
|
3172
3995
|
const pipeline = args.pipeline === true || args.pipeline === "true";
|
|
3173
3996
|
try {
|
|
@@ -3180,7 +4003,7 @@ async function sweepRun(args) {
|
|
|
3180
4003
|
const releasedLocalOrphans = [];
|
|
3181
4004
|
for (const name of Object.keys(run.workers || {})) {
|
|
3182
4005
|
const worker = readJson(
|
|
3183
|
-
|
|
4006
|
+
path17.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
3184
4007
|
void 0
|
|
3185
4008
|
);
|
|
3186
4009
|
if (!worker || !worker.dispatched || !worker.taskId) continue;
|
|
@@ -3223,11 +4046,11 @@ async function sweepRun(args) {
|
|
|
3223
4046
|
}
|
|
3224
4047
|
|
|
3225
4048
|
// src/worktree.ts
|
|
3226
|
-
import { existsSync as
|
|
3227
|
-
import
|
|
4049
|
+
import { existsSync as existsSync13, mkdirSync as mkdirSync5 } from "node:fs";
|
|
4050
|
+
import path19 from "node:path";
|
|
3228
4051
|
|
|
3229
4052
|
// src/validate.ts
|
|
3230
|
-
import
|
|
4053
|
+
import path18 from "node:path";
|
|
3231
4054
|
var RUN_ID_RE = /^[a-z0-9][a-z0-9._-]{0,127}$/i;
|
|
3232
4055
|
function validateRunId(runId) {
|
|
3233
4056
|
const trimmed = runId.trim();
|
|
@@ -3235,7 +4058,7 @@ function validateRunId(runId) {
|
|
|
3235
4058
|
return trimmed;
|
|
3236
4059
|
}
|
|
3237
4060
|
function validateRepo(repo) {
|
|
3238
|
-
const resolved =
|
|
4061
|
+
const resolved = path18.resolve(repo);
|
|
3239
4062
|
if (resolved.includes("..")) throw new Error("repo path must not contain .. segments");
|
|
3240
4063
|
return resolved;
|
|
3241
4064
|
}
|
|
@@ -3246,8 +4069,8 @@ function createRun(args) {
|
|
|
3246
4069
|
ensureGitRepo(repo);
|
|
3247
4070
|
const id = args.id ? validateRunId(String(args.id)) : timestampSlug(String(args.name || "run"));
|
|
3248
4071
|
const dir = runDirectory(id);
|
|
3249
|
-
if (
|
|
3250
|
-
|
|
4072
|
+
if (existsSync13(dir)) failExists(`run already exists: ${id}`);
|
|
4073
|
+
mkdirSync5(dir, { recursive: true });
|
|
3251
4074
|
const base = String(args.base || "origin/main");
|
|
3252
4075
|
const baseCommit = git(repo, ["rev-parse", base]).trim();
|
|
3253
4076
|
const run = {
|
|
@@ -3260,12 +4083,12 @@ function createRun(args) {
|
|
|
3260
4083
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3261
4084
|
workers: {}
|
|
3262
4085
|
};
|
|
3263
|
-
writeJson(
|
|
4086
|
+
writeJson(path19.join(dir, "run.json"), run);
|
|
3264
4087
|
console.log(JSON.stringify({ runId: id, runDir: dir, repo, base, baseCommit }, null, 2));
|
|
3265
4088
|
}
|
|
3266
4089
|
function listRuns() {
|
|
3267
4090
|
const { runsDir } = getPaths();
|
|
3268
|
-
const rows = listRunIds(runsDir).map((id) => readJson(
|
|
4091
|
+
const rows = listRunIds(runsDir).map((id) => readJson(path19.join(runDirectory(id), "run.json"), void 0)).filter(Boolean).map((run) => ({
|
|
3269
4092
|
id: run.id,
|
|
3270
4093
|
name: run.name,
|
|
3271
4094
|
status: run.status,
|
|
@@ -3280,13 +4103,64 @@ function failExists(message) {
|
|
|
3280
4103
|
}
|
|
3281
4104
|
|
|
3282
4105
|
// src/pipeline-tick.ts
|
|
3283
|
-
import
|
|
4106
|
+
import path28 from "node:path";
|
|
4107
|
+
|
|
4108
|
+
// src/pipeline-dispatch.ts
|
|
4109
|
+
var RESERVED_REVIEW_STARTS = 1;
|
|
4110
|
+
function countDispatchStarts(result) {
|
|
4111
|
+
if (!result || typeof result !== "object") return 0;
|
|
4112
|
+
const startedCount = result.startedCount;
|
|
4113
|
+
if (typeof startedCount === "number") return startedCount;
|
|
4114
|
+
const outcomes = result.outcomes;
|
|
4115
|
+
if (!Array.isArray(outcomes)) return 0;
|
|
4116
|
+
return outcomes.filter((o) => o.started).length;
|
|
4117
|
+
}
|
|
4118
|
+
function stripCliMaxStarts(args) {
|
|
4119
|
+
const { maxStarts: _maxStarts, ...rest } = args;
|
|
4120
|
+
return rest;
|
|
4121
|
+
}
|
|
4122
|
+
async function runPipelineDispatch(args, slots) {
|
|
4123
|
+
if (slots <= 0) {
|
|
4124
|
+
return { ok: true, skipped: true, reason: "no slots", maxStarts: 0, startedCount: 0 };
|
|
4125
|
+
}
|
|
4126
|
+
const base = stripCliMaxStarts(args);
|
|
4127
|
+
const reviewBudget = Math.min(slots, RESERVED_REVIEW_STARTS);
|
|
4128
|
+
const workBudget = Math.max(0, slots - reviewBudget);
|
|
4129
|
+
const review = await dispatchRun({
|
|
4130
|
+
...base,
|
|
4131
|
+
execute: true,
|
|
4132
|
+
pipeline: true,
|
|
4133
|
+
lane: "review",
|
|
4134
|
+
maxStarts: String(reviewBudget)
|
|
4135
|
+
});
|
|
4136
|
+
const reviewStarted = countDispatchStarts(review);
|
|
4137
|
+
const workSlots = workBudget + (reviewBudget - reviewStarted);
|
|
4138
|
+
if (workSlots <= 0) {
|
|
4139
|
+
return {
|
|
4140
|
+
...typeof review === "object" && review !== null ? review : {},
|
|
4141
|
+
passes: { review },
|
|
4142
|
+
startedCount: reviewStarted
|
|
4143
|
+
};
|
|
4144
|
+
}
|
|
4145
|
+
const work = await dispatchRun({
|
|
4146
|
+
...base,
|
|
4147
|
+
execute: true,
|
|
4148
|
+
pipeline: true,
|
|
4149
|
+
maxStarts: String(workSlots)
|
|
4150
|
+
});
|
|
4151
|
+
const workStarted = countDispatchStarts(work);
|
|
4152
|
+
return {
|
|
4153
|
+
passes: { review, work },
|
|
4154
|
+
startedCount: reviewStarted + workStarted,
|
|
4155
|
+
ok: true
|
|
4156
|
+
};
|
|
4157
|
+
}
|
|
3284
4158
|
|
|
3285
4159
|
// src/stale-reconcile.ts
|
|
3286
|
-
import
|
|
4160
|
+
import path21 from "node:path";
|
|
3287
4161
|
|
|
3288
4162
|
// src/finalize.ts
|
|
3289
|
-
import
|
|
4163
|
+
import path20 from "node:path";
|
|
3290
4164
|
var ACTIVE_RUN_STATUSES = /* @__PURE__ */ new Set(["running", "dispatching", "pending", "queued"]);
|
|
3291
4165
|
function terminalStatusFor(run) {
|
|
3292
4166
|
const names = Object.keys(run.workers || {});
|
|
@@ -3297,7 +4171,7 @@ function terminalStatusFor(run) {
|
|
|
3297
4171
|
let anyLandingBlocked = false;
|
|
3298
4172
|
for (const name of names) {
|
|
3299
4173
|
const worker = readJson(
|
|
3300
|
-
|
|
4174
|
+
path20.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
3301
4175
|
void 0
|
|
3302
4176
|
);
|
|
3303
4177
|
if (!worker) continue;
|
|
@@ -3349,7 +4223,7 @@ function reconcileStaleWorkers() {
|
|
|
3349
4223
|
const now = Date.now();
|
|
3350
4224
|
for (const run of listRunRecords()) {
|
|
3351
4225
|
for (const name of Object.keys(run.workers || {})) {
|
|
3352
|
-
const workerPath =
|
|
4226
|
+
const workerPath = path21.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json");
|
|
3353
4227
|
const worker = readJson(workerPath, void 0);
|
|
3354
4228
|
if (!worker || worker.status !== "running") {
|
|
3355
4229
|
outcomes.push({
|
|
@@ -3425,7 +4299,7 @@ function reconcileStaleWorkers() {
|
|
|
3425
4299
|
}
|
|
3426
4300
|
|
|
3427
4301
|
// src/plan-progress-daemon-sync.ts
|
|
3428
|
-
import
|
|
4302
|
+
import path22 from "node:path";
|
|
3429
4303
|
|
|
3430
4304
|
// src/plan-progress-sync.ts
|
|
3431
4305
|
async function syncPlanProgress(args) {
|
|
@@ -3449,7 +4323,7 @@ async function syncActiveWorkerPlanProgress(runId, args) {
|
|
|
3449
4323
|
const outcomes = [];
|
|
3450
4324
|
for (const name of Object.keys(run.workers || {})) {
|
|
3451
4325
|
const worker = readJson(
|
|
3452
|
-
|
|
4326
|
+
path22.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
3453
4327
|
void 0
|
|
3454
4328
|
);
|
|
3455
4329
|
if (!worker?.dispatched || !worker.taskId) continue;
|
|
@@ -3498,7 +4372,7 @@ async function fetchWorkspaceRuntimePreferences(agentOsId, args) {
|
|
|
3498
4372
|
}
|
|
3499
4373
|
|
|
3500
4374
|
// src/cleanup.ts
|
|
3501
|
-
import
|
|
4375
|
+
import path27 from "node:path";
|
|
3502
4376
|
|
|
3503
4377
|
// src/cleanup-types.ts
|
|
3504
4378
|
var DEFAULT_NODE_MODULES_AGE_MS = 6 * 60 * 60 * 1e3;
|
|
@@ -3569,14 +4443,14 @@ function skipNodeModulesRemoval(input) {
|
|
|
3569
4443
|
}
|
|
3570
4444
|
|
|
3571
4445
|
// src/cleanup-execute.ts
|
|
3572
|
-
import { existsSync as
|
|
3573
|
-
import
|
|
4446
|
+
import { existsSync as existsSync15, rmSync } from "node:fs";
|
|
4447
|
+
import path24 from "node:path";
|
|
3574
4448
|
|
|
3575
4449
|
// src/cleanup-dir-size.ts
|
|
3576
|
-
import { existsSync as
|
|
3577
|
-
import
|
|
4450
|
+
import { existsSync as existsSync14, readdirSync as readdirSync5, statSync as statSync2 } from "node:fs";
|
|
4451
|
+
import path23 from "node:path";
|
|
3578
4452
|
function directorySizeBytes(root, maxEntries = 5e4) {
|
|
3579
|
-
if (!
|
|
4453
|
+
if (!existsSync14(root)) return 0;
|
|
3580
4454
|
let total = 0;
|
|
3581
4455
|
let seen = 0;
|
|
3582
4456
|
const stack = [root];
|
|
@@ -3584,13 +4458,13 @@ function directorySizeBytes(root, maxEntries = 5e4) {
|
|
|
3584
4458
|
const current = stack.pop();
|
|
3585
4459
|
let entries;
|
|
3586
4460
|
try {
|
|
3587
|
-
entries =
|
|
4461
|
+
entries = readdirSync5(current);
|
|
3588
4462
|
} catch {
|
|
3589
4463
|
continue;
|
|
3590
4464
|
}
|
|
3591
4465
|
for (const name of entries) {
|
|
3592
4466
|
if (seen++ > maxEntries) return null;
|
|
3593
|
-
const full =
|
|
4467
|
+
const full = path23.join(current, name);
|
|
3594
4468
|
let st;
|
|
3595
4469
|
try {
|
|
3596
4470
|
st = statSync2(full);
|
|
@@ -3606,7 +4480,7 @@ function directorySizeBytes(root, maxEntries = 5e4) {
|
|
|
3606
4480
|
|
|
3607
4481
|
// src/cleanup-execute.ts
|
|
3608
4482
|
function removeNodeModules(candidate, execute) {
|
|
3609
|
-
if (!
|
|
4483
|
+
if (!existsSync15(candidate.path)) {
|
|
3610
4484
|
return {
|
|
3611
4485
|
...candidate,
|
|
3612
4486
|
executed: false,
|
|
@@ -3637,7 +4511,7 @@ function removeNodeModules(candidate, execute) {
|
|
|
3637
4511
|
}
|
|
3638
4512
|
}
|
|
3639
4513
|
function removeWorktree(candidate, execute) {
|
|
3640
|
-
if (!
|
|
4514
|
+
if (!existsSync15(candidate.path)) {
|
|
3641
4515
|
return {
|
|
3642
4516
|
...candidate,
|
|
3643
4517
|
executed: false,
|
|
@@ -3654,7 +4528,7 @@ function removeWorktree(candidate, execute) {
|
|
|
3654
4528
|
if (repo) {
|
|
3655
4529
|
git(repo, ["worktree", "remove", "--force", candidate.path], { allowFailure: true });
|
|
3656
4530
|
}
|
|
3657
|
-
if (
|
|
4531
|
+
if (existsSync15(candidate.path)) {
|
|
3658
4532
|
rmSync(candidate.path, { recursive: true, force: true });
|
|
3659
4533
|
}
|
|
3660
4534
|
return {
|
|
@@ -3674,20 +4548,20 @@ function removeWorktree(candidate, execute) {
|
|
|
3674
4548
|
}
|
|
3675
4549
|
}
|
|
3676
4550
|
function isHarnessNodeModulesPath(targetPath, harnessRoot, worktreesDir) {
|
|
3677
|
-
const resolved =
|
|
3678
|
-
const nm = resolved.endsWith(`${
|
|
4551
|
+
const resolved = path24.resolve(targetPath);
|
|
4552
|
+
const nm = resolved.endsWith(`${path24.sep}node_modules`) ? resolved : null;
|
|
3679
4553
|
if (!nm) return "path_outside_harness";
|
|
3680
|
-
const rel =
|
|
3681
|
-
if (rel.startsWith("..") ||
|
|
3682
|
-
const parts = rel.split(
|
|
4554
|
+
const rel = path24.relative(worktreesDir, nm);
|
|
4555
|
+
if (rel.startsWith("..") || path24.isAbsolute(rel)) return "path_outside_harness";
|
|
4556
|
+
const parts = rel.split(path24.sep);
|
|
3683
4557
|
if (parts.length < 3 || parts[parts.length - 1] !== "node_modules") return "path_outside_harness";
|
|
3684
|
-
if (!resolved.startsWith(
|
|
4558
|
+
if (!resolved.startsWith(path24.resolve(harnessRoot))) return "path_outside_harness";
|
|
3685
4559
|
return null;
|
|
3686
4560
|
}
|
|
3687
4561
|
|
|
3688
4562
|
// src/cleanup-scan.ts
|
|
3689
|
-
import { existsSync as
|
|
3690
|
-
import
|
|
4563
|
+
import { existsSync as existsSync16, readdirSync as readdirSync6, statSync as statSync3 } from "node:fs";
|
|
4564
|
+
import path25 from "node:path";
|
|
3691
4565
|
function pathAgeMs(target, now) {
|
|
3692
4566
|
try {
|
|
3693
4567
|
const mtime = statSync3(target).mtimeMs;
|
|
@@ -3697,17 +4571,17 @@ function pathAgeMs(target, now) {
|
|
|
3697
4571
|
}
|
|
3698
4572
|
}
|
|
3699
4573
|
function isPathInside(child, parent) {
|
|
3700
|
-
const rel =
|
|
3701
|
-
return rel === "" || !rel.startsWith("..") && !
|
|
4574
|
+
const rel = path25.relative(parent, child);
|
|
4575
|
+
return rel === "" || !rel.startsWith("..") && !path25.isAbsolute(rel);
|
|
3702
4576
|
}
|
|
3703
4577
|
function scanNodeModulesCandidates(opts) {
|
|
3704
4578
|
const candidates = [];
|
|
3705
4579
|
const seen = /* @__PURE__ */ new Set();
|
|
3706
4580
|
for (const entry of opts.index.values()) {
|
|
3707
4581
|
if (opts.runIdFilter && entry.runId !== opts.runIdFilter) continue;
|
|
3708
|
-
const nm =
|
|
3709
|
-
if (!
|
|
3710
|
-
const resolved =
|
|
4582
|
+
const nm = path25.join(entry.worktreePath, "node_modules");
|
|
4583
|
+
if (!existsSync16(nm)) continue;
|
|
4584
|
+
const resolved = path25.resolve(nm);
|
|
3711
4585
|
if (seen.has(resolved)) continue;
|
|
3712
4586
|
seen.add(resolved);
|
|
3713
4587
|
candidates.push({
|
|
@@ -3720,16 +4594,16 @@ function scanNodeModulesCandidates(opts) {
|
|
|
3720
4594
|
ageMs: pathAgeMs(resolved, opts.now)
|
|
3721
4595
|
});
|
|
3722
4596
|
}
|
|
3723
|
-
if (!opts.includeOrphans || !
|
|
3724
|
-
for (const runEntry of
|
|
4597
|
+
if (!opts.includeOrphans || !existsSync16(opts.worktreesDir)) return candidates;
|
|
4598
|
+
for (const runEntry of readdirSync6(opts.worktreesDir, { withFileTypes: true })) {
|
|
3725
4599
|
if (!runEntry.isDirectory()) continue;
|
|
3726
|
-
const runPath =
|
|
3727
|
-
for (const workerEntry of
|
|
4600
|
+
const runPath = path25.join(opts.worktreesDir, runEntry.name);
|
|
4601
|
+
for (const workerEntry of readdirSync6(runPath, { withFileTypes: true })) {
|
|
3728
4602
|
if (!workerEntry.isDirectory()) continue;
|
|
3729
|
-
const worktreePath =
|
|
3730
|
-
const nm =
|
|
3731
|
-
if (!
|
|
3732
|
-
const resolved =
|
|
4603
|
+
const worktreePath = path25.join(runPath, workerEntry.name);
|
|
4604
|
+
const nm = path25.join(worktreePath, "node_modules");
|
|
4605
|
+
if (!existsSync16(nm)) continue;
|
|
4606
|
+
const resolved = path25.resolve(nm);
|
|
3733
4607
|
if (seen.has(resolved)) continue;
|
|
3734
4608
|
if (!isPathInside(resolved, opts.harnessRoot)) continue;
|
|
3735
4609
|
seen.add(resolved);
|
|
@@ -3752,7 +4626,7 @@ function scanWorktreeCandidates(opts) {
|
|
|
3752
4626
|
for (const entry of opts.index.values()) {
|
|
3753
4627
|
if (opts.runIdFilter && entry.runId !== opts.runIdFilter) continue;
|
|
3754
4628
|
const resolved = entry.worktreePath;
|
|
3755
|
-
if (!
|
|
4629
|
+
if (!existsSync16(resolved)) continue;
|
|
3756
4630
|
if (seen.has(resolved)) continue;
|
|
3757
4631
|
seen.add(resolved);
|
|
3758
4632
|
candidates.push({
|
|
@@ -3769,17 +4643,17 @@ function scanWorktreeCandidates(opts) {
|
|
|
3769
4643
|
}
|
|
3770
4644
|
|
|
3771
4645
|
// src/cleanup-worktree-index.ts
|
|
3772
|
-
import
|
|
4646
|
+
import path26 from "node:path";
|
|
3773
4647
|
function buildWorktreeIndex() {
|
|
3774
4648
|
const index = /* @__PURE__ */ new Map();
|
|
3775
4649
|
for (const run of listRunRecords()) {
|
|
3776
4650
|
for (const name of Object.keys(run.workers || {})) {
|
|
3777
|
-
const workerPath =
|
|
4651
|
+
const workerPath = path26.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json");
|
|
3778
4652
|
const worker = readJson(workerPath, void 0);
|
|
3779
4653
|
if (!worker?.worktreePath) continue;
|
|
3780
4654
|
const status = computeWorkerStatus(worker, { base: run.base, baseCommit: run.baseCommit });
|
|
3781
|
-
index.set(
|
|
3782
|
-
worktreePath:
|
|
4655
|
+
index.set(path26.resolve(worker.worktreePath), {
|
|
4656
|
+
worktreePath: path26.resolve(worker.worktreePath),
|
|
3783
4657
|
runId: run.id,
|
|
3784
4658
|
workerName: name,
|
|
3785
4659
|
run,
|
|
@@ -3793,8 +4667,8 @@ function buildWorktreeIndex() {
|
|
|
3793
4667
|
|
|
3794
4668
|
// src/cleanup.ts
|
|
3795
4669
|
function resolveOptions(options = {}) {
|
|
3796
|
-
const harnessRoot = options.harnessRoot ?
|
|
3797
|
-
const { worktreesDir } = options.harnessRoot ? { worktreesDir:
|
|
4670
|
+
const harnessRoot = options.harnessRoot ? path27.resolve(options.harnessRoot) : resolveHarnessRoot();
|
|
4671
|
+
const { worktreesDir } = options.harnessRoot ? { worktreesDir: path27.join(harnessRoot, "worktrees") } : getHarnessPaths();
|
|
3798
4672
|
const execute = options.execute === true;
|
|
3799
4673
|
const nodeModulesAgeMs = options.nodeModulesAgeMs ?? DEFAULT_NODE_MODULES_AGE_MS;
|
|
3800
4674
|
const worktreesAgeMs = options.worktreesAgeMs ?? 0;
|
|
@@ -3838,7 +4712,7 @@ function runHarnessCleanup(options = {}) {
|
|
|
3838
4712
|
actions.push({ ...candidate, executed: false, skipped: true, skipReason: pathSkip });
|
|
3839
4713
|
continue;
|
|
3840
4714
|
}
|
|
3841
|
-
const worktreePath =
|
|
4715
|
+
const worktreePath = path27.resolve(candidate.path, "..");
|
|
3842
4716
|
const indexed = index.get(worktreePath) ?? null;
|
|
3843
4717
|
const guardReason = skipNodeModulesRemoval({
|
|
3844
4718
|
indexed,
|
|
@@ -3854,7 +4728,7 @@ function runHarnessCleanup(options = {}) {
|
|
|
3854
4728
|
actions.push(removeNodeModules(candidate, resolved.execute));
|
|
3855
4729
|
}
|
|
3856
4730
|
for (const candidate of scanWorktreeCandidates(scanOpts)) {
|
|
3857
|
-
const indexed = index.get(
|
|
4731
|
+
const indexed = index.get(path27.resolve(candidate.path)) ?? null;
|
|
3858
4732
|
const guardReason = skipWorktreeRemoval({
|
|
3859
4733
|
indexed,
|
|
3860
4734
|
includeOrphans: resolved.includeOrphans,
|
|
@@ -3923,7 +4797,7 @@ async function completeFinishedWorkers(runId, args) {
|
|
|
3923
4797
|
const outcomes = [];
|
|
3924
4798
|
for (const name of Object.keys(run.workers || {})) {
|
|
3925
4799
|
const worker = readJson(
|
|
3926
|
-
|
|
4800
|
+
path28.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
3927
4801
|
void 0
|
|
3928
4802
|
);
|
|
3929
4803
|
if (!worker?.taskId || worker.localOnly) continue;
|
|
@@ -3974,6 +4848,7 @@ async function runPipelineTick(args) {
|
|
|
3974
4848
|
configuredMaxWorkersOverride: workspacePrefs?.maxConcurrentWorkers
|
|
3975
4849
|
});
|
|
3976
4850
|
const operatorTick = await postOperatorTick(agentOsId, runId, resourceGate, args);
|
|
4851
|
+
const completionAckSync = syncCompletionAcknowledgedFromOperatorTick(runId, operatorTick);
|
|
3977
4852
|
const leaseRenewal = await renewActiveTaskLeases(runId, args);
|
|
3978
4853
|
const completedWorkers = await completeFinishedWorkers(runId, args);
|
|
3979
4854
|
const staleReconcile = reconcileStaleWorkers();
|
|
@@ -3988,14 +4863,14 @@ async function runPipelineTick(args) {
|
|
|
3988
4863
|
const sweep = await sweepRun({ run: runId, agentOsId, pipeline: true, ...args });
|
|
3989
4864
|
let dispatch = null;
|
|
3990
4865
|
if (execute && maxStarts > 0) {
|
|
3991
|
-
dispatch = await
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
|
|
4866
|
+
dispatch = await runPipelineDispatch(
|
|
4867
|
+
{
|
|
4868
|
+
...args,
|
|
4869
|
+
run: runId,
|
|
4870
|
+
agentOsId
|
|
4871
|
+
},
|
|
4872
|
+
maxStarts
|
|
4873
|
+
);
|
|
3999
4874
|
} else {
|
|
4000
4875
|
dispatch = {
|
|
4001
4876
|
ok: true,
|
|
@@ -4016,6 +4891,7 @@ async function runPipelineTick(args) {
|
|
|
4016
4891
|
staleReconcile,
|
|
4017
4892
|
harnessCleanup,
|
|
4018
4893
|
planProgressSync,
|
|
4894
|
+
completionAckSync,
|
|
4019
4895
|
operatorTick,
|
|
4020
4896
|
sweep,
|
|
4021
4897
|
dispatch,
|
|
@@ -4087,6 +4963,8 @@ async function emitPlanProgress(args) {
|
|
|
4087
4963
|
const url = `${base}/api/agent-os/by-id/${encodeURIComponent(agentOsId)}/plans/${encodeURIComponent(planId)}/progress-events`;
|
|
4088
4964
|
const cfg = loadUserConfig();
|
|
4089
4965
|
const provider = cfg.workerProvider ? `provider:${cfg.workerProvider}` : void 0;
|
|
4966
|
+
const explicitProposed = args.proposed === true || args.proposed === "true" ? true : args.proposed === false || args.proposed === "false" ? false : void 0;
|
|
4967
|
+
const proposed = explicitProposed ?? (status !== "done" && (roleLane === "implementer" || roleLane === "repair_implementer"));
|
|
4090
4968
|
const body = {
|
|
4091
4969
|
rowKey: args.row ? String(args.row) : void 0,
|
|
4092
4970
|
rowId: args.rowId ? String(args.rowId) : void 0,
|
|
@@ -4097,9 +4975,9 @@ async function emitPlanProgress(args) {
|
|
|
4097
4975
|
note: args.note ? String(args.note) : void 0,
|
|
4098
4976
|
remainingWork: args.remaining ? String(args.remaining) : void 0,
|
|
4099
4977
|
evidence: evidence.length ? evidence : void 0,
|
|
4100
|
-
proposed: args.proposed === true || args.proposed === "true",
|
|
4101
4978
|
executorRef: args.executorRef ? String(args.executorRef) : provider
|
|
4102
4979
|
};
|
|
4980
|
+
if (proposed !== void 0) body.proposed = proposed;
|
|
4103
4981
|
const res = await fetch(url, {
|
|
4104
4982
|
method: "POST",
|
|
4105
4983
|
headers: buildHarnessCallbackHeaders(secret),
|
|
@@ -4153,6 +5031,86 @@ async function verifyPlan(args) {
|
|
|
4153
5031
|
console.log(JSON.stringify(parsed, null, 2));
|
|
4154
5032
|
}
|
|
4155
5033
|
|
|
5034
|
+
// src/plan-persist-cli.ts
|
|
5035
|
+
import { readFileSync as readFileSync7 } from "node:fs";
|
|
5036
|
+
var OPERATIONS = ["create", "add_version", "update_metadata"];
|
|
5037
|
+
var FAILURE_KINDS = [
|
|
5038
|
+
"approval_guard",
|
|
5039
|
+
"auth",
|
|
5040
|
+
"network",
|
|
5041
|
+
"server",
|
|
5042
|
+
"tool_interruption"
|
|
5043
|
+
];
|
|
5044
|
+
function readBodyArg(args) {
|
|
5045
|
+
const bodyFile = args.bodyFile ? String(args.bodyFile) : void 0;
|
|
5046
|
+
if (bodyFile) {
|
|
5047
|
+
return { body: readFileSync7(bodyFile, "utf8"), bodyPathHint: bodyFile };
|
|
5048
|
+
}
|
|
5049
|
+
const inline = args.body ? String(args.body) : void 0;
|
|
5050
|
+
if (inline) return { body: inline };
|
|
5051
|
+
throw new Error("requires --body-file PATH or --body TEXT");
|
|
5052
|
+
}
|
|
5053
|
+
async function runPlanPersist(args) {
|
|
5054
|
+
const operationRaw = required(args.operation ? String(args.operation) : void 0, "operation");
|
|
5055
|
+
if (!OPERATIONS.includes(operationRaw)) {
|
|
5056
|
+
throw new Error(`invalid --operation ${operationRaw}`);
|
|
5057
|
+
}
|
|
5058
|
+
const operation = operationRaw;
|
|
5059
|
+
const cfg = loadUserConfig();
|
|
5060
|
+
const agentOsSlug = required(
|
|
5061
|
+
args.slug ? String(args.slug) : cfg.agentOsSlug,
|
|
5062
|
+
"slug (or agentOsSlug in ~/.kynver/config.json)"
|
|
5063
|
+
);
|
|
5064
|
+
const title = required(args.title ? String(args.title) : void 0, "title");
|
|
5065
|
+
const { body, bodyPathHint } = readBodyArg(args);
|
|
5066
|
+
if (bodyPathHint && isTmpOnlyPath(bodyPathHint)) {
|
|
5067
|
+
console.warn(
|
|
5068
|
+
JSON.stringify({
|
|
5069
|
+
warning: "/tmp-only body path is not durable; AgentOS persistence requires outbox or successful API write",
|
|
5070
|
+
bodyPathHint
|
|
5071
|
+
})
|
|
5072
|
+
);
|
|
5073
|
+
}
|
|
5074
|
+
const input = {
|
|
5075
|
+
operation,
|
|
5076
|
+
agentOsSlug,
|
|
5077
|
+
title,
|
|
5078
|
+
body,
|
|
5079
|
+
bodyPathHint,
|
|
5080
|
+
summary: args.summary ? String(args.summary) : void 0,
|
|
5081
|
+
planId: args.plan ? String(args.plan) : void 0,
|
|
5082
|
+
planSlug: args.planSlug ? String(args.planSlug) : void 0,
|
|
5083
|
+
changeSummary: args.changeSummary ? String(args.changeSummary) : void 0,
|
|
5084
|
+
author: args.author ? String(args.author) : void 0,
|
|
5085
|
+
model: args.model ? String(args.model) : void 0,
|
|
5086
|
+
maxRetries: args.maxRetries ? Number(args.maxRetries) : void 0,
|
|
5087
|
+
immediateFailure: parseImmediateFailure(args)
|
|
5088
|
+
};
|
|
5089
|
+
const result = await persistPlan(input);
|
|
5090
|
+
console.log(JSON.stringify(result, null, 2));
|
|
5091
|
+
if (result.userStatus === "failed and needs action") process.exit(1);
|
|
5092
|
+
}
|
|
5093
|
+
function parseImmediateFailure(args) {
|
|
5094
|
+
const kind = args.failureKind ? String(args.failureKind) : void 0;
|
|
5095
|
+
if (!kind) return void 0;
|
|
5096
|
+
if (!FAILURE_KINDS.includes(kind)) {
|
|
5097
|
+
throw new Error(`invalid --failure-kind ${kind}`);
|
|
5098
|
+
}
|
|
5099
|
+
const message = args.failureMessage ? String(args.failureMessage) : `immediate failure (${kind})`;
|
|
5100
|
+
return { kind, message };
|
|
5101
|
+
}
|
|
5102
|
+
async function runPlanOutboxList() {
|
|
5103
|
+
const items = listOutboxItems();
|
|
5104
|
+
console.log(JSON.stringify({ count: items.length, items }, null, 2));
|
|
5105
|
+
}
|
|
5106
|
+
async function runPlanOutboxDrain(args) {
|
|
5107
|
+
const max = args.max ? Number(args.max) : void 0;
|
|
5108
|
+
const outboxId = args.id ? String(args.id) : void 0;
|
|
5109
|
+
const result = await drainPlanOutbox({ max, outboxId });
|
|
5110
|
+
console.log(JSON.stringify(result, null, 2));
|
|
5111
|
+
if (result.failed > 0) process.exit(1);
|
|
5112
|
+
}
|
|
5113
|
+
|
|
4156
5114
|
// src/cleanup-cli.ts
|
|
4157
5115
|
function runCleanupCli(args) {
|
|
4158
5116
|
const execute = args.execute === true || args.execute === "true";
|
|
@@ -4173,6 +5131,555 @@ function runCleanupCli(args) {
|
|
|
4173
5131
|
}
|
|
4174
5132
|
}
|
|
4175
5133
|
|
|
5134
|
+
// src/monitor/monitor.service.ts
|
|
5135
|
+
import path30 from "node:path";
|
|
5136
|
+
|
|
5137
|
+
// src/monitor/monitor.classify.ts
|
|
5138
|
+
function expectedLeaseOwner(runId) {
|
|
5139
|
+
return `kynver-harness:${runId}`;
|
|
5140
|
+
}
|
|
5141
|
+
function classifyWorkerHealth(input) {
|
|
5142
|
+
const { worker, status, taskLease } = input;
|
|
5143
|
+
const leaseOwner = taskLease?.leaseOwner ?? null;
|
|
5144
|
+
const expectedOwner = expectedLeaseOwner(worker.runId);
|
|
5145
|
+
if (worker.dispatched && taskLease) {
|
|
5146
|
+
if (taskLease.status === "running" && leaseOwner && leaseOwner !== expectedOwner) {
|
|
5147
|
+
return {
|
|
5148
|
+
health: "orphaned",
|
|
5149
|
+
reason: `task lease held by ${leaseOwner}, expected ${expectedOwner}`
|
|
5150
|
+
};
|
|
5151
|
+
}
|
|
5152
|
+
if (taskLease.status === "running" && !status.alive && !status.finalResult) {
|
|
5153
|
+
return {
|
|
5154
|
+
health: "orphaned",
|
|
5155
|
+
reason: "board task running but worker process is not alive"
|
|
5156
|
+
};
|
|
5157
|
+
}
|
|
5158
|
+
}
|
|
5159
|
+
if (worker.status === "running" && !status.alive && !status.finalResult) {
|
|
5160
|
+
return {
|
|
5161
|
+
health: "orphaned",
|
|
5162
|
+
reason: "worker.json still running but process is dead"
|
|
5163
|
+
};
|
|
5164
|
+
}
|
|
5165
|
+
if (status.attention.state === "stale") {
|
|
5166
|
+
return { health: "stale", reason: status.attention.reason };
|
|
5167
|
+
}
|
|
5168
|
+
const hbMs = status.lastHeartbeatAt ? Date.parse(status.lastHeartbeatAt) : NaN;
|
|
5169
|
+
if (status.alive && Number.isFinite(hbMs) && Date.now() - hbMs > STALE_MS) {
|
|
5170
|
+
return {
|
|
5171
|
+
health: "stale",
|
|
5172
|
+
reason: `heartbeat older than ${Math.floor(STALE_MS / 1e3)}s`
|
|
5173
|
+
};
|
|
5174
|
+
}
|
|
5175
|
+
if (status.alive && worker.pid && !isPidAlive(worker.pid)) {
|
|
5176
|
+
return { health: "orphaned", reason: "pid recorded but process is not alive" };
|
|
5177
|
+
}
|
|
5178
|
+
if (taskLease?.status === "running" && !status.alive && status.finalResult) {
|
|
5179
|
+
return {
|
|
5180
|
+
health: "healthy",
|
|
5181
|
+
reason: "finished worker awaiting completion replay"
|
|
5182
|
+
};
|
|
5183
|
+
}
|
|
5184
|
+
return {
|
|
5185
|
+
health: "healthy",
|
|
5186
|
+
reason: status.attention.reason || "worker within expected lifecycle bounds"
|
|
5187
|
+
};
|
|
5188
|
+
}
|
|
5189
|
+
|
|
5190
|
+
// src/monitor/monitor.store.ts
|
|
5191
|
+
import { existsSync as existsSync17, mkdirSync as mkdirSync6, readdirSync as readdirSync7, unlinkSync as unlinkSync2 } from "node:fs";
|
|
5192
|
+
import path29 from "node:path";
|
|
5193
|
+
function monitorsDir() {
|
|
5194
|
+
const { harnessRoot } = getHarnessPaths();
|
|
5195
|
+
const dir = path29.join(harnessRoot, "monitors");
|
|
5196
|
+
mkdirSync6(dir, { recursive: true });
|
|
5197
|
+
return dir;
|
|
5198
|
+
}
|
|
5199
|
+
function monitorIdFor(runId, workerName) {
|
|
5200
|
+
return workerName ? `${safeSlug(runId)}--${safeSlug(workerName)}` : safeSlug(runId);
|
|
5201
|
+
}
|
|
5202
|
+
function monitorPath(monitorId) {
|
|
5203
|
+
return path29.join(monitorsDir(), `${monitorId}.json`);
|
|
5204
|
+
}
|
|
5205
|
+
function loadMonitorSession(monitorId) {
|
|
5206
|
+
return readJson(monitorPath(monitorId), void 0);
|
|
5207
|
+
}
|
|
5208
|
+
function saveMonitorSession(session) {
|
|
5209
|
+
writeJson(monitorPath(session.monitorId), session);
|
|
5210
|
+
}
|
|
5211
|
+
function deleteMonitorSession(monitorId) {
|
|
5212
|
+
const file = monitorPath(monitorId);
|
|
5213
|
+
if (!existsSync17(file)) return false;
|
|
5214
|
+
unlinkSync2(file);
|
|
5215
|
+
return true;
|
|
5216
|
+
}
|
|
5217
|
+
function listMonitorSessions() {
|
|
5218
|
+
const dir = monitorsDir();
|
|
5219
|
+
if (!existsSync17(dir)) return [];
|
|
5220
|
+
const entries = [];
|
|
5221
|
+
for (const name of readdirSync7(dir)) {
|
|
5222
|
+
if (!name.endsWith(".json")) continue;
|
|
5223
|
+
const session = readJson(
|
|
5224
|
+
path29.join(dir, name),
|
|
5225
|
+
void 0
|
|
5226
|
+
);
|
|
5227
|
+
if (!session?.monitorId) continue;
|
|
5228
|
+
entries.push({
|
|
5229
|
+
monitorId: session.monitorId,
|
|
5230
|
+
runId: session.runId,
|
|
5231
|
+
workerName: session.workerName,
|
|
5232
|
+
agentOsId: session.agentOsId,
|
|
5233
|
+
pid: session.pid,
|
|
5234
|
+
alive: session.pid ? isPidAlive(session.pid) : false,
|
|
5235
|
+
startedAt: session.startedAt,
|
|
5236
|
+
pollMs: session.pollMs,
|
|
5237
|
+
logPath: session.logPath
|
|
5238
|
+
});
|
|
5239
|
+
}
|
|
5240
|
+
return entries.sort((a, b) => a.startedAt.localeCompare(b.startedAt));
|
|
5241
|
+
}
|
|
5242
|
+
|
|
5243
|
+
// src/monitor/monitor.terminal.ts
|
|
5244
|
+
function assessAutoCompleteEligibility(input) {
|
|
5245
|
+
const { worker, status } = input;
|
|
5246
|
+
const blockers = [];
|
|
5247
|
+
if (worker.localOnly) {
|
|
5248
|
+
blockers.push("local-only worker (no board linkage)");
|
|
5249
|
+
}
|
|
5250
|
+
if (!worker.agentOsId || !worker.taskId) {
|
|
5251
|
+
blockers.push("missing agentOsId/taskId linkage");
|
|
5252
|
+
}
|
|
5253
|
+
if (hasCompletionAck(worker)) {
|
|
5254
|
+
blockers.push("completion already acknowledged");
|
|
5255
|
+
}
|
|
5256
|
+
if (worker.completionBlocker) {
|
|
5257
|
+
blockers.push(worker.completionBlocker);
|
|
5258
|
+
}
|
|
5259
|
+
if (status.heartbeatBlocker && status.alive) {
|
|
5260
|
+
blockers.push(`worker heartbeat blocker: ${status.heartbeatBlocker}`);
|
|
5261
|
+
}
|
|
5262
|
+
if (status.attention.state === "blocked") {
|
|
5263
|
+
blockers.push(status.attention.reason || "worker attention blocked");
|
|
5264
|
+
}
|
|
5265
|
+
if (isLandingBlockedWorkerStatus(status)) {
|
|
5266
|
+
blockers.push(status.attention.reason || "landing gate blocked");
|
|
5267
|
+
}
|
|
5268
|
+
const terminalVerified = isFinishedWorkerStatus(status);
|
|
5269
|
+
let terminalReason;
|
|
5270
|
+
if (terminalVerified) {
|
|
5271
|
+
if (status.finalResult) terminalReason = "final_result";
|
|
5272
|
+
else if (!status.alive) terminalReason = "process_exited";
|
|
5273
|
+
else terminalReason = "terminal_status";
|
|
5274
|
+
} else {
|
|
5275
|
+
blockers.push("worker has not reached a terminal condition");
|
|
5276
|
+
}
|
|
5277
|
+
const eligible = terminalVerified && blockers.length === 0;
|
|
5278
|
+
return {
|
|
5279
|
+
eligible,
|
|
5280
|
+
terminalVerified,
|
|
5281
|
+
terminalReason,
|
|
5282
|
+
blockers
|
|
5283
|
+
};
|
|
5284
|
+
}
|
|
5285
|
+
|
|
5286
|
+
// src/monitor/monitor.task-lease.ts
|
|
5287
|
+
async function fetchTaskLeasesForWorkers(input) {
|
|
5288
|
+
const out = /* @__PURE__ */ new Map();
|
|
5289
|
+
const agentOsId = input.agentOsId?.trim();
|
|
5290
|
+
if (!agentOsId || input.taskIds.length === 0) return out;
|
|
5291
|
+
const base = resolveBaseUrl(input.baseUrl);
|
|
5292
|
+
try {
|
|
5293
|
+
const secret = await resolveCallbackSecretWithMint(input.secret, agentOsId, { baseUrl: base });
|
|
5294
|
+
const url = `${base}/api/agent-os/by-id/${encodeURIComponent(agentOsId)}/harness/monitor/task-leases`;
|
|
5295
|
+
const res = await postJsonWithCredentialRefresh(
|
|
5296
|
+
url,
|
|
5297
|
+
secret,
|
|
5298
|
+
{ taskIds: [...new Set(input.taskIds)] },
|
|
5299
|
+
{ agentOsId, baseUrl: base }
|
|
5300
|
+
);
|
|
5301
|
+
if (!res.ok || !res.response || typeof res.response !== "object") return out;
|
|
5302
|
+
const rows = res.response.tasks;
|
|
5303
|
+
if (!Array.isArray(rows)) return out;
|
|
5304
|
+
for (const row of rows) {
|
|
5305
|
+
if (row?.taskId) out.set(row.taskId, row);
|
|
5306
|
+
}
|
|
5307
|
+
} catch {
|
|
5308
|
+
}
|
|
5309
|
+
return out;
|
|
5310
|
+
}
|
|
5311
|
+
|
|
5312
|
+
// src/monitor/monitor.service.ts
|
|
5313
|
+
function workerRecord2(runId, name) {
|
|
5314
|
+
return readJson(
|
|
5315
|
+
path30.join(runDirectory(runId), "workers", safeSlug(name), "worker.json"),
|
|
5316
|
+
void 0
|
|
5317
|
+
);
|
|
5318
|
+
}
|
|
5319
|
+
function workerNamesForRun(runId, scope) {
|
|
5320
|
+
const run = loadRun(runId);
|
|
5321
|
+
const names = Object.keys(run.workers || {});
|
|
5322
|
+
if (!scope) return names;
|
|
5323
|
+
const wanted = safeSlug(scope);
|
|
5324
|
+
return names.filter((n) => safeSlug(n) === wanted);
|
|
5325
|
+
}
|
|
5326
|
+
function buildWorkerView(worker, taskLeases) {
|
|
5327
|
+
const run = loadRun(worker.runId);
|
|
5328
|
+
const status = computeWorkerStatus(worker, { base: run.base, baseCommit: run.baseCommit });
|
|
5329
|
+
const taskLease = worker.taskId ? taskLeases.get(worker.taskId) ?? null : null;
|
|
5330
|
+
const health = classifyWorkerHealth({ worker, status, taskLease });
|
|
5331
|
+
const autoComplete = assessAutoCompleteEligibility({ worker, status });
|
|
5332
|
+
return {
|
|
5333
|
+
runId: worker.runId,
|
|
5334
|
+
worker: worker.name,
|
|
5335
|
+
health: health.health,
|
|
5336
|
+
healthReason: health.reason,
|
|
5337
|
+
workerStatus: status.status,
|
|
5338
|
+
attentionState: status.attention.state,
|
|
5339
|
+
attentionReason: status.attention.reason,
|
|
5340
|
+
alive: status.alive,
|
|
5341
|
+
taskId: worker.taskId,
|
|
5342
|
+
leaseOwner: taskLease?.leaseOwner ?? void 0,
|
|
5343
|
+
taskStatus: taskLease?.status,
|
|
5344
|
+
autoComplete,
|
|
5345
|
+
status
|
|
5346
|
+
};
|
|
5347
|
+
}
|
|
5348
|
+
async function runMonitorTick(args) {
|
|
5349
|
+
const runId = String(args.run || "");
|
|
5350
|
+
required(runId, "--run");
|
|
5351
|
+
const scope = args.name ? String(args.name) : void 0;
|
|
5352
|
+
const agentOsId = args.agentOsId ? String(args.agentOsId) : void 0;
|
|
5353
|
+
const run = loadRun(runId);
|
|
5354
|
+
const names = workerNamesForRun(runId, scope);
|
|
5355
|
+
const workers = [];
|
|
5356
|
+
for (const name of names) {
|
|
5357
|
+
const worker = workerRecord2(runId, name);
|
|
5358
|
+
if (worker) workers.push(worker);
|
|
5359
|
+
}
|
|
5360
|
+
const resolvedAgentOsId = agentOsId || workers.map((w) => w.agentOsId).find((id) => typeof id === "string" && id.trim()) || void 0;
|
|
5361
|
+
const taskIds = workers.map((w) => w.taskId).filter((id) => Boolean(id));
|
|
5362
|
+
const taskLeases = await fetchTaskLeasesForWorkers({
|
|
5363
|
+
agentOsId: resolvedAgentOsId,
|
|
5364
|
+
taskIds,
|
|
5365
|
+
baseUrl: args.baseUrl ? String(args.baseUrl) : void 0,
|
|
5366
|
+
secret: args.secret ? String(args.secret) : void 0
|
|
5367
|
+
});
|
|
5368
|
+
const views = workers.map((w) => buildWorkerView(w, taskLeases));
|
|
5369
|
+
let leaseRenewal;
|
|
5370
|
+
if (resolvedAgentOsId && args.renewLeases !== false && args.renewLeases !== "false") {
|
|
5371
|
+
leaseRenewal = await renewActiveTaskLeases(runId, {
|
|
5372
|
+
...args,
|
|
5373
|
+
agentOsId: resolvedAgentOsId
|
|
5374
|
+
});
|
|
5375
|
+
}
|
|
5376
|
+
const autoCompleted = [];
|
|
5377
|
+
const shouldAutoComplete = args.autoComplete === true || args.autoComplete === "true";
|
|
5378
|
+
if (shouldAutoComplete) {
|
|
5379
|
+
for (const view of views) {
|
|
5380
|
+
if (!view.autoComplete.eligible) {
|
|
5381
|
+
autoCompleted.push({
|
|
5382
|
+
worker: view.worker,
|
|
5383
|
+
outcome: "skipped",
|
|
5384
|
+
ok: false,
|
|
5385
|
+
reason: view.autoComplete.blockers.join("; ") || "not eligible"
|
|
5386
|
+
});
|
|
5387
|
+
continue;
|
|
5388
|
+
}
|
|
5389
|
+
const outcome = await autoCompleteWorker({
|
|
5390
|
+
run: runId,
|
|
5391
|
+
name: view.worker,
|
|
5392
|
+
...resolvedAgentOsId ? { agentOsId: resolvedAgentOsId } : {},
|
|
5393
|
+
...args.baseUrl ? { baseUrl: String(args.baseUrl) } : {},
|
|
5394
|
+
...args.secret ? { secret: String(args.secret) } : {}
|
|
5395
|
+
});
|
|
5396
|
+
autoCompleted.push({
|
|
5397
|
+
worker: view.worker,
|
|
5398
|
+
outcome: outcome.outcome,
|
|
5399
|
+
ok: outcome.outcome === "completed",
|
|
5400
|
+
reason: outcome.reason
|
|
5401
|
+
});
|
|
5402
|
+
}
|
|
5403
|
+
}
|
|
5404
|
+
return {
|
|
5405
|
+
runId,
|
|
5406
|
+
agentOsId: resolvedAgentOsId,
|
|
5407
|
+
workers: views,
|
|
5408
|
+
leaseRenewal,
|
|
5409
|
+
autoCompleted
|
|
5410
|
+
};
|
|
5411
|
+
}
|
|
5412
|
+
function getMonitorStatus(args) {
|
|
5413
|
+
const runId = String(args.run || "");
|
|
5414
|
+
required(runId, "--run");
|
|
5415
|
+
const scope = args.name ? String(args.name) : void 0;
|
|
5416
|
+
const names = workerNamesForRun(runId, scope);
|
|
5417
|
+
const workers = [];
|
|
5418
|
+
for (const name of names) {
|
|
5419
|
+
const worker = workerRecord2(runId, name);
|
|
5420
|
+
if (!worker) continue;
|
|
5421
|
+
workers.push(buildWorkerView(worker, /* @__PURE__ */ new Map()));
|
|
5422
|
+
}
|
|
5423
|
+
return { runId, workers, autoCompleted: [] };
|
|
5424
|
+
}
|
|
5425
|
+
function listMonitors() {
|
|
5426
|
+
return listMonitorSessions();
|
|
5427
|
+
}
|
|
5428
|
+
function stopMonitor(args) {
|
|
5429
|
+
const runId = String(args.run || "");
|
|
5430
|
+
required(runId, "--run");
|
|
5431
|
+
const monitorId = monitorIdFor(runId, args.name ? String(args.name) : void 0);
|
|
5432
|
+
const session = loadMonitorSession(monitorId);
|
|
5433
|
+
if (!session) {
|
|
5434
|
+
return { monitorId, stopped: false };
|
|
5435
|
+
}
|
|
5436
|
+
if (session.pid && isPidAlive(session.pid)) {
|
|
5437
|
+
try {
|
|
5438
|
+
process.kill(session.pid, "SIGTERM");
|
|
5439
|
+
} catch {
|
|
5440
|
+
}
|
|
5441
|
+
}
|
|
5442
|
+
session.stoppedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
5443
|
+
saveMonitorSession(session);
|
|
5444
|
+
deleteMonitorSession(monitorId);
|
|
5445
|
+
return { monitorId, stopped: true, pid: session.pid };
|
|
5446
|
+
}
|
|
5447
|
+
async function monitorAutoCompleteCli(args) {
|
|
5448
|
+
const runId = String(args.run || "");
|
|
5449
|
+
const name = String(args.name || "");
|
|
5450
|
+
required(runId, "--run");
|
|
5451
|
+
required(name, "--name");
|
|
5452
|
+
const worker = loadWorker(runId, name);
|
|
5453
|
+
const run = loadRun(runId);
|
|
5454
|
+
const status = computeWorkerStatus(worker, { base: run.base, baseCommit: run.baseCommit });
|
|
5455
|
+
const assessment = assessAutoCompleteEligibility({ worker, status });
|
|
5456
|
+
if (!assessment.eligible) {
|
|
5457
|
+
console.log(
|
|
5458
|
+
JSON.stringify(
|
|
5459
|
+
{
|
|
5460
|
+
runId,
|
|
5461
|
+
worker: name,
|
|
5462
|
+
outcome: "blocked",
|
|
5463
|
+
blockers: assessment.blockers,
|
|
5464
|
+
terminalVerified: assessment.terminalVerified
|
|
5465
|
+
},
|
|
5466
|
+
null,
|
|
5467
|
+
2
|
|
5468
|
+
)
|
|
5469
|
+
);
|
|
5470
|
+
process.exitCode = 1;
|
|
5471
|
+
return;
|
|
5472
|
+
}
|
|
5473
|
+
const outcome = await autoCompleteWorker({
|
|
5474
|
+
...args,
|
|
5475
|
+
run: runId,
|
|
5476
|
+
name
|
|
5477
|
+
});
|
|
5478
|
+
console.log(JSON.stringify(outcome, null, 2));
|
|
5479
|
+
if (outcome.outcome !== "completed" && outcome.outcome !== "blocked") {
|
|
5480
|
+
process.exitCode = 1;
|
|
5481
|
+
}
|
|
5482
|
+
}
|
|
5483
|
+
|
|
5484
|
+
// src/monitor/monitor-loop.ts
|
|
5485
|
+
var DEFAULT_POLL_MS2 = 5e3;
|
|
5486
|
+
var DEFAULT_MAX_TOTAL_MS2 = 6 * 60 * 60 * 1e3;
|
|
5487
|
+
async function runMonitorLoop(args) {
|
|
5488
|
+
const monitorId = String(args.monitorId || "");
|
|
5489
|
+
const pollMs = Number(args.pollMs) > 0 ? Math.floor(Number(args.pollMs)) : DEFAULT_POLL_MS2;
|
|
5490
|
+
const maxTotalMs = Number(args.maxTotalMs) > 0 ? Math.floor(Number(args.maxTotalMs)) : DEFAULT_MAX_TOTAL_MS2;
|
|
5491
|
+
const startMs = Date.now();
|
|
5492
|
+
while (Date.now() - startMs <= maxTotalMs) {
|
|
5493
|
+
const session = monitorId ? loadMonitorSession(monitorId) : void 0;
|
|
5494
|
+
if (session?.stoppedAt) break;
|
|
5495
|
+
const tick = await runMonitorTick({
|
|
5496
|
+
...args,
|
|
5497
|
+
autoComplete: args.autoComplete ?? true,
|
|
5498
|
+
renewLeases: args.renewLeases ?? true
|
|
5499
|
+
});
|
|
5500
|
+
console.log(JSON.stringify({ monitorId, phase: "tick", ...tick }));
|
|
5501
|
+
const allTerminal = tick.workers.length > 0 && tick.workers.every(
|
|
5502
|
+
(w) => w.autoComplete.terminalVerified && (w.autoComplete.eligible || w.autoComplete.blockers.some((b) => b.includes("already acknowledged")))
|
|
5503
|
+
);
|
|
5504
|
+
if (allTerminal && tick.autoCompleted.every((a) => a.ok || a.outcome === "skipped")) {
|
|
5505
|
+
if (monitorId && session) {
|
|
5506
|
+
session.stoppedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
5507
|
+
saveMonitorSession(session);
|
|
5508
|
+
}
|
|
5509
|
+
break;
|
|
5510
|
+
}
|
|
5511
|
+
sleepMs(pollMs);
|
|
5512
|
+
}
|
|
5513
|
+
}
|
|
5514
|
+
|
|
5515
|
+
// src/monitor/monitor-spawn.ts
|
|
5516
|
+
import { spawn as spawn4 } from "node:child_process";
|
|
5517
|
+
import { closeSync as closeSync4, existsSync as existsSync18, openSync as openSync4 } from "node:fs";
|
|
5518
|
+
import path31 from "node:path";
|
|
5519
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
5520
|
+
function resolveDefaultCliPath2() {
|
|
5521
|
+
return path31.join(fileURLToPath2(new URL(".", import.meta.url)), "..", "cli.js");
|
|
5522
|
+
}
|
|
5523
|
+
function spawnMonitorSidecar(opts) {
|
|
5524
|
+
const cliPath = opts.cliPath ?? resolveDefaultCliPath2();
|
|
5525
|
+
if (!existsSync18(cliPath)) return void 0;
|
|
5526
|
+
const monitorId = monitorIdFor(opts.runId, opts.workerName);
|
|
5527
|
+
const { harnessRoot } = getHarnessPaths();
|
|
5528
|
+
const logPath = path31.join(harnessRoot, "monitors", `${monitorId}.log`);
|
|
5529
|
+
let logFd;
|
|
5530
|
+
try {
|
|
5531
|
+
logFd = openSync4(logPath, "a");
|
|
5532
|
+
} catch {
|
|
5533
|
+
logFd = void 0;
|
|
5534
|
+
}
|
|
5535
|
+
const nodeExecutable = opts.nodeExecutable ?? process.execPath;
|
|
5536
|
+
const pollMs = opts.pollMs ?? 5e3;
|
|
5537
|
+
const args = [
|
|
5538
|
+
cliPath,
|
|
5539
|
+
"monitor",
|
|
5540
|
+
"run-loop",
|
|
5541
|
+
"--run",
|
|
5542
|
+
opts.runId,
|
|
5543
|
+
"--monitor-id",
|
|
5544
|
+
monitorId,
|
|
5545
|
+
"--poll-ms",
|
|
5546
|
+
String(pollMs),
|
|
5547
|
+
"--auto-complete",
|
|
5548
|
+
"true",
|
|
5549
|
+
"--renew-leases",
|
|
5550
|
+
"true"
|
|
5551
|
+
];
|
|
5552
|
+
if (opts.workerName) args.push("--name", opts.workerName);
|
|
5553
|
+
if (opts.agentOsId) args.push("--agent-os-id", opts.agentOsId);
|
|
5554
|
+
if (opts.baseUrl) args.push("--base-url", opts.baseUrl);
|
|
5555
|
+
if (opts.secret) args.push("--secret", opts.secret);
|
|
5556
|
+
const stdio = [
|
|
5557
|
+
"ignore",
|
|
5558
|
+
logFd ?? "ignore",
|
|
5559
|
+
logFd ?? "ignore"
|
|
5560
|
+
];
|
|
5561
|
+
try {
|
|
5562
|
+
const child = spawn4(
|
|
5563
|
+
nodeExecutable,
|
|
5564
|
+
args,
|
|
5565
|
+
hiddenSpawnOptions({
|
|
5566
|
+
detached: true,
|
|
5567
|
+
stdio,
|
|
5568
|
+
env: process.env
|
|
5569
|
+
})
|
|
5570
|
+
);
|
|
5571
|
+
if (logFd !== void 0) closeSync4(logFd);
|
|
5572
|
+
child.unref();
|
|
5573
|
+
const session = {
|
|
5574
|
+
monitorId,
|
|
5575
|
+
runId: opts.runId,
|
|
5576
|
+
workerName: opts.workerName,
|
|
5577
|
+
agentOsId: opts.agentOsId,
|
|
5578
|
+
pid: child.pid,
|
|
5579
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5580
|
+
pollMs,
|
|
5581
|
+
logPath
|
|
5582
|
+
};
|
|
5583
|
+
saveMonitorSession(session);
|
|
5584
|
+
return { monitorId, pid: child.pid, logPath, session };
|
|
5585
|
+
} catch {
|
|
5586
|
+
if (logFd !== void 0) {
|
|
5587
|
+
try {
|
|
5588
|
+
closeSync4(logFd);
|
|
5589
|
+
} catch {
|
|
5590
|
+
}
|
|
5591
|
+
}
|
|
5592
|
+
return void 0;
|
|
5593
|
+
}
|
|
5594
|
+
}
|
|
5595
|
+
|
|
5596
|
+
// src/monitor/monitor-cli.ts
|
|
5597
|
+
async function startMonitorCli(args) {
|
|
5598
|
+
const runId = String(args.run || "");
|
|
5599
|
+
required(runId, "--run");
|
|
5600
|
+
const workerName = args.name ? String(args.name) : void 0;
|
|
5601
|
+
const monitorId = monitorIdFor(runId, workerName);
|
|
5602
|
+
const existing = loadMonitorSession(monitorId);
|
|
5603
|
+
if (existing?.pid && !existing.stoppedAt) {
|
|
5604
|
+
return { monitorId, session: existing, spawned: false, pid: existing.pid };
|
|
5605
|
+
}
|
|
5606
|
+
const spawned = spawnMonitorSidecar({
|
|
5607
|
+
runId,
|
|
5608
|
+
workerName,
|
|
5609
|
+
agentOsId: args.agentOsId ? String(args.agentOsId) : void 0,
|
|
5610
|
+
pollMs: Number(args.pollMs) > 0 ? Math.floor(Number(args.pollMs)) : void 0,
|
|
5611
|
+
baseUrl: args.baseUrl ? String(args.baseUrl) : void 0,
|
|
5612
|
+
secret: args.secret ? String(args.secret) : void 0
|
|
5613
|
+
});
|
|
5614
|
+
if (!spawned) {
|
|
5615
|
+
throw new Error("failed to spawn monitor sidecar (cli.js missing or spawn error)");
|
|
5616
|
+
}
|
|
5617
|
+
return {
|
|
5618
|
+
monitorId,
|
|
5619
|
+
session: spawned.session,
|
|
5620
|
+
spawned: true,
|
|
5621
|
+
pid: spawned.pid
|
|
5622
|
+
};
|
|
5623
|
+
}
|
|
5624
|
+
async function monitorStatusCli(args) {
|
|
5625
|
+
const runId = String(args.run || "");
|
|
5626
|
+
if (runId) {
|
|
5627
|
+
const tick = args.tick === true || args.tick === "true" ? await runMonitorTick({ ...args, autoComplete: false }) : getMonitorStatus(args);
|
|
5628
|
+
console.log(JSON.stringify(tick, null, 2));
|
|
5629
|
+
return;
|
|
5630
|
+
}
|
|
5631
|
+
console.log(JSON.stringify({ monitors: listMonitors() }, null, 2));
|
|
5632
|
+
}
|
|
5633
|
+
function monitorStopCli(args) {
|
|
5634
|
+
console.log(JSON.stringify(stopMonitor(args), null, 2));
|
|
5635
|
+
}
|
|
5636
|
+
function monitorListCli() {
|
|
5637
|
+
console.log(JSON.stringify({ monitors: listMonitors() }, null, 2));
|
|
5638
|
+
}
|
|
5639
|
+
async function monitorRunLoopCli(args) {
|
|
5640
|
+
await runMonitorLoop(args);
|
|
5641
|
+
}
|
|
5642
|
+
async function monitorTickCli(args) {
|
|
5643
|
+
const tick = await runMonitorTick(args);
|
|
5644
|
+
console.log(JSON.stringify(tick, null, 2));
|
|
5645
|
+
}
|
|
5646
|
+
|
|
5647
|
+
// src/package-version.ts
|
|
5648
|
+
import { existsSync as existsSync19, readFileSync as readFileSync8 } from "node:fs";
|
|
5649
|
+
import { dirname, join } from "node:path";
|
|
5650
|
+
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
5651
|
+
function resolvePackageRoot(moduleUrl) {
|
|
5652
|
+
let dir = dirname(fileURLToPath3(moduleUrl));
|
|
5653
|
+
for (let depth = 0; depth < 6; depth += 1) {
|
|
5654
|
+
if (existsSync19(join(dir, "package.json"))) return dir;
|
|
5655
|
+
const parent = dirname(dir);
|
|
5656
|
+
if (parent === dir) break;
|
|
5657
|
+
dir = parent;
|
|
5658
|
+
}
|
|
5659
|
+
throw new Error(`package.json not found above ${dirname(fileURLToPath3(moduleUrl))}`);
|
|
5660
|
+
}
|
|
5661
|
+
function readOwnPackageVersion(moduleUrl = import.meta.url) {
|
|
5662
|
+
const pkgPath = join(resolvePackageRoot(moduleUrl), "package.json");
|
|
5663
|
+
const pkg = JSON.parse(readFileSync8(pkgPath, "utf8"));
|
|
5664
|
+
if (typeof pkg.version !== "string" || !pkg.version.trim()) {
|
|
5665
|
+
throw new Error(`Missing package.json version at ${pkgPath}`);
|
|
5666
|
+
}
|
|
5667
|
+
return pkg.version;
|
|
5668
|
+
}
|
|
5669
|
+
var PACKAGE_VERSION = readOwnPackageVersion();
|
|
5670
|
+
function wantsCliVersion(argv) {
|
|
5671
|
+
return argv.some((arg) => arg === "--version" || arg === "-v");
|
|
5672
|
+
}
|
|
5673
|
+
function printCliVersionAndExit(version, binName) {
|
|
5674
|
+
console.log(binName ? `${binName} ${version}` : version);
|
|
5675
|
+
process.exit(0);
|
|
5676
|
+
}
|
|
5677
|
+
function handleCliVersionFlag(argv, moduleUrl = import.meta.url, binName) {
|
|
5678
|
+
if (!wantsCliVersion(argv)) return false;
|
|
5679
|
+
printCliVersionAndExit(readOwnPackageVersion(moduleUrl), binName);
|
|
5680
|
+
return true;
|
|
5681
|
+
}
|
|
5682
|
+
|
|
4176
5683
|
// src/cli.ts
|
|
4177
5684
|
function isHelpFlag(arg) {
|
|
4178
5685
|
return arg === "help" || arg === "--help" || arg === "-h";
|
|
@@ -4204,17 +5711,28 @@ function usage(code = 0) {
|
|
|
4204
5711
|
" 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
5712
|
" 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
5713
|
" kynver plan verify --plan PLAN_ID [--worktree PATH] [--task TASK_ID] [--human-override]",
|
|
4207
|
-
" kynver
|
|
5714
|
+
" 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]",
|
|
5715
|
+
" kynver plan outbox list",
|
|
5716
|
+
" kynver plan outbox drain [--max N] [--id OUTBOX_ID]",
|
|
5717
|
+
" kynver cleanup [--execute] [--node-modules-age-ms MS] [--worktrees-age-ms MS] [--harness-root PATH] [--include-orphans]",
|
|
5718
|
+
" kynver monitor start --run RUN_ID [--name worker] [--agent-os-id AOS_ID] [--poll-ms MS]",
|
|
5719
|
+
" kynver monitor status [--run RUN_ID] [--name worker] [--tick]",
|
|
5720
|
+
" kynver monitor stop --run RUN_ID [--name worker]",
|
|
5721
|
+
" kynver monitor list",
|
|
5722
|
+
" kynver monitor tick --run RUN_ID [--name worker] [--agent-os-id AOS_ID] [--auto-complete] [--renew-leases]",
|
|
5723
|
+
" kynver monitor auto-complete --run RUN_ID --name worker [--agent-os-id AOS_ID] [--base-url URL] [--secret SECRET]",
|
|
5724
|
+
" 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
5725
|
].join("\n")
|
|
4209
5726
|
);
|
|
4210
5727
|
process.exit(code);
|
|
4211
5728
|
}
|
|
4212
5729
|
async function main(argv = process.argv.slice(2)) {
|
|
5730
|
+
if (handleCliVersionFlag(argv, import.meta.url, "kynver")) return;
|
|
4213
5731
|
if (argv.length === 0 || isHelpFlag(argv[0])) return usage(0);
|
|
4214
5732
|
const scope = argv.shift();
|
|
4215
5733
|
let action;
|
|
4216
5734
|
let rest;
|
|
4217
|
-
if (scope === "run" || scope === "worker" || scope === "plan" || scope === "runner") {
|
|
5735
|
+
if (scope === "run" || scope === "worker" || scope === "plan" || scope === "runner" || scope === "monitor") {
|
|
4218
5736
|
action = argv.shift();
|
|
4219
5737
|
rest = argv;
|
|
4220
5738
|
} else {
|
|
@@ -4223,14 +5741,21 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
4223
5741
|
if (action && isHelpFlag(action) || rest.some(isHelpFlag)) return usage(0);
|
|
4224
5742
|
const args = parseArgs(rest);
|
|
4225
5743
|
const { runsDir, worktreesDir } = getPaths();
|
|
4226
|
-
|
|
4227
|
-
|
|
5744
|
+
mkdirSync7(runsDir, { recursive: true });
|
|
5745
|
+
mkdirSync7(worktreesDir, { recursive: true });
|
|
4228
5746
|
if (scope === "login") return void await runLogin(args);
|
|
4229
5747
|
if (scope === "runner" && action === "credential") return void await mintRunnerCredential(args);
|
|
4230
5748
|
if (scope === "setup") return void await runSetup(args);
|
|
4231
5749
|
if (scope === "daemon") return void await runDaemon(args);
|
|
4232
5750
|
if (scope === "plan" && action === "progress") return void await emitPlanProgress(args);
|
|
4233
5751
|
if (scope === "plan" && action === "verify") return void await verifyPlan(args);
|
|
5752
|
+
if (scope === "plan" && action === "persist") return void await runPlanPersist(args);
|
|
5753
|
+
if (scope === "plan" && action === "outbox") {
|
|
5754
|
+
const outboxAction = rest.shift();
|
|
5755
|
+
if (outboxAction === "list") return void await runPlanOutboxList();
|
|
5756
|
+
if (outboxAction === "drain") return void await runPlanOutboxDrain(parseArgs(rest));
|
|
5757
|
+
unknownCommand("plan", `outbox ${outboxAction ?? ""}`.trim());
|
|
5758
|
+
}
|
|
4234
5759
|
if (scope === "cleanup") return runCleanupCli(args);
|
|
4235
5760
|
if (scope === "run" && action === "create") return createRun(args);
|
|
4236
5761
|
if (scope === "run" && action === "list") return listRuns();
|
|
@@ -4243,9 +5768,20 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
4243
5768
|
if (scope === "worker" && action === "stop") return stopWorker(args);
|
|
4244
5769
|
if (scope === "worker" && action === "complete") return void await completeWorker(args);
|
|
4245
5770
|
if (scope === "worker" && action === "auto-complete") return void await autoCompleteWorkerCli(args);
|
|
5771
|
+
if (scope === "monitor" && action === "start") {
|
|
5772
|
+
const result = await startMonitorCli(args);
|
|
5773
|
+
console.log(JSON.stringify(result, null, 2));
|
|
5774
|
+
return;
|
|
5775
|
+
}
|
|
5776
|
+
if (scope === "monitor" && action === "status") return void await monitorStatusCli(args);
|
|
5777
|
+
if (scope === "monitor" && action === "stop") return monitorStopCli(args);
|
|
5778
|
+
if (scope === "monitor" && action === "list") return monitorListCli();
|
|
5779
|
+
if (scope === "monitor" && action === "tick") return void await monitorTickCli(args);
|
|
5780
|
+
if (scope === "monitor" && action === "auto-complete") return void await monitorAutoCompleteCli(args);
|
|
5781
|
+
if (scope === "monitor" && action === "run-loop") return void await monitorRunLoopCli(args);
|
|
4246
5782
|
unknownCommand(scope, action);
|
|
4247
5783
|
}
|
|
4248
|
-
var isCliEntry = process.argv[1] && realpathSync.native(process.argv[1]) === realpathSync.native(
|
|
5784
|
+
var isCliEntry = process.argv[1] && realpathSync.native(process.argv[1]) === realpathSync.native(fileURLToPath4(import.meta.url));
|
|
4249
5785
|
if (isCliEntry) {
|
|
4250
5786
|
void main().catch((error) => {
|
|
4251
5787
|
console.error(error);
|