@kynver-app/runtime 0.1.47 → 0.1.49
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 +210 -36
- package/dist/cli.js.map +4 -4
- package/dist/doctor/runtime-takeover-scheduler.d.ts +20 -0
- package/dist/doctor/runtime-takeover.probes.d.ts +2 -0
- package/dist/harness-expert-review.d.ts +8 -0
- package/dist/index.js +210 -36
- package/dist/index.js.map +4 -4
- package/dist/pr-handoff/pr-handoff-assess.d.ts +16 -0
- package/dist/pr-handoff/pr-handoff.types.d.ts +1 -1
- package/dist/status.d.ts +5 -0
- package/dist/supervisor.d.ts +4 -0
- package/package.json +1 -1
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { DoctorCheck } from "./doctor.types.js";
|
|
2
|
+
export interface RuntimeTakeoverSchedulerEnv {
|
|
3
|
+
kynverSchedulerProvider?: string;
|
|
4
|
+
/** True when this process has QStash credentials (typical Kynver server deploy). */
|
|
5
|
+
qstashTokenPresent?: boolean;
|
|
6
|
+
/** Explicit hosted-deploy marker (`KYNVER_HOSTED_DEPLOYMENT=1|true|yes`). */
|
|
7
|
+
kynverHostedDeployment?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export interface RuntimeTakeoverSchedulerContext {
|
|
10
|
+
agentOsId?: string | null;
|
|
11
|
+
apiBaseUrl?: string | null;
|
|
12
|
+
hasScopedRunnerToken: boolean;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Scheduler readiness for runtime takeover.
|
|
16
|
+
*
|
|
17
|
+
* Runner hosts drive dispatch via `kynver daemon` → `pipeline-tick` → `operator/tick`
|
|
18
|
+
* and must not depend on OpenClaw cron. Server/hosted deploys should resolve to QStash.
|
|
19
|
+
*/
|
|
20
|
+
export declare function assessRuntimeTakeoverScheduler(env: RuntimeTakeoverSchedulerEnv, ctx: RuntimeTakeoverSchedulerContext): DoctorCheck;
|
|
@@ -26,6 +26,8 @@ export interface RuntimeTakeoverProbes {
|
|
|
26
26
|
kynverHarnessRoot?: string;
|
|
27
27
|
opusHarnessRoot?: string;
|
|
28
28
|
kynverSchedulerProvider?: string;
|
|
29
|
+
qstashTokenPresent?: boolean;
|
|
30
|
+
kynverHostedDeployment?: boolean;
|
|
29
31
|
};
|
|
30
32
|
harnessRoot(): string;
|
|
31
33
|
legacyOpenclawHarnessRoot(): string;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface HarnessExpertReviewTaskShape {
|
|
2
|
+
title?: string;
|
|
3
|
+
personaSlug?: string | null;
|
|
4
|
+
parentTaskId?: string | null;
|
|
5
|
+
executorRef?: string | null;
|
|
6
|
+
}
|
|
7
|
+
/** Expert review workers must not inherit landing-only PR gates or open new PRs. */
|
|
8
|
+
export declare function isHarnessExpertReviewWorker(worker: HarnessExpertReviewTaskShape): boolean;
|
package/dist/index.js
CHANGED
|
@@ -1956,7 +1956,7 @@ function inferModelRoutingFromTask(task) {
|
|
|
1956
1956
|
rule: "lane:landing"
|
|
1957
1957
|
};
|
|
1958
1958
|
}
|
|
1959
|
-
if (ref.includes("review") || title
|
|
1959
|
+
if (ref.includes("review") || /^review[\s:]/.test(title) || roleLane.includes("review")) {
|
|
1960
1960
|
if (isOpusLane(ref, title) || roleLane === "deep_reviewer") {
|
|
1961
1961
|
return { model: "claude-opus-4-7", provider: "claude", rule: "lane:deep_review" };
|
|
1962
1962
|
}
|
|
@@ -2149,6 +2149,7 @@ function buildPrompt(input) {
|
|
|
2149
2149
|
"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.",
|
|
2150
2150
|
"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.",
|
|
2151
2151
|
"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.",
|
|
2152
|
+
"Expert review / production-review workers (Dalton/Lorentz, plan-review-task, scheduledJob reviewer children): do NOT open new implementation PRs \u2014 review the parent task's existing PR and record reviewVerdict in finalResult; landing-contract targetPrReconciliation does not apply.",
|
|
2152
2153
|
"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.",
|
|
2153
2154
|
"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.",
|
|
2154
2155
|
"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.",
|
|
@@ -2390,8 +2391,39 @@ function completionPostSucceeded(summary) {
|
|
|
2390
2391
|
return summary.taskAdvanced;
|
|
2391
2392
|
}
|
|
2392
2393
|
|
|
2394
|
+
// src/harness-expert-review.ts
|
|
2395
|
+
var EXPERT_LANE_REVIEW_REF = "expert-lane-pr-review:";
|
|
2396
|
+
var PLAN_REVIEW_EXECUTOR_REF = "plan-review-task";
|
|
2397
|
+
var SCHEDULED_JOB_EXECUTOR_REF = "scheduledjob:";
|
|
2398
|
+
function normalizePersonaSlug(value) {
|
|
2399
|
+
if (!value) return null;
|
|
2400
|
+
const t = value.trim().toLowerCase();
|
|
2401
|
+
return t.length ? t : null;
|
|
2402
|
+
}
|
|
2403
|
+
function isHarnessExpertReviewWorker(worker) {
|
|
2404
|
+
const ref = (worker.executorRef ?? "").toLowerCase();
|
|
2405
|
+
if (ref.startsWith(EXPERT_LANE_REVIEW_REF)) return true;
|
|
2406
|
+
if (ref === PLAN_REVIEW_EXECUTOR_REF || ref.startsWith("daemon-review:")) return true;
|
|
2407
|
+
if (ref.startsWith(SCHEDULED_JOB_EXECUTOR_REF) && worker.parentTaskId) {
|
|
2408
|
+
const persona = normalizePersonaSlug(worker.personaSlug);
|
|
2409
|
+
if (persona === "lorentz" || persona === "dalton") return true;
|
|
2410
|
+
}
|
|
2411
|
+
const title = (worker.title ?? "").toLowerCase();
|
|
2412
|
+
if (title.includes("expert pr review")) return true;
|
|
2413
|
+
if (worker.parentTaskId && (title.startsWith("review:") || title.includes("review required") || title.includes("runtime review"))) {
|
|
2414
|
+
const persona = normalizePersonaSlug(worker.personaSlug);
|
|
2415
|
+
if (persona === "lorentz" || persona === "dalton") return true;
|
|
2416
|
+
}
|
|
2417
|
+
return false;
|
|
2418
|
+
}
|
|
2419
|
+
|
|
2393
2420
|
// src/pr-handoff/pr-handoff-assess.ts
|
|
2394
2421
|
var REVIEW_LANE_RULE = /^(lane:)?(review|deep_review|planning|landing)(:|$)/i;
|
|
2422
|
+
var REVIEW_PERSONA_SLUGS = /* @__PURE__ */ new Set(["lorentz", "sentinel"]);
|
|
2423
|
+
var NO_PR_COMMITS_BETWEEN_RE = /no commits between/i;
|
|
2424
|
+
function isGhNoCommitsBetweenError(detail) {
|
|
2425
|
+
return Boolean(detail && NO_PR_COMMITS_BETWEEN_RE.test(detail));
|
|
2426
|
+
}
|
|
2395
2427
|
function trimOrNull4(value) {
|
|
2396
2428
|
if (typeof value !== "string") return null;
|
|
2397
2429
|
const trimmed = value.trim();
|
|
@@ -2410,8 +2442,30 @@ function extractPrUrlFromText(value) {
|
|
|
2410
2442
|
);
|
|
2411
2443
|
return m ? trimOrNull4(m[0]) : null;
|
|
2412
2444
|
}
|
|
2413
|
-
function
|
|
2445
|
+
function countCommitsAheadOfBase(worktreePath, baseRef, exec) {
|
|
2446
|
+
const base = baseRef.trim();
|
|
2447
|
+
if (!base) return null;
|
|
2448
|
+
const count = exec.git(worktreePath, ["rev-list", "--count", `${base}..HEAD`]);
|
|
2449
|
+
if (count.status !== 0) return null;
|
|
2450
|
+
const n = Number.parseInt(count.stdout.trim(), 10);
|
|
2451
|
+
return Number.isFinite(n) ? n : null;
|
|
2452
|
+
}
|
|
2453
|
+
function isReviewArtifactWorker(worker, snapshot) {
|
|
2454
|
+
if (snapshot.changedFiles.length > 0) return false;
|
|
2455
|
+
const persona = trimOrNull4(worker.personaSlug)?.toLowerCase();
|
|
2456
|
+
if (persona && REVIEW_PERSONA_SLUGS.has(persona)) return true;
|
|
2457
|
+
const rule = trimOrNull4(worker.routingRule) ?? "";
|
|
2458
|
+
if (rule && REVIEW_LANE_RULE.test(rule)) return true;
|
|
2459
|
+
return false;
|
|
2460
|
+
}
|
|
2461
|
+
function hasWorkProduct(snapshot, options) {
|
|
2414
2462
|
if (snapshot.changedFiles.length > 0) return true;
|
|
2463
|
+
const baseRef = trimOrNull4(options?.baseRef);
|
|
2464
|
+
if (baseRef && options?.exec && options.worktreePath) {
|
|
2465
|
+
const ahead = countCommitsAheadOfBase(options.worktreePath, baseRef, options.exec);
|
|
2466
|
+
if (ahead === 0) return false;
|
|
2467
|
+
if (ahead !== null && ahead > 0) return true;
|
|
2468
|
+
}
|
|
2415
2469
|
if (trimOrNull4(snapshot.headCommit)) return true;
|
|
2416
2470
|
if (committedHead(snapshot.gitAncestry)) return true;
|
|
2417
2471
|
return false;
|
|
@@ -2420,18 +2474,38 @@ function assessPrHandoffRequirement(input) {
|
|
|
2420
2474
|
if (!input.dispatched) {
|
|
2421
2475
|
return { required: false, reason: "not_dispatched" };
|
|
2422
2476
|
}
|
|
2477
|
+
if (isHarnessExpertReviewWorker({
|
|
2478
|
+
title: input.taskTitle ?? void 0,
|
|
2479
|
+
personaSlug: input.personaSlug,
|
|
2480
|
+
parentTaskId: input.parentTaskId,
|
|
2481
|
+
executorRef: input.executorRef
|
|
2482
|
+
})) {
|
|
2483
|
+
return { required: false, reason: "expert_review_task" };
|
|
2484
|
+
}
|
|
2423
2485
|
const rule = trimOrNull4(input.routingRule) ?? "";
|
|
2424
2486
|
if (rule && REVIEW_LANE_RULE.test(rule)) {
|
|
2425
2487
|
return { required: false, reason: "review_lane" };
|
|
2426
2488
|
}
|
|
2489
|
+
const workerCtx = input.worker ?? {
|
|
2490
|
+
personaSlug: input.personaSlug,
|
|
2491
|
+
routingRule: input.routingRule
|
|
2492
|
+
};
|
|
2493
|
+
if (isReviewArtifactWorker(workerCtx, input.snapshot)) {
|
|
2494
|
+
return { required: false, reason: "review_artifact" };
|
|
2495
|
+
}
|
|
2427
2496
|
if (trimOrNull4(input.patchPath) || trimOrNull4(input.artifactBundlePath)) {
|
|
2428
2497
|
return { required: false, reason: "patch_or_bundle" };
|
|
2429
2498
|
}
|
|
2430
|
-
const prUrl = trimOrNull4(input.prUrl) ?? trimOrNull4(input.snapshot.prUrl);
|
|
2499
|
+
const prUrl = trimOrNull4(input.prUrl) ?? trimOrNull4(input.taskPrUrl) ?? trimOrNull4(input.snapshot.prUrl);
|
|
2431
2500
|
if (prUrl) {
|
|
2432
2501
|
return { required: false, reason: "already_has_pr" };
|
|
2433
2502
|
}
|
|
2434
|
-
|
|
2503
|
+
const workProductOpts = input.exec && input.baseRef ? {
|
|
2504
|
+
baseRef: input.baseRef,
|
|
2505
|
+
exec: input.exec,
|
|
2506
|
+
worktreePath: input.snapshot.worktreePath
|
|
2507
|
+
} : void 0;
|
|
2508
|
+
if (!hasWorkProduct(input.snapshot, workProductOpts)) {
|
|
2435
2509
|
return { required: false, reason: "no_work_product" };
|
|
2436
2510
|
}
|
|
2437
2511
|
return { required: true, snapshot: input.snapshot };
|
|
@@ -2640,10 +2714,19 @@ function ensurePrReadyHandoff(input, exec = defaultPrHandoffExec) {
|
|
|
2640
2714
|
prUrl: prUrlHint,
|
|
2641
2715
|
headCommit: null
|
|
2642
2716
|
});
|
|
2717
|
+
const baseRef = input.run.baseCommit?.trim() || input.run.base?.trim() || "origin/main";
|
|
2643
2718
|
const requirement = assessPrHandoffRequirement({
|
|
2644
2719
|
dispatched: input.worker.dispatched,
|
|
2645
2720
|
routingRule: input.worker.routingRule,
|
|
2721
|
+
personaSlug: input.worker.personaSlug,
|
|
2646
2722
|
prUrl: prUrlHint,
|
|
2723
|
+
taskTitle: input.worker.taskTitle,
|
|
2724
|
+
executorRef: input.worker.executorRef,
|
|
2725
|
+
parentTaskId: input.worker.parentTaskId,
|
|
2726
|
+
taskPrUrl: input.worker.taskPrUrl,
|
|
2727
|
+
baseRef,
|
|
2728
|
+
exec,
|
|
2729
|
+
worker: input.worker,
|
|
2647
2730
|
snapshot
|
|
2648
2731
|
});
|
|
2649
2732
|
if (!requirement.required) {
|
|
@@ -2728,6 +2811,14 @@ function ensurePrReadyHandoff(input, exec = defaultPrHandoffExec) {
|
|
|
2728
2811
|
exec
|
|
2729
2812
|
});
|
|
2730
2813
|
if (!pr.ok || !pr.prUrl) {
|
|
2814
|
+
if (isGhNoCommitsBetweenError(pr.detail)) {
|
|
2815
|
+
return {
|
|
2816
|
+
ok: true,
|
|
2817
|
+
headCommit: headCommit ?? void 0,
|
|
2818
|
+
committed,
|
|
2819
|
+
pushed
|
|
2820
|
+
};
|
|
2821
|
+
}
|
|
2731
2822
|
const dirty = snapshot.changedFiles.length;
|
|
2732
2823
|
const detail = dirty ? `${dirty} uncommitted change(s) and no PR URL after handoff attempt` : "no PR URL after handoff attempt";
|
|
2733
2824
|
return {
|
|
@@ -3523,6 +3614,10 @@ function spawnWorkerProcess(run, opts) {
|
|
|
3523
3614
|
...!opts.agentOsId || !opts.taskId ? { localOnly: true } : {},
|
|
3524
3615
|
routingRule: routing.rule,
|
|
3525
3616
|
...routing.requestedModel ? { requestedModel: routing.requestedModel } : {},
|
|
3617
|
+
...opts.executorRef ? { executorRef: String(opts.executorRef) } : {},
|
|
3618
|
+
...opts.parentTaskId ? { parentTaskId: String(opts.parentTaskId) } : {},
|
|
3619
|
+
...opts.taskTitle ? { taskTitle: String(opts.taskTitle) } : {},
|
|
3620
|
+
...opts.taskPrUrl ? { taskPrUrl: String(opts.taskPrUrl) } : {},
|
|
3526
3621
|
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3527
3622
|
};
|
|
3528
3623
|
saveWorker(run.id, worker);
|
|
@@ -4217,7 +4312,7 @@ function readHarnessWorkerContext(decision) {
|
|
|
4217
4312
|
personaInjectionReady
|
|
4218
4313
|
};
|
|
4219
4314
|
}
|
|
4220
|
-
function
|
|
4315
|
+
function normalizePersonaSlug2(value) {
|
|
4221
4316
|
if (typeof value !== "string") return null;
|
|
4222
4317
|
const trimmed = value.trim().toLowerCase();
|
|
4223
4318
|
return trimmed.length ? trimmed : null;
|
|
@@ -4348,7 +4443,7 @@ async function dispatchRun(args) {
|
|
|
4348
4443
|
const task = decision.task;
|
|
4349
4444
|
const harnessContext = readHarnessWorkerContext(decision);
|
|
4350
4445
|
const taskId = String(task.id);
|
|
4351
|
-
const expectedPersona =
|
|
4446
|
+
const expectedPersona = normalizePersonaSlug2(task.personaSlug);
|
|
4352
4447
|
if (expectedPersona && (!harnessContext?.personaInjectionReady || !harnessContext.personaMarkdown)) {
|
|
4353
4448
|
outcomes.push({
|
|
4354
4449
|
taskId,
|
|
@@ -4392,6 +4487,10 @@ async function dispatchRun(args) {
|
|
|
4392
4487
|
agentOsId,
|
|
4393
4488
|
taskId: String(task.id),
|
|
4394
4489
|
planId,
|
|
4490
|
+
executorRef: task.executorRef ? String(task.executorRef) : void 0,
|
|
4491
|
+
parentTaskId: task.parentTaskId ? String(task.parentTaskId) : void 0,
|
|
4492
|
+
taskTitle: task.title ? String(task.title) : void 0,
|
|
4493
|
+
taskPrUrl: task.prUrl ? String(task.prUrl) : void 0,
|
|
4395
4494
|
instructionPolicyMarkdown: harnessContext?.instructionPolicyMarkdown ?? null,
|
|
4396
4495
|
instructionPolicyFingerprint: harnessContext?.instructionPolicyFingerprint ?? null,
|
|
4397
4496
|
instructionPolicyEvidence: harnessContext?.instructionPolicyEvidence ?? null,
|
|
@@ -6590,7 +6689,12 @@ var defaultRuntimeTakeoverProbes = {
|
|
|
6590
6689
|
openclawCronSecret: Boolean(process.env.OPENCLAW_CRON_SECRET?.trim()),
|
|
6591
6690
|
kynverHarnessRoot: process.env.KYNVER_HARNESS_ROOT?.trim() || void 0,
|
|
6592
6691
|
opusHarnessRoot: process.env.OPUS_HARNESS_ROOT?.trim() || void 0,
|
|
6593
|
-
kynverSchedulerProvider: process.env.KYNVER_SCHEDULER_PROVIDER?.trim() || void 0
|
|
6692
|
+
kynverSchedulerProvider: process.env.KYNVER_SCHEDULER_PROVIDER?.trim() || void 0,
|
|
6693
|
+
qstashTokenPresent: Boolean(process.env.QSTASH_TOKEN?.trim()),
|
|
6694
|
+
kynverHostedDeployment: (() => {
|
|
6695
|
+
const v = process.env.KYNVER_HOSTED_DEPLOYMENT?.trim().toLowerCase();
|
|
6696
|
+
return v === "1" || v === "true" || v === "yes";
|
|
6697
|
+
})()
|
|
6594
6698
|
}),
|
|
6595
6699
|
harnessRoot: () => resolveHarnessRoot(),
|
|
6596
6700
|
legacyOpenclawHarnessRoot: () => path36.join(homedir6(), ".openclaw", "harness"),
|
|
@@ -6600,10 +6704,76 @@ var defaultRuntimeTakeoverProbes = {
|
|
|
6600
6704
|
vercelWhoami: () => captureCommand("vercel", ["whoami"])
|
|
6601
6705
|
};
|
|
6602
6706
|
|
|
6603
|
-
// src/doctor/runtime-takeover.ts
|
|
6707
|
+
// src/doctor/runtime-takeover-scheduler.ts
|
|
6604
6708
|
function check(partial) {
|
|
6605
6709
|
return partial;
|
|
6606
6710
|
}
|
|
6711
|
+
function assessRuntimeTakeoverScheduler(env, ctx) {
|
|
6712
|
+
const runnerOpenclaw = env.kynverSchedulerProvider === "openclaw-cron";
|
|
6713
|
+
const runnerQstash = env.kynverSchedulerProvider === "qstash";
|
|
6714
|
+
const hostedDeployment = Boolean(env.qstashTokenPresent) || Boolean(env.kynverHostedDeployment);
|
|
6715
|
+
const daemonDispatchReady = Boolean(ctx.agentOsId?.trim()) && Boolean(ctx.apiBaseUrl?.trim()) && ctx.hasScopedRunnerToken;
|
|
6716
|
+
const deploymentNeedsQstash = hostedDeployment && !env.qstashTokenPresent && env.kynverSchedulerProvider !== "qstash";
|
|
6717
|
+
const deploymentOpenclaw = hostedDeployment && env.kynverSchedulerProvider === "openclaw-cron";
|
|
6718
|
+
if (runnerOpenclaw) {
|
|
6719
|
+
return check({
|
|
6720
|
+
id: "hotspot_openclaw_scheduler",
|
|
6721
|
+
label: "Scheduler provider (runtime daemon vs OpenClaw cron)",
|
|
6722
|
+
status: "warn",
|
|
6723
|
+
summary: "KYNVER_SCHEDULER_PROVIDER=openclaw-cron on this runner \u2014 hosted dispatch still depends on the OpenClaw local-cron adapter",
|
|
6724
|
+
remediation: "Unset KYNVER_SCHEDULER_PROVIDER on user runners. Use `kynver daemon` (pipeline-tick \u2192 operator/tick). On the Kynver server set KYNVER_SCHEDULER_PROVIDER=qstash when QStash is configured.",
|
|
6725
|
+
details: { schedulerProvider: env.kynverSchedulerProvider ?? null, hostedDeployment }
|
|
6726
|
+
});
|
|
6727
|
+
}
|
|
6728
|
+
if (deploymentOpenclaw || deploymentNeedsQstash) {
|
|
6729
|
+
return check({
|
|
6730
|
+
id: "hotspot_openclaw_scheduler",
|
|
6731
|
+
label: "Scheduler provider (runtime daemon vs OpenClaw cron)",
|
|
6732
|
+
status: "warn",
|
|
6733
|
+
summary: deploymentOpenclaw ? "Hosted deployment has KYNVER_SCHEDULER_PROVIDER=openclaw-cron \u2014 AgentOS scheduled ticks should use QStash" : "Hosted deployment without QSTASH_TOKEN \u2014 scheduler may fall back to openclaw-cron",
|
|
6734
|
+
remediation: "Set QSTASH_TOKEN and KYNVER_SCHEDULER_PROVIDER=qstash on the Kynver server. User runners use `kynver daemon` for dispatch and should not set a scheduler provider.",
|
|
6735
|
+
details: {
|
|
6736
|
+
schedulerProvider: env.kynverSchedulerProvider ?? null,
|
|
6737
|
+
qstashTokenPresent: env.qstashTokenPresent ?? false,
|
|
6738
|
+
hostedDeployment
|
|
6739
|
+
}
|
|
6740
|
+
});
|
|
6741
|
+
}
|
|
6742
|
+
if (daemonDispatchReady) {
|
|
6743
|
+
return check({
|
|
6744
|
+
id: "hotspot_openclaw_scheduler",
|
|
6745
|
+
label: "Scheduler provider (runtime daemon vs OpenClaw cron)",
|
|
6746
|
+
status: "pass",
|
|
6747
|
+
summary: runnerQstash ? "Runner override qstash present; hosted dispatch still owned by kynver daemon pipeline-tick" : "Hosted dispatch owned by kynver daemon (pipeline-tick \u2192 operator/tick); no OpenClaw cron on runner",
|
|
6748
|
+
details: {
|
|
6749
|
+
schedulerProvider: env.kynverSchedulerProvider ?? null,
|
|
6750
|
+
dispatchPath: "kynver-daemon-pipeline-tick",
|
|
6751
|
+
hostedDeployment
|
|
6752
|
+
}
|
|
6753
|
+
});
|
|
6754
|
+
}
|
|
6755
|
+
if (!env.kynverSchedulerProvider) {
|
|
6756
|
+
return check({
|
|
6757
|
+
id: "hotspot_openclaw_scheduler",
|
|
6758
|
+
label: "Scheduler provider (runtime daemon vs OpenClaw cron)",
|
|
6759
|
+
status: "pass",
|
|
6760
|
+
summary: "No KYNVER_SCHEDULER_PROVIDER on runner (expected) \u2014 finish runner setup so daemon pipeline-tick owns dispatch",
|
|
6761
|
+
details: { schedulerProvider: null, dispatchPath: "kynver-daemon-pipeline-tick-pending" }
|
|
6762
|
+
});
|
|
6763
|
+
}
|
|
6764
|
+
return check({
|
|
6765
|
+
id: "hotspot_openclaw_scheduler",
|
|
6766
|
+
label: "Scheduler provider (runtime daemon vs OpenClaw cron)",
|
|
6767
|
+
status: "pass",
|
|
6768
|
+
summary: `KYNVER_SCHEDULER_PROVIDER=${env.kynverSchedulerProvider}`,
|
|
6769
|
+
details: { schedulerProvider: env.kynverSchedulerProvider ?? null }
|
|
6770
|
+
});
|
|
6771
|
+
}
|
|
6772
|
+
|
|
6773
|
+
// src/doctor/runtime-takeover.ts
|
|
6774
|
+
function check2(partial) {
|
|
6775
|
+
return partial;
|
|
6776
|
+
}
|
|
6607
6777
|
function summarizeCounts(sections) {
|
|
6608
6778
|
const counts = { pass: 0, warn: 0, fail: 0 };
|
|
6609
6779
|
for (const section of sections) {
|
|
@@ -6620,14 +6790,14 @@ function assessCliPackage(probes) {
|
|
|
6620
6790
|
const firstPath = onPath ? which.stdout.split(/\r?\n/)[0]?.trim() : void 0;
|
|
6621
6791
|
const displayCliPath = firstPath ? displayUserPath(firstPath) : void 0;
|
|
6622
6792
|
const checks = [
|
|
6623
|
-
|
|
6793
|
+
check2({
|
|
6624
6794
|
id: "cli_running_version",
|
|
6625
6795
|
label: "Running @kynver-app/runtime version",
|
|
6626
6796
|
status: "pass",
|
|
6627
6797
|
summary: `@kynver-app/runtime ${runningVersion}`,
|
|
6628
6798
|
details: { version: runningVersion }
|
|
6629
6799
|
}),
|
|
6630
|
-
|
|
6800
|
+
check2({
|
|
6631
6801
|
id: "cli_on_path",
|
|
6632
6802
|
label: "kynver executable on PATH",
|
|
6633
6803
|
status: onPath ? "pass" : "warn",
|
|
@@ -6641,7 +6811,7 @@ function assessCliPackage(probes) {
|
|
|
6641
6811
|
const installedVersion = versionProbe.stdout.replace(/^kynver\s+/i, "").trim() || void 0;
|
|
6642
6812
|
const versionMatch = versionProbe.ok && (!installedVersion || installedVersion === runningVersion);
|
|
6643
6813
|
checks.push(
|
|
6644
|
-
|
|
6814
|
+
check2({
|
|
6645
6815
|
id: "cli_installed_version",
|
|
6646
6816
|
label: "Installed kynver CLI version matches running package",
|
|
6647
6817
|
status: versionMatch ? "pass" : "warn",
|
|
@@ -6659,7 +6829,7 @@ function assessUserConfig(probes) {
|
|
|
6659
6829
|
const exists = probes.pathExists(configPath);
|
|
6660
6830
|
const config = probes.loadConfig();
|
|
6661
6831
|
const checks = [
|
|
6662
|
-
|
|
6832
|
+
check2({
|
|
6663
6833
|
id: "config_file",
|
|
6664
6834
|
label: "~/.kynver/config.json present",
|
|
6665
6835
|
status: exists ? "pass" : "fail",
|
|
@@ -6674,7 +6844,7 @@ function assessUserConfig(probes) {
|
|
|
6674
6844
|
const defaultRepo = config.defaultRepo?.trim();
|
|
6675
6845
|
const displayDefaultRepo = defaultRepo ? displayUserPath(defaultRepo) : null;
|
|
6676
6846
|
checks.push(
|
|
6677
|
-
|
|
6847
|
+
check2({
|
|
6678
6848
|
id: "config_api_base_url",
|
|
6679
6849
|
label: "Default API base URL",
|
|
6680
6850
|
status: apiBaseUrl ? "pass" : "warn",
|
|
@@ -6682,7 +6852,7 @@ function assessUserConfig(probes) {
|
|
|
6682
6852
|
remediation: apiBaseUrl ? void 0 : "Set `apiBaseUrl` via `kynver setup --api-base-url https://\u2026`.",
|
|
6683
6853
|
details: { apiBaseUrl: apiBaseUrl ?? null }
|
|
6684
6854
|
}),
|
|
6685
|
-
|
|
6855
|
+
check2({
|
|
6686
6856
|
id: "config_agent_os_id",
|
|
6687
6857
|
label: "Default AgentOS id",
|
|
6688
6858
|
status: agentOsId ? "pass" : "warn",
|
|
@@ -6690,7 +6860,7 @@ function assessUserConfig(probes) {
|
|
|
6690
6860
|
remediation: agentOsId ? void 0 : "Set `agentOsId` via `kynver setup --agent-os-id <uuid>`.",
|
|
6691
6861
|
details: { agentOsId: agentOsId ?? null, agentOsSlug: config.agentOsSlug ?? null }
|
|
6692
6862
|
}),
|
|
6693
|
-
|
|
6863
|
+
check2({
|
|
6694
6864
|
id: "config_default_repo",
|
|
6695
6865
|
label: "Default repo path",
|
|
6696
6866
|
status: defaultRepo ? "pass" : "warn",
|
|
@@ -6718,7 +6888,7 @@ function assessRunnerToken(probes) {
|
|
|
6718
6888
|
const scopedSaved = Boolean(savedToken) && (!targetAgentOsId || !savedAgentOsId || savedAgentOsId === targetAgentOsId);
|
|
6719
6889
|
const hasScoped = Boolean(envToken?.startsWith("krc1.")) || scopedSaved && savedToken?.startsWith("krc1.");
|
|
6720
6890
|
const checks = [
|
|
6721
|
-
|
|
6891
|
+
check2({
|
|
6722
6892
|
id: "runner_token_scoped",
|
|
6723
6893
|
label: "Scoped runner token (krc1.*) ready",
|
|
6724
6894
|
status: hasScoped ? "pass" : "fail",
|
|
@@ -6731,7 +6901,7 @@ function assessRunnerToken(probes) {
|
|
|
6731
6901
|
credentialsPath: displayCredPath
|
|
6732
6902
|
}
|
|
6733
6903
|
}),
|
|
6734
|
-
|
|
6904
|
+
check2({
|
|
6735
6905
|
id: "runner_token_agent_os_match",
|
|
6736
6906
|
label: "Saved runner token matches configured agentOsId",
|
|
6737
6907
|
status: !savedToken || !targetAgentOsId || !savedAgentOsId || savedAgentOsId === targetAgentOsId ? "pass" : "warn",
|
|
@@ -6739,7 +6909,7 @@ function assessRunnerToken(probes) {
|
|
|
6739
6909
|
remediation: savedToken && targetAgentOsId && savedAgentOsId && savedAgentOsId !== targetAgentOsId ? "`kynver runner credential --agent-os-id <configured-id>` to mint a workspace-bound token." : void 0,
|
|
6740
6910
|
details: { configuredAgentOsId: targetAgentOsId ?? null, savedAgentOsId: savedAgentOsId ?? null }
|
|
6741
6911
|
}),
|
|
6742
|
-
|
|
6912
|
+
check2({
|
|
6743
6913
|
id: "runner_api_key_for_refresh",
|
|
6744
6914
|
label: "API key available for token refresh",
|
|
6745
6915
|
status: creds.hasApiKey || Boolean(process.env.KYNVER_API_KEY?.trim()) ? "pass" : "warn",
|
|
@@ -6758,7 +6928,7 @@ function assessVercelCli(probes) {
|
|
|
6758
6928
|
id: "vercel_cli",
|
|
6759
6929
|
label: "Vercel CLI",
|
|
6760
6930
|
checks: [
|
|
6761
|
-
|
|
6931
|
+
check2({
|
|
6762
6932
|
id: "vercel_installed",
|
|
6763
6933
|
label: "Vercel CLI installed",
|
|
6764
6934
|
status: installed ? "pass" : "warn",
|
|
@@ -6766,7 +6936,7 @@ function assessVercelCli(probes) {
|
|
|
6766
6936
|
remediation: installed ? void 0 : "Install Vercel CLI (`npm i -g vercel`) for deploy evidence and env pulls.",
|
|
6767
6937
|
details: { stderr: version.stderr || null }
|
|
6768
6938
|
}),
|
|
6769
|
-
|
|
6939
|
+
check2({
|
|
6770
6940
|
id: "vercel_authenticated",
|
|
6771
6941
|
label: "Vercel CLI authenticated",
|
|
6772
6942
|
status: !installed ? "warn" : whoami.ok ? "pass" : "warn",
|
|
@@ -6790,14 +6960,14 @@ function assessHarnessDirs(probes) {
|
|
|
6790
6960
|
id: "harness_dirs",
|
|
6791
6961
|
label: "Harness / daemon directories",
|
|
6792
6962
|
checks: [
|
|
6793
|
-
|
|
6963
|
+
check2({
|
|
6794
6964
|
id: "harness_root",
|
|
6795
6965
|
label: "Harness root resolved",
|
|
6796
6966
|
status: "pass",
|
|
6797
6967
|
summary: displayHarnessRoot,
|
|
6798
6968
|
details: { harnessRoot: displayHarnessRoot }
|
|
6799
6969
|
}),
|
|
6800
|
-
|
|
6970
|
+
check2({
|
|
6801
6971
|
id: "harness_runs_dir",
|
|
6802
6972
|
label: "Runs directory ready",
|
|
6803
6973
|
status: runsExists && probes.pathWritable(runsDir) ? "pass" : runsExists ? "warn" : "warn",
|
|
@@ -6805,7 +6975,7 @@ function assessHarnessDirs(probes) {
|
|
|
6805
6975
|
remediation: runsExists && !probes.pathWritable(runsDir) ? `Fix permissions on ${displayRunsDir} or set KYNVER_HARNESS_ROOT to a writable path.` : void 0,
|
|
6806
6976
|
details: { runsDir: displayRunsDir, exists: runsExists, writable: probes.pathWritable(runsDir) }
|
|
6807
6977
|
}),
|
|
6808
|
-
|
|
6978
|
+
check2({
|
|
6809
6979
|
id: "harness_worktrees_dir",
|
|
6810
6980
|
label: "Worktrees directory ready",
|
|
6811
6981
|
status: worktreesExists && probes.pathWritable(worktreesDir) ? "pass" : "warn",
|
|
@@ -6826,7 +6996,7 @@ function assessCallbackAuth(probes) {
|
|
|
6826
6996
|
const creds = probes.readCredentials();
|
|
6827
6997
|
const savedScoped = creds.runnerTokenPrefix?.startsWith("krc1.");
|
|
6828
6998
|
const checks = [
|
|
6829
|
-
|
|
6999
|
+
check2({
|
|
6830
7000
|
id: "callback_base_url",
|
|
6831
7001
|
label: "Callback base URL configured",
|
|
6832
7002
|
status: baseUrl ? usingLegacyBase ? "warn" : "pass" : "fail",
|
|
@@ -6837,7 +7007,7 @@ function assessCallbackAuth(probes) {
|
|
|
6837
7007
|
baseUrl: baseUrl ?? null
|
|
6838
7008
|
}
|
|
6839
7009
|
}),
|
|
6840
|
-
|
|
7010
|
+
check2({
|
|
6841
7011
|
id: "callback_auth_mode",
|
|
6842
7012
|
label: "Callback auth uses scoped runner token",
|
|
6843
7013
|
status: envScoped || savedScoped ? "pass" : legacySecret ? "warn" : "fail",
|
|
@@ -6853,15 +7023,22 @@ function assessCallbackAuth(probes) {
|
|
|
6853
7023
|
}
|
|
6854
7024
|
function assessOpenclawHotspots(probes) {
|
|
6855
7025
|
const env = probes.envSnapshot();
|
|
7026
|
+
const config = probes.loadConfig();
|
|
7027
|
+
const creds = probes.readCredentials();
|
|
6856
7028
|
const harnessRoot = probes.harnessRoot();
|
|
6857
7029
|
const legacyRoot = probes.legacyOpenclawHarnessRoot();
|
|
6858
7030
|
const displayHarnessRoot = redactHomePath(harnessRoot);
|
|
6859
7031
|
const displayLegacyRoot = redactHomePath(legacyRoot);
|
|
6860
7032
|
const displayOpusHarnessRoot = env.opusHarnessRoot ? redactHomePath(env.opusHarnessRoot) : null;
|
|
6861
7033
|
const legacyHarnessActive = harnessRoot === legacyRoot && probes.pathExists(legacyRoot);
|
|
6862
|
-
const
|
|
7034
|
+
const targetAgentOsId = config.agentOsId?.trim();
|
|
7035
|
+
const envToken = env.kynverRunnerTokenPrefix;
|
|
7036
|
+
const savedToken = creds.runnerTokenPrefix;
|
|
7037
|
+
const savedAgentOsId = creds.runnerTokenAgentOsId;
|
|
7038
|
+
const scopedSaved = Boolean(savedToken) && (!targetAgentOsId || !savedAgentOsId || savedAgentOsId === targetAgentOsId);
|
|
7039
|
+
const hasScopedRunnerToken = Boolean(envToken?.startsWith("krc1.")) || scopedSaved && Boolean(savedToken?.startsWith("krc1."));
|
|
6863
7040
|
const checks = [
|
|
6864
|
-
|
|
7041
|
+
check2({
|
|
6865
7042
|
id: "hotspot_legacy_harness_root",
|
|
6866
7043
|
label: "Legacy ~/.openclaw/harness still active",
|
|
6867
7044
|
status: legacyHarnessActive ? "warn" : "pass",
|
|
@@ -6869,7 +7046,7 @@ function assessOpenclawHotspots(probes) {
|
|
|
6869
7046
|
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,
|
|
6870
7047
|
details: { harnessRoot: displayHarnessRoot, legacyRoot: displayLegacyRoot, opusHarnessRoot: displayOpusHarnessRoot }
|
|
6871
7048
|
}),
|
|
6872
|
-
|
|
7049
|
+
check2({
|
|
6873
7050
|
id: "hotspot_openclaw_env_secrets",
|
|
6874
7051
|
label: "OpenClaw deployment secrets in runner env",
|
|
6875
7052
|
status: env.openclawCronSecret || env.openclawCronFireBaseUrl ? "warn" : "pass",
|
|
@@ -6879,15 +7056,12 @@ function assessOpenclawHotspots(probes) {
|
|
|
6879
7056
|
].filter(Boolean).join("; ") : "No OpenClaw cron env overrides on this runner",
|
|
6880
7057
|
remediation: env.openclawCronSecret || env.openclawCronFireBaseUrl ? "Move to KYNVER_API_URL + scoped runner tokens; unset OpenClaw cron env on user-hosted runners." : void 0
|
|
6881
7058
|
}),
|
|
6882
|
-
|
|
6883
|
-
|
|
6884
|
-
|
|
6885
|
-
|
|
6886
|
-
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}`,
|
|
6887
|
-
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,
|
|
6888
|
-
details: { schedulerProvider: env.kynverSchedulerProvider ?? null }
|
|
7059
|
+
assessRuntimeTakeoverScheduler(env, {
|
|
7060
|
+
agentOsId: targetAgentOsId ?? null,
|
|
7061
|
+
apiBaseUrl: config.apiBaseUrl?.trim() ?? env.kynverApiUrl ?? null,
|
|
7062
|
+
hasScopedRunnerToken
|
|
6889
7063
|
}),
|
|
6890
|
-
|
|
7064
|
+
check2({
|
|
6891
7065
|
id: "hotspot_lease_source_names",
|
|
6892
7066
|
label: "Harness lease/completion source names",
|
|
6893
7067
|
status: "pass",
|