@bilalimamoglu/sift 0.2.2 → 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
@@ -53,6 +53,29 @@ function evaluateGate(args) {
53
53
  return { shouldFail: false };
54
54
  }
55
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
+
56
79
  // src/core/run.ts
57
80
  import pc from "picocolors";
58
81
 
@@ -130,7 +153,7 @@ var OpenAIProvider = class {
130
153
  if (!text) {
131
154
  throw new Error("Provider returned an empty response");
132
155
  }
133
- return {
156
+ const result = {
134
157
  text,
135
158
  usage: data?.usage ? {
136
159
  inputTokens: data.usage.input_tokens,
@@ -139,13 +162,14 @@ var OpenAIProvider = class {
139
162
  } : void 0,
140
163
  raw: data
141
164
  };
165
+ clearTimeout(timeout);
166
+ return result;
142
167
  } catch (error) {
168
+ clearTimeout(timeout);
143
169
  if (error.name === "AbortError") {
144
170
  throw new Error("Provider request timed out");
145
171
  }
146
172
  throw error;
147
- } finally {
148
- clearTimeout(timeout);
149
173
  }
150
174
  }
151
175
  };
@@ -227,7 +251,7 @@ var OpenAICompatibleProvider = class {
227
251
  if (!text.trim()) {
228
252
  throw new Error("Provider returned an empty response");
229
253
  }
230
- return {
254
+ const result = {
231
255
  text,
232
256
  usage: data?.usage ? {
233
257
  inputTokens: data.usage.prompt_tokens,
@@ -236,13 +260,14 @@ var OpenAICompatibleProvider = class {
236
260
  } : void 0,
237
261
  raw: data
238
262
  };
263
+ clearTimeout(timeout);
264
+ return result;
239
265
  } catch (error) {
266
+ clearTimeout(timeout);
240
267
  if (error.name === "AbortError") {
241
268
  throw new Error("Provider request timed out");
242
269
  }
243
270
  throw error;
244
- } finally {
245
- clearTimeout(timeout);
246
271
  }
247
272
  }
248
273
  };
