@kynver-app/runtime 0.1.23 → 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 +99 -10
- package/dist/cli.js.map +4 -4
- package/dist/index.js +100 -10
- package/dist/index.js.map +4 -4
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -680,6 +680,64 @@ function classifyExitFailure(errorText) {
|
|
|
680
680
|
return null;
|
|
681
681
|
}
|
|
682
682
|
|
|
683
|
+
// src/exited-salvage.ts
|
|
684
|
+
function trimOrNull(value) {
|
|
685
|
+
if (typeof value !== "string") return null;
|
|
686
|
+
const trimmed = value.trim();
|
|
687
|
+
return trimmed.length ? trimmed : null;
|
|
688
|
+
}
|
|
689
|
+
function hasFinalResult(value) {
|
|
690
|
+
if (value === void 0 || value === null) return false;
|
|
691
|
+
if (typeof value === "string") return value.trim().length > 0;
|
|
692
|
+
if (typeof value === "boolean") return value;
|
|
693
|
+
if (Array.isArray(value)) return value.length > 0;
|
|
694
|
+
if (typeof value === "object") return Object.keys(value).length > 0;
|
|
695
|
+
return true;
|
|
696
|
+
}
|
|
697
|
+
function committedHeadFromAncestry(ancestry) {
|
|
698
|
+
if (!ancestry?.checked) return null;
|
|
699
|
+
if (ancestry.headIsAncestorOfBase !== false) return null;
|
|
700
|
+
return trimOrNull(ancestry.head);
|
|
701
|
+
}
|
|
702
|
+
function buildAttentionReason(kind, uncommittedCount, headCommit) {
|
|
703
|
+
const parts = ["exited_with_changes_salvage"];
|
|
704
|
+
if (kind === "uncommitted" || kind === "both") {
|
|
705
|
+
parts.push(
|
|
706
|
+
`${uncommittedCount} uncommitted change${uncommittedCount === 1 ? "" : "s"} with no final result`
|
|
707
|
+
);
|
|
708
|
+
}
|
|
709
|
+
if ((kind === "committed_ahead" || kind === "both") && headCommit) {
|
|
710
|
+
const sha = headCommit.length > 12 ? headCommit.slice(0, 12) : headCommit;
|
|
711
|
+
parts.push(`commit ${sha} ahead of base with no final result`);
|
|
712
|
+
}
|
|
713
|
+
parts.push("review worktree \u2014 commit, open a PR, or run a salvage worker before discarding");
|
|
714
|
+
return parts.join(": ");
|
|
715
|
+
}
|
|
716
|
+
function assessExitedWorkerSalvage(input) {
|
|
717
|
+
if (input.alive || hasFinalResult(input.finalResult)) return null;
|
|
718
|
+
const uncommittedCount = (input.changedFiles ?? []).filter((line) => line.trim()).length;
|
|
719
|
+
const headCommit = trimOrNull(input.headCommit) ?? committedHeadFromAncestry(input.gitAncestry);
|
|
720
|
+
const hasUncommitted = uncommittedCount > 0;
|
|
721
|
+
const hasCommittedAhead = Boolean(headCommit);
|
|
722
|
+
if (!hasUncommitted && !hasCommittedAhead) {
|
|
723
|
+
return {
|
|
724
|
+
kind: "none",
|
|
725
|
+
salvageable: false,
|
|
726
|
+
uncommittedCount: 0,
|
|
727
|
+
headCommit: null,
|
|
728
|
+
attentionReason: "process exited without a final result"
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
const kind = hasUncommitted && hasCommittedAhead ? "both" : hasUncommitted ? "uncommitted" : "committed_ahead";
|
|
732
|
+
return {
|
|
733
|
+
kind,
|
|
734
|
+
salvageable: true,
|
|
735
|
+
uncommittedCount,
|
|
736
|
+
headCommit,
|
|
737
|
+
attentionReason: buildAttentionReason(kind, uncommittedCount, headCommit)
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
|
|
683
741
|
// src/git.ts
|
|
684
742
|
import { spawnSync } from "node:child_process";
|
|
685
743
|
function git(cwd, args, options = {}) {
|
|
@@ -804,12 +862,12 @@ function scrubClaudeEnv(env) {
|
|
|
804
862
|
}
|
|
805
863
|
|
|
806
864
|
// src/landing-gate.ts
|
|
807
|
-
function
|
|
865
|
+
function trimOrNull2(value) {
|
|
808
866
|
if (typeof value !== "string") return null;
|
|
809
867
|
const trimmed = value.trim();
|
|
810
868
|
return trimmed.length ? trimmed : null;
|
|
811
869
|
}
|
|
812
|
-
function
|
|
870
|
+
function hasFinalResult2(value) {
|
|
813
871
|
if (value === void 0 || value === null) return false;
|
|
814
872
|
if (typeof value === "string") return value.trim().length > 0;
|
|
815
873
|
if (typeof value === "boolean") return value;
|
|
@@ -818,18 +876,18 @@ function hasFinalResult(value) {
|
|
|
818
876
|
return true;
|
|
819
877
|
}
|
|
820
878
|
function hasCommittedLandingRef(snapshot) {
|
|
821
|
-
if (
|
|
822
|
-
if (
|
|
823
|
-
if (
|
|
824
|
-
if (
|
|
879
|
+
if (trimOrNull2(snapshot.headCommit)) return true;
|
|
880
|
+
if (trimOrNull2(snapshot.prUrl)) return true;
|
|
881
|
+
if (trimOrNull2(snapshot.artifactBundlePath)) return true;
|
|
882
|
+
if (trimOrNull2(snapshot.patchPath)) return true;
|
|
825
883
|
const ancestry = snapshot.gitAncestry;
|
|
826
|
-
if (ancestry?.checked && ancestry.headIsAncestorOfBase === false &&
|
|
884
|
+
if (ancestry?.checked && ancestry.headIsAncestorOfBase === false && trimOrNull2(ancestry.head)) {
|
|
827
885
|
return true;
|
|
828
886
|
}
|
|
829
887
|
return false;
|
|
830
888
|
}
|
|
831
889
|
function assessWorkerLanding(snapshot) {
|
|
832
|
-
if (!
|
|
890
|
+
if (!hasFinalResult2(snapshot.finalResult)) return { blocked: false };
|
|
833
891
|
if (snapshot.changedFiles.length === 0) return { blocked: false };
|
|
834
892
|
if (!hasCommittedLandingRef(snapshot)) {
|
|
835
893
|
return {
|
|
@@ -874,10 +932,23 @@ function computeAttention(input) {
|
|
|
874
932
|
if (!input.alive) {
|
|
875
933
|
const classified = classifyExitFailure(input.error);
|
|
876
934
|
if (classified) return { state: "blocked", reason: classified.reason };
|
|
935
|
+
const salvage = assessExitedWorkerSalvage({
|
|
936
|
+
alive: false,
|
|
937
|
+
finalResult: null,
|
|
938
|
+
changedFiles: input.changedFiles,
|
|
939
|
+
gitAncestry: input.gitAncestry
|
|
940
|
+
});
|
|
941
|
+
if (salvage?.salvageable) {
|
|
942
|
+
const tail2 = input.error?.trim();
|
|
943
|
+
return {
|
|
944
|
+
state: "needs_attention",
|
|
945
|
+
reason: tail2 ? `${salvage.attentionReason} (${tail2})` : salvage.attentionReason
|
|
946
|
+
};
|
|
947
|
+
}
|
|
877
948
|
const tail = input.error?.trim();
|
|
878
949
|
return {
|
|
879
950
|
state: "needs_attention",
|
|
880
|
-
reason: tail ? `process exited without a final result: ${tail}` : "process exited without a final result"
|
|
951
|
+
reason: tail ? `process exited without a final result: ${tail}` : salvage?.attentionReason ?? "process exited without a final result"
|
|
881
952
|
};
|
|
882
953
|
}
|
|
883
954
|
if (input.heartbeatBlocker) {
|
|
@@ -1394,6 +1465,8 @@ function buildPrompt(input) {
|
|
|
1394
1465
|
`Progress heartbeat file: ${input.heartbeatPath}`,
|
|
1395
1466
|
"After each major step, append one JSON line to the heartbeat file with fields: ts, phase, summary, changedFiles, blocker.",
|
|
1396
1467
|
"Final response must include files changed, verification commands, and unresolved risks.",
|
|
1468
|
+
"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.",
|
|
1469
|
+
"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.",
|
|
1397
1470
|
"",
|
|
1398
1471
|
...progressLines,
|
|
1399
1472
|
"",
|
|
@@ -1726,6 +1799,13 @@ function buildRunBoard(runId) {
|
|
|
1726
1799
|
baseCommit: run.baseCommit
|
|
1727
1800
|
});
|
|
1728
1801
|
const headCommit = status.gitAncestry.headIsAncestorOfBase === false && status.gitAncestry.head ? status.gitAncestry.head : void 0;
|
|
1802
|
+
const exitedSalvage = assessExitedWorkerSalvage({
|
|
1803
|
+
alive: status.alive,
|
|
1804
|
+
finalResult: status.finalResult,
|
|
1805
|
+
changedFiles: status.changedFiles,
|
|
1806
|
+
gitAncestry: status.gitAncestry,
|
|
1807
|
+
headCommit
|
|
1808
|
+
});
|
|
1729
1809
|
const rawBlocker = worker.completionBlocker;
|
|
1730
1810
|
const completionBlocker = typeof rawBlocker === "string" && rawBlocker ? rawBlocker : void 0;
|
|
1731
1811
|
const boardStatus = completionBlocker ? "blocked" : status.status;
|
|
@@ -1736,6 +1816,9 @@ function buildRunBoard(runId) {
|
|
|
1736
1816
|
attention: boardAttention,
|
|
1737
1817
|
attentionReason: completionBlocker ?? status.attention.reason,
|
|
1738
1818
|
landingBlocked: status.finalResult ? boardAttention === "needs_attention" || boardAttention === "blocked" : false,
|
|
1819
|
+
exitedWithoutFinalResult: !status.finalResult && !status.alive,
|
|
1820
|
+
salvageState: exitedSalvage?.salvageable ? "review_needed" : "none",
|
|
1821
|
+
salvageReason: exitedSalvage?.salvageable ? exitedSalvage.attentionReason : void 0,
|
|
1739
1822
|
pid: status.pid,
|
|
1740
1823
|
alive: status.alive,
|
|
1741
1824
|
currentTool: status.currentTool,
|
|
@@ -2687,7 +2770,13 @@ async function completeFinishedWorkers(runId, args) {
|
|
|
2687
2770
|
if (!worker?.taskId) continue;
|
|
2688
2771
|
const status = computeWorkerStatus(worker);
|
|
2689
2772
|
if (!isFinishedWorkerStatus(status)) continue;
|
|
2690
|
-
|
|
2773
|
+
const exitedSalvage = assessExitedWorkerSalvage({
|
|
2774
|
+
alive: status.alive,
|
|
2775
|
+
finalResult: status.finalResult,
|
|
2776
|
+
changedFiles: status.changedFiles,
|
|
2777
|
+
gitAncestry: status.gitAncestry
|
|
2778
|
+
});
|
|
2779
|
+
if (!worker.dispatched && !status.finalResult && !exitedSalvage?.salvageable) continue;
|
|
2691
2780
|
const result = await tryCompleteWorker({
|
|
2692
2781
|
run: runId,
|
|
2693
2782
|
name,
|
|
@@ -2976,6 +3065,7 @@ if (isCliEntry) {
|
|
|
2976
3065
|
}
|
|
2977
3066
|
export {
|
|
2978
3067
|
DEFAULT_DISPATCH_LEASE_MS,
|
|
3068
|
+
assessExitedWorkerSalvage,
|
|
2979
3069
|
assessWorkerLanding,
|
|
2980
3070
|
autoCompleteWorker,
|
|
2981
3071
|
autoCompleteWorkerCli,
|