@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 +102 -13
- package/dist/cli.js.map +4 -4
- package/dist/index.js +103 -13
- package/dist/index.js.map +4 -4
- package/package.json +1 -1
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
|
|
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
|
|
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 (
|
|
821
|
-
if (
|
|
822
|
-
if (
|
|
823
|
-
if (
|
|
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 &&
|
|
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 (!
|
|
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
|
-
|
|
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;
|