@@ -447,6 +472,19 @@ function buildPrompt(args) {
447
472
  policyName: args.policyName,
448
473
  outputContract: args.outputContract
449
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
+ ] : [];
450
488
  const prompt = [
451
489
  "You are Sift, a CLI output reduction assistant for downstream agents and automation.",
452
490
  "Hard rules:",
@@ -454,6 +492,7 @@ function buildPrompt(args) {
454
492
  "",
455
493
  `Task policy: ${policy.name}`,
456
494
  ...policy.taskRules.map((rule) => `- ${rule}`),
495
+ ...detailRules.map((rule) => `- ${rule}`),
457
496
  ...policy.outputContract ? ["", `Output contract: ${policy.outputContract}`] : [],
458
497
  "",
459
498
  `Question: ${args.question}`,
@@ -566,6 +605,410 @@ function inferPackage(line) {
566
605
  function inferRemediation(pkg) {
567
606
  return `Upgrade ${pkg} to a patched version.`;
568
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
+ }
569
1012
  function auditCriticalHeuristic(input) {
570
1013
  const vulnerabilities = input.split("\n").map((line) => line.trim()).filter(Boolean).map((line) => {
571
1014
  if (!/\b(critical|high)\b/i.test(line)) {
@@ -636,7 +1079,7 @@ function infraRiskHeuristic(input) {
636
1079
  }
637
1080
  return null;
638
1081
  }
639
- function applyHeuristicPolicy(policyName, input) {
1082
+ function applyHeuristicPolicy(policyName, input, detail) {
640
1083
  if (!policyName) {
641
1084
  return null;
642
1085
  }
@@ -646,6 +1089,9 @@ function applyHeuristicPolicy(policyName, input) {
646
1089
  if (policyName === "infra-risk") {
647
1090
  return infraRiskHeuristic(input);
648
1091
  }
1092
+ if (policyName === "test-status") {
1093
+ return testStatusHeuristic(input, detail);
1094
+ }
649
1095
  return null;
650
1096
  }
651
1097
 
@@ -775,6 +1221,7 @@ function buildDryRunOutput(args) {
775
1221
  },
776
1222
  question: args.request.question,
777
1223
  format: args.request.format,
1224
+ detail: args.request.detail ?? null,
778
1225
  responseMode: args.responseMode,
779
1226
  policy: args.request.policyName ?? null,
780
1227
  heuristicOutput: args.heuristicOutput ?? null,
@@ -794,35 +1241,42 @@ function buildDryRunOutput(args) {
794
1241
  async function delay(ms) {
795
1242
  await new Promise((resolve) => setTimeout(resolve, ms));
796
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
+ }
797
1254
  async function generateWithRetry(args) {
798
- let lastError;
799
- for (let attempt = 0; attempt < 2; attempt += 1) {
800
- try {
801
- return await args.provider.generate({
802
- model: args.request.config.provider.model,
803
- prompt: args.prompt,
804
- temperature: args.request.config.provider.temperature,
805
- maxOutputTokens: args.request.config.provider.maxOutputTokens,
806
- timeoutMs: args.request.config.provider.timeoutMs,
807
- responseMode: args.responseMode,
808
- jsonResponseFormat: args.request.config.provider.jsonResponseFormat
809
- });
810
- } catch (error) {
811
- lastError = error;
812
- const reason = error instanceof Error ? error.message : "unknown_error";
813
- if (attempt > 0 || !isRetriableReason(reason)) {
814
- throw error;
815
- }
816
- if (args.request.config.runtime.verbose) {
817
- process.stderr.write(
818
- `${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}
819
1274
  `
820
- );
821
- }
822
- await delay(RETRY_DELAY_MS);
1275
+ );
823
1276
  }
1277
+ await delay(RETRY_DELAY_MS);
824
1278
  }
825
- throw lastError instanceof Error ? lastError : new Error("unknown_error");
1279
+ return generate();
826
1280
  }
827
1281
  async function runSift(request) {
828
1282
  const prepared = prepareInput(request.stdin, request.config.input);
@@ -830,6 +1284,7 @@ async function runSift(request) {
830
1284
  question: request.question,
831
1285
  format: request.format,
832
1286
  input: prepared.truncated,
1287
+ detail: request.detail,
833
1288
  policyName: request.policyName,
834
1289
  outputContract: request.outputContract
835
1290
  });
@@ -842,7 +1297,8 @@ async function runSift(request) {
842
1297
  }
843
1298
  const heuristicOutput = applyHeuristicPolicy(
844
1299
  request.policyName,
845
- prepared.truncated
1300
+ prepared.truncated,
1301
+ request.detail
846
1302
  );
847
1303
  if (heuristicOutput) {
848
1304
  if (request.config.runtime.verbose) {
@@ -859,7 +1315,11 @@ async function runSift(request) {
859
1315
  heuristicOutput
860
1316
  });
861
1317
  }
862
- return heuristicOutput;
1318
+ return withInsufficientHint({
1319
+ output: heuristicOutput,
1320
+ request,
1321
+ prepared
1322
+ });
863
1323
  }
864
1324
  if (request.dryRun) {
865
1325
  return buildDryRunOutput({
@@ -885,15 +1345,23 @@ async function runSift(request) {
885
1345
  })) {
886
1346
  throw new Error("Model output rejected by quality gate");
887
1347
  }
888
- return normalizeOutput(result.text, responseMode);
1348
+ return withInsufficientHint({
1349
+ output: normalizeOutput(result.text, responseMode),
1350
+ request,
1351
+ prepared
1352
+ });
889
1353
  } catch (error) {
890
1354
  const reason = error instanceof Error ? error.message : "unknown_error";
891
- return buildFallbackOutput({
892
- format: request.format,
893
- reason,
894
- rawInput: prepared.truncated,
895
- rawFallback: request.config.runtime.rawFallback,
896
- 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
897
1365
  });
898
1366
  }
899
1367
  }
@@ -1003,7 +1471,6 @@ async function runExec(request) {
1003
1471
  let bypassed = false;
1004
1472
  let childStatus = null;
1005
1473
  let childSignal = null;
1006
- let childSpawnError = null;
1007
1474
  const child = hasShellCommand ? spawn(shellPath, ["-lc", request.shellCommand], {
1008
1475
  stdio: ["inherit", "pipe", "pipe"]
1009
1476
  }) : spawn(request.command[0], request.command.slice(1), {
@@ -1031,7 +1498,6 @@ async function runExec(request) {
1031
1498
  child.stderr.on("data", handleChunk);
1032
1499
  await new Promise((resolve, reject) => {
1033
1500
  child.on("error", (error) => {
1034
- childSpawnError = error;
1035
1501
  reject(error);
1036
1502
  });
1037
1503
  child.on("close", (status, signal) => {
@@ -1045,9 +1511,6 @@ async function runExec(request) {
1045
1511
  }
1046
1512
  throw new Error("Failed to start child process.");
1047
1513
  });
1048
- if (childSpawnError) {
1049
- throw childSpawnError;
1050
- }
1051
1514
  const exitCode = normalizeChildExitCode(childStatus, childSignal);
1052
1515
  const capturedOutput = capture.render();
1053
1516
  if (request.config.runtime.verbose) {
@@ -1057,6 +1520,12 @@ async function runExec(request) {
1057
1520
  );
1058
1521
  }
1059
1522
  if (!bypassed) {
1523
+ if (request.showRaw && capturedOutput.length > 0) {
1524
+ process.stderr.write(capturedOutput);
1525
+ if (!capturedOutput.endsWith("\n")) {
1526
+ process.stderr.write("\n");
1527
+ }
1528
+ }
1060
1529
  const execSuccessShortcut = getExecSuccessShortcut({
1061
1530
  presetName: request.presetName,
1062
1531
  exitCode,
@@ -1073,10 +1542,18 @@ async function runExec(request) {
1073
1542
  `);
1074
1543
  return exitCode;
1075
1544
  }
1076
- const output = await runSift({
1545
+ let output = await runSift({
1077
1546
  ...request,
1078
1547
  stdin: capturedOutput
1079
1548
  });
1549
+ if (isInsufficientSignalOutput(output)) {
1550
+ output = buildInsufficientSignalOutput({
1551
+ presetName: request.presetName,
1552
+ originalLength: capture.getTotalChars(),
1553
+ truncatedApplied: capture.wasTruncated(),
1554
+ exitCode
1555
+ });
1556
+ }
1080
1557
  process.stdout.write(`${output}
1081
1558
  `);
1082
1559
  if (request.failOn && !request.dryRun && exitCode === 0 && supportsFailOnPreset(request.presetName) && evaluateGate({
@@ -1386,6 +1863,12 @@ function resolveConfig(options = {}) {
1386
1863
  return siftConfigSchema.parse(merged);
1387
1864
  }
1388
1865
  export {
1866
+ BoundedCapture,
1867
+ buildCommandPreview,
1868
+ getExecSuccessShortcut,
1869
+ looksInteractivePrompt,
1870
+ mergeDefined,
1871
+ normalizeChildExitCode,
1389
1872
  resolveConfig,
1390
1873
  runExec,
1391
1874
  runSift