@kynver-app/runtime 0.1.22 → 0.1.24

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 CHANGED
@@ -679,6 +679,64 @@ function classifyExitFailure(errorText) {
679
679
  return null;
680
680
  }
681
681
 
682
+ // src/exited-salvage.ts
683
+ function trimOrNull(value) {
684
+ if (typeof value !== "string") return null;
685
+ const trimmed = value.trim();
686
+ return trimmed.length ? trimmed : null;
687
+ }
688
+ function hasFinalResult(value) {
689
+ if (value === void 0 || value === null) return false;
690
+ if (typeof value === "string") return value.trim().length > 0;
691
+ if (typeof value === "boolean") return value;
692
+ if (Array.isArray(value)) return value.length > 0;
693
+ if (typeof value === "object") return Object.keys(value).length > 0;
694
+ return true;
695
+ }
696
+ function committedHeadFromAncestry(ancestry) {
697
+ if (!ancestry?.checked) return null;
698
+ if (ancestry.headIsAncestorOfBase !== false) return null;
699
+ return trimOrNull(ancestry.head);
700
+ }
701
+ function buildAttentionReason(kind, uncommittedCount, headCommit) {
702
+ const parts = ["exited_with_changes_salvage"];
703
+ if (kind === "uncommitted" || kind === "both") {
704
+ parts.push(
705
+ `${uncommittedCount} uncommitted change${uncommittedCount === 1 ? "" : "s"} with no final result`
706
+ );
707
+ }
708
+ if ((kind === "committed_ahead" || kind === "both") && headCommit) {
709
+ const sha = headCommit.length > 12 ? headCommit.slice(0, 12) : headCommit;
710
+ parts.push(`commit ${sha} ahead of base with no final result`);
711
+ }
712
+ parts.push("review worktree \u2014 commit, open a PR, or run a salvage worker before discarding");
713
+ return parts.join(": ");
714
+ }
715
+ function assessExitedWorkerSalvage(input) {
716
+ if (input.alive || hasFinalResult(input.finalResult)) return null;
717
+ const uncommittedCount = (input.changedFiles ?? []).filter((line) => line.trim()).length;
718
+ const headCommit = trimOrNull(input.headCommit) ?? committedHeadFromAncestry(input.gitAncestry);
719
+ const hasUncommitted = uncommittedCount > 0;
720
+ const hasCommittedAhead = Boolean(headCommit);
721
+ if (!hasUncommitted && !hasCommittedAhead) {
722
+ return {
723
+ kind: "none",
724
+ salvageable: false,
725
+ uncommittedCount: 0,
726
+ headCommit: null,
727
+ attentionReason: "process exited without a final result"
728
+ };
729
+ }
730
+ const kind = hasUncommitted && hasCommittedAhead ? "both" : hasUncommitted ? "uncommitted" : "committed_ahead";
731
+ return {
732
+ kind,
733
+ salvageable: true,
734
+ uncommittedCount,
735
+ headCommit,
736
+ attentionReason: buildAttentionReason(kind, uncommittedCount, headCommit)
737
+ };
738
+ }
739
+
682
740
  // src/git.ts
683
741
  import { spawnSync } from "node:child_process";
