@bilalimamoglu/sift 0.2.1 → 0.2.3

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
@@ -6,12 +6,17 @@ import pc2 from "picocolors";
6
6
  // src/constants.ts
7
7
  import os from "os";
8
8
  import path from "path";
9
- var DEFAULT_CONFIG_SEARCH_PATHS = [
10
- path.resolve(process.cwd(), "sift.config.yaml"),
11
- path.resolve(process.cwd(), "sift.config.yml"),
12
- path.join(os.homedir(), ".config", "sift", "config.yaml"),
13
- path.join(os.homedir(), ".config", "sift", "config.yml")
14
- ];
9
+ function getDefaultGlobalConfigPath() {
10
+ return path.join(os.homedir(), ".config", "sift", "config.yaml");
11
+ }
12
+ function getDefaultConfigSearchPaths() {
13
+ return [
14
+ path.resolve(process.cwd(), "sift.config.yaml"),
15
+ path.resolve(process.cwd(), "sift.config.yml"),
16
+ getDefaultGlobalConfigPath(),
17
+ path.join(os.homedir(), ".config", "sift", "config.yml")
18
+ ];
19
+ }
15
20
  var INSUFFICIENT_SIGNAL_TEXT = "Insufficient signal in the provided input.";
16
21
  var GENERIC_JSON_CONTRACT = '{"answer":string,"evidence":string[],"risks":string[]}';
17
22
  var CAPTURE_OMITTED_MARKER = "\n...[captured output omitted]...\n";
@@ -48,6 +53,29 @@ function evaluateGate(args) {
48
53
  return { shouldFail: false };
49
54
  }
50
55
 
56
+ // src/core/insufficient.ts
57
+ function isInsufficientSignalOutput(output) {
58
+ const trimmed = output.trim();
59
+ return trimmed === INSUFFICIENT_SIGNAL_TEXT || trimmed.startsWith(`${INSUFFICIENT_SIGNAL_TEXT}
60
+ Hint:`);
61
+ }
62
+ function buildInsufficientSignalOutput(input) {
63
+ let hint;
64
+ if (input.originalLength === 0) {
65
+ hint = "Hint: no command output was captured.";
66
+ } else if (input.truncatedApplied) {
67
+ hint = "Hint: captured output was truncated before a clear summary was found.";
68
+ } else if (input.presetName === "test-status" && input.exitCode === 0) {
69
+ hint = "Hint: command succeeded, but no recognizable test summary was found.";
70
+ } else if (input.presetName === "test-status" && typeof input.exitCode === "number") {
71
+ hint = "Hint: command failed, but the captured output did not include a recognizable test summary.";
72
+ } else {
73
+ hint = "Hint: the captured output did not contain a clear answer for this preset.";
74
+ }
75
+ return `${INSUFFICIENT_SIGNAL_TEXT}
76
+ ${hint}`;
77
+ }
78
+
51
79
  // src/core/run.ts
52
80
  import pc from "picocolors";
53
81
 
@@ -125,7 +153,7 @@ var OpenAIProvider = class {
125
153
  if (!text) {
126
154
  throw new Error("Provider returned an empty response");
127
155
  }
128
- return {
156
+ const result = {
129
157
  text,
130
158
  usage: data?.usage ? {
131
159
  inputTokens: data.usage.input_tokens,
@@ -134,13 +162,14 @@ var OpenAIProvider = class {
134
162
  } : void 0,
135
163
  raw: data
136
164
  };
165
+ clearTimeout(timeout);
166
+ return result;
137
167
  } catch (error) {
168
+ clearTimeout(timeout);
138
169
  if (error.name === "AbortError") {
139
170
  throw new Error("Provider request timed out");
140
171
  }
141
172
  throw error;
142
- } finally {
143
- clearTimeout(timeout);
144
173
  }
145
174
  }
146
175
  };
@@ -222,7 +251,7 @@ var OpenAICompatibleProvider = class {
222
251
  if (!text.trim()) {
223
252
  throw new Error("Provider returned an empty response");
224
253
  }
225
- return {
254
+ const result = {
226
255
  text,
227
256
  usage: data?.usage ? {
228
257
  inputTokens: data.usage.prompt_tokens,
@@ -231,13 +260,14 @@ var OpenAICompatibleProvider = class {
231
260
  } : void 0,
232
261
  raw: data
233
262
  };
263
+ clearTimeout(timeout);
264
+ return result;
234
265
  } catch (error) {
266
+ clearTimeout(timeout);
235
267
  if (error.name === "AbortError") {
236
268
  throw new Error("Provider request timed out");
237
269
  }
238
270
  throw error;
239
- } finally {
240
- clearTimeout(timeout);
241
271
  }
242
272
  }
243
273
  };
