@kynver-app/runtime 0.1.108 → 0.1.112

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -214,7 +214,11 @@ function scrubClaudeEnv(env) {
214
214
 
215
215
  // src/git.ts
216
216
  function git(cwd, args, options = {}) {
217
- const res = spawnSync("git", args, { cwd, encoding: "utf8" });
217
+ const res = spawnSync(
218
+ "git",
219
+ args,
220
+ hiddenSpawnOptions({ cwd, encoding: "utf8" })
221
+ );
218
222
  if (res.status !== 0 && !options.allowFailure) {
219
223
  const message = `git ${args.join(" ")} failed: ${res.stderr || res.stdout}`;
220
224
  if (options.throwError) throw new Error(message);
@@ -230,7 +234,11 @@ function gitStatusShort(worktreePath) {
230
234
  }
231
235
  function gitCapture(cwd, args) {
232
236
  try {
233
- const res = spawnSync("git", args, { cwd, encoding: "utf8" });
237
+ const res = spawnSync(
238
+ "git",
239
+ args,
240
+ hiddenSpawnOptions({ cwd, encoding: "utf8" })
241
+ );
234
242
  return {
235
243
  status: res.status,
236
244
  stdout: res.stdout || "",
@@ -6049,32 +6057,98 @@ function workerStatus(args) {
6049
6057
  writeJson(path21.join(worker.workerDir, "last-status.json"), status);
6050
6058
  console.log(JSON.stringify(status, null, 2));
6051
6059
  }
6060
+ function buildWorkerListDrilldownCommands(runId) {
6061
+ return {
6062
+ full: `kynver worker list --run ${runId} --full`,
6063
+ blocked: `kynver worker list --run ${runId} --blocked`,
6064
+ running: `kynver worker list --run ${runId} --running`,
6065
+ task: `kynver worker list --run ${runId} --task <task-id>`,
6066
+ worker: `kynver worker list --run ${runId} --worker <worker>`,
6067
+ runFull: `kynver run status --run ${runId} --full`,
6068
+ workerFull: `kynver worker status --run ${runId} --name <worker>`,
6069
+ workerTail: `kynver worker tail --run ${runId} --name <worker> --lines 80`
6070
+ };
6071
+ }
6072
+ function asOptionalArg(value) {
6073
+ if (typeof value !== "string") return void 0;
6074
+ const trimmed = value.trim();
6075
+ return trimmed.length ? trimmed : void 0;
6076
+ }
6077
+ function activeWorkerFilters(args) {
6078
+ const filters = {};
6079
+ if (args.blocked === true || args.blocked === "true") filters.blocked = true;
6080
+ if (args.running === true || args.running === "true") filters.running = true;
6081
+ const task = asOptionalArg(args.task ?? args.taskId);
6082
+ if (task) filters.task = task;
6083
+ const worker = asOptionalArg(args.worker ?? args.name);
6084
+ if (worker) filters.worker = worker;
6085
+ const status = asOptionalArg(args.status);
6086
+ if (status) filters.status = status;
6087
+ return filters;
6088
+ }
6089
+ function workerMatchesFilters(worker, filters) {
6090
+ if (filters.blocked) {
6091
+ const attention = typeof worker.attention === "string" ? worker.attention : "";
6092
+ const status = typeof worker.status === "string" ? worker.status : "";
6093
+ if (!(attention === "blocked" || attention === "needs_attention" || attention === "stale" || status === "blocked")) {
6094
+ return false;
6095
+ }
6096
+ }
6097
+ if (filters.running) {
6098
+ if (!(worker.status === "running" && worker.attention !== "blocked" && worker.attention !== "needs_attention")) {
6099
+ return false;
6100
+ }
6101
+ }
6102
+ if (typeof filters.task === "string" && worker.taskId !== filters.task) return false;
6103
+ if (typeof filters.worker === "string" && worker.worker !== filters.worker) return false;
6104
+ if (typeof filters.status === "string" && worker.status !== filters.status) return false;
6105
+ return true;
6106
+ }
6107
+ function applyWorkerFilters(board, args) {
6108
+ const filters = activeWorkerFilters(args);
6109
+ if (Object.keys(filters).length === 0) return board;
6110
+ const workers = board.workers.filter((worker) => workerMatchesFilters(worker, filters));
6111
+ const filtered = {
6112
+ ...board,
6113
+ workerCount: workers.length,
6114
+ needsAttention: workers.filter((w) => w.attention && w.attention !== "ok" && w.attention !== "done").map((w) => w.worker),
6115
+ workers,
6116
+ activeFilters: filters
6117
+ };
6118
+ if (board.summary) {
6119
+ filtered.summary = {
6120
+ statusCounts: countBy(workers, "status"),
6121
+ attentionCounts: countBy(workers, "attention"),
6122
+ lifecycleCounts: countBy(workers, "lifecycleStage")
6123
+ };
6124
+ }
6125
+ if (board.controller) filtered.controller = buildControllerSummary(workers);
6126
+ return filtered;
6127
+ }
6052
6128
  function workerList(args) {
6053
6129
  const runId = resolveRunTargetArg(args);
6054
- const explicitCompact = args.compact === true || args.compact === "true";
6055
6130
  const explicitFull = args.full === true || args.full === "true";
6056
- const run = loadRun(runId);
6057
- const workerCount = Object.keys(run.workers || {}).length;
6058
- const compact = explicitCompact || !explicitFull && workerCount > 100;
6059
- const board = compact ? buildCompactRunBoard(runId) : buildRunBoard(runId);
6060
- const autoCompact = compact && !explicitCompact;
6061
- const outputWorkers = autoCompact ? board.workers.filter((worker) => worker.attention && worker.attention !== "done" && worker.attention !== "ok") : board.workers;
6131
+ const board = applyWorkerFilters(explicitFull ? buildRunBoard(runId) : buildCompactRunBoard(runId), args);
6132
+ const outputWorkers = board.workers;
6062
6133
  console.log(
6063
6134
  JSON.stringify(
6064
6135
  {
6065
6136
  runId: board.runId,
6066
6137
  status: board.status,
6138
+ projection: explicitFull ? "full" : "compact",
6067
6139
  workerCount: board.workerCount,
6068
- ...autoCompact ? {
6069
- compact: true,
6070
- compactReason: "large-worker-board",
6071
- omittedWorkerCount: board.workers.length - outputWorkers.length,
6072
- fullCommand: `kynver worker list --run ${runId} --json --full`,
6073
- compactCommand: `kynver worker list --run ${runId} --json --compact`
6140
+ ...board.activeFilters ? { activeFilters: board.activeFilters } : {},
6141
+ ...!explicitFull ? {
6142
+ resultContract: {
6143
+ projection: "compact",
6144
+ rawWorkerPayloadsOmitted: true,
6145
+ deepDetailAvailableVia: ["--full", "worker status", "worker tail", "run status --full"]
6146
+ },
6147
+ drilldownCommands: buildWorkerListDrilldownCommands(runId)
6074
6148
  } : {},
6075
6149
  needsAttention: board.needsAttention,
6076
- ...compact && "summary" in board ? { summary: board.summary } : {},
6077
- ...compact && "controller" in board ? { controller: board.controller } : {},
6150
+ ..."summary" in board ? { summary: board.summary } : {},
6151
+ ..."controller" in board ? { controller: board.controller } : {},
6078
6152
  workers: outputWorkers
6079
6153
  },
6080
6154
  null,
@@ -6342,9 +6416,33 @@ async function publishHarnessBoardSnapshot(args, source) {
6342
6416
  authRefreshFailure: res.authRefreshFailure
6343
6417
  };
6344
6418
  }
6419
+ function buildRunStatusDrilldownCommands(runId) {
6420
+ return {
6421
+ full: `kynver run status --run ${runId} --full`,
6422
+ blocked: `kynver status --run ${runId} --blocked`,
6423
+ running: `kynver status --run ${runId} --running`,
6424
+ task: `kynver status --run ${runId} --task <task-id>`,
6425
+ worker: `kynver status --run ${runId} --worker <worker>`,
6426
+ workersFull: `kynver worker list --run ${runId} --full`,
6427
+ workerFull: `kynver worker status --run ${runId} --name <worker>`,
6428
+ workerTail: `kynver worker tail --run ${runId} --name <worker> --lines 80`,
6429
+ monitorTick: `kynver monitor status --run ${runId} --tick`
6430
+ };
6431
+ }
6345
6432
  function runStatus(args) {
6346
- const compact = args.compact === true || args.compact === "true";
6347
- const board = compact ? buildCompactRunBoard(resolveRunTargetArg(args)) : buildRunBoard(resolveRunTargetArg(args));
6433
+ const runId = resolveRunTargetArg(args);
6434
+ const explicitFull = args.full === true || args.full === "true";
6435
+ const board = applyWorkerFilters(explicitFull ? buildRunBoard(runId) : buildCompactRunBoard(runId), args);
6436
+ board.projection = explicitFull ? "full" : "compact";
6437
+ if (!explicitFull) {
6438
+ board.resultContract = {
6439
+ projection: "compact",
6440
+ rawWorkerPayloadsOmitted: true,
6441
+ omittedWorkerFields: ["finalResult", "changedFiles", "gitAncestry", "completionResponse", "stdout/stderr tails"],
6442
+ deepDetailAvailableVia: ["--full", "worker status", "worker tail", "monitor status --tick"]
6443
+ };
6444
+ board.drilldownCommands = buildRunStatusDrilldownCommands(runId);
6445
+ }
6348
6446
  console.log(JSON.stringify(board, null, 2));
6349
6447
  }
6350
6448
  function tailWorker(args) {
@@ -7482,6 +7580,10 @@ var READY_MERGE_STATES = /* @__PURE__ */ new Set(["CLEAN", "HAS_HOOKS"]);
7482
7580
  function repoArgs(repo) {
7483
7581
  return repo?.trim() ? ["--repo", repo.trim()] : [];
7484
7582
  }
7583
+ function repoFromPrUrl(prUrl) {
7584
+ const m = /github\.com\/([^/]+)\/([^/]+)\/pull\/\d+/i.exec(prUrl.trim());
7585
+ return m ? `${m[1]}/${m[2]}` : null;
7586
+ }
7485
7587
  function ghJson(exec, cwd, args) {
7486
7588
  const res = exec.gh(cwd, args);
7487
7589
  if (res.status !== 0) {
@@ -7537,14 +7639,21 @@ var STRUCTURED_SECTION_RE = /^##\s*(test\s*plan|verification|test\s*evidence|ver
7537
7639
  var LOCAL_VERIFICATION_RE = /typecheck|npm run (test|build|typecheck)|vitest|tsc\b|verify-pr-local|local verify|local-verify|tests? (pass|green)|build green|node --test/i;
7538
7640
  var DOCS_TITLE_RE = /^docs[(:]/i;
7539
7641
  var DOCS_BODY_RE = /docs[- ]only|documentation only|no[- ]code change|no code changes|low[- ]risk|plan tracker only|markdown only/i;
7540
- function assertVerificationEvidence(pr) {
7541
- const title = typeof pr.title === "string" ? pr.title : "";
7542
- const body = typeof pr.body === "string" ? pr.body : "";
7642
+ function runtimeVerificationEvidenceSufficient(input) {
7643
+ const title = typeof input.title === "string" ? input.title : "";
7644
+ const body = typeof input.body === "string" ? input.body : "";
7543
7645
  const docsLowRisk = DOCS_TITLE_RE.test(title) || DOCS_BODY_RE.test(body);
7544
7646
  const structuredSection = STRUCTURED_SECTION_RE.test(body);
7545
7647
  const localVerification = LOCAL_VERIFICATION_RE.test(body);
7546
- const vercelOk = vercelCheckSuccess(pr.statusCheckRollup);
7547
- if (docsLowRisk || structuredSection || localVerification || vercelOk) return;
7648
+ return docsLowRisk || structuredSection || localVerification || input.vercelCheckSuccess === true;
7649
+ }
7650
+ function assertVerificationEvidence(pr) {
7651
+ const sufficient = runtimeVerificationEvidenceSufficient({
7652
+ title: pr.title,
7653
+ body: pr.body,
7654
+ vercelCheckSuccess: vercelCheckSuccess(pr.statusCheckRollup)
7655
+ });
7656
+ if (sufficient) return;
7548
7657
  throw new Error(
7549
7658
  `PR #${pr.number} lacks landing verification evidence \u2014 add ## Test plan / ## Verification with local commands, Vercel preview, or docs/no-code rationale`
7550
7659
  );
@@ -7645,6 +7754,14 @@ async function executeLandPrMerge(input) {
7645
7754
  "mergeCommit"
7646
7755
  ].join(",")
7647
7756
  ]);
7757
+ if (before.state === "MERGED" || before.mergedAt) {
7758
+ return {
7759
+ prUrl: before.url || prTarget,
7760
+ outcome: "skipped",
7761
+ mergeCommit: before.mergeCommit?.oid ?? null,
7762
+ reason: `PR #${before.number} is already merged \u2014 land_pr no-op (redelivery)`
7763
+ };
7764
+ }
7648
7765
  const notReadyReason = landingReadinessError(before);
7649
7766
  if (notReadyReason) {
7650
7767
  if (input.skipNotReady) {
@@ -7999,7 +8116,11 @@ async function dispatchRun(args) {
7999
8116
  return abortClaimedSpawn(task, "land_pr task missing prUrl");
8000
8117
  }
8001
8118
  try {
8002
- const report = await executeLandPrMerge({ prUrl, cwd: process.cwd() });
8119
+ const report = await executeLandPrMerge({
8120
+ prUrl,
8121
+ repo: repoFromPrUrl(prUrl),
8122
+ cwd: run.repo
8123
+ });
8003
8124
  const post = await postLandPrHarnessCompletion({
8004
8125
  baseUrl: base,
8005
8126
  secret,
@@ -8026,6 +8147,27 @@ async function dispatchRun(args) {
8026
8147
  return abortClaimedSpawn(task, error.message);
8027
8148
  }
8028
8149
  }
8150
+ async function releaseDuplicateLocalClaim(task) {
8151
+ const error = "duplicate_dispatch_prevented: live local worker already owns this task";
8152
+ const release = await releaseDispatchClaimAfterSpawnFailure({
8153
+ baseUrl: base,
8154
+ secret,
8155
+ agentOsId,
8156
+ taskId: String(task.id),
8157
+ leaseOwner,
8158
+ failureDetail: error
8159
+ });
8160
+ outcomes.push({
8161
+ taskId: task.id,
8162
+ started: false,
8163
+ error,
8164
+ alreadyRunning: true,
8165
+ nonFatal: true,
8166
+ released: release.released,
8167
+ releaseResponse: release.releaseResponse
8168
+ });
8169
+ return false;
8170
+ }
8029
8171
  async function spawnClaimed(decision) {
8030
8172
  const task = decision.task;
8031
8173
  const harnessContext = readHarnessWorkerContext(decision);
@@ -8047,10 +8189,7 @@ async function dispatchRun(args) {
8047
8189
  );
8048
8190
  }
8049
8191
  if (hasLiveWorkerForTask(run.id, taskId)) {
8050
- return abortClaimedSpawn(
8051
- task,
8052
- "duplicate_dispatch_prevented: live local worker already owns this task"
8053
- );
8192
+ return releaseDuplicateLocalClaim(task);
8054
8193
  }
8055
8194
  const attempt = Number(task.attempt) || 1;
8056
8195
  if (attempt > retryLimits.maxTaskAttempts) {
@@ -8220,11 +8359,12 @@ async function dispatchRun(args) {
8220
8359
  diskGate: result.diskGate,
8221
8360
  resourceGate: result.resourceGate
8222
8361
  };
8362
+ const fatalOutcome = (outcome) => !outcome.started && outcome.nonFatal !== true;
8223
8363
  if (pipeline) {
8224
- return { ok: !outcomes.some((o) => !o.started), ...summary };
8364
+ return { ok: !outcomes.some(fatalOutcome), ...summary };
8225
8365
  }
8226
8366
  console.log(JSON.stringify(summary, null, 2));
8227
- if (outcomes.some((o) => !o.started)) process.exit(1);
8367
+ if (outcomes.some(fatalOutcome)) process.exit(1);
8228
8368
  } catch (error) {
8229
8369
  if (pipeline) return { ok: false, error: error.message };
8230
8370
  console.error(`run dispatch failed: ${error.message}`);
@@ -10151,23 +10291,6 @@ function materialWorktreeChanges2(changedFiles) {
10151
10291
  });
10152
10292
  }
10153
10293
 
10154
- // src/cleanup-index-status.ts
10155
- function indexedWorktreeStatus(entry) {
10156
- if (!entry.status) {
10157
- entry.status = computeWorkerStatus(entry.worker, {
10158
- base: entry.run.base,
10159
- baseCommit: entry.run.baseCommit
10160
- });
10161
- }
10162
- return entry.status;
10163
- }
10164
- function indexedWorktreeHasMaterialChanges(entry) {
10165
- if (entry.status) {
10166
- return materialWorktreeChanges2(entry.status.changedFiles).length > 0;
10167
- }
10168
- return materialWorktreeChanges2(gitStatusShort(entry.worktreePath)).length > 0;
10169
- }
10170
-
10171
10294
  // src/cleanup-worktree-salvage.ts
10172
10295
  function prUrlFromFinalResult(finalResult) {
10173
10296
  if (typeof finalResult === "string") {
@@ -10194,6 +10317,83 @@ function isPrOrUnmergedWork(status) {
10194
10317
  return false;
10195
10318
  }
10196
10319
 
10320
+ // src/cleanup-index-status.ts
10321
+ function indexedWorktreeStatus(entry) {
10322
+ if (!entry.status) {
10323
+ entry.status = computeWorkerStatus(entry.worker, {
10324
+ base: entry.run.base,
10325
+ baseCommit: entry.run.baseCommit
10326
+ });
10327
+ }
10328
+ return entry.status;
10329
+ }
10330
+ function indexedWorktreeHasMaterialChanges(entry, gitStatusCache) {
10331
+ if (entry.status) {
10332
+ return materialWorktreeChanges2(entry.status.changedFiles).length > 0;
10333
+ }
10334
+ const porcelain = gitStatusCache ? gitStatusCache.porcelain(entry.worktreePath) : gitStatusShort(entry.worktreePath);
10335
+ return materialWorktreeChanges2(porcelain).length > 0;
10336
+ }
10337
+ function finalResultFromWorkerJson(entry) {
10338
+ const snapshot = entry.worker.completionSnapshot?.finalResult;
10339
+ if (snapshot !== void 0 && snapshot !== null) return snapshot;
10340
+ if (entry.worker.taskPrUrl) {
10341
+ return { prUrl: entry.worker.taskPrUrl };
10342
+ }
10343
+ return null;
10344
+ }
10345
+ function resolveWorktreeGuardStatus(entry, ctx) {
10346
+ if (entry.status) return entry.status;
10347
+ const worker = entry.worker;
10348
+ const completionAcknowledged = typeof worker.completionReportedAt === "string" && worker.completionReportedAt.trim().length > 0;
10349
+ const workerJsonTerminal = Boolean(worker.status && ["done", "exited", "blocked", "failed", "abandoned"].includes(worker.status)) || completionAcknowledged;
10350
+ const finalResult = finalResultFromWorkerJson(entry);
10351
+ if (workerJsonTerminal && !isPidAlive(worker.pid)) {
10352
+ const changedFiles = ctx?.gitStatusCache ? ctx.gitStatusCache.porcelain(entry.worktreePath) : gitStatusShort(entry.worktreePath);
10353
+ const baseLabel = entry.run.baseCommit?.trim() || entry.run.base?.trim() || "origin/main";
10354
+ const ahead = ctx?.gitRevCache?.countAheadOfMain(entry.worktreePath, baseLabel);
10355
+ const gitAncestry = ahead === 0 ? {
10356
+ checked: true,
10357
+ base: baseLabel,
10358
+ relation: "synced"
10359
+ } : computeGitAncestry(entry.worktreePath, {
10360
+ base: entry.run.base,
10361
+ baseCommit: entry.run.baseCommit
10362
+ });
10363
+ const status = {
10364
+ runId: entry.runId,
10365
+ worker: entry.workerName,
10366
+ pid: worker.pid,
10367
+ alive: false,
10368
+ status: worker.status ?? (completionAcknowledged ? "done" : "exited"),
10369
+ attention: { state: completionAcknowledged ? "done" : "stale" },
10370
+ branch: worker.branch,
10371
+ worktreePath: entry.worktreePath,
10372
+ ownedPaths: worker.ownedPaths,
10373
+ stdoutBytes: 0,
10374
+ stderrBytes: 0,
10375
+ heartbeatBytes: 0,
10376
+ firstEventAt: null,
10377
+ lastEventAt: null,
10378
+ lastActivityAt: worker.completionReportedAt ?? null,
10379
+ currentTool: null,
10380
+ heartbeatCount: 0,
10381
+ lastHeartbeatAt: null,
10382
+ lastHeartbeatPhase: null,
10383
+ lastHeartbeatSummary: null,
10384
+ heartbeatBlocker: null,
10385
+ changedFiles,
10386
+ gitAncestry,
10387
+ finalResult,
10388
+ completionBlocker: typeof worker.completionBlocker === "string" ? worker.completionBlocker.trim() || null : null,
10389
+ prUrl: worker.repairTargetPrUrl ?? worker.taskPrUrl ?? prUrlFromFinalResult(finalResult)
10390
+ };
10391
+ entry.status = status;
10392
+ return status;
10393
+ }
10394
+ return indexedWorktreeStatus(entry);
10395
+ }
10396
+
10197
10397
  // src/cleanup-completion-blocker.ts
10198
10398
  function completionBlockerBlocksWorktreeRemoval(indexed, status) {
10199
10399
  const blocker = typeof indexed.worker.completionBlocker === "string" ? indexed.worker.completionBlocker.trim() : "";
@@ -10214,13 +10414,28 @@ function completionBlockerBlocksWorktreeRemoval(indexed, status) {
10214
10414
  }
10215
10415
 
10216
10416
  // src/cleanup-run-liveness.ts
10417
+ var TERMINAL_WORKER_JSON_STATUSES = /* @__PURE__ */ new Set([
10418
+ "done",
10419
+ "exited",
10420
+ "blocked",
10421
+ "failed",
10422
+ "abandoned"
10423
+ ]);
10217
10424
  function deriveRunTerminal(indexed, ctx) {
10218
10425
  if (ctx) return ctx.runTerminalCache.derive(indexed.run);
10219
10426
  return deriveTerminalRunStatus(indexed.run);
10220
10427
  }
10221
10428
  function isWorkerProcessLive(indexed) {
10222
10429
  if (isPidAlive(indexed.worker.pid)) return true;
10223
- if (!indexed.worker.pid) return indexedWorktreeStatus(indexed).alive;
10430
+ if (typeof indexed.worker.completionReportedAt === "string" && indexed.worker.completionReportedAt.trim().length > 0) {
10431
+ return false;
10432
+ }
10433
+ const workerStatus2 = indexed.worker.status;
10434
+ if (workerStatus2 && TERMINAL_WORKER_JSON_STATUSES.has(workerStatus2)) return false;
10435
+ if (!indexed.worker.pid) {
10436
+ if (workerStatus2 !== "running") return false;
10437
+ return indexedWorktreeStatus(indexed).alive;
10438
+ }
10224
10439
  return false;
10225
10440
  }
10226
10441
  function isRunStaleActive(indexed, ctx) {
@@ -10229,6 +10444,10 @@ function isRunStaleActive(indexed, ctx) {
10229
10444
  }
10230
10445
  function runBlocksWorktreeRemoval(indexed, ctx) {
10231
10446
  if (isWorkerProcessLive(indexed)) return true;
10447
+ const workerStatus2 = indexed.worker.status;
10448
+ if (workerStatus2 && TERMINAL_WORKER_JSON_STATUSES.has(workerStatus2) && !indexed.worker.completionBlocker) {
10449
+ return false;
10450
+ }
10232
10451
  const status = indexedWorktreeStatus(indexed);
10233
10452
  if (completionBlockerBlocksWorktreeRemoval(indexed, status)) return true;
10234
10453
  if (isFinishedWorkerStatus(status)) return false;
@@ -10247,7 +10466,7 @@ function effectiveWorktreeAgeMs(input) {
10247
10466
  if (input.liveness && isRunStaleActive(indexed, input.liveness)) {
10248
10467
  return terminalWorktreesAgeMs;
10249
10468
  }
10250
- if (input.liveness && isFinishedWorkerStatus(indexedWorktreeStatus(indexed)) && !isWorkerProcessLive(indexed)) {
10469
+ if (input.liveness && isFinishedWorkerStatus(resolveWorktreeGuardStatus(indexed, input.liveness)) && !isWorkerProcessLive(indexed)) {
10251
10470
  return terminalWorktreesAgeMs;
10252
10471
  }
10253
10472
  return worktreesAgeMs;
@@ -10261,8 +10480,13 @@ function skipWorktreeRemoval(input) {
10261
10480
  const ageThresholdMs = effectiveWorktreeAgeMs(input);
10262
10481
  if (worktreesAgeMs <= 0 && !includeOrphans && ageThresholdMs <= 0) return "worktrees_disabled";
10263
10482
  if (ageThresholdMs > 0 && ageMs < ageThresholdMs) return "below_age_threshold";
10264
- const status = indexedWorktreeStatus(indexed);
10265
10483
  if (isWorkerProcessLive(indexed)) return "active_worker";
10484
+ if (indexedWorktreeHasMaterialChanges(indexed, input.liveness?.gitStatusCache)) {
10485
+ return "dirty_worktree";
10486
+ }
10487
+ const ahead = input.liveness?.gitRevCache?.countAheadOfMain(input.worktreePath);
10488
+ if (ahead !== null && ahead !== void 0 && ahead > 0) return "pr_or_unmerged_commits";
10489
+ const status = resolveWorktreeGuardStatus(indexed, input.liveness);
10266
10490
  if (completionBlockerBlocksWorktreeRemoval(indexed, status)) return "completion_blocked";
10267
10491
  if (runBlocksWorktreeRemoval(indexed, input.liveness)) return "run_still_active";
10268
10492
  if (!isFinishedWorkerStatus(status)) return "run_still_active";
@@ -10293,7 +10517,9 @@ function skipDependencyCacheRemoval(input) {
10293
10517
  if (!diskPressure && ageMs < nodeModulesAgeMs) return "below_age_threshold";
10294
10518
  if (activeWorktreePaths.has(path42.resolve(worktreePath))) return "active_worker";
10295
10519
  if (indexed && isWorkerProcessLive(indexed)) return "active_worker";
10296
- if (indexed && indexedWorktreeHasMaterialChanges(indexed)) return "dirty_worktree";
10520
+ if (indexed && indexedWorktreeHasMaterialChanges(indexed, input.gitStatusCache)) {
10521
+ return "dirty_worktree";
10522
+ }
10297
10523
  return null;
10298
10524
  }
10299
10525
  function skipBuildCacheRemoval(input) {
@@ -11276,6 +11502,37 @@ function emitCleanupProgress(phase, detail) {
11276
11502
  console.error(`[kynver cleanup] ${phase}${suffix}`);
11277
11503
  }
11278
11504
 
11505
+ // src/cleanup-git-rev-cache.ts
11506
+ var CleanupGitRevCache = class {
11507
+ aheadOfMain = /* @__PURE__ */ new Map();
11508
+ countAheadOfMain(worktreePath, base = "origin/main") {
11509
+ const key = `${worktreePath}\0${base}`;
11510
+ if (this.aheadOfMain.has(key)) return this.aheadOfMain.get(key) ?? null;
11511
+ const result = gitCapture(worktreePath, ["rev-list", "--count", `${base}..HEAD`]);
11512
+ if (result.status !== 0) {
11513
+ this.aheadOfMain.set(key, null);
11514
+ return null;
11515
+ }
11516
+ const count = Number(result.stdout.trim());
11517
+ const parsed = Number.isFinite(count) ? count : null;
11518
+ this.aheadOfMain.set(key, parsed);
11519
+ return parsed;
11520
+ }
11521
+ };
11522
+
11523
+ // src/cleanup-git-status-cache.ts
11524
+ var CleanupGitStatusCache = class {
11525
+ cache = /* @__PURE__ */ new Map();
11526
+ porcelain(worktreePath) {
11527
+ const resolved = worktreePath;
11528
+ const cached = this.cache.get(resolved);
11529
+ if (cached !== void 0) return cached;
11530
+ const lines = gitStatusShort(resolved);
11531
+ this.cache.set(resolved, lines);
11532
+ return lines;
11533
+ }
11534
+ };
11535
+
11279
11536
  // src/cleanup-run-terminal-cache.ts
11280
11537
  var CleanupRunTerminalCache = class {
11281
11538
  cache = /* @__PURE__ */ new Map();
@@ -11393,7 +11650,11 @@ function runHarnessCleanup(options = {}) {
11393
11650
  emitCleanupProgress("index", "building worktree index");
11394
11651
  const index = mergeWorktreeIndexes(paths.scanRoots);
11395
11652
  emitCleanupProgress("index", `${index.size} indexed worktree(s)`);
11396
- const liveness = { runTerminalCache: new CleanupRunTerminalCache() };
11653
+ const liveness = {
11654
+ runTerminalCache: new CleanupRunTerminalCache(),
11655
+ gitStatusCache: new CleanupGitStatusCache(),
11656
+ gitRevCache: new CleanupGitRevCache()
11657
+ };
11397
11658
  const skips = [];
11398
11659
  const actions = [];
11399
11660
  const processedPaths = /* @__PURE__ */ new Set();
@@ -11413,8 +11674,15 @@ function runHarnessCleanup(options = {}) {
11413
11674
  index,
11414
11675
  now: paths.now
11415
11676
  };
11416
- for (const raw of scanDependencyCacheCandidates(scanOpts)) {
11677
+ const dependencyCandidates = scanDependencyCacheCandidates(scanOpts);
11678
+ emitCleanupProgress("dependency", `${dependencyCandidates.length} cache candidate(s) at ${harnessRoot}`);
11679
+ let dependencyProcessed = 0;
11680
+ for (const raw of dependencyCandidates) {
11417
11681
  if (atSweepCap()) break;
11682
+ dependencyProcessed += 1;
11683
+ if (dependencyProcessed % 50 === 0) {
11684
+ emitCleanupProgress("dependency", `${dependencyProcessed}/${dependencyCandidates.length} evaluated`);
11685
+ }
11418
11686
  const resolved = path53.resolve(raw.path);
11419
11687
  if (processedPaths.has(resolved)) continue;
11420
11688
  processedPaths.add(resolved);
@@ -11434,7 +11702,8 @@ function runHarnessCleanup(options = {}) {
11434
11702
  ageMs: candidate.ageMs,
11435
11703
  worktreePath,
11436
11704
  activeWorktreePaths: activeGuards.activeWorktreePaths,
11437
- diskPressure: retention.diskPressure
11705
+ diskPressure: retention.diskPressure,
11706
+ gitStatusCache: liveness.gitStatusCache
11438
11707
  });
11439
11708
  if (guardReason) {
11440
11709
  recordSkip(skips, candidate.path, guardReason);
@@ -11469,7 +11738,8 @@ function runHarnessCleanup(options = {}) {
11469
11738
  ageMs: candidate.ageMs,
11470
11739
  worktreePath,
11471
11740
  activeWorktreePaths: activeGuards.activeWorktreePaths,
11472
- diskPressure: retention.diskPressure
11741
+ diskPressure: retention.diskPressure,
11742
+ gitStatusCache: liveness.gitStatusCache
11473
11743
  });
11474
11744
  if (guardReason) {
11475
11745
  recordSkip(skips, candidate.path, guardReason);
@@ -11489,8 +11759,13 @@ function runHarnessCleanup(options = {}) {
11489
11759
  ];
11490
11760
  emitCleanupProgress("worktrees", `${worktreeCandidates.length} candidate(s) at ${harnessRoot}`);
11491
11761
  const worktreeSeen = /* @__PURE__ */ new Set();
11762
+ let worktreeProcessed = 0;
11492
11763
  for (const raw of worktreeCandidates) {
11493
11764
  if (atSweepCap()) break;
11765
+ worktreeProcessed += 1;
11766
+ if (worktreeProcessed % 50 === 0) {
11767
+ emitCleanupProgress("worktrees", `${worktreeProcessed}/${worktreeCandidates.length} evaluated`);
11768
+ }
11494
11769
  const resolved = path53.resolve(raw.path);
11495
11770
  if (worktreeSeen.has(resolved)) continue;
11496
11771
  worktreeSeen.add(resolved);
@@ -11771,11 +12046,29 @@ function validateDaemonInstallIdentity(config = loadUserConfig(), env = process.
11771
12046
  };
11772
12047
  }
11773
12048
 
12049
+ // src/daemon-platform-guard.ts
12050
+ function envFlag3(name) {
12051
+ const raw = process.env[name]?.trim().toLowerCase();
12052
+ return raw === "1" || raw === "true" || raw === "yes" || raw === "on";
12053
+ }
12054
+ function assertNativeDaemonAllowed() {
12055
+ if (process.platform !== "win32") return;
12056
+ if (envFlag3("KYNVER_DAEMON_ALLOW_NATIVE_WINDOWS")) return;
12057
+ console.error(
12058
+ JSON.stringify({
12059
+ event: "daemon_start_blocked",
12060
+ reason: "native_windows_console_flash",
12061
+ remedy: "Run the daemon inside WSL: .\\scripts\\start-tier2-wsl.ps1 \u2014 or set KYNVER_DAEMON_ALLOW_NATIVE_WINDOWS=1 to override (flashes visible consoles)."
12062
+ })
12063
+ );
12064
+ process.exit(1);
12065
+ }
12066
+
11774
12067
  // src/cron/cron-env.ts
11775
12068
  import { existsSync as existsSync42 } from "node:fs";
11776
12069
  import { homedir as homedir14 } from "node:os";
11777
12070
  import path55 from "node:path";
11778
- function envFlag3(name, defaultValue) {
12071
+ function envFlag4(name, defaultValue) {
11779
12072
  const raw = process.env[name]?.trim().toLowerCase();
11780
12073
  if (!raw) return defaultValue;
11781
12074
  if (raw === "0" || raw === "false" || raw === "no" || raw === "off") return false;
@@ -11811,14 +12104,14 @@ function resolveKynverCronEnv() {
11811
12104
  const secret = resolveKynverCronSecret();
11812
12105
  const credsReady = Boolean(fireBaseUrl && secret);
11813
12106
  const storeExists = existsSync42(storePath);
11814
- const defaultEnabled = credsReady && (storeExists || envFlag3("KYNVER_CRON_TICK_FORCE", false));
12107
+ const defaultEnabled = credsReady && (storeExists || envFlag4("KYNVER_CRON_TICK_FORCE", false));
11815
12108
  return {
11816
12109
  storePath,
11817
12110
  statePath,
11818
12111
  lockPath: `${statePath}.lock`,
11819
12112
  fireBaseUrl,
11820
12113
  secret,
11821
- tickEnabled: envFlag3("KYNVER_CRON_TICK_ENABLED", defaultEnabled),
12114
+ tickEnabled: envFlag4("KYNVER_CRON_TICK_ENABLED", defaultEnabled),
11822
12115
  tickIntervalMs: envInt("KYNVER_CRON_TICK_INTERVAL_MS", 6e4, 5e3),
11823
12116
  missedRunPolicy: process.env.KYNVER_CRON_MISSED_RUN_POLICY?.trim().toLowerCase() === "skip" ? "skip" : "catch_up",
11824
12117
  maxCatchUpPerTick: envInt("KYNVER_CRON_MAX_CATCH_UP_PER_TICK", 3, 0),
@@ -12610,6 +12903,7 @@ async function awaitDaemonBackoff(ms, isStopping) {
12610
12903
  }
12611
12904
  }
12612
12905
  async function runDaemon(args) {
12906
+ assertNativeDaemonAllowed();
12613
12907
  const runId = String(required(String(args.run || ""), "--run"));
12614
12908
  const agentOsId = String(required(String(args.agentOsId || loadUserConfig().agentOsId || ""), "--agent-os-id"));
12615
12909
  const execute = args.execute !== false && args.execute !== "false";
@@ -14713,12 +15007,12 @@ function assessRuntimeTakeoverScheduler(env, ctx) {
14713
15007
  return check({
14714
15008
  id: "hotspot_openclaw_scheduler",
14715
15009
  label: "Scheduler provider (runtime daemon vs OpenClaw cron)",
14716
- status: "pass",
14717
- summary: "KYNVER_SCHEDULER_PROVIDER unset on hosted deployment; scheduler resolves to kynver-cron (Kynver-owned, not OpenClaw)",
14718
- remediation: "For production hosted AgentOS schedules, set QSTASH_TOKEN and KYNVER_SCHEDULER_PROVIDER=qstash on the Kynver server. Dev/harness hosts may rely on kynver-cron or `kynver daemon` without OpenClaw.",
15010
+ status: "warn",
15011
+ summary: "Hosted deployment with no QSTASH_TOKEN and no KYNVER_SCHEDULER_PROVIDER \u2014 kynver-cron is an in-process stub here and will NOT fire; no firing scheduler is configured",
15012
+ remediation: "Set QSTASH_TOKEN + KYNVER_SCHEDULER_PROVIDER=qstash on the Kynver server for hosted AgentOS schedules. Only use kynver-cron when a `kynver daemon` (or local cron store) actually owns firing on this box.",
14719
15013
  details: {
14720
15014
  schedulerProvider: null,
14721
- resolvedFallback: "kynver-cron",
15015
+ resolvedFallback: "none",
14722
15016
  qstashTokenPresent: false,
14723
15017
  hostedDeployment
14724
15018
  }
@@ -15520,14 +15814,15 @@ function usage(code = 0) {
15520
15814
  " kynver runner credential [--agent-os-id ID] [--base-url URL]",
15521
15815
  " kynver setup [--api-base-url URL] [--agent-os-id ID] [--agent-os-slug SLUG] [--box-kind forge|ghost] [--repo PATH] [--discover-repo] [--max-workers N] [--provider claude|cursor]",
15522
15816
  " kynver daemon --run RUN_ID --agent-os-id AOS_ID [--execute] [--interval-ms MS]",
15817
+ " kynver status --run RUN_ID [--blocked] [--running] [--task TASK_ID] [--worker WORKER] [--full] # top-level compact run status",
15523
15818
  " kynver run create [--repo /path/repo] [--name name] [--base origin/main]",
15524
15819
  " kynver run list",
15525
15820
  " kynver run resolve --name RUN_NAME",
15526
- " kynver run status --run RUN_ID [--json] [--compact]",
15821
+ " kynver run status --run RUN_ID [--full] # defaults to compact shallow map with drill-down commands",
15527
15822
  " kynver run dispatch --run RUN_ID --agent-os-id AOS_ID [--base-url URL] [--secret SECRET] [--execute] [--lane any|implementation|review|landing] [--target-task-id TASK_ID] [--executor harness] [--max-starts 1] [--lease-ms MS] [--owned path[,path]] [--model claude-opus-4-8] [--disk-path /] [--reconcile-stale-blockers]",
15528
15823
  " kynver run sweep --run RUN_ID --agent-os-id AOS_ID [--base-url URL] [--secret SECRET] [--grace-ms MS]",
15529
15824
  ' 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]',
15530
- " kynver worker list --run RUN_ID [--json] [--compact] [--full]",
15825
+ " kynver worker list --run RUN_ID [--full] # defaults to compact shallow map with drill-down commands",
15531
15826
  " kynver worker status --run RUN_ID --name worker",
15532
15827
  " kynver worker tail --run RUN_ID --name worker [--lines 40] [--raw]",
15533
15828
  " kynver worker stop --run RUN_ID --name worker",
@@ -15581,6 +15876,9 @@ async function main(argv = process.argv.slice(2)) {
15581
15876
  const { runsDir, worktreesDir } = getPaths();
15582
15877
  mkdirSync10(runsDir, { recursive: true });
15583
15878
  mkdirSync10(worktreesDir, { recursive: true });
15879
+ if (scope === "daemon") {
15880
+ assertNativeDaemonAllowed();
15881
+ }
15584
15882
  if (shouldEnforceMemoryCostPackageGuardCli(scope, action)) {
15585
15883
  let repoRoot;
15586
15884
  const runId = args.run ? String(args.run).trim() : "";
@@ -15597,6 +15895,7 @@ async function main(argv = process.argv.slice(2)) {
15597
15895
  });
15598
15896
  }
15599
15897
  if (scope === "login") return void await runLogin(args);
15898
+ if (scope === "status") return runStatus(args);
15600
15899
  if (scope === "runner" && action === "credential") return void await mintRunnerCredential(args);
15601
15900
  if (scope === "setup") return void await runSetup(args);
15602
15901
  if (scope === "daemon") return void await runDaemon(args);