@kynver-app/runtime 0.1.38 → 0.1.42
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 +696 -170
- package/dist/cli.js.map +4 -4
- package/dist/index.js +997 -173
- package/dist/index.js.map +4 -4
- package/package.json +6 -4
package/dist/cli.js
CHANGED
|
@@ -6,12 +6,39 @@ 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";
|
|
9
|
+
import { homedir as homedir2 } from "node:os";
|
|
10
|
+
import path3 from "node:path";
|
|
11
|
+
|
|
12
|
+
// src/path-values.ts
|
|
9
13
|
import { homedir } from "node:os";
|
|
10
|
-
import
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
function expandHomePath(value) {
|
|
16
|
+
if (value === "~") return homedir();
|
|
17
|
+
if (value.startsWith("~/") || value.startsWith("~\\")) {
|
|
18
|
+
return path.join(homedir(), value.slice(2));
|
|
19
|
+
}
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
function resolveUserPath(value) {
|
|
23
|
+
return path.resolve(expandHomePath(value));
|
|
24
|
+
}
|
|
25
|
+
function redactHomePath(value) {
|
|
26
|
+
const expanded = expandHomePath(value);
|
|
27
|
+
const resolved = path.resolve(expanded);
|
|
28
|
+
const home = path.resolve(homedir());
|
|
29
|
+
if (resolved === home) return "~";
|
|
30
|
+
if (resolved.startsWith(`${home}${path.sep}`)) {
|
|
31
|
+
return `~/${path.relative(home, resolved).split(path.sep).join("/")}`;
|
|
32
|
+
}
|
|
33
|
+
return resolved.replace(/^\/home\/[^/]+(?=\/|$)/, "~").replace(/^\/Users\/[^/]+(?=\/|$)/, "~");
|
|
34
|
+
}
|
|
35
|
+
function displayUserPath(value) {
|
|
36
|
+
return redactHomePath(value);
|
|
37
|
+
}
|
|
11
38
|
|
|
12
39
|
// src/util.ts
|
|
13
40
|
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
14
|
-
import
|
|
41
|
+
import path2 from "node:path";
|
|
15
42
|
function fail(message) {
|
|
16
43
|
console.error(message);
|
|
17
44
|
process.exit(1);
|
|
@@ -40,7 +67,7 @@ function readJson(file, fallback) {
|
|
|
40
67
|
}
|
|
41
68
|
}
|
|
42
69
|
function writeJson(file, value) {
|
|
43
|
-
mkdirSync(
|
|
70
|
+
mkdirSync(path2.dirname(file), { recursive: true });
|
|
44
71
|
writeFileSync(file, `${JSON.stringify(value, null, 2)}
|
|
45
72
|
`);
|
|
46
73
|
}
|
|
@@ -76,7 +103,7 @@ function tailFile(file, lines) {
|
|
|
76
103
|
return data.split("\n").slice(-lines).join("\n");
|
|
77
104
|
}
|
|
78
105
|
function readMaybeFile(file) {
|
|
79
|
-
return file ? readFileSync(
|
|
106
|
+
return file ? readFileSync(path2.resolve(file), "utf8") : "";
|
|
80
107
|
}
|
|
81
108
|
function listRunIds(runsDir) {
|
|
82
109
|
if (!existsSync(runsDir)) return [];
|
|
@@ -119,9 +146,9 @@ function secsAgo(ms) {
|
|
|
119
146
|
}
|
|
120
147
|
|
|
121
148
|
// src/config.ts
|
|
122
|
-
var CONFIG_DIR =
|
|
123
|
-
var CONFIG_FILE =
|
|
124
|
-
var CREDENTIALS_FILE =
|
|
149
|
+
var CONFIG_DIR = path3.join(homedir2(), ".kynver");
|
|
150
|
+
var CONFIG_FILE = path3.join(CONFIG_DIR, "config.json");
|
|
151
|
+
var CREDENTIALS_FILE = path3.join(CONFIG_DIR, "credentials");
|
|
125
152
|
function loadUserConfig() {
|
|
126
153
|
if (!existsSync2(CONFIG_FILE)) return {};
|
|
127
154
|
try {
|
|
@@ -132,9 +159,33 @@ function loadUserConfig() {
|
|
|
132
159
|
}
|
|
133
160
|
function saveUserConfig(config) {
|
|
134
161
|
mkdirSync2(CONFIG_DIR, { recursive: true });
|
|
135
|
-
writeFileSync2(CONFIG_FILE, `${JSON.stringify(config, null, 2)}
|
|
162
|
+
writeFileSync2(CONFIG_FILE, `${JSON.stringify(normalizeConfigPaths(config), null, 2)}
|
|
136
163
|
`, { mode: 384 });
|
|
137
164
|
}
|
|
165
|
+
function normalizeConfigPaths(config) {
|
|
166
|
+
return {
|
|
167
|
+
...config,
|
|
168
|
+
...config.harnessRoot?.trim() ? { harnessRoot: redactHomePath(config.harnessRoot.trim()) } : {},
|
|
169
|
+
...config.defaultRepo?.trim() ? { defaultRepo: redactHomePath(config.defaultRepo.trim()) } : {}
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
function presentUserConfig(config) {
|
|
173
|
+
return normalizeConfigPaths(config);
|
|
174
|
+
}
|
|
175
|
+
function inferSetupFields(existing, args) {
|
|
176
|
+
const creds = loadCredentialsFile();
|
|
177
|
+
const apiBaseUrl = (typeof args.apiBaseUrl === "string" ? args.apiBaseUrl : void 0) || existing.apiBaseUrl?.trim() || process.env.KYNVER_API_URL?.trim() || process.env.OPENCLAW_CRON_FIRE_BASE_URL?.trim();
|
|
178
|
+
const agentOsId = (typeof args.agentOsId === "string" ? args.agentOsId : void 0) || existing.agentOsId?.trim() || process.env.KYNVER_AGENT_OS_ID?.trim() || (creds.runnerToken?.trim().startsWith("krc1.") ? creds.runnerTokenAgentOsId?.trim() : void 0);
|
|
179
|
+
const defaultRepo = (typeof args.repo === "string" ? args.repo : void 0) || existing.defaultRepo?.trim() || process.env.KYNVER_DEFAULT_REPO?.trim() || process.env.KYNVER_HARNESS_REPO?.trim();
|
|
180
|
+
const harnessRoot = (typeof args.harnessRoot === "string" ? args.harnessRoot : void 0) || existing.harnessRoot?.trim() || process.env.KYNVER_HARNESS_ROOT?.trim() || process.env.OPUS_HARNESS_ROOT?.trim();
|
|
181
|
+
return {
|
|
182
|
+
...apiBaseUrl ? { apiBaseUrl: trimTrailingSlash(apiBaseUrl) } : {},
|
|
183
|
+
...agentOsId ? { agentOsId } : {},
|
|
184
|
+
...defaultRepo ? { defaultRepo } : {},
|
|
185
|
+
...harnessRoot ? { harnessRoot } : {},
|
|
186
|
+
...typeof args.agentOsSlug === "string" ? { agentOsSlug: args.agentOsSlug } : existing.agentOsSlug ? { agentOsSlug: existing.agentOsSlug } : {}
|
|
187
|
+
};
|
|
188
|
+
}
|
|
138
189
|
function loadCredentialsFile() {
|
|
139
190
|
if (!existsSync2(CREDENTIALS_FILE)) return {};
|
|
140
191
|
try {
|
|
@@ -280,7 +331,7 @@ async function mintRunnerCredential(args) {
|
|
|
280
331
|
{
|
|
281
332
|
ok: true,
|
|
282
333
|
agentOsId,
|
|
283
|
-
credentialsPath: CREDENTIALS_FILE,
|
|
334
|
+
credentialsPath: displayUserPath(CREDENTIALS_FILE),
|
|
284
335
|
tokenPrefix: `${token.slice(0, 12)}\u2026`,
|
|
285
336
|
note: "Scoped runner token saved; callbacks use X-Kynver-Runner-Token."
|
|
286
337
|
},
|
|
@@ -315,16 +366,12 @@ function parseArgs(argv) {
|
|
|
315
366
|
async function runSetup(args) {
|
|
316
367
|
const existing = loadUserConfig();
|
|
317
368
|
const maxWorkersRaw = typeof args.maxWorkers === "string" ? args.maxWorkers : typeof args.maxConcurrentWorkers === "string" ? args.maxConcurrentWorkers : void 0;
|
|
318
|
-
const config = {
|
|
369
|
+
const config = normalizeConfigPaths({
|
|
319
370
|
...existing,
|
|
320
|
-
...
|
|
321
|
-
...typeof args.agentOsSlug === "string" ? { agentOsSlug: args.agentOsSlug } : {},
|
|
322
|
-
...typeof args.agentOsId === "string" ? { agentOsId: args.agentOsId } : {},
|
|
323
|
-
...typeof args.repo === "string" ? { defaultRepo: args.repo } : {},
|
|
324
|
-
...typeof args.harnessRoot === "string" ? { harnessRoot: args.harnessRoot } : {},
|
|
371
|
+
...inferSetupFields(existing, args),
|
|
325
372
|
...maxWorkersRaw ? { maxConcurrentWorkers: Math.max(1, Math.floor(Number(maxWorkersRaw))) } : {},
|
|
326
373
|
workerProvider: typeof args.provider === "string" ? args.provider : existing.workerProvider || "claude"
|
|
327
|
-
};
|
|
374
|
+
});
|
|
328
375
|
saveUserConfig(config);
|
|
329
376
|
let runnerCredentialNote;
|
|
330
377
|
const apiKey = loadApiKey();
|
|
@@ -345,8 +392,8 @@ async function runSetup(args) {
|
|
|
345
392
|
JSON.stringify(
|
|
346
393
|
{
|
|
347
394
|
ok: true,
|
|
348
|
-
configPath: CONFIG_FILE,
|
|
349
|
-
config,
|
|
395
|
+
configPath: displayUserPath(CONFIG_FILE),
|
|
396
|
+
config: presentUserConfig(config),
|
|
350
397
|
note: runnerCredentialNote ?? "Set worker limit once with --max-workers N (or omit to auto-size from RAM). Run `kynver login` + `kynver runner credential` for scoped callbacks."
|
|
351
398
|
},
|
|
352
399
|
null,
|
|
@@ -358,11 +405,11 @@ async function runLogin(args) {
|
|
|
358
405
|
const apiKey = typeof args.apiKey === "string" ? args.apiKey : process.env.KYNVER_API_KEY;
|
|
359
406
|
if (!apiKey) failConfig("kynver login requires --api-key or KYNVER_API_KEY");
|
|
360
407
|
saveApiKey(apiKey);
|
|
361
|
-
console.log(JSON.stringify({ ok: true, credentialsPath: CREDENTIALS_FILE }, null, 2));
|
|
408
|
+
console.log(JSON.stringify({ ok: true, credentialsPath: displayUserPath(CREDENTIALS_FILE) }, null, 2));
|
|
362
409
|
}
|
|
363
410
|
|
|
364
411
|
// src/dispatch.ts
|
|
365
|
-
import
|
|
412
|
+
import path17 from "node:path";
|
|
366
413
|
|
|
367
414
|
// src/callback-headers.ts
|
|
368
415
|
function buildHarnessCallbackHeaders(secret) {
|
|
@@ -424,12 +471,12 @@ var DEFAULT_CRITICAL_FREE_BYTES = 15 * 1024 * 1024 * 1024;
|
|
|
424
471
|
var DEFAULT_MAX_USED_PERCENT = 80;
|
|
425
472
|
var DEFAULT_HARD_MAX_USED_PERCENT = 90;
|
|
426
473
|
function observeRunnerDiskGate(input = {}) {
|
|
427
|
-
const
|
|
474
|
+
const path35 = input.diskPath?.trim() || "/";
|
|
428
475
|
const warnBelowBytes = input.diskFreeWarnBytes ?? DEFAULT_WARN_FREE_BYTES;
|
|
429
476
|
const criticalBelowBytes = input.diskFreeCriticalBytes ?? DEFAULT_CRITICAL_FREE_BYTES;
|
|
430
477
|
const maxUsedPercent = input.diskMaxUsedPercent ?? DEFAULT_MAX_USED_PERCENT;
|
|
431
478
|
const hardMaxUsedPercent = input.diskHardMaxUsedPercent ?? DEFAULT_HARD_MAX_USED_PERCENT;
|
|
432
|
-
const stats = statfsSync(
|
|
479
|
+
const stats = statfsSync(path35);
|
|
433
480
|
const freeBytes = Number(stats.bavail) * Number(stats.bsize);
|
|
434
481
|
const totalBytes = Number(stats.blocks) * Number(stats.bsize);
|
|
435
482
|
const usedPercent = totalBytes > 0 ? (totalBytes - freeBytes) / totalBytes * 100 : 100;
|
|
@@ -449,7 +496,7 @@ function observeRunnerDiskGate(input = {}) {
|
|
|
449
496
|
}
|
|
450
497
|
return {
|
|
451
498
|
ok,
|
|
452
|
-
path:
|
|
499
|
+
path: path35,
|
|
453
500
|
freeBytes,
|
|
454
501
|
totalBytes,
|
|
455
502
|
usedPercent,
|
|
@@ -464,21 +511,23 @@ function observeRunnerDiskGate(input = {}) {
|
|
|
464
511
|
// src/resource-gate.ts
|
|
465
512
|
import { readFileSync as readFileSync5 } from "node:fs";
|
|
466
513
|
import os from "node:os";
|
|
467
|
-
import
|
|
514
|
+
import path6 from "node:path";
|
|
468
515
|
|
|
469
516
|
// src/run-store.ts
|
|
470
517
|
import { existsSync as existsSync4, readdirSync as readdirSync2 } from "node:fs";
|
|
471
|
-
import
|
|
518
|
+
import path5 from "node:path";
|
|
472
519
|
|
|
473
520
|
// src/paths.ts
|
|
474
521
|
import { existsSync as existsSync3 } from "node:fs";
|
|
475
|
-
import { homedir as
|
|
476
|
-
import
|
|
477
|
-
var LEGACY_ROOT =
|
|
522
|
+
import { homedir as homedir3 } from "node:os";
|
|
523
|
+
import path4 from "node:path";
|
|
524
|
+
var LEGACY_ROOT = path4.join(homedir3(), ".openclaw", "harness");
|
|
478
525
|
function resolveHarnessRoot() {
|
|
479
526
|
const env = process.env.KYNVER_HARNESS_ROOT || process.env.OPUS_HARNESS_ROOT;
|
|
480
|
-
if (env) return
|
|
481
|
-
const
|
|
527
|
+
if (env) return resolveUserPath(env);
|
|
528
|
+
const configured = loadUserConfig().harnessRoot?.trim();
|
|
529
|
+
if (configured) return resolveUserPath(configured);
|
|
530
|
+
const kynverRoot = path4.join(homedir3(), ".kynver", "harness");
|
|
482
531
|
if (existsSync3(kynverRoot)) return kynverRoot;
|
|
483
532
|
if (existsSync3(LEGACY_ROOT)) return LEGACY_ROOT;
|
|
484
533
|
return kynverRoot;
|
|
@@ -487,12 +536,12 @@ function getHarnessPaths() {
|
|
|
487
536
|
const harnessRoot = resolveHarnessRoot();
|
|
488
537
|
return {
|
|
489
538
|
harnessRoot,
|
|
490
|
-
runsDir:
|
|
491
|
-
worktreesDir:
|
|
539
|
+
runsDir: path4.join(harnessRoot, "runs"),
|
|
540
|
+
worktreesDir: path4.join(harnessRoot, "worktrees")
|
|
492
541
|
};
|
|
493
542
|
}
|
|
494
543
|
function runDir(runsDir, id) {
|
|
495
|
-
return
|
|
544
|
+
return path4.join(runsDir, safeSlug(id));
|
|
496
545
|
}
|
|
497
546
|
|
|
498
547
|
// src/run-store.ts
|
|
@@ -501,7 +550,7 @@ function getPaths() {
|
|
|
501
550
|
}
|
|
502
551
|
function loadRun(id) {
|
|
503
552
|
const { runsDir } = getPaths();
|
|
504
|
-
return readJson(
|
|
553
|
+
return readJson(path5.join(runDir(runsDir, safeSlug(id)), "run.json"));
|
|
505
554
|
}
|
|
506
555
|
function listRunRecords() {
|
|
507
556
|
const { runsDir } = getPaths();
|
|
@@ -510,7 +559,7 @@ function listRunRecords() {
|
|
|
510
559
|
for (const entry of readdirSync2(runsDir, { withFileTypes: true })) {
|
|
511
560
|
if (!entry.isDirectory()) continue;
|
|
512
561
|
const run = readJson(
|
|
513
|
-
|
|
562
|
+
path5.join(runsDir, entry.name, "run.json"),
|
|
514
563
|
void 0
|
|
515
564
|
);
|
|
516
565
|
if (run?.id) runs.push(run);
|
|
@@ -520,16 +569,16 @@ function listRunRecords() {
|
|
|
520
569
|
function loadWorker(runId, name) {
|
|
521
570
|
const { runsDir } = getPaths();
|
|
522
571
|
return readJson(
|
|
523
|
-
|
|
572
|
+
path5.join(runDir(runsDir, safeSlug(runId)), "workers", safeSlug(name), "worker.json")
|
|
524
573
|
);
|
|
525
574
|
}
|
|
526
575
|
function saveRun(run) {
|
|
527
576
|
const { runsDir } = getPaths();
|
|
528
|
-
writeJson(
|
|
577
|
+
writeJson(path5.join(runDir(runsDir, run.id), "run.json"), run);
|
|
529
578
|
}
|
|
530
579
|
function saveWorker(runId, worker) {
|
|
531
580
|
const { runsDir } = getPaths();
|
|
532
|
-
writeJson(
|
|
581
|
+
writeJson(path5.join(runDir(runsDir, runId), "workers", worker.name, "worker.json"), worker);
|
|
533
582
|
}
|
|
534
583
|
function runDirectory(id) {
|
|
535
584
|
const { runsDir } = getPaths();
|
|
@@ -1282,7 +1331,7 @@ function countActiveWorkersForRun(run) {
|
|
|
1282
1331
|
let active = 0;
|
|
1283
1332
|
for (const name of Object.keys(run.workers || {})) {
|
|
1284
1333
|
const worker = readJson(
|
|
1285
|
-
|
|
1334
|
+
path6.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
1286
1335
|
void 0
|
|
1287
1336
|
);
|
|
1288
1337
|
if (!worker || !isActiveHarnessWorker(worker)) continue;
|
|
@@ -1490,6 +1539,7 @@ var claudeProvider = {
|
|
|
1490
1539
|
const model = preflight.model;
|
|
1491
1540
|
const stdoutFd = openSync(opts.stdoutPath, "a");
|
|
1492
1541
|
const stderrFd = openSync(opts.stderrPath, "a");
|
|
1542
|
+
const stdio = ["ignore", stdoutFd, stderrFd];
|
|
1493
1543
|
const child = spawn(
|
|
1494
1544
|
"claude",
|
|
1495
1545
|
[
|
|
@@ -1507,7 +1557,7 @@ var claudeProvider = {
|
|
|
1507
1557
|
hiddenSpawnOptions({
|
|
1508
1558
|
cwd: opts.worktreePath,
|
|
1509
1559
|
detached: true,
|
|
1510
|
-
stdio
|
|
1560
|
+
stdio,
|
|
1511
1561
|
env: scrubClaudeEnv(process.env)
|
|
1512
1562
|
})
|
|
1513
1563
|
);
|
|
@@ -1666,10 +1716,10 @@ function readHarnessRetryLimits() {
|
|
|
1666
1716
|
}
|
|
1667
1717
|
|
|
1668
1718
|
// src/lease-renewal.ts
|
|
1669
|
-
import
|
|
1719
|
+
import path7 from "node:path";
|
|
1670
1720
|
function workerRecord(runId, name) {
|
|
1671
1721
|
return readJson(
|
|
1672
|
-
|
|
1722
|
+
path7.join(runDirectory(runId), "workers", safeSlug(name), "worker.json"),
|
|
1673
1723
|
void 0
|
|
1674
1724
|
);
|
|
1675
1725
|
}
|
|
@@ -1736,7 +1786,7 @@ function hasLiveWorkerForTask(runId, taskId) {
|
|
|
1736
1786
|
|
|
1737
1787
|
// src/supervisor.ts
|
|
1738
1788
|
import { existsSync as existsSync10, mkdirSync as mkdirSync3 } from "node:fs";
|
|
1739
|
-
import
|
|
1789
|
+
import path13 from "node:path";
|
|
1740
1790
|
|
|
1741
1791
|
// src/prompt.ts
|
|
1742
1792
|
function buildPrompt(input) {
|
|
@@ -1755,6 +1805,16 @@ function buildPrompt(input) {
|
|
|
1755
1805
|
"- After implementation: wait for report_reviewer then deep_reviewer confirmation (via MCP/session agents) before follow-up rows close.",
|
|
1756
1806
|
input.planId ? `Active planId: ${input.planId}${input.taskId ? ` \xB7 taskId: ${input.taskId}` : ""}` : "No planId on this worker \u2014 still emit progress when you touch plan-scoped work."
|
|
1757
1807
|
];
|
|
1808
|
+
const mergeGateLines = compact ? [
|
|
1809
|
+
"Merge-gate cost control: run `node scripts/verify-pr-local.mjs --emit-json` and `node scripts/collect-pr-vercel-evidence.mjs --pr <url> --emit-json` (trusts GitHub Vercel status for dashboard target_url; never passes dashboard URLs to `vercel inspect`) before any GitHub Actions run; request merge-gate only via POST pr-merge-gate/request-run (one Actions run per PR head unless human approves extra)."
|
|
1810
|
+
] : [
|
|
1811
|
+
"GitHub Actions merge-gate cost control (Kynver/Hermes PRs):",
|
|
1812
|
+
"- Prefer local cached package verification (`node scripts/verify-pr-local.mjs --emit-json`) and Vercel preview evidence before GitHub Actions.",
|
|
1813
|
+
"- Do not push empty commits to re-trigger CI. One budgeted merge-gate Actions run per PR candidate (head SHA) unless a human approves extra.",
|
|
1814
|
+
"- Record evidence: POST `/api/agent-os/by-id/<agentOsId>/pr-merge-gate/refresh` with prUrl + local/vercel payloads.",
|
|
1815
|
+
"- Request the final Actions run only when local + Vercel are green: POST `.../pr-merge-gate/request-run` (applies `merge-gate` label).",
|
|
1816
|
+
"- Empty failed Actions jobs (no runner/steps/logs) are infra/quota \u2014 do not enter repair loops; escalate to operator."
|
|
1817
|
+
];
|
|
1758
1818
|
const planArtifactLines = compact ? [
|
|
1759
1819
|
"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."
|
|
1760
1820
|
] : [
|
|
@@ -1776,10 +1836,14 @@ function buildPrompt(input) {
|
|
|
1776
1836
|
"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.",
|
|
1777
1837
|
"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.",
|
|
1778
1838
|
"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.",
|
|
1839
|
+
"npm publish boundary: do not run `npm publish`, do not republish `@kynver-app/*` packages, and do not block on an operator to publish. When you need newer runtime code than npm, use this repo checkout (`npm run kynver:build`, `npm run kynver`) and record evidence: packages/kynver-runtime/package.json version + git ref in your completion report.",
|
|
1779
1840
|
"If verification fails (including OOM), append a heartbeat line immediately with the last command, failure reason, dirty-file status, commit/PR handoff state, and next action so recovery does not require log spelunking.",
|
|
1841
|
+
"Landing-wrapper cleanup on a git ref: use `node scripts/agent-os-land-pr-cleanup-verify.mjs --ref origin/main` (JSON, exit 0). Do not use `git show <ref>:scripts/agent-os-land-pr.mjs | rg \u2026` \u2014 ripgrep exit 1 when markers are absent is reported as a failed command in Telegram/status tooling.",
|
|
1780
1842
|
"",
|
|
1781
1843
|
...progressLines,
|
|
1782
1844
|
"",
|
|
1845
|
+
...mergeGateLines,
|
|
1846
|
+
"",
|
|
1783
1847
|
...planArtifactLines,
|
|
1784
1848
|
"",
|
|
1785
1849
|
...input.personaMarkdown?.trim() ? [input.personaMarkdown.trim(), ""] : [],
|
|
@@ -1792,11 +1856,11 @@ function buildPrompt(input) {
|
|
|
1792
1856
|
// src/providers/cursor.ts
|
|
1793
1857
|
import { closeSync as closeSync2, existsSync as existsSync8, openSync as openSync2 } from "node:fs";
|
|
1794
1858
|
import { spawn as spawn2 } from "node:child_process";
|
|
1795
|
-
import
|
|
1859
|
+
import path9 from "node:path";
|
|
1796
1860
|
|
|
1797
1861
|
// src/providers/cursor-windows.ts
|
|
1798
1862
|
import { existsSync as existsSync7, readdirSync as readdirSync3 } from "node:fs";
|
|
1799
|
-
import
|
|
1863
|
+
import path8 from "node:path";
|
|
1800
1864
|
var CURSOR_VERSION_DIR = /^\d{4}\.\d{1,2}\.\d{1,2}-[a-f0-9]+$/i;
|
|
1801
1865
|
function parseCursorVersionSortKey(versionName) {
|
|
1802
1866
|
const datePart = versionName.split("-")[0];
|
|
@@ -1807,7 +1871,7 @@ function parseCursorVersionSortKey(versionName) {
|
|
|
1807
1871
|
return Number(`${year}${month.padStart(2, "0")}${day.padStart(2, "0")}`);
|
|
1808
1872
|
}
|
|
1809
1873
|
function pickLatestCursorVersionDir(agentRoot) {
|
|
1810
|
-
const versionsRoot =
|
|
1874
|
+
const versionsRoot = path8.join(agentRoot, "versions");
|
|
1811
1875
|
if (!existsSync7(versionsRoot)) return null;
|
|
1812
1876
|
let bestDir = null;
|
|
1813
1877
|
let bestKey = -1;
|
|
@@ -1816,21 +1880,21 @@ function pickLatestCursorVersionDir(agentRoot) {
|
|
|
1816
1880
|
const key = parseCursorVersionSortKey(entry.name);
|
|
1817
1881
|
if (key == null || key <= bestKey) continue;
|
|
1818
1882
|
bestKey = key;
|
|
1819
|
-
bestDir =
|
|
1883
|
+
bestDir = path8.join(versionsRoot, entry.name);
|
|
1820
1884
|
}
|
|
1821
1885
|
return bestDir;
|
|
1822
1886
|
}
|
|
1823
1887
|
function resolveWindowsCursorBundled(agentRoot) {
|
|
1824
|
-
const root = agentRoot?.trim() ||
|
|
1825
|
-
const directNode =
|
|
1826
|
-
const directIndex =
|
|
1888
|
+
const root = agentRoot?.trim() || path8.join(process.env.LOCALAPPDATA || "", "cursor-agent");
|
|
1889
|
+
const directNode = path8.join(root, "node.exe");
|
|
1890
|
+
const directIndex = path8.join(root, "index.js");
|
|
1827
1891
|
if (existsSync7(directNode) && existsSync7(directIndex)) {
|
|
1828
1892
|
return { nodeExe: directNode, indexJs: directIndex, versionDir: root };
|
|
1829
1893
|
}
|
|
1830
1894
|
const versionDir = pickLatestCursorVersionDir(root);
|
|
1831
1895
|
if (!versionDir) return null;
|
|
1832
|
-
const nodeExe =
|
|
1833
|
-
const indexJs =
|
|
1896
|
+
const nodeExe = path8.join(versionDir, "node.exe");
|
|
1897
|
+
const indexJs = path8.join(versionDir, "index.js");
|
|
1834
1898
|
if (!existsSync7(nodeExe) || !existsSync7(indexJs)) return null;
|
|
1835
1899
|
return { nodeExe, indexJs, versionDir };
|
|
1836
1900
|
}
|
|
@@ -1849,13 +1913,13 @@ function bundledSpawnTarget(nodeExe, indexJs, versionDir) {
|
|
|
1849
1913
|
function resolveCursorSpawn(agentBin) {
|
|
1850
1914
|
if (process.platform === "win32") {
|
|
1851
1915
|
const isCursorWrapper = /\.(cmd|bat)$/i.test(agentBin);
|
|
1852
|
-
const isBundledNode = /node\.exe$/i.test(agentBin) && existsSync8(
|
|
1916
|
+
const isBundledNode = /node\.exe$/i.test(agentBin) && existsSync8(path9.join(path9.dirname(agentBin), "index.js"));
|
|
1853
1917
|
const isDefaultShim = agentBin === "agent";
|
|
1854
1918
|
if (isCursorWrapper || isBundledNode || isDefaultShim) {
|
|
1855
|
-
const bundled = isCursorWrapper ? resolveWindowsCursorBundled(
|
|
1919
|
+
const bundled = isCursorWrapper ? resolveWindowsCursorBundled(path9.dirname(agentBin)) : isBundledNode ? {
|
|
1856
1920
|
nodeExe: agentBin,
|
|
1857
|
-
indexJs:
|
|
1858
|
-
versionDir:
|
|
1921
|
+
indexJs: path9.join(path9.dirname(agentBin), "index.js"),
|
|
1922
|
+
versionDir: path9.dirname(agentBin)
|
|
1859
1923
|
} : resolveWindowsCursorBundled();
|
|
1860
1924
|
if (bundled) {
|
|
1861
1925
|
return bundledSpawnTarget(bundled.nodeExe, bundled.indexJs, bundled.versionDir);
|
|
@@ -1875,7 +1939,7 @@ function resolveAgentBin() {
|
|
|
1875
1939
|
process.env.KYNVER_CURSOR_AGENT_ROOT?.trim() || void 0
|
|
1876
1940
|
);
|
|
1877
1941
|
if (bundled) return bundled.nodeExe;
|
|
1878
|
-
const localAgent =
|
|
1942
|
+
const localAgent = path9.join(process.env.LOCALAPPDATA || "", "cursor-agent", "agent.cmd");
|
|
1879
1943
|
if (existsSync8(localAgent)) return localAgent;
|
|
1880
1944
|
}
|
|
1881
1945
|
return "agent";
|
|
@@ -1885,7 +1949,7 @@ function cursorWorkerEnv(agentBin, spawnTarget) {
|
|
|
1885
1949
|
...process.env,
|
|
1886
1950
|
CI: "1",
|
|
1887
1951
|
NO_COLOR: "1",
|
|
1888
|
-
...spawnTarget.bundledVersionDir ? { CURSOR_INVOKED_AS:
|
|
1952
|
+
...spawnTarget.bundledVersionDir ? { CURSOR_INVOKED_AS: path9.basename(agentBin) || "agent.cmd" } : {}
|
|
1889
1953
|
};
|
|
1890
1954
|
}
|
|
1891
1955
|
var cursorProvider = {
|
|
@@ -1902,6 +1966,7 @@ var cursorProvider = {
|
|
|
1902
1966
|
const model = preflight.model;
|
|
1903
1967
|
const stdoutFd = openSync2(opts.stdoutPath, "a");
|
|
1904
1968
|
const stderrFd = openSync2(opts.stderrPath, "a");
|
|
1969
|
+
const stdio = ["ignore", stdoutFd, stderrFd];
|
|
1905
1970
|
const agentBin = resolveAgentBin();
|
|
1906
1971
|
const spawnTarget = resolveCursorSpawn(agentBin);
|
|
1907
1972
|
const child = spawn2(
|
|
@@ -1924,7 +1989,7 @@ var cursorProvider = {
|
|
|
1924
1989
|
cwd: opts.worktreePath,
|
|
1925
1990
|
detached: spawnTarget.detached,
|
|
1926
1991
|
shell: spawnTarget.shell,
|
|
1927
|
-
stdio
|
|
1992
|
+
stdio,
|
|
1928
1993
|
env: cursorWorkerEnv(agentBin, spawnTarget)
|
|
1929
1994
|
})
|
|
1930
1995
|
);
|
|
@@ -1959,11 +2024,24 @@ function resolveWorkerProvider(name) {
|
|
|
1959
2024
|
// src/auto-complete.ts
|
|
1960
2025
|
import { spawn as spawn3 } from "node:child_process";
|
|
1961
2026
|
import { existsSync as existsSync9, openSync as openSync3, closeSync as closeSync3 } from "node:fs";
|
|
1962
|
-
import
|
|
2027
|
+
import path12 from "node:path";
|
|
1963
2028
|
import { fileURLToPath } from "node:url";
|
|
1964
2029
|
|
|
2030
|
+
// src/completion-ack.ts
|
|
2031
|
+
function hasCompletionAck(worker) {
|
|
2032
|
+
return Boolean(worker.completionReportedAt?.trim());
|
|
2033
|
+
}
|
|
2034
|
+
function persistCompletionAck(worker, runId, fields) {
|
|
2035
|
+
worker.completionReportedAt = fields.completionReportedAt;
|
|
2036
|
+
worker.completionOutcome = fields.completionOutcome;
|
|
2037
|
+
if (fields.completionResponse !== void 0) {
|
|
2038
|
+
worker.completionResponse = fields.completionResponse;
|
|
2039
|
+
}
|
|
2040
|
+
saveWorker(runId, worker);
|
|
2041
|
+
}
|
|
2042
|
+
|
|
1965
2043
|
// src/worker-ops.ts
|
|
1966
|
-
import
|
|
2044
|
+
import path11 from "node:path";
|
|
1967
2045
|
|
|
1968
2046
|
// src/pr-handoff/pr-handoff-assess.ts
|
|
1969
2047
|
var REVIEW_LANE_RULE = /^(lane:)?(review|deep_review|planning|landing)(:|$)/i;
|
|
@@ -2299,21 +2377,8 @@ function ensurePrReadyHandoff(input, exec = defaultPrHandoffExec) {
|
|
|
2299
2377
|
};
|
|
2300
2378
|
}
|
|
2301
2379
|
|
|
2302
|
-
// src/completion-ack.ts
|
|
2303
|
-
function hasCompletionAck(worker) {
|
|
2304
|
-
return Boolean(worker.completionReportedAt?.trim());
|
|
2305
|
-
}
|
|
2306
|
-
function persistCompletionAck(worker, runId, fields) {
|
|
2307
|
-
worker.completionReportedAt = fields.completionReportedAt;
|
|
2308
|
-
worker.completionOutcome = fields.completionOutcome;
|
|
2309
|
-
if (fields.completionResponse !== void 0) {
|
|
2310
|
-
worker.completionResponse = fields.completionResponse;
|
|
2311
|
-
}
|
|
2312
|
-
saveWorker(runId, worker);
|
|
2313
|
-
}
|
|
2314
|
-
|
|
2315
2380
|
// src/worker-lifecycle.ts
|
|
2316
|
-
import
|
|
2381
|
+
import path10 from "node:path";
|
|
2317
2382
|
var TASK_LEFT_RUNNING = /* @__PURE__ */ new Set([
|
|
2318
2383
|
"awaiting_review",
|
|
2319
2384
|
"blocked",
|
|
@@ -2369,7 +2434,7 @@ function syncCompletionAcknowledgedFromOperatorTick(runId, operatorTick) {
|
|
|
2369
2434
|
const synced = [];
|
|
2370
2435
|
for (const name of Object.keys(run.workers || {})) {
|
|
2371
2436
|
const worker = readJson(
|
|
2372
|
-
|
|
2437
|
+
path10.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
2373
2438
|
void 0
|
|
2374
2439
|
);
|
|
2375
2440
|
if (!worker?.taskId || isCompletionAcknowledged(worker)) continue;
|
|
@@ -2606,7 +2671,7 @@ function workerStatus(args) {
|
|
|
2606
2671
|
const worker = loadWorker(String(args.run), String(args.name));
|
|
2607
2672
|
const run = loadRun(worker.runId);
|
|
2608
2673
|
const status = computeWorkerStatus(worker, workerStatusOptions(run));
|
|
2609
|
-
writeJson(
|
|
2674
|
+
writeJson(path11.join(worker.workerDir, "last-status.json"), status);
|
|
2610
2675
|
console.log(JSON.stringify(status, null, 2));
|
|
2611
2676
|
}
|
|
2612
2677
|
function buildRunBoard(runId) {
|
|
@@ -2614,7 +2679,7 @@ function buildRunBoard(runId) {
|
|
|
2614
2679
|
const names = Object.keys(run.workers || {});
|
|
2615
2680
|
const workers = names.map((name) => {
|
|
2616
2681
|
const worker = readJson(
|
|
2617
|
-
|
|
2682
|
+
path11.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
2618
2683
|
void 0
|
|
2619
2684
|
);
|
|
2620
2685
|
if (!worker) {
|
|
@@ -2642,16 +2707,17 @@ function buildRunBoard(runId) {
|
|
|
2642
2707
|
const completionRouteStatus = asString(completionResponse?.status);
|
|
2643
2708
|
const completionWarnings = Array.isArray(completionResponse?.warnings) ? completionResponse.warnings.filter((w) => typeof w === "string" && w.trim().length > 0) : [];
|
|
2644
2709
|
const prUrl = asString(completionTask?.prUrl) ?? asString(completionResponse?.prUrl);
|
|
2710
|
+
const completionReportedAt = asString(worker.completionReportedAt);
|
|
2645
2711
|
const lifecycleStage = deriveLifecycleStage({
|
|
2646
2712
|
finished: isFinishedWorkerStatus(status),
|
|
2647
2713
|
completionBlocker,
|
|
2648
2714
|
completionOutcome,
|
|
2649
|
-
completionReportedAt
|
|
2715
|
+
completionReportedAt
|
|
2650
2716
|
});
|
|
2651
2717
|
const nextAction = deriveNextAction({
|
|
2652
2718
|
completionBlocker,
|
|
2653
2719
|
completionOutcome,
|
|
2654
|
-
completionReportedAt
|
|
2720
|
+
completionReportedAt,
|
|
2655
2721
|
finished: isFinishedWorkerStatus(status)
|
|
2656
2722
|
});
|
|
2657
2723
|
const handoffState = deriveHandoffState({
|
|
@@ -2697,7 +2763,7 @@ function buildRunBoard(runId) {
|
|
|
2697
2763
|
gitAncestry: status.gitAncestry,
|
|
2698
2764
|
finalResult: status.finalResult,
|
|
2699
2765
|
lifecycleStage,
|
|
2700
|
-
completionReportedAt
|
|
2766
|
+
completionReportedAt,
|
|
2701
2767
|
completionOutcome: worker.completionOutcome ?? null,
|
|
2702
2768
|
completionRouteStatus,
|
|
2703
2769
|
completionRouteOutcome: completionOutcome,
|
|
@@ -2724,7 +2790,7 @@ function buildRunBoard(runId) {
|
|
|
2724
2790
|
needsAttention: workers.filter((w) => w.attention && w.attention !== "ok" && w.attention !== "done").map((w) => w.worker),
|
|
2725
2791
|
workers
|
|
2726
2792
|
};
|
|
2727
|
-
writeJson(
|
|
2793
|
+
writeJson(path11.join(runDirectory(run.id), "last-board.json"), board);
|
|
2728
2794
|
return board;
|
|
2729
2795
|
}
|
|
2730
2796
|
async function publishHarnessBoardSnapshot(args, source) {
|
|
@@ -2808,7 +2874,7 @@ async function autoCompleteWorker(raw) {
|
|
|
2808
2874
|
const maxTotalMs = args.maxTotalMs ?? DEFAULT_MAX_TOTAL_MS;
|
|
2809
2875
|
const completeAttempts = args.completeAttempts ?? DEFAULT_COMPLETE_ATTEMPTS;
|
|
2810
2876
|
const completeBackoffMs = args.completeBackoffMs ?? DEFAULT_COMPLETE_BACKOFF_MS;
|
|
2811
|
-
|
|
2877
|
+
let worker = loadWorker(args.run, args.name);
|
|
2812
2878
|
if (!worker.agentOsId || !worker.taskId) {
|
|
2813
2879
|
return {
|
|
2814
2880
|
worker: worker.name,
|
|
@@ -2818,8 +2884,29 @@ async function autoCompleteWorker(raw) {
|
|
|
2818
2884
|
reason: "worker has no agentOsId/taskId \u2014 nothing to attribute completion to"
|
|
2819
2885
|
};
|
|
2820
2886
|
}
|
|
2887
|
+
if (hasCompletionAck(worker)) {
|
|
2888
|
+
return {
|
|
2889
|
+
worker: worker.name,
|
|
2890
|
+
runId: worker.runId,
|
|
2891
|
+
outcome: "completed",
|
|
2892
|
+
httpStatus: 200,
|
|
2893
|
+
attempts: 0,
|
|
2894
|
+
reason: "completion-already-acknowledged"
|
|
2895
|
+
};
|
|
2896
|
+
}
|
|
2821
2897
|
const startMs = Date.now();
|
|
2822
2898
|
while (true) {
|
|
2899
|
+
worker = loadWorker(args.run, args.name);
|
|
2900
|
+
if (hasCompletionAck(worker)) {
|
|
2901
|
+
return {
|
|
2902
|
+
worker: worker.name,
|
|
2903
|
+
runId: worker.runId,
|
|
2904
|
+
outcome: "completed",
|
|
2905
|
+
httpStatus: 200,
|
|
2906
|
+
attempts: 0,
|
|
2907
|
+
reason: "completion-already-acknowledged"
|
|
2908
|
+
};
|
|
2909
|
+
}
|
|
2823
2910
|
const status = computeWorkerStatus(worker);
|
|
2824
2911
|
if (isFinishedWorkerStatus(status)) break;
|
|
2825
2912
|
if (!isPidAlive(worker.pid)) break;
|
|
@@ -2883,20 +2970,21 @@ async function autoCompleteWorkerCli(raw) {
|
|
|
2883
2970
|
const outcome = await autoCompleteWorker(raw);
|
|
2884
2971
|
console.log(JSON.stringify(outcome, null, 2));
|
|
2885
2972
|
if (outcome.outcome === "missing_link" || outcome.outcome === "timed_out") {
|
|
2886
|
-
process.
|
|
2973
|
+
process.exit(1);
|
|
2887
2974
|
}
|
|
2975
|
+
process.exit(0);
|
|
2888
2976
|
} catch (error) {
|
|
2889
2977
|
console.error(`worker auto-complete failed: ${error.message}`);
|
|
2890
|
-
process.
|
|
2978
|
+
process.exit(1);
|
|
2891
2979
|
}
|
|
2892
2980
|
}
|
|
2893
2981
|
function resolveDefaultCliPath() {
|
|
2894
|
-
return
|
|
2982
|
+
return path12.join(fileURLToPath(new URL(".", import.meta.url)), "cli.js");
|
|
2895
2983
|
}
|
|
2896
2984
|
function spawnCompletionSidecar(opts) {
|
|
2897
2985
|
const cliPath = opts.cliPath ?? resolveDefaultCliPath();
|
|
2898
2986
|
if (!existsSync9(cliPath)) return void 0;
|
|
2899
|
-
const logPath =
|
|
2987
|
+
const logPath = path12.join(opts.workerDir, "auto-complete.log");
|
|
2900
2988
|
let logFd;
|
|
2901
2989
|
try {
|
|
2902
2990
|
logFd = openSync3(logPath, "a");
|
|
@@ -2978,16 +3066,16 @@ function spawnWorkerProcess(run, opts) {
|
|
|
2978
3066
|
launchModel = preflight.model;
|
|
2979
3067
|
}
|
|
2980
3068
|
const { worktreesDir } = getPaths();
|
|
2981
|
-
const workerDir =
|
|
3069
|
+
const workerDir = path13.join(runDirectory(run.id), "workers", name);
|
|
2982
3070
|
mkdirSync3(workerDir, { recursive: true });
|
|
2983
|
-
const worktreePath =
|
|
3071
|
+
const worktreePath = path13.join(worktreesDir, run.id, name);
|
|
2984
3072
|
const branch = opts.branch || `agent/${run.id}/${name}`;
|
|
2985
3073
|
if (existsSync10(worktreePath)) throw new Error(`worktree path already exists: ${worktreePath}`);
|
|
2986
3074
|
git(run.repo, ["fetch", "origin", "--prune"], { allowFailure: true });
|
|
2987
3075
|
git(run.repo, ["worktree", "add", "-b", branch, worktreePath, run.baseCommit], { throwError: true });
|
|
2988
|
-
const stdoutPath =
|
|
2989
|
-
const stderrPath =
|
|
2990
|
-
const heartbeatPath =
|
|
3076
|
+
const stdoutPath = path13.join(workerDir, "stdout.jsonl");
|
|
3077
|
+
const stderrPath = path13.join(workerDir, "stderr.log");
|
|
3078
|
+
const heartbeatPath = path13.join(workerDir, "heartbeat.jsonl");
|
|
2991
3079
|
const prompt = buildPrompt({
|
|
2992
3080
|
task: opts.task,
|
|
2993
3081
|
ownedPaths: opts.ownedPaths || [],
|
|
@@ -3048,7 +3136,7 @@ function spawnWorkerProcess(run, opts) {
|
|
|
3048
3136
|
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3049
3137
|
};
|
|
3050
3138
|
saveWorker(run.id, worker);
|
|
3051
|
-
run.workers = { ...run.workers || {}, [name]: { workerDir, statusPath:
|
|
3139
|
+
run.workers = { ...run.workers || {}, [name]: { workerDir, statusPath: path13.join(workerDir, "worker.json") } };
|
|
3052
3140
|
run.status = "running";
|
|
3053
3141
|
saveRun(run);
|
|
3054
3142
|
if (worker.agentOsId && worker.taskId) {
|
|
@@ -3340,18 +3428,18 @@ function buildPlanPersistIdempotencyKey(input) {
|
|
|
3340
3428
|
|
|
3341
3429
|
// src/plan-persist/paths.ts
|
|
3342
3430
|
import { mkdirSync as mkdirSync4 } from "node:fs";
|
|
3343
|
-
import { homedir as
|
|
3344
|
-
import
|
|
3431
|
+
import { homedir as homedir4 } from "node:os";
|
|
3432
|
+
import path14 from "node:path";
|
|
3345
3433
|
function resolveKynverStateRoot() {
|
|
3346
3434
|
const env = process.env.KYNVER_STATE_ROOT;
|
|
3347
|
-
if (env) return
|
|
3348
|
-
return
|
|
3435
|
+
if (env) return path14.resolve(env);
|
|
3436
|
+
return path14.join(homedir4(), ".kynver", "state");
|
|
3349
3437
|
}
|
|
3350
3438
|
function planOutboxDir() {
|
|
3351
|
-
return
|
|
3439
|
+
return path14.join(resolveKynverStateRoot(), "plan-outbox");
|
|
3352
3440
|
}
|
|
3353
3441
|
function planOutboxArchiveDir() {
|
|
3354
|
-
return
|
|
3442
|
+
return path14.join(resolveKynverStateRoot(), "plan-outbox-archive");
|
|
3355
3443
|
}
|
|
3356
3444
|
function ensurePlanOutboxDirs() {
|
|
3357
3445
|
const outboxDir = planOutboxDir();
|
|
@@ -3362,8 +3450,8 @@ function ensurePlanOutboxDirs() {
|
|
|
3362
3450
|
}
|
|
3363
3451
|
function isTmpOnlyPath(filePath) {
|
|
3364
3452
|
if (filePath.startsWith("/tmp/") || filePath.startsWith("/var/folders/")) return true;
|
|
3365
|
-
const resolved =
|
|
3366
|
-
return resolved.startsWith("/tmp/") || resolved.startsWith(
|
|
3453
|
+
const resolved = path14.resolve(filePath);
|
|
3454
|
+
return resolved.startsWith("/tmp/") || resolved.startsWith(path14.join("/var", "folders"));
|
|
3367
3455
|
}
|
|
3368
3456
|
|
|
3369
3457
|
// src/plan-persist/outbox-store.ts
|
|
@@ -3375,7 +3463,7 @@ import {
|
|
|
3375
3463
|
writeFileSync as writeFileSync3,
|
|
3376
3464
|
unlinkSync
|
|
3377
3465
|
} from "node:fs";
|
|
3378
|
-
import
|
|
3466
|
+
import path15 from "node:path";
|
|
3379
3467
|
import { randomUUID } from "node:crypto";
|
|
3380
3468
|
var DEFAULT_MAX_RETRIES = 12;
|
|
3381
3469
|
function listOutboxItems() {
|
|
@@ -3383,7 +3471,7 @@ function listOutboxItems() {
|
|
|
3383
3471
|
const files = readdirSync4(outboxDir).filter((f) => f.endsWith(".json"));
|
|
3384
3472
|
const items = [];
|
|
3385
3473
|
for (const file of files) {
|
|
3386
|
-
const item = readOutboxItem(
|
|
3474
|
+
const item = readOutboxItem(path15.join(outboxDir, file));
|
|
3387
3475
|
if (item && item.queueStatus === "queued") items.push(item);
|
|
3388
3476
|
}
|
|
3389
3477
|
return items.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
@@ -3404,7 +3492,7 @@ function readOutboxItem(jsonPath) {
|
|
|
3404
3492
|
}
|
|
3405
3493
|
function readOutboxBody(item) {
|
|
3406
3494
|
const { outboxDir } = ensurePlanOutboxDirs();
|
|
3407
|
-
const bodyFile =
|
|
3495
|
+
const bodyFile = path15.join(outboxDir, item.bodyPath);
|
|
3408
3496
|
return readFileSync6(bodyFile, "utf8");
|
|
3409
3497
|
}
|
|
3410
3498
|
function writeOutboxItem(input, opts) {
|
|
@@ -3412,8 +3500,8 @@ function writeOutboxItem(input, opts) {
|
|
|
3412
3500
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3413
3501
|
const id = opts.existing?.id ?? randomUUID();
|
|
3414
3502
|
const bodyPath = opts.existing?.bodyPath ?? `${id}.body.md`;
|
|
3415
|
-
const jsonPath =
|
|
3416
|
-
const bodyFile =
|
|
3503
|
+
const jsonPath = path15.join(outboxDir, `${id}.json`);
|
|
3504
|
+
const bodyFile = path15.join(outboxDir, bodyPath);
|
|
3417
3505
|
if (!opts.existing) {
|
|
3418
3506
|
writeFileSync3(bodyFile, input.body, "utf8");
|
|
3419
3507
|
}
|
|
@@ -3449,24 +3537,24 @@ function writeOutboxItem(input, opts) {
|
|
|
3449
3537
|
}
|
|
3450
3538
|
function saveOutboxItem(item) {
|
|
3451
3539
|
const { outboxDir } = ensurePlanOutboxDirs();
|
|
3452
|
-
const jsonPath =
|
|
3540
|
+
const jsonPath = path15.join(outboxDir, `${item.id}.json`);
|
|
3453
3541
|
writeFileSync3(jsonPath, `${JSON.stringify(item, null, 2)}
|
|
3454
3542
|
`, { mode: 384 });
|
|
3455
3543
|
}
|
|
3456
3544
|
function archiveOutboxItem(item) {
|
|
3457
3545
|
const { outboxDir, archiveDir } = ensurePlanOutboxDirs();
|
|
3458
|
-
const jsonSrc =
|
|
3459
|
-
const bodySrc =
|
|
3460
|
-
const jsonDst =
|
|
3461
|
-
const bodyDst =
|
|
3546
|
+
const jsonSrc = path15.join(outboxDir, `${item.id}.json`);
|
|
3547
|
+
const bodySrc = path15.join(outboxDir, item.bodyPath);
|
|
3548
|
+
const jsonDst = path15.join(archiveDir, `${item.id}.json`);
|
|
3549
|
+
const bodyDst = path15.join(archiveDir, item.bodyPath);
|
|
3462
3550
|
if (existsSync12(jsonSrc)) renameSync(jsonSrc, jsonDst);
|
|
3463
3551
|
if (existsSync12(bodySrc)) renameSync(bodySrc, bodyDst);
|
|
3464
3552
|
}
|
|
3465
3553
|
function outboxItemPaths(item) {
|
|
3466
3554
|
const { outboxDir } = ensurePlanOutboxDirs();
|
|
3467
3555
|
return {
|
|
3468
|
-
jsonPath:
|
|
3469
|
-
bodyPath:
|
|
3556
|
+
jsonPath: path15.join(outboxDir, `${item.id}.json`),
|
|
3557
|
+
bodyPath: path15.join(outboxDir, item.bodyPath)
|
|
3470
3558
|
};
|
|
3471
3559
|
}
|
|
3472
3560
|
function outboxInputFromItem(item, body) {
|
|
@@ -3652,7 +3740,7 @@ function markOutboxFailed(item, message) {
|
|
|
3652
3740
|
}
|
|
3653
3741
|
|
|
3654
3742
|
// src/plan-persist/drain.ts
|
|
3655
|
-
import
|
|
3743
|
+
import path16 from "node:path";
|
|
3656
3744
|
async function drainPlanOutbox(opts = {}, deps = {}) {
|
|
3657
3745
|
const items = listOutboxItems().filter(
|
|
3658
3746
|
(item) => opts.outboxId ? item.id === opts.outboxId : true
|
|
@@ -3686,7 +3774,7 @@ async function drainPlanOutbox(opts = {}, deps = {}) {
|
|
|
3686
3774
|
return result;
|
|
3687
3775
|
}
|
|
3688
3776
|
function loadOutboxById(outboxId) {
|
|
3689
|
-
const jsonPath =
|
|
3777
|
+
const jsonPath = path16.join(planOutboxDir(), `${outboxId}.json`);
|
|
3690
3778
|
return readOutboxItem(jsonPath);
|
|
3691
3779
|
}
|
|
3692
3780
|
|
|
@@ -3786,7 +3874,7 @@ async function dispatchRun(args) {
|
|
|
3786
3874
|
const activeHarnessWorkers = [];
|
|
3787
3875
|
for (const name of Object.keys(run.workers || {})) {
|
|
3788
3876
|
const worker = readJson(
|
|
3789
|
-
|
|
3877
|
+
path17.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
3790
3878
|
void 0
|
|
3791
3879
|
);
|
|
3792
3880
|
if (!worker?.taskId || !isPidAlive(worker.pid)) continue;
|
|
@@ -3990,7 +4078,7 @@ async function dispatchRun(args) {
|
|
|
3990
4078
|
}
|
|
3991
4079
|
|
|
3992
4080
|
// src/sweep.ts
|
|
3993
|
-
import
|
|
4081
|
+
import path18 from "node:path";
|
|
3994
4082
|
async function sweepRun(args) {
|
|
3995
4083
|
const pipeline = args.pipeline === true || args.pipeline === "true";
|
|
3996
4084
|
try {
|
|
@@ -4003,7 +4091,7 @@ async function sweepRun(args) {
|
|
|
4003
4091
|
const releasedLocalOrphans = [];
|
|
4004
4092
|
for (const name of Object.keys(run.workers || {})) {
|
|
4005
4093
|
const worker = readJson(
|
|
4006
|
-
|
|
4094
|
+
path18.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
4007
4095
|
void 0
|
|
4008
4096
|
);
|
|
4009
4097
|
if (!worker || !worker.dispatched || !worker.taskId) continue;
|
|
@@ -4047,10 +4135,10 @@ async function sweepRun(args) {
|
|
|
4047
4135
|
|
|
4048
4136
|
// src/worktree.ts
|
|
4049
4137
|
import { existsSync as existsSync13, mkdirSync as mkdirSync5 } from "node:fs";
|
|
4050
|
-
import
|
|
4138
|
+
import path20 from "node:path";
|
|
4051
4139
|
|
|
4052
4140
|
// src/validate.ts
|
|
4053
|
-
import
|
|
4141
|
+
import path19 from "node:path";
|
|
4054
4142
|
var RUN_ID_RE = /^[a-z0-9][a-z0-9._-]{0,127}$/i;
|
|
4055
4143
|
function validateRunId(runId) {
|
|
4056
4144
|
const trimmed = runId.trim();
|
|
@@ -4058,7 +4146,7 @@ function validateRunId(runId) {
|
|
|
4058
4146
|
return trimmed;
|
|
4059
4147
|
}
|
|
4060
4148
|
function validateRepo(repo) {
|
|
4061
|
-
const resolved =
|
|
4149
|
+
const resolved = path19.resolve(repo);
|
|
4062
4150
|
if (resolved.includes("..")) throw new Error("repo path must not contain .. segments");
|
|
4063
4151
|
return resolved;
|
|
4064
4152
|
}
|
|
@@ -4083,12 +4171,12 @@ function createRun(args) {
|
|
|
4083
4171
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4084
4172
|
workers: {}
|
|
4085
4173
|
};
|
|
4086
|
-
writeJson(
|
|
4174
|
+
writeJson(path20.join(dir, "run.json"), run);
|
|
4087
4175
|
console.log(JSON.stringify({ runId: id, runDir: dir, repo, base, baseCommit }, null, 2));
|
|
4088
4176
|
}
|
|
4089
4177
|
function listRuns() {
|
|
4090
4178
|
const { runsDir } = getPaths();
|
|
4091
|
-
const rows = listRunIds(runsDir).map((id) => readJson(
|
|
4179
|
+
const rows = listRunIds(runsDir).map((id) => readJson(path20.join(runDirectory(id), "run.json"), void 0)).filter(Boolean).map((run) => ({
|
|
4092
4180
|
id: run.id,
|
|
4093
4181
|
name: run.name,
|
|
4094
4182
|
status: run.status,
|
|
@@ -4103,7 +4191,7 @@ function failExists(message) {
|
|
|
4103
4191
|
}
|
|
4104
4192
|
|
|
4105
4193
|
// src/pipeline-tick.ts
|
|
4106
|
-
import
|
|
4194
|
+
import path29 from "node:path";
|
|
4107
4195
|
|
|
4108
4196
|
// src/pipeline-dispatch.ts
|
|
4109
4197
|
var RESERVED_REVIEW_STARTS = 1;
|
|
@@ -4157,10 +4245,10 @@ async function runPipelineDispatch(args, slots) {
|
|
|
4157
4245
|
}
|
|
4158
4246
|
|
|
4159
4247
|
// src/stale-reconcile.ts
|
|
4160
|
-
import
|
|
4248
|
+
import path22 from "node:path";
|
|
4161
4249
|
|
|
4162
4250
|
// src/finalize.ts
|
|
4163
|
-
import
|
|
4251
|
+
import path21 from "node:path";
|
|
4164
4252
|
var ACTIVE_RUN_STATUSES = /* @__PURE__ */ new Set(["running", "dispatching", "pending", "queued"]);
|
|
4165
4253
|
function terminalStatusFor(run) {
|
|
4166
4254
|
const names = Object.keys(run.workers || {});
|
|
@@ -4171,7 +4259,7 @@ function terminalStatusFor(run) {
|
|
|
4171
4259
|
let anyLandingBlocked = false;
|
|
4172
4260
|
for (const name of names) {
|
|
4173
4261
|
const worker = readJson(
|
|
4174
|
-
|
|
4262
|
+
path21.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
4175
4263
|
void 0
|
|
4176
4264
|
);
|
|
4177
4265
|
if (!worker) continue;
|
|
@@ -4223,7 +4311,7 @@ function reconcileStaleWorkers() {
|
|
|
4223
4311
|
const now = Date.now();
|
|
4224
4312
|
for (const run of listRunRecords()) {
|
|
4225
4313
|
for (const name of Object.keys(run.workers || {})) {
|
|
4226
|
-
const workerPath =
|
|
4314
|
+
const workerPath = path22.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json");
|
|
4227
4315
|
const worker = readJson(workerPath, void 0);
|
|
4228
4316
|
if (!worker || worker.status !== "running") {
|
|
4229
4317
|
outcomes.push({
|
|
@@ -4297,9 +4385,27 @@ function reconcileStaleWorkers() {
|
|
|
4297
4385
|
}
|
|
4298
4386
|
return { workers: outcomes, finalizedRuns: finalizeStaleRuns() };
|
|
4299
4387
|
}
|
|
4388
|
+
function reconcileRunsCli() {
|
|
4389
|
+
const result = reconcileStaleWorkers();
|
|
4390
|
+
const markedExited = result.workers.filter((w) => w.action === "marked_exited").length;
|
|
4391
|
+
const killedStale = result.workers.filter((w) => w.action === "killed_stale").length;
|
|
4392
|
+
const skipped = result.workers.filter((w) => w.action === "skipped").length;
|
|
4393
|
+
console.log(
|
|
4394
|
+
JSON.stringify(
|
|
4395
|
+
{
|
|
4396
|
+
ok: true,
|
|
4397
|
+
workers: { markedExited, killedStale, skipped, total: result.workers.length },
|
|
4398
|
+
finalizedRuns: result.finalizedRuns.length,
|
|
4399
|
+
details: { workers: result.workers, finalizedRuns: result.finalizedRuns }
|
|
4400
|
+
},
|
|
4401
|
+
null,
|
|
4402
|
+
2
|
|
4403
|
+
)
|
|
4404
|
+
);
|
|
4405
|
+
}
|
|
4300
4406
|
|
|
4301
4407
|
// src/plan-progress-daemon-sync.ts
|
|
4302
|
-
import
|
|
4408
|
+
import path23 from "node:path";
|
|
4303
4409
|
|
|
4304
4410
|
// src/plan-progress-sync.ts
|
|
4305
4411
|
async function syncPlanProgress(args) {
|
|
@@ -4323,7 +4429,7 @@ async function syncActiveWorkerPlanProgress(runId, args) {
|
|
|
4323
4429
|
const outcomes = [];
|
|
4324
4430
|
for (const name of Object.keys(run.workers || {})) {
|
|
4325
4431
|
const worker = readJson(
|
|
4326
|
-
|
|
4432
|
+
path23.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
4327
4433
|
void 0
|
|
4328
4434
|
);
|
|
4329
4435
|
if (!worker?.dispatched || !worker.taskId) continue;
|
|
@@ -4372,7 +4478,7 @@ async function fetchWorkspaceRuntimePreferences(agentOsId, args) {
|
|
|
4372
4478
|
}
|
|
4373
4479
|
|
|
4374
4480
|
// src/cleanup.ts
|
|
4375
|
-
import
|
|
4481
|
+
import path28 from "node:path";
|
|
4376
4482
|
|
|
4377
4483
|
// src/cleanup-types.ts
|
|
4378
4484
|
var DEFAULT_NODE_MODULES_AGE_MS = 6 * 60 * 60 * 1e3;
|
|
@@ -4444,11 +4550,11 @@ function skipNodeModulesRemoval(input) {
|
|
|
4444
4550
|
|
|
4445
4551
|
// src/cleanup-execute.ts
|
|
4446
4552
|
import { existsSync as existsSync15, rmSync } from "node:fs";
|
|
4447
|
-
import
|
|
4553
|
+
import path25 from "node:path";
|
|
4448
4554
|
|
|
4449
4555
|
// src/cleanup-dir-size.ts
|
|
4450
4556
|
import { existsSync as existsSync14, readdirSync as readdirSync5, statSync as statSync2 } from "node:fs";
|
|
4451
|
-
import
|
|
4557
|
+
import path24 from "node:path";
|
|
4452
4558
|
function directorySizeBytes(root, maxEntries = 5e4) {
|
|
4453
4559
|
if (!existsSync14(root)) return 0;
|
|
4454
4560
|
let total = 0;
|
|
@@ -4464,7 +4570,7 @@ function directorySizeBytes(root, maxEntries = 5e4) {
|
|
|
4464
4570
|
}
|
|
4465
4571
|
for (const name of entries) {
|
|
4466
4572
|
if (seen++ > maxEntries) return null;
|
|
4467
|
-
const full =
|
|
4573
|
+
const full = path24.join(current, name);
|
|
4468
4574
|
let st;
|
|
4469
4575
|
try {
|
|
4470
4576
|
st = statSync2(full);
|
|
@@ -4548,20 +4654,20 @@ function removeWorktree(candidate, execute) {
|
|
|
4548
4654
|
}
|
|
4549
4655
|
}
|
|
4550
4656
|
function isHarnessNodeModulesPath(targetPath, harnessRoot, worktreesDir) {
|
|
4551
|
-
const resolved =
|
|
4552
|
-
const nm = resolved.endsWith(`${
|
|
4657
|
+
const resolved = path25.resolve(targetPath);
|
|
4658
|
+
const nm = resolved.endsWith(`${path25.sep}node_modules`) ? resolved : null;
|
|
4553
4659
|
if (!nm) return "path_outside_harness";
|
|
4554
|
-
const rel =
|
|
4555
|
-
if (rel.startsWith("..") ||
|
|
4556
|
-
const parts = rel.split(
|
|
4660
|
+
const rel = path25.relative(worktreesDir, nm);
|
|
4661
|
+
if (rel.startsWith("..") || path25.isAbsolute(rel)) return "path_outside_harness";
|
|
4662
|
+
const parts = rel.split(path25.sep);
|
|
4557
4663
|
if (parts.length < 3 || parts[parts.length - 1] !== "node_modules") return "path_outside_harness";
|
|
4558
|
-
if (!resolved.startsWith(
|
|
4664
|
+
if (!resolved.startsWith(path25.resolve(harnessRoot))) return "path_outside_harness";
|
|
4559
4665
|
return null;
|
|
4560
4666
|
}
|
|
4561
4667
|
|
|
4562
4668
|
// src/cleanup-scan.ts
|
|
4563
4669
|
import { existsSync as existsSync16, readdirSync as readdirSync6, statSync as statSync3 } from "node:fs";
|
|
4564
|
-
import
|
|
4670
|
+
import path26 from "node:path";
|
|
4565
4671
|
function pathAgeMs(target, now) {
|
|
4566
4672
|
try {
|
|
4567
4673
|
const mtime = statSync3(target).mtimeMs;
|
|
@@ -4571,17 +4677,17 @@ function pathAgeMs(target, now) {
|
|
|
4571
4677
|
}
|
|
4572
4678
|
}
|
|
4573
4679
|
function isPathInside(child, parent) {
|
|
4574
|
-
const rel =
|
|
4575
|
-
return rel === "" || !rel.startsWith("..") && !
|
|
4680
|
+
const rel = path26.relative(parent, child);
|
|
4681
|
+
return rel === "" || !rel.startsWith("..") && !path26.isAbsolute(rel);
|
|
4576
4682
|
}
|
|
4577
4683
|
function scanNodeModulesCandidates(opts) {
|
|
4578
4684
|
const candidates = [];
|
|
4579
4685
|
const seen = /* @__PURE__ */ new Set();
|
|
4580
4686
|
for (const entry of opts.index.values()) {
|
|
4581
4687
|
if (opts.runIdFilter && entry.runId !== opts.runIdFilter) continue;
|
|
4582
|
-
const nm =
|
|
4688
|
+
const nm = path26.join(entry.worktreePath, "node_modules");
|
|
4583
4689
|
if (!existsSync16(nm)) continue;
|
|
4584
|
-
const resolved =
|
|
4690
|
+
const resolved = path26.resolve(nm);
|
|
4585
4691
|
if (seen.has(resolved)) continue;
|
|
4586
4692
|
seen.add(resolved);
|
|
4587
4693
|
candidates.push({
|
|
@@ -4597,13 +4703,13 @@ function scanNodeModulesCandidates(opts) {
|
|
|
4597
4703
|
if (!opts.includeOrphans || !existsSync16(opts.worktreesDir)) return candidates;
|
|
4598
4704
|
for (const runEntry of readdirSync6(opts.worktreesDir, { withFileTypes: true })) {
|
|
4599
4705
|
if (!runEntry.isDirectory()) continue;
|
|
4600
|
-
const runPath =
|
|
4706
|
+
const runPath = path26.join(opts.worktreesDir, runEntry.name);
|
|
4601
4707
|
for (const workerEntry of readdirSync6(runPath, { withFileTypes: true })) {
|
|
4602
4708
|
if (!workerEntry.isDirectory()) continue;
|
|
4603
|
-
const worktreePath =
|
|
4604
|
-
const nm =
|
|
4709
|
+
const worktreePath = path26.join(runPath, workerEntry.name);
|
|
4710
|
+
const nm = path26.join(worktreePath, "node_modules");
|
|
4605
4711
|
if (!existsSync16(nm)) continue;
|
|
4606
|
-
const resolved =
|
|
4712
|
+
const resolved = path26.resolve(nm);
|
|
4607
4713
|
if (seen.has(resolved)) continue;
|
|
4608
4714
|
if (!isPathInside(resolved, opts.harnessRoot)) continue;
|
|
4609
4715
|
seen.add(resolved);
|
|
@@ -4643,17 +4749,17 @@ function scanWorktreeCandidates(opts) {
|
|
|
4643
4749
|
}
|
|
4644
4750
|
|
|
4645
4751
|
// src/cleanup-worktree-index.ts
|
|
4646
|
-
import
|
|
4752
|
+
import path27 from "node:path";
|
|
4647
4753
|
function buildWorktreeIndex() {
|
|
4648
4754
|
const index = /* @__PURE__ */ new Map();
|
|
4649
4755
|
for (const run of listRunRecords()) {
|
|
4650
4756
|
for (const name of Object.keys(run.workers || {})) {
|
|
4651
|
-
const workerPath =
|
|
4757
|
+
const workerPath = path27.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json");
|
|
4652
4758
|
const worker = readJson(workerPath, void 0);
|
|
4653
4759
|
if (!worker?.worktreePath) continue;
|
|
4654
4760
|
const status = computeWorkerStatus(worker, { base: run.base, baseCommit: run.baseCommit });
|
|
4655
|
-
index.set(
|
|
4656
|
-
worktreePath:
|
|
4761
|
+
index.set(path27.resolve(worker.worktreePath), {
|
|
4762
|
+
worktreePath: path27.resolve(worker.worktreePath),
|
|
4657
4763
|
runId: run.id,
|
|
4658
4764
|
workerName: name,
|
|
4659
4765
|
run,
|
|
@@ -4667,8 +4773,8 @@ function buildWorktreeIndex() {
|
|
|
4667
4773
|
|
|
4668
4774
|
// src/cleanup.ts
|
|
4669
4775
|
function resolveOptions(options = {}) {
|
|
4670
|
-
const harnessRoot = options.harnessRoot ?
|
|
4671
|
-
const { worktreesDir } = options.harnessRoot ? { worktreesDir:
|
|
4776
|
+
const harnessRoot = options.harnessRoot ? resolveUserPath(options.harnessRoot) : resolveHarnessRoot();
|
|
4777
|
+
const { worktreesDir } = options.harnessRoot ? { worktreesDir: path28.join(harnessRoot, "worktrees") } : getHarnessPaths();
|
|
4672
4778
|
const execute = options.execute === true;
|
|
4673
4779
|
const nodeModulesAgeMs = options.nodeModulesAgeMs ?? DEFAULT_NODE_MODULES_AGE_MS;
|
|
4674
4780
|
const worktreesAgeMs = options.worktreesAgeMs ?? 0;
|
|
@@ -4712,7 +4818,7 @@ function runHarnessCleanup(options = {}) {
|
|
|
4712
4818
|
actions.push({ ...candidate, executed: false, skipped: true, skipReason: pathSkip });
|
|
4713
4819
|
continue;
|
|
4714
4820
|
}
|
|
4715
|
-
const worktreePath =
|
|
4821
|
+
const worktreePath = path28.resolve(candidate.path, "..");
|
|
4716
4822
|
const indexed = index.get(worktreePath) ?? null;
|
|
4717
4823
|
const guardReason = skipNodeModulesRemoval({
|
|
4718
4824
|
indexed,
|
|
@@ -4728,7 +4834,7 @@ function runHarnessCleanup(options = {}) {
|
|
|
4728
4834
|
actions.push(removeNodeModules(candidate, resolved.execute));
|
|
4729
4835
|
}
|
|
4730
4836
|
for (const candidate of scanWorktreeCandidates(scanOpts)) {
|
|
4731
|
-
const indexed = index.get(
|
|
4837
|
+
const indexed = index.get(path28.resolve(candidate.path)) ?? null;
|
|
4732
4838
|
const guardReason = skipWorktreeRemoval({
|
|
4733
4839
|
indexed,
|
|
4734
4840
|
includeOrphans: resolved.includeOrphans,
|
|
@@ -4797,7 +4903,7 @@ async function completeFinishedWorkers(runId, args) {
|
|
|
4797
4903
|
const outcomes = [];
|
|
4798
4904
|
for (const name of Object.keys(run.workers || {})) {
|
|
4799
4905
|
const worker = readJson(
|
|
4800
|
-
|
|
4906
|
+
path29.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
4801
4907
|
void 0
|
|
4802
4908
|
);
|
|
4803
4909
|
if (!worker?.taskId || worker.localOnly) continue;
|
|
@@ -5132,7 +5238,7 @@ function runCleanupCli(args) {
|
|
|
5132
5238
|
}
|
|
5133
5239
|
|
|
5134
5240
|
// src/monitor/monitor.service.ts
|
|
5135
|
-
import
|
|
5241
|
+
import path31 from "node:path";
|
|
5136
5242
|
|
|
5137
5243
|
// src/monitor/monitor.classify.ts
|
|
5138
5244
|
function expectedLeaseOwner(runId) {
|
|
@@ -5189,10 +5295,10 @@ function classifyWorkerHealth(input) {
|
|
|
5189
5295
|
|
|
5190
5296
|
// src/monitor/monitor.store.ts
|
|
5191
5297
|
import { existsSync as existsSync17, mkdirSync as mkdirSync6, readdirSync as readdirSync7, unlinkSync as unlinkSync2 } from "node:fs";
|
|
5192
|
-
import
|
|
5298
|
+
import path30 from "node:path";
|
|
5193
5299
|
function monitorsDir() {
|
|
5194
5300
|
const { harnessRoot } = getHarnessPaths();
|
|
5195
|
-
const dir =
|
|
5301
|
+
const dir = path30.join(harnessRoot, "monitors");
|
|
5196
5302
|
mkdirSync6(dir, { recursive: true });
|
|
5197
5303
|
return dir;
|
|
5198
5304
|
}
|
|
@@ -5200,7 +5306,7 @@ function monitorIdFor(runId, workerName) {
|
|
|
5200
5306
|
return workerName ? `${safeSlug(runId)}--${safeSlug(workerName)}` : safeSlug(runId);
|
|
5201
5307
|
}
|
|
5202
5308
|
function monitorPath(monitorId) {
|
|
5203
|
-
return
|
|
5309
|
+
return path30.join(monitorsDir(), `${monitorId}.json`);
|
|
5204
5310
|
}
|
|
5205
5311
|
function loadMonitorSession(monitorId) {
|
|
5206
5312
|
return readJson(monitorPath(monitorId), void 0);
|
|
@@ -5221,7 +5327,7 @@ function listMonitorSessions() {
|
|
|
5221
5327
|
for (const name of readdirSync7(dir)) {
|
|
5222
5328
|
if (!name.endsWith(".json")) continue;
|
|
5223
5329
|
const session = readJson(
|
|
5224
|
-
|
|
5330
|
+
path30.join(dir, name),
|
|
5225
5331
|
void 0
|
|
5226
5332
|
);
|
|
5227
5333
|
if (!session?.monitorId) continue;
|
|
@@ -5312,7 +5418,7 @@ async function fetchTaskLeasesForWorkers(input) {
|
|
|
5312
5418
|
// src/monitor/monitor.service.ts
|
|
5313
5419
|
function workerRecord2(runId, name) {
|
|
5314
5420
|
return readJson(
|
|
5315
|
-
|
|
5421
|
+
path31.join(runDirectory(runId), "workers", safeSlug(name), "worker.json"),
|
|
5316
5422
|
void 0
|
|
5317
5423
|
);
|
|
5318
5424
|
}
|
|
@@ -5515,17 +5621,17 @@ async function runMonitorLoop(args) {
|
|
|
5515
5621
|
// src/monitor/monitor-spawn.ts
|
|
5516
5622
|
import { spawn as spawn4 } from "node:child_process";
|
|
5517
5623
|
import { closeSync as closeSync4, existsSync as existsSync18, openSync as openSync4 } from "node:fs";
|
|
5518
|
-
import
|
|
5624
|
+
import path32 from "node:path";
|
|
5519
5625
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
5520
5626
|
function resolveDefaultCliPath2() {
|
|
5521
|
-
return
|
|
5627
|
+
return path32.join(fileURLToPath2(new URL(".", import.meta.url)), "..", "cli.js");
|
|
5522
5628
|
}
|
|
5523
5629
|
function spawnMonitorSidecar(opts) {
|
|
5524
5630
|
const cliPath = opts.cliPath ?? resolveDefaultCliPath2();
|
|
5525
5631
|
if (!existsSync18(cliPath)) return void 0;
|
|
5526
5632
|
const monitorId = monitorIdFor(opts.runId, opts.workerName);
|
|
5527
5633
|
const { harnessRoot } = getHarnessPaths();
|
|
5528
|
-
const logPath =
|
|
5634
|
+
const logPath = path32.join(harnessRoot, "monitors", `${monitorId}.log`);
|
|
5529
5635
|
let logFd;
|
|
5530
5636
|
try {
|
|
5531
5637
|
logFd = openSync4(logPath, "a");
|
|
@@ -5680,6 +5786,422 @@ function handleCliVersionFlag(argv, moduleUrl = import.meta.url, binName) {
|
|
|
5680
5786
|
return true;
|
|
5681
5787
|
}
|
|
5682
5788
|
|
|
5789
|
+
// src/doctor/runtime-takeover.ts
|
|
5790
|
+
import path34 from "node:path";
|
|
5791
|
+
|
|
5792
|
+
// src/doctor/runtime-takeover.probes.ts
|
|
5793
|
+
import { accessSync, constants, existsSync as existsSync20, readFileSync as readFileSync9 } from "node:fs";
|
|
5794
|
+
import { homedir as homedir5 } from "node:os";
|
|
5795
|
+
import path33 from "node:path";
|
|
5796
|
+
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
5797
|
+
function captureCommand(bin, args) {
|
|
5798
|
+
try {
|
|
5799
|
+
const res = spawnSync3(bin, args, { encoding: "utf8" });
|
|
5800
|
+
const stdout = (res.stdout || "").trim();
|
|
5801
|
+
const stderr = (res.stderr || "").trim();
|
|
5802
|
+
const ok = res.status === 0;
|
|
5803
|
+
return {
|
|
5804
|
+
ok,
|
|
5805
|
+
stdout,
|
|
5806
|
+
stderr,
|
|
5807
|
+
error: res.error?.message
|
|
5808
|
+
};
|
|
5809
|
+
} catch (error) {
|
|
5810
|
+
return {
|
|
5811
|
+
ok: false,
|
|
5812
|
+
stdout: "",
|
|
5813
|
+
stderr: "",
|
|
5814
|
+
error: error.message
|
|
5815
|
+
};
|
|
5816
|
+
}
|
|
5817
|
+
}
|
|
5818
|
+
function tokenPrefix(token) {
|
|
5819
|
+
const trimmed = token?.trim();
|
|
5820
|
+
if (!trimmed) return void 0;
|
|
5821
|
+
return trimmed.length <= 12 ? `${trimmed}\u2026` : `${trimmed.slice(0, 12)}\u2026`;
|
|
5822
|
+
}
|
|
5823
|
+
function isWritable(target) {
|
|
5824
|
+
if (!existsSync20(target)) return false;
|
|
5825
|
+
try {
|
|
5826
|
+
accessSync(target, constants.W_OK);
|
|
5827
|
+
return true;
|
|
5828
|
+
} catch {
|
|
5829
|
+
return false;
|
|
5830
|
+
}
|
|
5831
|
+
}
|
|
5832
|
+
var defaultRuntimeTakeoverProbes = {
|
|
5833
|
+
packageVersion: () => PACKAGE_VERSION,
|
|
5834
|
+
commandOnPath: (bin) => captureCommand(process.platform === "win32" ? "where" : "which", [bin]),
|
|
5835
|
+
kynverVersion: (bin) => captureCommand(bin, ["--version"]),
|
|
5836
|
+
loadConfig: () => loadUserConfig(),
|
|
5837
|
+
configFilePath: () => path33.join(homedir5(), ".kynver", "config.json"),
|
|
5838
|
+
credentialsFilePath: () => path33.join(homedir5(), ".kynver", "credentials"),
|
|
5839
|
+
readCredentials: () => {
|
|
5840
|
+
const credPath = path33.join(homedir5(), ".kynver", "credentials");
|
|
5841
|
+
if (!existsSync20(credPath)) {
|
|
5842
|
+
return { hasApiKey: false };
|
|
5843
|
+
}
|
|
5844
|
+
try {
|
|
5845
|
+
const parsed = JSON.parse(readFileSync9(credPath, "utf8"));
|
|
5846
|
+
return {
|
|
5847
|
+
hasApiKey: Boolean(parsed.apiKey?.trim()),
|
|
5848
|
+
runnerTokenPrefix: tokenPrefix(parsed.runnerToken),
|
|
5849
|
+
runnerTokenAgentOsId: parsed.runnerTokenAgentOsId
|
|
5850
|
+
};
|
|
5851
|
+
} catch {
|
|
5852
|
+
return { hasApiKey: false };
|
|
5853
|
+
}
|
|
5854
|
+
},
|
|
5855
|
+
envSnapshot: () => ({
|
|
5856
|
+
kynverApiUrl: process.env.KYNVER_API_URL?.trim() || void 0,
|
|
5857
|
+
openclawCronFireBaseUrl: process.env.OPENCLAW_CRON_FIRE_BASE_URL?.trim() || void 0,
|
|
5858
|
+
kynverRunnerTokenPrefix: tokenPrefix(process.env.KYNVER_RUNNER_TOKEN),
|
|
5859
|
+
kynverRuntimeSecret: Boolean(process.env.KYNVER_RUNTIME_SECRET?.trim()),
|
|
5860
|
+
openclawCronSecret: Boolean(process.env.OPENCLAW_CRON_SECRET?.trim()),
|
|
5861
|
+
kynverHarnessRoot: process.env.KYNVER_HARNESS_ROOT?.trim() || void 0,
|
|
5862
|
+
opusHarnessRoot: process.env.OPUS_HARNESS_ROOT?.trim() || void 0,
|
|
5863
|
+
kynverSchedulerProvider: process.env.KYNVER_SCHEDULER_PROVIDER?.trim() || void 0
|
|
5864
|
+
}),
|
|
5865
|
+
harnessRoot: () => resolveHarnessRoot(),
|
|
5866
|
+
legacyOpenclawHarnessRoot: () => path33.join(homedir5(), ".openclaw", "harness"),
|
|
5867
|
+
pathExists: (target) => existsSync20(target),
|
|
5868
|
+
pathWritable: (target) => isWritable(target),
|
|
5869
|
+
vercelVersion: () => captureCommand("vercel", ["--version"]),
|
|
5870
|
+
vercelWhoami: () => captureCommand("vercel", ["whoami"])
|
|
5871
|
+
};
|
|
5872
|
+
|
|
5873
|
+
// src/doctor/runtime-takeover.ts
|
|
5874
|
+
function check(partial) {
|
|
5875
|
+
return partial;
|
|
5876
|
+
}
|
|
5877
|
+
function summarizeCounts(sections) {
|
|
5878
|
+
const counts = { pass: 0, warn: 0, fail: 0 };
|
|
5879
|
+
for (const section of sections) {
|
|
5880
|
+
for (const item of section.checks) {
|
|
5881
|
+
counts[item.status] += 1;
|
|
5882
|
+
}
|
|
5883
|
+
}
|
|
5884
|
+
return counts;
|
|
5885
|
+
}
|
|
5886
|
+
function assessCliPackage(probes) {
|
|
5887
|
+
const runningVersion = probes.packageVersion();
|
|
5888
|
+
const which = probes.commandOnPath("kynver");
|
|
5889
|
+
const onPath = which.ok && which.stdout.length > 0;
|
|
5890
|
+
const firstPath = onPath ? which.stdout.split(/\r?\n/)[0]?.trim() : void 0;
|
|
5891
|
+
const displayCliPath = firstPath ? displayUserPath(firstPath) : void 0;
|
|
5892
|
+
const checks = [
|
|
5893
|
+
check({
|
|
5894
|
+
id: "cli_running_version",
|
|
5895
|
+
label: "Running @kynver-app/runtime version",
|
|
5896
|
+
status: "pass",
|
|
5897
|
+
summary: `@kynver-app/runtime ${runningVersion}`,
|
|
5898
|
+
details: { version: runningVersion }
|
|
5899
|
+
}),
|
|
5900
|
+
check({
|
|
5901
|
+
id: "cli_on_path",
|
|
5902
|
+
label: "kynver executable on PATH",
|
|
5903
|
+
status: onPath ? "pass" : "warn",
|
|
5904
|
+
summary: onPath ? `Found ${displayCliPath}` : "kynver not found on PATH (invoked via node/npx?)",
|
|
5905
|
+
remediation: onPath ? void 0 : "Install globally (`npm i -g @kynver-app/runtime`) or invoke via `npx @kynver-app/runtime`.",
|
|
5906
|
+
details: { path: displayCliPath }
|
|
5907
|
+
})
|
|
5908
|
+
];
|
|
5909
|
+
if (onPath && firstPath) {
|
|
5910
|
+
const versionProbe = probes.kynverVersion(firstPath);
|
|
5911
|
+
const installedVersion = versionProbe.stdout.replace(/^kynver\s+/i, "").trim() || void 0;
|
|
5912
|
+
const versionMatch = versionProbe.ok && (!installedVersion || installedVersion === runningVersion);
|
|
5913
|
+
checks.push(
|
|
5914
|
+
check({
|
|
5915
|
+
id: "cli_installed_version",
|
|
5916
|
+
label: "Installed kynver CLI version matches running package",
|
|
5917
|
+
status: versionMatch ? "pass" : "warn",
|
|
5918
|
+
summary: versionProbe.ok && installedVersion ? versionMatch ? `Installed ${installedVersion}` : `Installed ${installedVersion} differs from running ${runningVersion}` : versionProbe.error ? `Could not read installed version (${versionProbe.error})` : versionProbe.stderr || "Could not read installed version",
|
|
5919
|
+
remediation: versionMatch ? void 0 : "Reinstall or rebuild @kynver-app/runtime so PATH kynver matches the harness worktree version.",
|
|
5920
|
+
details: { installedVersion, runningVersion, path: displayCliPath }
|
|
5921
|
+
})
|
|
5922
|
+
);
|
|
5923
|
+
}
|
|
5924
|
+
return { id: "cli_package", label: "CLI / package", checks };
|
|
5925
|
+
}
|
|
5926
|
+
function assessUserConfig(probes) {
|
|
5927
|
+
const configPath = probes.configFilePath();
|
|
5928
|
+
const displayConfigPath = displayUserPath(configPath);
|
|
5929
|
+
const exists = probes.pathExists(configPath);
|
|
5930
|
+
const config = probes.loadConfig();
|
|
5931
|
+
const checks = [
|
|
5932
|
+
check({
|
|
5933
|
+
id: "config_file",
|
|
5934
|
+
label: "~/.kynver/config.json present",
|
|
5935
|
+
status: exists ? "pass" : "fail",
|
|
5936
|
+
summary: exists ? `Found ${displayConfigPath}` : `Missing ${displayConfigPath}`,
|
|
5937
|
+
remediation: exists ? void 0 : "Run `kynver setup --api-base-url <url> --agent-os-id <id> --repo <path>`.",
|
|
5938
|
+
details: { configPath: displayConfigPath }
|
|
5939
|
+
})
|
|
5940
|
+
];
|
|
5941
|
+
if (exists) {
|
|
5942
|
+
const apiBaseUrl = config.apiBaseUrl?.trim();
|
|
5943
|
+
const agentOsId = config.agentOsId?.trim();
|
|
5944
|
+
const defaultRepo = config.defaultRepo?.trim();
|
|
5945
|
+
const displayDefaultRepo = defaultRepo ? displayUserPath(defaultRepo) : null;
|
|
5946
|
+
checks.push(
|
|
5947
|
+
check({
|
|
5948
|
+
id: "config_api_base_url",
|
|
5949
|
+
label: "Default API base URL",
|
|
5950
|
+
status: apiBaseUrl ? "pass" : "warn",
|
|
5951
|
+
summary: apiBaseUrl ?? "Not set in config (KYNVER_API_URL / --base-url required per command)",
|
|
5952
|
+
remediation: apiBaseUrl ? void 0 : "Set `apiBaseUrl` via `kynver setup --api-base-url https://\u2026`.",
|
|
5953
|
+
details: { apiBaseUrl: apiBaseUrl ?? null }
|
|
5954
|
+
}),
|
|
5955
|
+
check({
|
|
5956
|
+
id: "config_agent_os_id",
|
|
5957
|
+
label: "Default AgentOS id",
|
|
5958
|
+
status: agentOsId ? "pass" : "warn",
|
|
5959
|
+
summary: agentOsId ?? "Not set (pass --agent-os-id on daemon/dispatch/worker commands)",
|
|
5960
|
+
remediation: agentOsId ? void 0 : "Set `agentOsId` via `kynver setup --agent-os-id <uuid>`.",
|
|
5961
|
+
details: { agentOsId: agentOsId ?? null, agentOsSlug: config.agentOsSlug ?? null }
|
|
5962
|
+
}),
|
|
5963
|
+
check({
|
|
5964
|
+
id: "config_default_repo",
|
|
5965
|
+
label: "Default repo path",
|
|
5966
|
+
status: defaultRepo ? "pass" : "warn",
|
|
5967
|
+
summary: displayDefaultRepo ?? "Not set (pass --repo on `kynver run create`)",
|
|
5968
|
+
remediation: defaultRepo ? void 0 : "Set `defaultRepo` via `kynver setup --repo /path/to/repo`.",
|
|
5969
|
+
details: {
|
|
5970
|
+
defaultRepo: displayDefaultRepo,
|
|
5971
|
+
harnessRoot: config.harnessRoot ? redactHomePath(config.harnessRoot) : null
|
|
5972
|
+
}
|
|
5973
|
+
})
|
|
5974
|
+
);
|
|
5975
|
+
}
|
|
5976
|
+
return { id: "user_config", label: "User config (~/.kynver)", checks };
|
|
5977
|
+
}
|
|
5978
|
+
function assessRunnerToken(probes) {
|
|
5979
|
+
const config = probes.loadConfig();
|
|
5980
|
+
const targetAgentOsId = config.agentOsId?.trim();
|
|
5981
|
+
const creds = probes.readCredentials();
|
|
5982
|
+
const env = probes.envSnapshot();
|
|
5983
|
+
const credPath = probes.credentialsFilePath();
|
|
5984
|
+
const displayCredPath = displayUserPath(credPath);
|
|
5985
|
+
const envToken = env.kynverRunnerTokenPrefix;
|
|
5986
|
+
const savedToken = creds.runnerTokenPrefix;
|
|
5987
|
+
const savedAgentOsId = creds.runnerTokenAgentOsId;
|
|
5988
|
+
const scopedSaved = Boolean(savedToken) && (!targetAgentOsId || !savedAgentOsId || savedAgentOsId === targetAgentOsId);
|
|
5989
|
+
const hasScoped = Boolean(envToken?.startsWith("krc1.")) || scopedSaved && savedToken?.startsWith("krc1.");
|
|
5990
|
+
const checks = [
|
|
5991
|
+
check({
|
|
5992
|
+
id: "runner_token_scoped",
|
|
5993
|
+
label: "Scoped runner token (krc1.*) ready",
|
|
5994
|
+
status: hasScoped ? "pass" : "fail",
|
|
5995
|
+
summary: hasScoped ? envToken ? "KYNVER_RUNNER_TOKEN is set (scoped prefix shown)" : `Saved scoped token for agentOsId ${savedAgentOsId ?? targetAgentOsId ?? "(unknown)"}` : "No scoped runner token for the configured AgentOS workspace",
|
|
5996
|
+
remediation: hasScoped ? void 0 : "Run `kynver login` then `kynver runner credential --agent-os-id <id>` (or `kynver setup` with API key).",
|
|
5997
|
+
details: {
|
|
5998
|
+
source: envToken ? "env" : savedToken ? "credentials" : "none",
|
|
5999
|
+
tokenPrefix: envToken ?? (scopedSaved ? savedToken : void 0),
|
|
6000
|
+
agentOsId: targetAgentOsId ?? savedAgentOsId ?? null,
|
|
6001
|
+
credentialsPath: displayCredPath
|
|
6002
|
+
}
|
|
6003
|
+
}),
|
|
6004
|
+
check({
|
|
6005
|
+
id: "runner_token_agent_os_match",
|
|
6006
|
+
label: "Saved runner token matches configured agentOsId",
|
|
6007
|
+
status: !savedToken || !targetAgentOsId || !savedAgentOsId || savedAgentOsId === targetAgentOsId ? "pass" : "warn",
|
|
6008
|
+
summary: !savedToken || !savedAgentOsId ? "No saved token agentOsId to compare" : !targetAgentOsId ? "Config agentOsId unset \u2014 token scope not validated against config" : savedAgentOsId === targetAgentOsId ? "Saved token scoped to configured agentOsId" : `Saved token is for ${savedAgentOsId}, config expects ${targetAgentOsId}`,
|
|
6009
|
+
remediation: savedToken && targetAgentOsId && savedAgentOsId && savedAgentOsId !== targetAgentOsId ? "`kynver runner credential --agent-os-id <configured-id>` to mint a workspace-bound token." : void 0,
|
|
6010
|
+
details: { configuredAgentOsId: targetAgentOsId ?? null, savedAgentOsId: savedAgentOsId ?? null }
|
|
6011
|
+
}),
|
|
6012
|
+
check({
|
|
6013
|
+
id: "runner_api_key_for_refresh",
|
|
6014
|
+
label: "API key available for token refresh",
|
|
6015
|
+
status: creds.hasApiKey || Boolean(process.env.KYNVER_API_KEY?.trim()) ? "pass" : "warn",
|
|
6016
|
+
summary: creds.hasApiKey || process.env.KYNVER_API_KEY ? "KYNVER API key present for runner credential mint/refresh" : "No API key \u2014 401 callback recovery cannot auto-mint a replacement token",
|
|
6017
|
+
remediation: creds.hasApiKey || process.env.KYNVER_API_KEY ? void 0 : "Run `kynver login --api-key \u2026`.",
|
|
6018
|
+
details: { credentialsPath: displayCredPath, hasApiKey: creds.hasApiKey || Boolean(process.env.KYNVER_API_KEY) }
|
|
6019
|
+
})
|
|
6020
|
+
];
|
|
6021
|
+
return { id: "runner_token", label: "Runner token readiness", checks };
|
|
6022
|
+
}
|
|
6023
|
+
function assessVercelCli(probes) {
|
|
6024
|
+
const version = probes.vercelVersion();
|
|
6025
|
+
const whoami = probes.vercelWhoami();
|
|
6026
|
+
const installed = version.ok;
|
|
6027
|
+
return {
|
|
6028
|
+
id: "vercel_cli",
|
|
6029
|
+
label: "Vercel CLI",
|
|
6030
|
+
checks: [
|
|
6031
|
+
check({
|
|
6032
|
+
id: "vercel_installed",
|
|
6033
|
+
label: "Vercel CLI installed",
|
|
6034
|
+
status: installed ? "pass" : "warn",
|
|
6035
|
+
summary: installed ? version.stdout || "vercel CLI found" : version.error ?? "vercel not found on PATH",
|
|
6036
|
+
remediation: installed ? void 0 : "Install Vercel CLI (`npm i -g vercel`) for deploy evidence and env pulls.",
|
|
6037
|
+
details: { stderr: version.stderr || null }
|
|
6038
|
+
}),
|
|
6039
|
+
check({
|
|
6040
|
+
id: "vercel_authenticated",
|
|
6041
|
+
label: "Vercel CLI authenticated",
|
|
6042
|
+
status: !installed ? "warn" : whoami.ok ? "pass" : "warn",
|
|
6043
|
+
summary: !installed ? "Skipped \u2014 Vercel CLI not installed" : whoami.ok ? `Authenticated as ${whoami.stdout}` : whoami.stderr || whoami.error || "Not logged in",
|
|
6044
|
+
remediation: installed && !whoami.ok ? "Run `vercel login` and link the Kynver project if needed." : void 0,
|
|
6045
|
+
details: { account: whoami.ok ? whoami.stdout : null }
|
|
6046
|
+
})
|
|
6047
|
+
]
|
|
6048
|
+
};
|
|
6049
|
+
}
|
|
6050
|
+
function assessHarnessDirs(probes) {
|
|
6051
|
+
const harnessRoot = probes.harnessRoot();
|
|
6052
|
+
const runsDir = path34.join(harnessRoot, "runs");
|
|
6053
|
+
const worktreesDir = path34.join(harnessRoot, "worktrees");
|
|
6054
|
+
const displayHarnessRoot = redactHomePath(harnessRoot);
|
|
6055
|
+
const displayRunsDir = redactHomePath(runsDir);
|
|
6056
|
+
const displayWorktreesDir = redactHomePath(worktreesDir);
|
|
6057
|
+
const runsExists = probes.pathExists(runsDir);
|
|
6058
|
+
const worktreesExists = probes.pathExists(worktreesDir);
|
|
6059
|
+
return {
|
|
6060
|
+
id: "harness_dirs",
|
|
6061
|
+
label: "Harness / daemon directories",
|
|
6062
|
+
checks: [
|
|
6063
|
+
check({
|
|
6064
|
+
id: "harness_root",
|
|
6065
|
+
label: "Harness root resolved",
|
|
6066
|
+
status: "pass",
|
|
6067
|
+
summary: displayHarnessRoot,
|
|
6068
|
+
details: { harnessRoot: displayHarnessRoot }
|
|
6069
|
+
}),
|
|
6070
|
+
check({
|
|
6071
|
+
id: "harness_runs_dir",
|
|
6072
|
+
label: "Runs directory ready",
|
|
6073
|
+
status: runsExists && probes.pathWritable(runsDir) ? "pass" : runsExists ? "warn" : "warn",
|
|
6074
|
+
summary: runsExists ? probes.pathWritable(runsDir) ? `Writable ${displayRunsDir}` : `Exists but not writable: ${displayRunsDir}` : `Will be created on first run: ${displayRunsDir}`,
|
|
6075
|
+
remediation: runsExists && !probes.pathWritable(runsDir) ? `Fix permissions on ${displayRunsDir} or set KYNVER_HARNESS_ROOT to a writable path.` : void 0,
|
|
6076
|
+
details: { runsDir: displayRunsDir, exists: runsExists, writable: probes.pathWritable(runsDir) }
|
|
6077
|
+
}),
|
|
6078
|
+
check({
|
|
6079
|
+
id: "harness_worktrees_dir",
|
|
6080
|
+
label: "Worktrees directory ready",
|
|
6081
|
+
status: worktreesExists && probes.pathWritable(worktreesDir) ? "pass" : "warn",
|
|
6082
|
+
summary: worktreesExists ? probes.pathWritable(worktreesDir) ? `Writable ${displayWorktreesDir}` : `Exists but not writable: ${displayWorktreesDir}` : `Will be created on first worker: ${displayWorktreesDir}`,
|
|
6083
|
+
remediation: worktreesExists && !probes.pathWritable(worktreesDir) ? `Fix permissions on ${displayWorktreesDir}.` : void 0,
|
|
6084
|
+
details: { worktreesDir: displayWorktreesDir, exists: worktreesExists, writable: probes.pathWritable(worktreesDir) }
|
|
6085
|
+
})
|
|
6086
|
+
]
|
|
6087
|
+
};
|
|
6088
|
+
}
|
|
6089
|
+
function assessCallbackAuth(probes) {
|
|
6090
|
+
const config = probes.loadConfig();
|
|
6091
|
+
const env = probes.envSnapshot();
|
|
6092
|
+
const baseUrl = env.kynverApiUrl ?? config.apiBaseUrl?.trim() ?? env.openclawCronFireBaseUrl;
|
|
6093
|
+
const usingLegacyBase = !env.kynverApiUrl && !config.apiBaseUrl && Boolean(env.openclawCronFireBaseUrl);
|
|
6094
|
+
const legacySecret = env.openclawCronSecret || env.kynverRuntimeSecret;
|
|
6095
|
+
const envScoped = env.kynverRunnerTokenPrefix?.startsWith("krc1.");
|
|
6096
|
+
const creds = probes.readCredentials();
|
|
6097
|
+
const savedScoped = creds.runnerTokenPrefix?.startsWith("krc1.");
|
|
6098
|
+
const checks = [
|
|
6099
|
+
check({
|
|
6100
|
+
id: "callback_base_url",
|
|
6101
|
+
label: "Callback base URL configured",
|
|
6102
|
+
status: baseUrl ? usingLegacyBase ? "warn" : "pass" : "fail",
|
|
6103
|
+
summary: baseUrl ? usingLegacyBase ? `Using legacy OPENCLAW_CRON_FIRE_BASE_URL (${baseUrl})` : baseUrl : "No KYNVER_API_URL, config apiBaseUrl, or legacy OpenClaw base URL",
|
|
6104
|
+
remediation: baseUrl ? usingLegacyBase ? "Migrate to KYNVER_API_URL or `kynver setup --api-base-url` and drop OPENCLAW_CRON_FIRE_BASE_URL." : void 0 : "Set KYNVER_API_URL or run `kynver setup --api-base-url https://\u2026`.",
|
|
6105
|
+
details: {
|
|
6106
|
+
source: env.kynverApiUrl ? "KYNVER_API_URL" : config.apiBaseUrl ? "config.apiBaseUrl" : env.openclawCronFireBaseUrl ? "OPENCLAW_CRON_FIRE_BASE_URL" : "none",
|
|
6107
|
+
baseUrl: baseUrl ?? null
|
|
6108
|
+
}
|
|
6109
|
+
}),
|
|
6110
|
+
check({
|
|
6111
|
+
id: "callback_auth_mode",
|
|
6112
|
+
label: "Callback auth uses scoped runner token",
|
|
6113
|
+
status: envScoped || savedScoped ? "pass" : legacySecret ? "warn" : "fail",
|
|
6114
|
+
summary: envScoped ? "KYNVER_RUNNER_TOKEN scoped token will be sent as X-Kynver-Runner-Token" : savedScoped ? "Saved krc1.* token in ~/.kynver/credentials" : legacySecret ? "Deployment-level KYNVER_RUNTIME_SECRET / OPENCLAW_CRON_SECRET in use (legacy headers)" : "No callback secret configured",
|
|
6115
|
+
remediation: envScoped || savedScoped ? void 0 : "Mint a scoped token: `kynver runner credential --agent-os-id <id>`. Avoid shared OPENCLAW_CRON_SECRET on user runners.",
|
|
6116
|
+
details: {
|
|
6117
|
+
mode: envScoped || savedScoped ? "scoped" : legacySecret ? "legacy_global_secret" : "missing",
|
|
6118
|
+
legacyHeadersWhenNotScoped: ["X-OpenClaw-Cron-Secret", "X-Kynver-Runtime-Secret"]
|
|
6119
|
+
}
|
|
6120
|
+
})
|
|
6121
|
+
];
|
|
6122
|
+
return { id: "callback_auth", label: "Callback auth / config", checks };
|
|
6123
|
+
}
|
|
6124
|
+
function assessOpenclawHotspots(probes) {
|
|
6125
|
+
const env = probes.envSnapshot();
|
|
6126
|
+
const harnessRoot = probes.harnessRoot();
|
|
6127
|
+
const legacyRoot = probes.legacyOpenclawHarnessRoot();
|
|
6128
|
+
const displayHarnessRoot = redactHomePath(harnessRoot);
|
|
6129
|
+
const displayLegacyRoot = redactHomePath(legacyRoot);
|
|
6130
|
+
const displayOpusHarnessRoot = env.opusHarnessRoot ? redactHomePath(env.opusHarnessRoot) : null;
|
|
6131
|
+
const legacyHarnessActive = harnessRoot === legacyRoot && probes.pathExists(legacyRoot);
|
|
6132
|
+
const schedulerOpenclaw = !env.kynverSchedulerProvider || env.kynverSchedulerProvider === "openclaw-cron";
|
|
6133
|
+
const checks = [
|
|
6134
|
+
check({
|
|
6135
|
+
id: "hotspot_legacy_harness_root",
|
|
6136
|
+
label: "Legacy ~/.openclaw/harness still active",
|
|
6137
|
+
status: legacyHarnessActive ? "warn" : "pass",
|
|
6138
|
+
summary: legacyHarnessActive ? `Harness root is legacy ${displayLegacyRoot}` : env.opusHarnessRoot ? `OPUS_HARNESS_ROOT override in use (${displayOpusHarnessRoot})` : `Using ${displayHarnessRoot}`,
|
|
6139
|
+
remediation: legacyHarnessActive ? "Set KYNVER_HARNESS_ROOT=~/.kynver/harness (or run setup), migrate artifacts, retire OPUS_HARNESS_ROOT." : env.opusHarnessRoot ? "Prefer KYNVER_HARNESS_ROOT over OPUS_HARNESS_ROOT." : void 0,
|
|
6140
|
+
details: { harnessRoot: displayHarnessRoot, legacyRoot: displayLegacyRoot, opusHarnessRoot: displayOpusHarnessRoot }
|
|
6141
|
+
}),
|
|
6142
|
+
check({
|
|
6143
|
+
id: "hotspot_openclaw_env_secrets",
|
|
6144
|
+
label: "OpenClaw deployment secrets in runner env",
|
|
6145
|
+
status: env.openclawCronSecret || env.openclawCronFireBaseUrl ? "warn" : "pass",
|
|
6146
|
+
summary: env.openclawCronSecret || env.openclawCronFireBaseUrl ? [
|
|
6147
|
+
env.openclawCronSecret ? "OPENCLAW_CRON_SECRET set" : null,
|
|
6148
|
+
env.openclawCronFireBaseUrl ? "OPENCLAW_CRON_FIRE_BASE_URL set" : null
|
|
6149
|
+
].filter(Boolean).join("; ") : "No OpenClaw cron env overrides on this runner",
|
|
6150
|
+
remediation: env.openclawCronSecret || env.openclawCronFireBaseUrl ? "Move to KYNVER_API_URL + scoped runner tokens; unset OpenClaw cron env on user-hosted runners." : void 0
|
|
6151
|
+
}),
|
|
6152
|
+
check({
|
|
6153
|
+
id: "hotspot_openclaw_scheduler",
|
|
6154
|
+
label: "openclaw-cron scheduler dependency (deployment)",
|
|
6155
|
+
status: schedulerOpenclaw ? "warn" : "pass",
|
|
6156
|
+
summary: schedulerOpenclaw ? env.kynverSchedulerProvider === "openclaw-cron" ? "KYNVER_SCHEDULER_PROVIDER=openclaw-cron \u2014 AgentOS ticks still routed via OpenClaw local cron adapter" : "KYNVER_SCHEDULER_PROVIDER unset \u2014 server may fall back to openclaw-cron when QStash is absent" : `KYNVER_SCHEDULER_PROVIDER=${env.kynverSchedulerProvider}`,
|
|
6157
|
+
remediation: schedulerOpenclaw ? "On Kynver-hosted scheduler: set KYNVER_SCHEDULER_PROVIDER=qstash where QStash is configured; retire openclaw-cron stub when runtime daemon owns dispatch." : void 0,
|
|
6158
|
+
details: { schedulerProvider: env.kynverSchedulerProvider ?? null }
|
|
6159
|
+
}),
|
|
6160
|
+
check({
|
|
6161
|
+
id: "hotspot_lease_source_names",
|
|
6162
|
+
label: "Harness lease/completion source names",
|
|
6163
|
+
status: "pass",
|
|
6164
|
+
summary: "Runtime uses kynver-harness:* lease owners and completion source kynver-harness (OpenClaw names retired in runtime)",
|
|
6165
|
+
details: {
|
|
6166
|
+
leaseOwnerPattern: "kynver-harness:<runId>",
|
|
6167
|
+
completionSource: "kynver-harness",
|
|
6168
|
+
note: "OpenClaw adapter remains optional in packages/kynver-openclaw-agent-os only"
|
|
6169
|
+
}
|
|
6170
|
+
})
|
|
6171
|
+
];
|
|
6172
|
+
return { id: "openclaw_hotspots", label: "OpenClaw dependency hotspots", checks };
|
|
6173
|
+
}
|
|
6174
|
+
function assessRuntimeTakeoverReadiness(probes = defaultRuntimeTakeoverProbes) {
|
|
6175
|
+
const sections = [
|
|
6176
|
+
assessCliPackage(probes),
|
|
6177
|
+
assessUserConfig(probes),
|
|
6178
|
+
assessRunnerToken(probes),
|
|
6179
|
+
assessVercelCli(probes),
|
|
6180
|
+
assessHarnessDirs(probes),
|
|
6181
|
+
assessCallbackAuth(probes),
|
|
6182
|
+
assessOpenclawHotspots(probes)
|
|
6183
|
+
];
|
|
6184
|
+
const counts = summarizeCounts(sections);
|
|
6185
|
+
const ready = counts.fail === 0;
|
|
6186
|
+
const summary = ready ? counts.warn > 0 ? `Ready with ${counts.warn} warning(s) \u2014 review remediation before retiring OpenClaw as primary runtime agent.` : "Ready \u2014 Kynver runtime can serve as primary runtime agent on this host." : `${counts.fail} blocking check(s) \u2014 fix failures before OpenClaw takeover.`;
|
|
6187
|
+
return {
|
|
6188
|
+
command: "doctor runtime-takeover",
|
|
6189
|
+
ready,
|
|
6190
|
+
summary,
|
|
6191
|
+
counts,
|
|
6192
|
+
sections
|
|
6193
|
+
};
|
|
6194
|
+
}
|
|
6195
|
+
|
|
6196
|
+
// src/doctor/runtime-takeover-cli.ts
|
|
6197
|
+
function runRuntimeTakeoverDoctorCli() {
|
|
6198
|
+
const report = assessRuntimeTakeoverReadiness();
|
|
6199
|
+
console.log(JSON.stringify(report, null, 2));
|
|
6200
|
+
if (!report.ready) {
|
|
6201
|
+
process.exitCode = 1;
|
|
6202
|
+
}
|
|
6203
|
+
}
|
|
6204
|
+
|
|
5683
6205
|
// src/cli.ts
|
|
5684
6206
|
function isHelpFlag(arg) {
|
|
5685
6207
|
return arg === "help" || arg === "--help" || arg === "-h";
|
|
@@ -5701,7 +6223,7 @@ function usage(code = 0) {
|
|
|
5701
6223
|
" kynver run create --repo /path/repo [--name name] [--base origin/main]",
|
|
5702
6224
|
" kynver run list",
|
|
5703
6225
|
" kynver run status --run RUN_ID",
|
|
5704
|
-
" kynver run dispatch --run RUN_ID --agent-os-id AOS_ID [--base-url URL] [--secret SECRET] [--execute] [--lane any|implementation|review|landing] [--executor harness] [--max-starts 1] [--lease-ms MS] [--owned path[,path]] [--model claude-opus-4-
|
|
6226
|
+
" kynver run dispatch --run RUN_ID --agent-os-id AOS_ID [--base-url URL] [--secret SECRET] [--execute] [--lane any|implementation|review|landing] [--executor harness] [--max-starts 1] [--lease-ms MS] [--owned path[,path]] [--model claude-opus-4-8] [--disk-path /]",
|
|
5705
6227
|
" kynver run sweep --run RUN_ID --agent-os-id AOS_ID [--base-url URL] [--secret SECRET] [--grace-ms MS]",
|
|
5706
6228
|
' kynver worker start --run RUN_ID --name worker --task "..." [--owned path[,path]] [--model MODEL] [--provider claude|cursor] [--agent-os-id AOS_ID] [--task-id TASK_ID] [--wait]',
|
|
5707
6229
|
" kynver worker status --run RUN_ID --name worker",
|
|
@@ -5709,6 +6231,7 @@ function usage(code = 0) {
|
|
|
5709
6231
|
" kynver worker stop --run RUN_ID --name worker",
|
|
5710
6232
|
" kynver worker complete --run RUN_ID --name worker [--agent-os-id AOS_ID] [--task-id TASK_ID] [--base-url URL] [--secret SECRET]",
|
|
5711
6233
|
" 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]",
|
|
6234
|
+
" kynver run reconcile",
|
|
5712
6235
|
" 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]",
|
|
5713
6236
|
" kynver plan verify --plan PLAN_ID [--worktree PATH] [--task TASK_ID] [--human-override]",
|
|
5714
6237
|
" 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]",
|
|
@@ -5721,7 +6244,8 @@ function usage(code = 0) {
|
|
|
5721
6244
|
" kynver monitor list",
|
|
5722
6245
|
" kynver monitor tick --run RUN_ID [--name worker] [--agent-os-id AOS_ID] [--auto-complete] [--renew-leases]",
|
|
5723
6246
|
" 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]"
|
|
6247
|
+
" kynver monitor run-loop --run RUN_ID --monitor-id ID [--name worker] [--agent-os-id AOS_ID] [--poll-ms MS] [--auto-complete] [--renew-leases]",
|
|
6248
|
+
" kynver doctor runtime-takeover"
|
|
5725
6249
|
].join("\n")
|
|
5726
6250
|
);
|
|
5727
6251
|
process.exit(code);
|
|
@@ -5732,7 +6256,7 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
5732
6256
|
const scope = argv.shift();
|
|
5733
6257
|
let action;
|
|
5734
6258
|
let rest;
|
|
5735
|
-
if (scope === "run" || scope === "worker" || scope === "plan" || scope === "runner" || scope === "monitor") {
|
|
6259
|
+
if (scope === "run" || scope === "worker" || scope === "plan" || scope === "runner" || scope === "monitor" || scope === "doctor") {
|
|
5736
6260
|
action = argv.shift();
|
|
5737
6261
|
rest = argv;
|
|
5738
6262
|
} else {
|
|
@@ -5757,11 +6281,13 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
5757
6281
|
unknownCommand("plan", `outbox ${outboxAction ?? ""}`.trim());
|
|
5758
6282
|
}
|
|
5759
6283
|
if (scope === "cleanup") return runCleanupCli(args);
|
|
6284
|
+
if (scope === "doctor" && action === "runtime-takeover") return runRuntimeTakeoverDoctorCli();
|
|
5760
6285
|
if (scope === "run" && action === "create") return createRun(args);
|
|
5761
6286
|
if (scope === "run" && action === "list") return listRuns();
|
|
5762
6287
|
if (scope === "run" && action === "status") return runStatus(args);
|
|
5763
6288
|
if (scope === "run" && action === "dispatch") return void await dispatchRun(args);
|
|
5764
6289
|
if (scope === "run" && action === "sweep") return void await sweepRun(args);
|
|
6290
|
+
if (scope === "run" && action === "reconcile") return reconcileRunsCli();
|
|
5765
6291
|
if (scope === "worker" && action === "start") return void await startWorker(args);
|
|
5766
6292
|
if (scope === "worker" && action === "status") return workerStatus(args);
|
|
5767
6293
|
if (scope === "worker" && action === "tail") return tailWorker(args);
|