@@ -442,6 +472,19 @@ function buildPrompt(args) {
442
472
  policyName: args.policyName,
443
473
  outputContract: args.outputContract
444
474
  });
475
+ const detailRules = args.policyName === "test-status" && args.detail === "focused" ? [
476
+ "Use a focused failure view.",
477
+ "When the output clearly maps failures to specific tests or modules, group them by dominant error type first.",
478
+ "Within each error group, prefer compact bullets in the form '- test-or-module -> dominant reason'.",
479
+ "Cap focused entries at 6 per error group and end with '- and N more failing modules' if more clear mappings are visible.",
480
+ "If per-test or per-module mapping is unclear, fall back to grouped root causes instead of guessing."
481
+ ] : args.policyName === "test-status" && args.detail === "verbose" ? [
482
+ "Use a verbose failure view.",
483
+ "When the output clearly maps failures to specific tests or modules, list each visible failing test or module on its own line in the form '- test-or-module -> normalized reason'.",
484
+ "Preserve the original file or module order when the mapping is visible.",
485
+ "Prefer concrete normalized reasons such as missing modules or assertion failures over traceback plumbing.",
486
+ "If per-test or per-module mapping is unclear, fall back to the focused grouped-cause view instead of guessing."
487
+ ] : [];
445
488
  const prompt = [
446
489
  "You are Sift, a CLI output reduction assistant for downstream agents and automation.",
447
490
  "Hard rules:",
@@ -449,6 +492,7 @@ function buildPrompt(args) {
449
492
  "",
450
493
  `Task policy: ${policy.name}`,
451
494
  ...policy.taskRules.map((rule) => `- ${rule}`),
495
+ ...detailRules.map((rule) => `- ${rule}`),
452
496
  ...policy.outputContract ? ["", `Output contract: ${policy.outputContract}`] : [],
453
497
  "",
454
498
  `Question: ${args.question}`,
@@ -561,6 +605,410 @@ function inferPackage(line) {
561
605
  function inferRemediation(pkg) {
562
606
  return `Upgrade ${pkg} to a patched version.`;
563
607
  }
608
+ function getCount(input, label) {
609
+ const matches = [...input.matchAll(new RegExp(`(\\d+)\\s+${label}`, "gi"))];
610
+ const lastMatch = matches.at(-1);
611
+ return lastMatch ? Number(lastMatch[1]) : 0;
612
+ }
613
+ function formatCount(count, singular, plural = `${singular}s`) {
614
+ return `${count} ${count === 1 ? singular : plural}`;
615
+ }
616
+ function countPattern(input, matcher) {
617
+ return [...input.matchAll(matcher)].length;
618
+ }
619
+ function collectUniqueMatches(input, matcher, limit = 6) {
620
+ const values = [];
621
+ for (const match of input.matchAll(matcher)) {
622
+ const candidate = match[1]?.trim();
623
+ if (!candidate || values.includes(candidate)) {
624
+ continue;
625
+ }
626
+ values.push(candidate);
627
+ if (values.length >= limit) {
628
+ break;
629
+ }
630
+ }
631
+ return values;
632
+ }
633
+ function cleanFailureLabel(label) {
634
+ return label.trim().replace(/^['"]|['"]$/g, "");
635
+ }
636
+ function isLowValueInternalReason(normalized) {
637
+ return /^Hint:\s+make sure your test modules\/packages have valid Python names\.?$/i.test(
638
+ normalized
639
+ ) || /^Traceback\b/i.test(normalized) || /^return _bootstrap\._gcd_import/i.test(normalized) || /(?:^|[/\\])(?:site-packages[/\\])?_pytest(?:[/\\]|$)/i.test(normalized) || /(?:^|[/\\])importlib[/\\]__init__\.py:\d+:\s+in\s+import_module\b/i.test(
640
+ normalized
641
+ ) || /\bpython\.py:\d+:\s+in\s+importtestmodule\b/i.test(normalized) || /\bpython\.py:\d+:\s+in\s+import_path\b/i.test(normalized);
642
+ }
643
+ function scoreFailureReason(reason) {
644
+ if (reason.startsWith("missing module:")) {
645
+ return 5;
646
+ }
647
+ if (reason.startsWith("assertion failed:")) {
648
+ return 4;
649
+ }
650
+ if (/^[A-Z][A-Za-z]+(?:Error|Exception):/.test(reason)) {
651
+ return 3;
652
+ }
653
+ if (reason === "import error during collection") {
654
+ return 2;
655
+ }
656
+ return 1;
657
+ }
658
+ function classifyFailureReason(line, options) {
659
+ const normalized = line.trim().replace(/^[A-Z]\s+/, "");
660
+ if (normalized.length === 0) {
661
+ return null;
662
+ }
663
+ if (isLowValueInternalReason(normalized)) {
664
+ return null;
665
+ }
666
+ const pythonMissingModule = normalized.match(
667
+ /ModuleNotFoundError:\s+No module named ['"]([^'"]+)['"]/i
668
+ );
669
+ if (pythonMissingModule) {
670
+ return {
671
+ reason: `missing module: ${pythonMissingModule[1]}`,
672
+ group: options.duringCollection ? "import/dependency errors during collection" : "missing dependency/module errors"
673
+ };
674
+ }
675
+ const nodeMissingModule = normalized.match(/Cannot find module ['"]([^'"]+)['"]/i);
676
+ if (nodeMissingModule) {
677
+ return {
678
+ reason: `missing module: ${nodeMissingModule[1]}`,
679
+ group: options.duringCollection ? "import/dependency errors during collection" : "missing dependency/module errors"
680
+ };
681
+ }
682
+ const assertionFailure = normalized.match(/AssertionError:\s*(.+)$/i);
683
+ if (assertionFailure) {
684
+ return {
685
+ reason: `assertion failed: ${assertionFailure[1]}`.slice(0, 120),
686
+ group: "assertion failures"
687
+ };
688
+ }
689
+ const genericError = normalized.match(/\b([A-Z][A-Za-z]+(?:Error|Exception)):\s*(.+)$/);
690
+ if (genericError) {
691
+ const errorType = genericError[1];
692
+ return {
693
+ reason: `${errorType}: ${genericError[2]}`.slice(0, 120),
694
+ group: options.duringCollection && errorType === "ImportError" ? "import/dependency errors during collection" : `${errorType} failures`
695
+ };
696
+ }
697
+ if (/ImportError while importing test module/i.test(normalized)) {
698
+ return {
699
+ reason: "import error during collection",
700
+ group: "import/dependency errors during collection"
701
+ };
702
+ }
703
+ if (!/[A-Za-z]/.test(normalized)) {
704
+ return null;
705
+ }
706
+ return {
707
+ reason: normalized.slice(0, 120),
708
+ group: options.duringCollection ? "collection/import errors" : "other failures"
709
+ };
710
+ }
711
+ function pushFocusedFailureItem(items, candidate) {
712
+ if (items.some((item) => item.label === candidate.label && item.reason === candidate.reason)) {
713
+ return;
714
+ }
715
+ items.push(candidate);
716
+ }
717
+ function chooseStrongestFailureItems(items) {
718
+ const strongest = /* @__PURE__ */ new Map();
719
+ const order = [];
720
+ for (const item of items) {
721
+ const existing = strongest.get(item.label);
722
+ if (!existing) {
723
+ strongest.set(item.label, item);
724
+ order.push(item.label);
725
+ continue;
726
+ }
727
+ if (scoreFailureReason(item.reason) > scoreFailureReason(existing.reason)) {
728
+ strongest.set(item.label, item);
729
+ }
730
+ }
731
+ return order.map((label) => strongest.get(label));
732
+ }
733
+ function collectCollectionFailureItems(input) {
734
+ const items = [];
735
+ const lines = input.split("\n");
736
+ let currentLabel = null;
737
+ let pendingGenericReason = null;
738
+ for (const line of lines) {
739
+ const collecting = line.match(/^_+\s+ERROR collecting\s+(.+?)\s+_+\s*$/);
740
+ if (collecting) {
741
+ if (currentLabel && pendingGenericReason) {
742
+ pushFocusedFailureItem(
743
+ items,
744
+ {
745
+ label: currentLabel,
746
+ reason: pendingGenericReason.reason,
747
+ group: pendingGenericReason.group
748
+ }
749
+ );
750
+ }
751
+ currentLabel = cleanFailureLabel(collecting[1]);
752
+ pendingGenericReason = null;
753
+ continue;
754
+ }
755
+ if (!currentLabel) {
756
+ continue;
757
+ }
758
+ const classification = classifyFailureReason(line, {
759
+ duringCollection: true
760
+ });
761
+ if (!classification) {
762
+ continue;
763
+ }
764
+ if (classification.reason === "import error during collection") {
765
+ pendingGenericReason = classification;
766
+ continue;
767
+ }
768
+ pushFocusedFailureItem(
769
+ items,
770
+ {
771
+ label: currentLabel,
772
+ reason: classification.reason,
773
+ group: classification.group
774
+ }
775
+ );
776
+ currentLabel = null;
777
+ pendingGenericReason = null;
778
+ }
779
+ if (currentLabel && pendingGenericReason) {
780
+ pushFocusedFailureItem(
781
+ items,
782
+ {
783
+ label: currentLabel,
784
+ reason: pendingGenericReason.reason,
785
+ group: pendingGenericReason.group
786
+ }
787
+ );
788
+ }
789
+ return items;
790
+ }
791
+ function collectInlineFailureItems(input) {
792
+ const items = [];
793
+ for (const line of input.split("\n")) {
794
+ const inlineFailure = line.match(/^(FAILED|ERROR)\s+(.+?)\s+-\s+(.+)$/);
795
+ if (!inlineFailure) {
796
+ continue;
797
+ }
798
+ const classification = classifyFailureReason(inlineFailure[3], {
799
+ duringCollection: false
800
+ });
801
+ if (!classification) {
802
+ continue;
803
+ }
804
+ pushFocusedFailureItem(
805
+ items,
806
+ {
807
+ label: cleanFailureLabel(inlineFailure[2]),
808
+ reason: classification.reason,
809
+ group: classification.group
810
+ }
811
+ );
812
+ }
813
+ return items;
814
+ }
815
+ function formatFocusedFailureGroups(args) {
816
+ const maxGroups = args.maxGroups ?? 3;
817
+ const maxPerGroup = args.maxPerGroup ?? 6;
818
+ const grouped = /* @__PURE__ */ new Map();
819
+ for (const item of args.items) {
820
+ const entries = grouped.get(item.group) ?? [];
821
+ entries.push(item);
822
+ grouped.set(item.group, entries);
823
+ }
824
+ const lines = [];
825
+ const visibleGroups = [...grouped.entries()].slice(0, maxGroups);
826
+ for (const [group, entries] of visibleGroups) {
827
+ lines.push(`- ${group}`);
828
+ for (const item of entries.slice(0, maxPerGroup)) {
829
+ lines.push(` - ${item.label} -> ${item.reason}`);
830
+ }
831
+ const remaining = entries.length - Math.min(entries.length, maxPerGroup);
832
+ if (remaining > 0) {
833
+ lines.push(` - and ${remaining} more failing ${args.remainderLabel}`);
834
+ }
835
+ }
836
+ const hiddenGroups = grouped.size - visibleGroups.length;
837
+ if (hiddenGroups > 0) {
838
+ lines.push(`- and ${hiddenGroups} more error group${hiddenGroups === 1 ? "" : "s"}`);
839
+ }
840
+ return lines;
841
+ }
842
+ function formatVerboseFailureItems(args) {
843
+ return chooseStrongestFailureItems(args.items).map(
844
+ (item) => `- ${item.label} -> ${item.reason}`
845
+ );
846
+ }
847
+ function summarizeRepeatedTestCauses(input, options) {
848
+ const pythonMissingModules = collectUniqueMatches(
849
+ input,
850
+ /ModuleNotFoundError:\s+No module named ['"]([^'"]+)['"]/gi
851
+ );
852
+ const nodeMissingModules = collectUniqueMatches(
853
+ input,
854
+ /Cannot find module ['"]([^'"]+)['"]/gi
855
+ );
856
+ const missingModules = [...pythonMissingModules];
857
+ for (const moduleName of nodeMissingModules) {
858
+ if (!missingModules.includes(moduleName)) {
859
+ missingModules.push(moduleName);
860
+ }
861
+ }
862
+ const missingModuleHits = countPattern(
863
+ input,
864
+ /ModuleNotFoundError:\s+No module named ['"]([^'"]+)['"]/gi
865
+ ) + countPattern(input, /Cannot find module ['"]([^'"]+)['"]/gi);
866
+ const importCollectionHits = countPattern(input, /ImportError while importing test module/gi) + countPattern(input, /^\s*_+\s+ERROR collecting\b/gim);
867
+ const genericErrorTypes = collectUniqueMatches(
868
+ input,
869
+ /\b((?:Assertion|Import|Type|Value|Runtime|Reference|Key|Attribute)[A-Za-z]*Error)\b/gi,
870
+ 4
871
+ );
872
+ const bullets = [];
873
+ if (options.duringCollection && (importCollectionHits >= 2 || missingModuleHits >= 2) || !options.duringCollection && missingModuleHits >= 2) {
874
+ bullets.push(
875
+ options.duringCollection ? "- Most failures are import/dependency errors during test collection." : "- Most failures are import/dependency errors."
876
+ );
877
+ }
878
+ if (missingModules.length > 1) {
879
+ bullets.push(`- Missing modules include ${missingModules.join(", ")}.`);
880
+ } else if (missingModules.length === 1 && missingModuleHits >= 2) {
881
+ bullets.push(`- Missing module repeated across failures: ${missingModules[0]}.`);
882
+ }
883
+ if (bullets.length < 2 && genericErrorTypes.length >= 2) {
884
+ bullets.push(`- Repeated error types include ${genericErrorTypes.join(", ")}.`);
885
+ }
886
+ return bullets.slice(0, 2);
887
+ }
888
+ function testStatusHeuristic(input, detail = "standard") {
889
+ const normalized = input.trim();
890
+ if (normalized === "") {
891
+ return null;
892
+ }
893
+ const passed = getCount(input, "passed");
894
+ const failed = getCount(input, "failed");
895
+ const errors = Math.max(
896
+ getCount(input, "errors"),
897
+ getCount(input, "error")
898
+ );
899
+ const skipped = getCount(input, "skipped");
900
+ const collectionErrors = input.match(/(\d+)\s+errors?\s+during collection/i);
901
+ const noTestsCollected = /\bcollected\s+0\s+items\b/i.test(input) || /\bno tests ran\b/i.test(input);
902
+ const interrupted = /\binterrupted\b/i.test(input) || /\bKeyboardInterrupt\b/i.test(input);
903
+ const inlineItems = collectInlineFailureItems(input);
904
+ if (collectionErrors) {
905
+ const count = Number(collectionErrors[1]);
906
+ const items = chooseStrongestFailureItems(collectCollectionFailureItems(input));
907
+ if (detail === "verbose") {
908
+ if (items.length > 0) {
909
+ return [
910
+ "- Tests did not complete.",
911
+ `- ${formatCount(count, "error")} occurred during collection.`,
912
+ ...formatVerboseFailureItems({
913
+ items
914
+ })
915
+ ].join("\n");
916
+ }
917
+ }
918
+ if (detail === "focused") {
919
+ if (items.length > 0) {
920
+ const groupedLines = formatFocusedFailureGroups({
921
+ items,
922
+ remainderLabel: "modules"
923
+ });
924
+ if (groupedLines.length > 0) {
925
+ return [
926
+ "- Tests did not complete.",
927
+ `- ${formatCount(count, "error")} occurred during collection.`,
928
+ ...groupedLines
929
+ ].join("\n");
930
+ }
931
+ }
932
+ }
933
+ const causes = summarizeRepeatedTestCauses(input, {
934
+ duringCollection: true
935
+ });
936
+ return [
937
+ "- Tests did not complete.",
938
+ `- ${formatCount(count, "error")} occurred during collection.`,
939
+ ...causes
940
+ ].join("\n");
941
+ }
942
+ if (noTestsCollected) {
943
+ return ["- Tests did not run.", "- Collected 0 items."].join("\n");
944
+ }
945
+ if (interrupted && failed === 0 && errors === 0) {
946
+ return "- Test run was interrupted.";
947
+ }
948
+ if (failed === 0 && errors === 0 && passed > 0) {
949
+ const details = [formatCount(passed, "test")];
950
+ if (skipped > 0) {
951
+ details.push(formatCount(skipped, "skip"));
952
+ }
953
+ return [
954
+ "- Tests passed.",
955
+ `- ${details.join(", ")}.`
956
+ ].join("\n");
957
+ }
958
+ if (failed > 0 || errors > 0 || inlineItems.length > 0) {
959
+ const summarizedInlineItems = chooseStrongestFailureItems(inlineItems);
960
+ if (detail === "verbose") {
961
+ if (summarizedInlineItems.length > 0) {
962
+ const detailLines2 = [];
963
+ if (failed > 0) {
964
+ detailLines2.push(`- ${formatCount(failed, "test")} failed.`);
965
+ }
966
+ if (errors > 0) {
967
+ detailLines2.push(`- ${formatCount(errors, "error")} occurred.`);
968
+ }
969
+ return [
970
+ "- Tests did not pass.",
971
+ ...detailLines2,
972
+ ...formatVerboseFailureItems({
973
+ items: summarizedInlineItems
974
+ })
975
+ ].join("\n");
976
+ }
977
+ }
978
+ if (detail === "focused") {
979
+ if (summarizedInlineItems.length > 0) {
980
+ const detailLines2 = [];
981
+ if (failed > 0) {
982
+ detailLines2.push(`- ${formatCount(failed, "test")} failed.`);
983
+ }
984
+ if (errors > 0) {
985
+ detailLines2.push(`- ${formatCount(errors, "error")} occurred.`);
986
+ }
987
+ return [
988
+ "- Tests did not pass.",
989
+ ...detailLines2,
990
+ ...formatFocusedFailureGroups({
991
+ items: summarizedInlineItems,
992
+ remainderLabel: "tests or modules"
993
+ })
994
+ ].join("\n");
995
+ }
996
+ }
997
+ const detailLines = [];
998
+ const causes = summarizeRepeatedTestCauses(input, {
999
+ duringCollection: false
1000
+ });
1001
+ if (failed > 0) {
1002
+ detailLines.push(`- ${formatCount(failed, "test")} failed.`);
1003
+ }
1004
+ if (errors > 0) {
1005
+ detailLines.push(`- ${formatCount(errors, "error")} occurred.`);
1006
+ }
1007
+ const evidence = input.split("\n").map((line) => line.trim()).filter((line) => /\b(FAILED|ERROR)\b/.test(line)).slice(0, 3).map((line) => `- ${line}`);
1008
+ return ["- Tests did not pass.", ...detailLines, ...causes, ...evidence].join("\n");
1009
+ }
1010
+ return null;
1011
+ }
564
1012
  function auditCriticalHeuristic(input) {
565
1013
  const vulnerabilities = input.split("\n").map((line) => line.trim()).filter(Boolean).map((line) => {
566
1014
  if (!/\b(critical|high)\b/i.test(line)) {
@@ -631,7 +1079,7 @@ function infraRiskHeuristic(input) {
631
1079
  }
632
1080
  return null;
633
1081
  }
634
- function applyHeuristicPolicy(policyName, input) {
1082
+ function applyHeuristicPolicy(policyName, input, detail) {
635
1083
  if (!policyName) {
636
1084
  return null;
637
1085
  }
@@ -641,6 +1089,9 @@ function applyHeuristicPolicy(policyName, input) {
641
1089
  if (policyName === "infra-risk") {
642
1090
  return infraRiskHeuristic(input);
643
1091
  }
1092
+ if (policyName === "test-status") {
1093
+ return testStatusHeuristic(input, detail);
1094
+ }
644
1095
  return null;
645
1096
  }
646
1097
 
@@ -770,6 +1221,7 @@ function buildDryRunOutput(args) {
770
1221
  },
771
1222
  question: args.request.question,
772
1223
  format: args.request.format,
1224
+ detail: args.request.detail ?? null,
773
1225
  responseMode: args.responseMode,
774
1226
  policy: args.request.policyName ?? null,
775
1227
  heuristicOutput: args.heuristicOutput ?? null,
@@ -789,35 +1241,42 @@ function buildDryRunOutput(args) {
789
1241
  async function delay(ms) {
790
1242
  await new Promise((resolve) => setTimeout(resolve, ms));
791
1243
  }
1244
+ function withInsufficientHint(args) {
1245
+ if (!isInsufficientSignalOutput(args.output)) {
1246
+ return args.output;
1247
+ }
1248
+ return buildInsufficientSignalOutput({
1249
+ presetName: args.request.presetName,
1250
+ originalLength: args.prepared.meta.originalLength,
1251
+ truncatedApplied: args.prepared.meta.truncatedApplied
1252
+ });
1253
+ }
792
1254
  async function generateWithRetry(args) {
793
- let lastError;
794
- for (let attempt = 0; attempt < 2; attempt += 1) {
795
- try {
796
- return await args.provider.generate({
797
- model: args.request.config.provider.model,
798
- prompt: args.prompt,
799
- temperature: args.request.config.provider.temperature,
800
- maxOutputTokens: args.request.config.provider.maxOutputTokens,
801
- timeoutMs: args.request.config.provider.timeoutMs,
802
- responseMode: args.responseMode,
803
- jsonResponseFormat: args.request.config.provider.jsonResponseFormat
804
- });
805
- } catch (error) {
806
- lastError = error;
807
- const reason = error instanceof Error ? error.message : "unknown_error";
808
- if (attempt > 0 || !isRetriableReason(reason)) {
809
- throw error;
810
- }
811
- if (args.request.config.runtime.verbose) {
812
- process.stderr.write(
813
- `${pc.dim("sift")} retry=1 reason=${reason} delay_ms=${RETRY_DELAY_MS}
1255
+ const generate = () => args.provider.generate({
1256
+ model: args.request.config.provider.model,
1257
+ prompt: args.prompt,
1258
+ temperature: args.request.config.provider.temperature,
1259
+ maxOutputTokens: args.request.config.provider.maxOutputTokens,
1260
+ timeoutMs: args.request.config.provider.timeoutMs,
1261
+ responseMode: args.responseMode,
1262
+ jsonResponseFormat: args.request.config.provider.jsonResponseFormat
1263
+ });
1264
+ try {
1265
+ return await generate();
1266
+ } catch (error) {
1267
+ const reason = error instanceof Error ? error.message : "unknown_error";
1268
+ if (!isRetriableReason(reason)) {
1269
+ throw error;
1270
+ }
1271
+ if (args.request.config.runtime.verbose) {
1272
+ process.stderr.write(
1273
+ `${pc.dim("sift")} retry=1 reason=${reason} delay_ms=${RETRY_DELAY_MS}
814
1274
  `
815
- );
816
- }
817
- await delay(RETRY_DELAY_MS);
1275
+ );
818
1276
  }
1277
+ await delay(RETRY_DELAY_MS);
819
1278
  }
820
- throw lastError instanceof Error ? lastError : new Error("unknown_error");
1279
+ return generate();
821
1280
  }
822
1281
  async function runSift(request) {
823
1282
  const prepared = prepareInput(request.stdin, request.config.input);
@@ -825,6 +1284,7 @@ async function runSift(request) {
825
1284
  question: request.question,
826
1285
  format: request.format,
827
1286
  input: prepared.truncated,
1287
+ detail: request.detail,
828
1288
  policyName: request.policyName,
829
1289
  outputContract: request.outputContract
830
1290
  });
@@ -837,7 +1297,8 @@ async function runSift(request) {
837
1297
  }
838
1298
  const heuristicOutput = applyHeuristicPolicy(
839
1299
  request.policyName,
840
- prepared.truncated
1300
+ prepared.truncated,
1301
+ request.detail
841
1302
  );
842
1303
  if (heuristicOutput) {
843
1304
  if (request.config.runtime.verbose) {
@@ -854,7 +1315,11 @@ async function runSift(request) {
854
1315
  heuristicOutput
855
1316
  });
856
1317
  }
857
- return heuristicOutput;
1318
+ return withInsufficientHint({
1319
+ output: heuristicOutput,
1320
+ request,
1321
+ prepared
1322
+ });
858
1323
  }
859
1324
  if (request.dryRun) {
860
1325
  return buildDryRunOutput({
@@ -880,15 +1345,23 @@ async function runSift(request) {
880
1345
  })) {
881
1346
  throw new Error("Model output rejected by quality gate");
882
1347
  }
883
- return normalizeOutput(result.text, responseMode);
1348
+ return withInsufficientHint({
1349
+ output: normalizeOutput(result.text, responseMode),
1350
+ request,
1351
+ prepared
1352
+ });
884
1353
  } catch (error) {
885
1354
  const reason = error instanceof Error ? error.message : "unknown_error";
886
- return buildFallbackOutput({
887
- format: request.format,
888
- reason,
889
- rawInput: prepared.truncated,
890
- rawFallback: request.config.runtime.rawFallback,
891
- jsonFallback: request.fallbackJson
1355
+ return withInsufficientHint({
1356
+ output: buildFallbackOutput({
1357
+ format: request.format,
1358
+ reason,
1359
+ rawInput: prepared.truncated,
1360
+ rawFallback: request.config.runtime.rawFallback,
1361
+ jsonFallback: request.fallbackJson
1362
+ }),
1363
+ request,
1364
+ prepared
892
1365
  });
893
1366
  }
894
1367
  }
@@ -971,6 +1444,15 @@ function buildCommandPreview(request) {
971
1444
  }
972
1445
  return (request.command ?? []).join(" ");
973
1446
  }
1447
+ function getExecSuccessShortcut(args) {
1448
+ if (args.exitCode !== 0) {
1449
+ return null;
1450
+ }
1451
+ if (args.presetName === "typecheck-summary" && args.capturedOutput.trim() === "") {
1452
+ return "No type errors.";
1453
+ }
1454
+ return null;
1455
+ }
974
1456
  async function runExec(request) {
975
1457
  const hasArgvCommand = Array.isArray(request.command) && request.command.length > 0;
976
1458
  const hasShellCommand = typeof request.shellCommand === "string";
@@ -989,7 +1471,6 @@ async function runExec(request) {
989
1471
  let bypassed = false;
990
1472
  let childStatus = null;
991
1473
  let childSignal = null;
992
- let childSpawnError = null;
993
1474
  const child = hasShellCommand ? spawn(shellPath, ["-lc", request.shellCommand], {
994
1475
  stdio: ["inherit", "pipe", "pipe"]
995
1476
  }) : spawn(request.command[0], request.command.slice(1), {
@@ -1017,7 +1498,6 @@ async function runExec(request) {
1017
1498
  child.stderr.on("data", handleChunk);
1018
1499
  await new Promise((resolve, reject) => {
1019
1500
  child.on("error", (error) => {
1020
- childSpawnError = error;
1021
1501
  reject(error);
1022
1502
  });
1023
1503
  child.on("close", (status, signal) => {
@@ -1031,10 +1511,8 @@ async function runExec(request) {
1031
1511
  }
1032
1512
  throw new Error("Failed to start child process.");
1033
1513
  });
1034
- if (childSpawnError) {
1035
- throw childSpawnError;
1036
- }
1037
1514
  const exitCode = normalizeChildExitCode(childStatus, childSignal);
1515
+ const capturedOutput = capture.render();
1038
1516
  if (request.config.runtime.verbose) {
1039
1517
  process.stderr.write(
1040
1518
  `${pc2.dim("sift")} child_exit=${exitCode} captured_chars=${capture.getTotalChars()} capture_truncated=${capture.wasTruncated()}
@@ -1042,10 +1520,40 @@ async function runExec(request) {
1042
1520
  );
1043
1521
  }
1044
1522
  if (!bypassed) {
1045
- const output = await runSift({
1523
+ if (request.showRaw && capturedOutput.length > 0) {
1524
+ process.stderr.write(capturedOutput);
1525
+ if (!capturedOutput.endsWith("\n")) {
1526
+ process.stderr.write("\n");
1527
+ }
1528
+ }
1529
+ const execSuccessShortcut = getExecSuccessShortcut({
1530
+ presetName: request.presetName,
1531
+ exitCode,
1532
+ capturedOutput
1533
+ });
1534
+ if (execSuccessShortcut && !request.dryRun) {
1535
+ if (request.config.runtime.verbose) {
1536
+ process.stderr.write(
1537
+ `${pc2.dim("sift")} exec_shortcut=${request.presetName}
1538
+ `
1539
+ );
1540
+ }
1541
+ process.stdout.write(`${execSuccessShortcut}
1542
+ `);
1543
+ return exitCode;
1544
+ }
1545
+ let output = await runSift({
1046
1546
  ...request,
1047
- stdin: capture.render()
1547
+ stdin: capturedOutput
1048
1548
  });
1549
+ if (isInsufficientSignalOutput(output)) {
1550
+ output = buildInsufficientSignalOutput({
1551
+ presetName: request.presetName,
1552
+ originalLength: capture.getTotalChars(),
1553
+ truncatedApplied: capture.wasTruncated(),
1554
+ exitCode
1555
+ });
1556
+ }
1049
1557
  process.stdout.write(`${output}
1050
1558
  `);
1051
1559
  if (request.failOn && !request.dryRun && exitCode === 0 && supportsFailOnPreset(request.presetName) && evaluateGate({
@@ -1141,7 +1649,7 @@ function findConfigPath(explicitPath) {
1141
1649
  }
1142
1650
  return resolved;
1143
1651
  }
1144
- for (const candidate of DEFAULT_CONFIG_SEARCH_PATHS) {
1652
+ for (const candidate of getDefaultConfigSearchPaths()) {
1145
1653
  if (fs.existsSync(candidate)) {
1146
1654
  return candidate;
1147
1655
  }
@@ -1355,6 +1863,12 @@ function resolveConfig(options = {}) {
1355
1863
  return siftConfigSchema.parse(merged);
1356
1864
  }
1357
1865
  export {
1866
+ BoundedCapture,
1867
+ buildCommandPreview,
1868
+ getExecSuccessShortcut,
1869
+ looksInteractivePrompt,
1870
+ mergeDefined,
1871
+ normalizeChildExitCode,
1358
1872
  resolveConfig,
1359
1873
  runExec,
1360
1874
  runSift