684
742
  function git(cwd, args, options = {}) {
@@ -803,12 +861,12 @@ function scrubClaudeEnv(env) {
803
861
  }
804
862
 
805
863
  // src/landing-gate.ts
806
- function trimOrNull(value) {
864
+ function trimOrNull2(value) {
807
865
  if (typeof value !== "string") return null;
808
866
  const trimmed = value.trim();
809
867
  return trimmed.length ? trimmed : null;
810
868
  }
811
- function hasFinalResult(value) {
869
+ function hasFinalResult2(value) {
812
870
  if (value === void 0 || value === null) return false;
813
871
  if (typeof value === "string") return value.trim().length > 0;
814
872
  if (typeof value === "boolean") return value;
@@ -817,18 +875,18 @@ function hasFinalResult(value) {
817
875
  return true;
818
876
  }
819
877
  function hasCommittedLandingRef(snapshot) {
820
- if (trimOrNull(snapshot.headCommit)) return true;
821
- if (trimOrNull(snapshot.prUrl)) return true;
822
- if (trimOrNull(snapshot.artifactBundlePath)) return true;
823
- if (trimOrNull(snapshot.patchPath)) return true;
878
+ if (trimOrNull2(snapshot.headCommit)) return true;
879
+ if (trimOrNull2(snapshot.prUrl)) return true;
880
+ if (trimOrNull2(snapshot.artifactBundlePath)) return true;
881
+ if (trimOrNull2(snapshot.patchPath)) return true;
824
882
  const ancestry = snapshot.gitAncestry;
825
- if (ancestry?.checked && ancestry.headIsAncestorOfBase === false && trimOrNull(ancestry.head)) {
883
+ if (ancestry?.checked && ancestry.headIsAncestorOfBase === false && trimOrNull2(ancestry.head)) {
826
884
  return true;
827
885
  }
828
886
  return false;
829
887
  }
830
888
  function assessWorkerLanding(snapshot) {
831
- if (!hasFinalResult(snapshot.finalResult)) return { blocked: false };
889
+ if (!hasFinalResult2(snapshot.finalResult)) return { blocked: false };
832
890
  if (snapshot.changedFiles.length === 0) return { blocked: false };
833
891
  if (!hasCommittedLandingRef(snapshot)) {
834
892
  return {
@@ -873,10 +931,23 @@ function computeAttention(input) {
873
931
  if (!input.alive) {
874
932
  const classified = classifyExitFailure(input.error);
875
933
  if (classified) return { state: "blocked", reason: classified.reason };
934
+ const salvage = assessExitedWorkerSalvage({
935
+ alive: false,
936
+ finalResult: null,
937
+ changedFiles: input.changedFiles,
938
+ gitAncestry: input.gitAncestry
939
+ });
940
+ if (salvage?.salvageable) {
941
+ const tail2 = input.error?.trim();
942
+ return {
943
+ state: "needs_attention",
944
+ reason: tail2 ? `${salvage.attentionReason} (${tail2})` : salvage.attentionReason
945
+ };
946
+ }
876
947
  const tail = input.error?.trim();
877
948
  return {
878
949
  state: "needs_attention",
879
- reason: tail ? `process exited without a final result: ${tail}` : "process exited without a final result"
950
+ reason: tail ? `process exited without a final result: ${tail}` : salvage?.attentionReason ?? "process exited without a final result"
880
951
  };
881
952
  }
882
953
  if (input.heartbeatBlocker) {
@@ -1393,6 +1464,8 @@ function buildPrompt(input) {
1393
1464
  `Progress heartbeat file: ${input.heartbeatPath}`,
1394
1465
  "After each major step, append one JSON line to the heartbeat file with fields: ts, phase, summary, changedFiles, blocker.",
1395
1466
  "Final response must include files changed, verification commands, and unresolved risks.",
1467
+ "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, commit or open a PR; exiting with only dirty files and no final result routes to salvage review, not production review.",
1468
+ "Long-running commands: prefer bounded verification (targeted tests/typecheck for touched paths). If a full build is required, note it in heartbeat phase `verify` so a silent exit is not mistaken for success.",
1396
1469
  "",
1397
1470
  ...progressLines,
1398
1471
  "",
@@ -1725,6 +1798,13 @@ function buildRunBoard(runId) {
1725
1798
  baseCommit: run.baseCommit
1726
1799
  });
1727
1800
  const headCommit = status.gitAncestry.headIsAncestorOfBase === false && status.gitAncestry.head ? status.gitAncestry.head : void 0;
1801
+ const exitedSalvage = assessExitedWorkerSalvage({
1802
+ alive: status.alive,
1803
+ finalResult: status.finalResult,
1804
+ changedFiles: status.changedFiles,
1805
+ gitAncestry: status.gitAncestry,
1806
+ headCommit
1807
+ });
1728
1808
  const rawBlocker = worker.completionBlocker;
1729
1809
  const completionBlocker = typeof rawBlocker === "string" && rawBlocker ? rawBlocker : void 0;
1730
1810
  const boardStatus = completionBlocker ? "blocked" : status.status;
@@ -1735,6 +1815,9 @@ function buildRunBoard(runId) {
1735
1815
  attention: boardAttention,
1736
1816
  attentionReason: completionBlocker ?? status.attention.reason,
1737
1817
  landingBlocked: status.finalResult ? boardAttention === "needs_attention" || boardAttention === "blocked" : false,
1818
+ exitedWithoutFinalResult: !status.finalResult && !status.alive,
1819
+ salvageState: exitedSalvage?.salvageable ? "review_needed" : "none",
1820
+ salvageReason: exitedSalvage?.salvageable ? exitedSalvage.attentionReason : void 0,
1738
1821
  pid: status.pid,
1739
1822
  alive: status.alive,
1740
1823
  currentTool: status.currentTool,
@@ -2655,7 +2738,13 @@ async function completeFinishedWorkers(runId, args) {
2655
2738
  if (!worker?.taskId) continue;
2656
2739
  const status = computeWorkerStatus(worker);
2657
2740
  if (!isFinishedWorkerStatus(status)) continue;
2658
- if (!worker.dispatched && !status.finalResult) continue;
2741
+ const exitedSalvage = assessExitedWorkerSalvage({
2742
+ alive: status.alive,
2743
+ finalResult: status.finalResult,
2744
+ changedFiles: status.changedFiles,
2745
+ gitAncestry: status.gitAncestry
2746
+ });
2747
+ if (!worker.dispatched && !status.finalResult && !exitedSalvage?.salvageable) continue;
2659
2748
  const result = await tryCompleteWorker({
2660
2749
  run: runId,
2661
2750
  name,
@@ -2684,15 +2773,15 @@ async function runPipelineTick(args) {
2684
2773
  const agentOsId = String(required(String(args.agentOsId || ""), "--agent-os-id"));
2685
2774
  const execute = args.execute !== false && args.execute !== "false";
2686
2775
  runStatus({ run: runId });
2687
- const completedWorkers = await completeFinishedWorkers(runId, args);
2688
- const staleReconcile = reconcileStaleWorkers();
2689
- const planProgressSync = await syncActiveWorkerPlanProgress(runId, args);
2690
2776
  const workspacePrefs = await fetchWorkspaceRuntimePreferences(agentOsId, args);
2691
2777
  const resourceGate = observeRunnerResourceGate({
2692
2778
  runId,
2693
2779
  configuredMaxWorkersOverride: workspacePrefs?.maxConcurrentWorkers
2694
2780
  });
2695
2781
  const operatorTick = await postOperatorTick(agentOsId, runId, resourceGate, args);
2782
+ const completedWorkers = await completeFinishedWorkers(runId, args);
2783
+ const staleReconcile = reconcileStaleWorkers();
2784
+ const planProgressSync = await syncActiveWorkerPlanProgress(runId, args);
2696
2785
  let maxStarts = resourceGate.slotsAvailable;
2697
2786
  const tickBody = operatorTick;
2698
2787
  const advised = tickBody.response?.dispatch?.recommendedMaxStarts;