@bilalimamoglu/sift 0.4.4 → 0.4.5

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
@@ -109,6 +109,7 @@ var defaultConfig = {
109
109
  tailChars: 2e4
110
110
  },
111
111
  runtime: {
112
+ operationMode: "agent-escalation",
112
113
  rawFallback: true,
113
114
  verbose: false
114
115
  },
@@ -158,12 +159,62 @@ var defaultConfig = {
158
159
  }
159
160
  };
160
161
 
162
+ // src/config/provider-models.ts
163
+ var OPENAI_MODELS = [
164
+ {
165
+ model: "gpt-5-nano",
166
+ label: "gpt-5-nano",
167
+ note: "default, cheapest, fast enough for most fallback passes",
168
+ isDefault: true
169
+ },
170
+ {
171
+ model: "gpt-5.4-nano",
172
+ label: "gpt-5.4-nano",
173
+ note: "newer nano backup, a touch smarter, a touch pricier"
174
+ },
175
+ {
176
+ model: "gpt-5-mini",
177
+ label: "gpt-5-mini",
178
+ note: "smarter fallback, still saner than the expensive stuff"
179
+ }
180
+ ];
181
+ var OPENROUTER_MODELS = [
182
+ {
183
+ model: "openrouter/free",
184
+ label: "openrouter/free",
185
+ note: "default, free, a little slower sometimes, still hard to argue with free",
186
+ isDefault: true
187
+ },
188
+ {
189
+ model: "qwen/qwen3-coder:free",
190
+ label: "qwen/qwen3-coder:free",
191
+ note: "free, code-focused, good when you want a named coding fallback"
192
+ },
193
+ {
194
+ model: "deepseek/deepseek-r1:free",
195
+ label: "deepseek/deepseek-r1:free",
196
+ note: "free, stronger reasoning, usually slower"
197
+ }
198
+ ];
199
+ function getProviderModelOptions(provider) {
200
+ return provider === "openrouter" ? OPENROUTER_MODELS : OPENAI_MODELS;
201
+ }
202
+ function getDefaultProviderModel(provider) {
203
+ return getProviderModelOptions(provider).find((option) => option.isDefault)?.model ?? getProviderModelOptions(provider)[0]?.model ?? "";
204
+ }
205
+ function findProviderModelOption(provider, model) {
206
+ if (!model) {
207
+ return void 0;
208
+ }
209
+ return getProviderModelOptions(provider).find((option) => option.model === model);
210
+ }
211
+
161
212
  // src/config/native-provider.ts
162
213
  function getNativeProviderDefaults(provider) {
163
214
  if (provider === "openrouter") {
164
215
  return {
165
216
  provider,
166
- model: "openrouter/free",
217
+ model: getDefaultProviderModel("openrouter"),
167
218
  baseUrl: "https://openrouter.ai/api/v1"
168
219
  };
169
220
  }
@@ -305,6 +356,11 @@ function getProviderApiKeyEnvNames(provider, baseUrl) {
305
356
 
306
357
  // src/config/schema.ts
307
358
  import { z } from "zod";
359
+ var operationModeSchema = z.enum([
360
+ "agent-escalation",
361
+ "provider-assisted",
362
+ "local-only"
363
+ ]);
308
364
  var providerNameSchema = z.enum([
309
365
  "openai",
310
366
  "openai-compatible",
@@ -357,6 +413,7 @@ var inputConfigSchema = z.object({
357
413
  tailChars: z.number().int().positive()
358
414
  });
359
415
  var runtimeConfigSchema = z.object({
416
+ operationMode: operationModeSchema,
360
417
  rawFallback: z.boolean(),
361
418
  verbose: z.boolean()
362
419
  });
@@ -379,7 +436,7 @@ var siftConfigSchema = z.object({
379
436
  var PROVIDER_DEFAULT_OVERRIDES = {
380
437
  openrouter: {
381
438
  provider: {
382
- model: "openrouter/free",
439
+ model: getDefaultProviderModel("openrouter"),
383
440
  baseUrl: "https://openrouter.ai/api/v1"
384
441
  }
385
442
  }
@@ -419,13 +476,16 @@ function stripApiKey(overrides) {
419
476
  }
420
477
  function buildNonCredentialEnvOverrides(env) {
421
478
  const overrides = {};
422
- if (env.SIFT_PROVIDER || env.SIFT_MODEL || env.SIFT_BASE_URL || env.SIFT_TIMEOUT_MS) {
479
+ if (env.SIFT_PROVIDER || env.SIFT_MODEL || env.SIFT_BASE_URL || env.SIFT_TIMEOUT_MS || env.SIFT_OPERATION_MODE) {
423
480
  overrides.provider = {
424
481
  provider: env.SIFT_PROVIDER,
425
482
  model: env.SIFT_MODEL,
426
483
  baseUrl: env.SIFT_BASE_URL,
427
484
  timeoutMs: env.SIFT_TIMEOUT_MS ? Number(env.SIFT_TIMEOUT_MS) : void 0
428
485
  };
486
+ overrides.runtime = {
487
+ operationMode: env.SIFT_OPERATION_MODE
488
+ };
429
489
  }
430
490
  if (env.SIFT_MAX_INPUT_CHARS || env.SIFT_MAX_CAPTURE_CHARS) {
431
491
  overrides.input = {
@@ -492,6 +552,15 @@ function resolveConfig(options = {}) {
492
552
  );
493
553
  return siftConfigSchema.parse(merged);
494
554
  }
555
+ function hasUsableProvider(config) {
556
+ return config.provider.apiKey !== void 0 && config.provider.apiKey.trim().length > 0;
557
+ }
558
+ function resolveEffectiveOperationMode(config) {
559
+ if (config.runtime.operationMode === "provider-assisted") {
560
+ return hasUsableProvider(config) ? "provider-assisted" : "agent-escalation";
561
+ }
562
+ return config.runtime.operationMode;
563
+ }
495
564
 
496
565
  // src/config/write.ts
497
566
  import fs3 from "fs";
@@ -616,10 +685,129 @@ import { emitKeypressEvents } from "readline";
616
685
  import { createInterface } from "readline/promises";
617
686
  import { stderr as defaultStderr, stdin as defaultStdin2, stdout as defaultStdout } from "process";
618
687
 
688
+ // src/config/operation-mode.ts
689
+ function getOperationModeLabel(mode) {
690
+ switch (mode) {
691
+ case "agent-escalation":
692
+ return "Agent escalation";
693
+ case "provider-assisted":
694
+ return "Provider-assisted";
695
+ case "local-only":
696
+ return "Local-only";
697
+ }
698
+ }
699
+ function describeOperationMode(mode) {
700
+ switch (mode) {
701
+ case "agent-escalation":
702
+ return "Best when you already have an agent open. sift does the quick first pass, then the agent can read code, tests, or logs and keep going.";
703
+ case "provider-assisted":
704
+ return "Best when you want sift itself to take one more cheap swing at the problem before you fall back to an agent or raw logs. Built-in rules first, API-backed backup second.";
705
+ case "local-only":
706
+ return "Best when you are not pairing sift with another agent and do not want API keys. Everything stays local.";
707
+ }
708
+ }
709
+ function describeInsufficientBehavior(mode) {
710
+ switch (mode) {
711
+ case "agent-escalation":
712
+ return "If sift is still not enough, that is the handoff point: log it, narrow the problem, and let the agent keep digging.";
713
+ case "provider-assisted":
714
+ return "If the first pass is still fuzzy, sift can ask the fallback model so you do not have to escalate every annoying edge case by hand.";
715
+ case "local-only":
716
+ return "If sift is still not enough, stay local: rerun something narrower, use a better preset, or read the relevant source and tests yourself.";
717
+ }
718
+ }
719
+
619
720
  // src/ui/terminal.ts
620
721
  import { execFileSync } from "child_process";
621
722
  import { clearScreenDown, cursorTo, moveCursor } from "readline";
622
723
  import { stdin as defaultStdin } from "process";
724
+ var PROMPT_BACK = "__sift_back__";
725
+ var PROMPT_BACK_LABEL = "\u2190 Back";
726
+ function color(text, rgb, args = {}) {
727
+ const codes = [];
728
+ if (args.bold) {
729
+ codes.push("1");
730
+ }
731
+ if (args.dim) {
732
+ codes.push("2");
733
+ }
734
+ codes.push(`38;2;${rgb[0]};${rgb[1]};${rgb[2]}`);
735
+ return `\x1B[${codes.join(";")}m${text}\x1B[0m`;
736
+ }
737
+ function splitOptionLeading(option) {
738
+ const boundaries = [" - ", ": ", ":", " ("].map((token) => {
739
+ const index = option.indexOf(token);
740
+ return index >= 0 ? { index, token } : void 0;
741
+ }).filter((entry) => Boolean(entry)).sort((left, right) => left.index - right.index);
742
+ const boundary = boundaries[0];
743
+ if (!boundary) {
744
+ return { leading: option, trailing: "" };
745
+ }
746
+ return {
747
+ leading: option.slice(0, boundary.index),
748
+ trailing: option.slice(boundary.index)
749
+ };
750
+ }
751
+ function getOptionPalette(leading) {
752
+ const normalized = leading.trim();
753
+ if (normalized.startsWith("With an agent")) {
754
+ return { rgb: [214, 168, 76] };
755
+ }
756
+ if (normalized.startsWith("With provider fallback")) {
757
+ return { rgb: [100, 141, 214] };
758
+ }
759
+ if (normalized.startsWith("Solo, local-only")) {
760
+ return { rgb: [122, 142, 116], dimWhenIdle: true };
761
+ }
762
+ if (normalized.startsWith("Codex")) {
763
+ return { rgb: [233, 183, 78] };
764
+ }
765
+ if (normalized.startsWith("Claude")) {
766
+ return { rgb: [171, 138, 224] };
767
+ }
768
+ if (normalized === "All") {
769
+ return { rgb: [95, 181, 201] };
770
+ }
771
+ if (normalized.startsWith("Global")) {
772
+ return { rgb: [205, 168, 83] };
773
+ }
774
+ if (normalized.startsWith("Local")) {
775
+ return { rgb: [138, 144, 150], dimWhenIdle: true };
776
+ }
777
+ if (normalized.startsWith("OpenAI")) {
778
+ return { rgb: [82, 177, 124] };
779
+ }
780
+ if (normalized.startsWith("OpenRouter")) {
781
+ return { rgb: [106, 144, 221] };
782
+ }
783
+ if (normalized.startsWith("Use saved key") || normalized.startsWith("Use existing key")) {
784
+ return { rgb: [111, 181, 123] };
785
+ }
786
+ if (normalized.startsWith("Use environment key")) {
787
+ return { rgb: [102, 146, 219] };
788
+ }
789
+ if (normalized.startsWith("Enter a different key") || normalized.startsWith("Custom model")) {
790
+ return { rgb: [191, 157, 92], dimWhenIdle: true };
791
+ }
792
+ return void 0;
793
+ }
794
+ function styleOption(option, selected, colorize) {
795
+ if (!colorize) {
796
+ return option;
797
+ }
798
+ if (option === PROMPT_BACK_LABEL) {
799
+ return color(option, [164, 169, 178], { bold: selected, dim: !selected });
800
+ }
801
+ const { leading, trailing } = splitOptionLeading(option);
802
+ const palette = getOptionPalette(leading);
803
+ if (!palette) {
804
+ return option;
805
+ }
806
+ return `${color(leading, palette.rgb, {
807
+ bold: selected,
808
+ dim: !selected && Boolean(palette.dimWhenIdle)
809
+ })}${trailing}`;
810
+ }
623
811
  function setPosixEcho(enabled) {
624
812
  const command = enabled ? "echo" : "-echo";
625
813
  try {
@@ -637,10 +825,15 @@ function setPosixEcho(enabled) {
637
825
  }
638
826
  }
639
827
  function renderSelectionBlock(args) {
828
+ const options = args.allowBack ? [...args.options, args.backLabel ?? PROMPT_BACK_LABEL] : args.options;
640
829
  return [
641
- `${args.prompt} (use \u2191/\u2193 and Enter)`,
642
- ...args.options.map(
643
- (option, index) => `${index === args.selectedIndex ? "\u203A" : " "} ${option}${index === args.selectedIndex ? " (selected)" : ""}`
830
+ `${args.prompt}${args.allowBack ? " (use \u2191/\u2193 to move, Enter to select, Esc to go back)" : " (use \u2191/\u2193 and Enter)"}`,
831
+ ...options.map(
832
+ (option, index) => `${index === args.selectedIndex ? "\u203A" : " "} ${styleOption(
833
+ option,
834
+ index === args.selectedIndex,
835
+ Boolean(args.colorize)
836
+ )}${index === args.selectedIndex ? " (selected)" : ""}`
644
837
  )
645
838
  ];
646
839
  }
@@ -648,6 +841,8 @@ async function promptSelect(args) {
648
841
  const { input, output, prompt, options } = args;
649
842
  const stream = output;
650
843
  const selectedLabel = args.selectedLabel ?? prompt;
844
+ const backLabel = args.backLabel ?? PROMPT_BACK_LABEL;
845
+ const allOptions = args.allowBack ? [...options, backLabel] : options;
651
846
  let index = 0;
652
847
  let previousLineCount = 0;
653
848
  const render = () => {
@@ -659,7 +854,10 @@ async function promptSelect(args) {
659
854
  const lines = renderSelectionBlock({
660
855
  prompt,
661
856
  options,
662
- selectedIndex: index
857
+ selectedIndex: index,
858
+ allowBack: args.allowBack,
859
+ backLabel,
860
+ colorize: Boolean(stream?.isTTY)
663
861
  });
664
862
  output.write(`${lines.join("\n")}
665
863
  `);
@@ -691,22 +889,30 @@ async function promptSelect(args) {
691
889
  return;
692
890
  }
693
891
  if (key.name === "up") {
694
- index = index === 0 ? options.length - 1 : index - 1;
892
+ index = index === 0 ? allOptions.length - 1 : index - 1;
695
893
  render();
696
894
  return;
697
895
  }
698
896
  if (key.name === "down") {
699
- index = (index + 1) % options.length;
897
+ index = (index + 1) % allOptions.length;
700
898
  render();
701
899
  return;
702
900
  }
901
+ if (args.allowBack && key.name === "escape") {
902
+ input.off("keypress", onKeypress);
903
+ cleanup();
904
+ input.setRawMode?.(wasRaw);
905
+ input.pause?.();
906
+ resolve(PROMPT_BACK);
907
+ return;
908
+ }
703
909
  if (key.name === "return" || key.name === "enter") {
704
- const selected = options[index] ?? options[0] ?? "";
910
+ const selected = allOptions[index] ?? allOptions[0] ?? "";
705
911
  input.off("keypress", onKeypress);
706
- cleanup(selected);
912
+ cleanup(selected === backLabel ? void 0 : selected);
707
913
  input.setRawMode?.(wasRaw);
708
914
  input.pause?.();
709
- resolve(selected);
915
+ resolve(selected === backLabel ? PROMPT_BACK : selected);
710
916
  }
711
917
  };
712
918
  input.on("keypress", onKeypress);
@@ -739,6 +945,13 @@ async function promptSecret(args) {
739
945
  reject(new Error("Aborted."));
740
946
  return;
741
947
  }
948
+ if (args.allowBack && key.name === "escape") {
949
+ input.off("keypress", onKeypress);
950
+ restoreInputState();
951
+ output.write("\n");
952
+ resolve(PROMPT_BACK);
953
+ return;
954
+ }
742
955
  if (key.name === "return" || key.name === "enter") {
743
956
  input.off("keypress", onKeypress);
744
957
  restoreInputState();
@@ -759,6 +972,7 @@ async function promptSecret(args) {
759
972
  }
760
973
 
761
974
  // src/commands/config-setup.ts
975
+ var CONFIG_SETUP_BACK = 2;
762
976
  function createTerminalIO() {
763
977
  let rl;
764
978
  function getInterface() {
@@ -771,22 +985,24 @@ function createTerminalIO() {
771
985
  }
772
986
  return rl;
773
987
  }
774
- async function select(prompt, options) {
988
+ async function select(prompt, options, selectedLabel, allowBack) {
775
989
  emitKeypressEvents(defaultStdin2);
776
990
  return await promptSelect({
777
991
  input: defaultStdin2,
778
992
  output: defaultStdout,
779
993
  prompt,
780
994
  options,
781
- selectedLabel: "Provider"
995
+ selectedLabel,
996
+ allowBack
782
997
  });
783
998
  }
784
- async function secret(prompt) {
999
+ async function secret(prompt, allowBack) {
785
1000
  emitKeypressEvents(defaultStdin2);
786
1001
  return await promptSecret({
787
1002
  input: defaultStdin2,
788
1003
  output: defaultStdout,
789
- prompt
1004
+ prompt,
1005
+ allowBack
790
1006
  });
791
1007
  }
792
1008
  return {
@@ -817,12 +1033,58 @@ function getSetupPresenter(io) {
817
1033
  function getProviderLabel(provider) {
818
1034
  return provider === "openrouter" ? "OpenRouter" : "OpenAI";
819
1035
  }
1036
+ function isBackSelection(value) {
1037
+ return value === PROMPT_BACK || value === PROMPT_BACK_LABEL;
1038
+ }
1039
+ async function promptForOperationMode(io) {
1040
+ if (io.select) {
1041
+ const choice = await io.select(
1042
+ "Choose how sift should work",
1043
+ [
1044
+ "With an agent: recommended if Codex or Claude is already with you; sift does the fast local first pass, the agent only steps in when repo context is truly needed",
1045
+ "With provider fallback: recommended if you want sift to finish more ambiguous cases on its own before handing them back to you or your agent; requires an API key, cheap model only when needed",
1046
+ "Solo, local-only: recommended if you want zero model calls; great for supported presets, ambiguous cases stay with you"
1047
+ ],
1048
+ "Mode"
1049
+ );
1050
+ if (choice.startsWith("With an agent")) {
1051
+ return "agent-escalation";
1052
+ }
1053
+ if (choice.startsWith("With provider fallback")) {
1054
+ return "provider-assisted";
1055
+ }
1056
+ if (choice.startsWith("Solo, local-only")) {
1057
+ return "local-only";
1058
+ }
1059
+ }
1060
+ while (true) {
1061
+ const answer = (await io.ask("Use style [agent/provider/local]: ")).trim().toLowerCase();
1062
+ if (answer === "" || answer === "agent" || answer === "agent-escalation") {
1063
+ return "agent-escalation";
1064
+ }
1065
+ if (answer === "provider" || answer === "provider-assisted") {
1066
+ return "provider-assisted";
1067
+ }
1068
+ if (answer === "local" || answer === "local-only") {
1069
+ return "local-only";
1070
+ }
1071
+ io.error("Please answer agent, provider, or local.\n");
1072
+ }
1073
+ }
820
1074
  async function promptForProvider(io) {
821
1075
  if (io.select) {
822
- const choice = await io.select("Select provider for this machine", [
823
- "OpenAI",
824
- "OpenRouter"
825
- ]);
1076
+ const choice = await io.select(
1077
+ "Okay, whose API key are we borrowing for fallback duty?",
1078
+ [
1079
+ "OpenAI",
1080
+ "OpenRouter"
1081
+ ],
1082
+ "Provider",
1083
+ true
1084
+ );
1085
+ if (isBackSelection(choice)) {
1086
+ return PROMPT_BACK;
1087
+ }
826
1088
  if (choice === "OpenAI") {
827
1089
  return "openai";
828
1090
  }
@@ -832,6 +1094,9 @@ async function promptForProvider(io) {
832
1094
  }
833
1095
  while (true) {
834
1096
  const answer = (await io.ask("Provider [OpenAI/OpenRouter]: ")).trim().toLowerCase();
1097
+ if (answer === "back" || answer === "b") {
1098
+ return PROMPT_BACK;
1099
+ }
835
1100
  if (answer === "" || answer === "openai") {
836
1101
  return "openai";
837
1102
  }
@@ -846,7 +1111,13 @@ async function promptForApiKey(io, provider) {
846
1111
  const promptText = `Enter your ${providerLabel} API key (input hidden): `;
847
1112
  const visiblePromptText = `Enter your ${providerLabel} API key: `;
848
1113
  while (true) {
849
- const answer = (await (io.secret ? io.secret(promptText) : io.ask(visiblePromptText))).trim();
1114
+ const answer = (await (io.secret ? io.secret(promptText, true) : io.ask(visiblePromptText))).trim();
1115
+ if (answer === PROMPT_BACK) {
1116
+ return PROMPT_BACK;
1117
+ }
1118
+ if (!io.secret && (answer.toLowerCase() === "back" || answer.toLowerCase() === "b")) {
1119
+ return PROMPT_BACK;
1120
+ }
850
1121
  if (answer.length > 0) {
851
1122
  return answer;
852
1123
  }
@@ -862,17 +1133,25 @@ async function promptForApiKeyChoice(args) {
862
1133
  if (args.io.select) {
863
1134
  const choice = await args.io.select(
864
1135
  `Found both a saved ${providerLabel} API key and ${args.envName} in your environment`,
865
- ["Use saved key", "Use env key", "Override"]
1136
+ ["Use saved key", "Use environment key", "Enter a different key"],
1137
+ "API key",
1138
+ true
866
1139
  );
1140
+ if (isBackSelection(choice)) {
1141
+ return PROMPT_BACK;
1142
+ }
867
1143
  if (choice === "Use saved key") {
868
1144
  return "saved";
869
1145
  }
870
- if (choice === "Use env key") {
1146
+ if (choice === "Use environment key") {
871
1147
  return "env";
872
1148
  }
873
1149
  }
874
1150
  while (true) {
875
1151
  const answer = (await args.io.ask("API key choice [saved/env/override]: ")).trim().toLowerCase();
1152
+ if (answer === "back" || answer === "b") {
1153
+ return PROMPT_BACK;
1154
+ }
876
1155
  if (answer === "" || answer === "saved") {
877
1156
  return "saved";
878
1157
  }
@@ -889,15 +1168,23 @@ async function promptForApiKeyChoice(args) {
889
1168
  if (args.io.select) {
890
1169
  const choice = await args.io.select(
891
1170
  `Found an existing ${providerLabel} API key via ${sourceLabel}`,
892
- ["Use existing key", "Override"]
1171
+ ["Use saved key", "Enter a different key"],
1172
+ "API key",
1173
+ true
893
1174
  );
894
- if (choice === "Override") {
1175
+ if (isBackSelection(choice)) {
1176
+ return PROMPT_BACK;
1177
+ }
1178
+ if (choice === "Enter a different key") {
895
1179
  return "override";
896
1180
  }
897
1181
  return args.hasSavedKey ? "saved" : "env";
898
1182
  }
899
1183
  while (true) {
900
1184
  const answer = (await args.io.ask("API key choice [existing/override]: ")).trim().toLowerCase();
1185
+ if (answer === "back" || answer === "b") {
1186
+ return PROMPT_BACK;
1187
+ }
901
1188
  if (answer === "" || answer === "existing") {
902
1189
  return args.hasSavedKey ? "saved" : "env";
903
1190
  }
@@ -907,12 +1194,41 @@ async function promptForApiKeyChoice(args) {
907
1194
  args.io.error("Please answer existing or override.\n");
908
1195
  }
909
1196
  }
910
- function writeSetupSuccess(io, writtenPath) {
1197
+ function writeModeSummary(io, mode) {
1198
+ const ui = getSetupPresenter(io);
1199
+ io.write(`${ui.info(`Operating mode: ${getOperationModeLabel(mode)}`)}
1200
+ `);
1201
+ io.write(`${ui.note(describeOperationMode(mode))}
1202
+ `);
1203
+ io.write(`${ui.note(describeInsufficientBehavior(mode))}
1204
+ `);
1205
+ if (mode === "agent-escalation") {
1206
+ io.write(
1207
+ `${ui.note("Plain English: pick this if you already use Codex or Claude. sift gives the first answer; the agent only steps in when deeper repo context is really needed. No API key.")}
1208
+ `
1209
+ );
1210
+ return;
1211
+ }
1212
+ if (mode === "provider-assisted") {
1213
+ io.write(
1214
+ `${ui.note("Plain English: pick this if you want sift itself to finish more fuzzy cases before you have to step in or re-prompt an agent. Yes, that means an API key, but the model is intentionally the cheap backup, not the fancy main act.")}
1215
+ `
1216
+ );
1217
+ return;
1218
+ }
1219
+ io.write(
1220
+ `${ui.note("Plain English: pick this if sift is working alone. No API key, no model fallback. If the answer is still fuzzy, you inspect the code or logs yourself.")}
1221
+ `
1222
+ );
1223
+ }
1224
+ function writeSetupSuccess(io, writtenPath, mode) {
911
1225
  const ui = getSetupPresenter(io);
912
1226
  io.write(`
913
1227
  ${ui.success("You're set.")}
914
1228
  `);
915
1229
  io.write(`${ui.info(`Machine-wide config: ${writtenPath}`)}
1230
+ `);
1231
+ io.write(`${ui.labelValue("operation mode", getOperationModeLabel(mode))}
916
1232
  `);
917
1233
  io.write(`${ui.note("sift is ready to use from any terminal on this machine.")}
918
1234
  `);
@@ -928,31 +1244,114 @@ function writeOverrideWarning(io, activeConfigPath) {
928
1244
  `
929
1245
  );
930
1246
  }
931
- function writeNextSteps(io) {
1247
+ function writeNextSteps(io, mode) {
932
1248
  const ui = getSetupPresenter(io);
933
1249
  io.write(`
934
1250
  ${ui.section("Try next")}
935
1251
  `);
936
1252
  io.write(` ${ui.command("sift doctor")}
937
1253
  `);
1254
+ if (mode === "provider-assisted") {
1255
+ io.write(` ${ui.command("sift config show --show-secrets")}
1256
+ `);
1257
+ } else {
1258
+ io.write(
1259
+ ` ${ui.command("sift config show")}${ui.note(" # rerun setup later if you want provider-assisted fallback")}
1260
+ `
1261
+ );
1262
+ }
938
1263
  io.write(` ${ui.command("sift exec --preset test-status -- npm test")}
939
1264
  `);
940
1265
  }
941
- function writeProviderDefaults(io, provider) {
1266
+ async function promptForProviderModel(args) {
1267
+ const options = getProviderModelOptions(args.provider);
1268
+ const customCurrent = args.currentModel && !findProviderModelOption(args.provider, args.currentModel) ? `Keep current custom model (${args.currentModel})` : "Custom model";
1269
+ if (args.io.select) {
1270
+ const labels = options.map((option) => `${option.label} - ${option.note}`);
1271
+ labels.push(customCurrent);
1272
+ const choice = await args.io.select(
1273
+ "Pick the fallback model. Cheap is usually the right answer here; this only wakes up when sift needs help.",
1274
+ labels,
1275
+ "Model",
1276
+ true
1277
+ );
1278
+ if (isBackSelection(choice)) {
1279
+ return PROMPT_BACK;
1280
+ }
1281
+ const match = options.find((option) => choice.startsWith(option.label));
1282
+ if (match) {
1283
+ return match.model;
1284
+ }
1285
+ if (customCurrent.startsWith("Keep current custom model")) {
1286
+ return args.currentModel ?? getDefaultProviderModel(args.provider);
1287
+ }
1288
+ } else {
1289
+ args.io.write("\nPick the fallback model.\n\n");
1290
+ options.forEach((option, index) => {
1291
+ args.io.write(` ${index + 1}) ${option.label} - ${option.note}
1292
+ `);
1293
+ });
1294
+ args.io.write(` ${options.length + 1}) ${customCurrent}
1295
+
1296
+ `);
1297
+ while (true) {
1298
+ const answer = (await args.io.ask("Model choice [1]: ")).trim();
1299
+ if (answer.toLowerCase() === "back" || answer.toLowerCase() === "b") {
1300
+ return PROMPT_BACK;
1301
+ }
1302
+ if (answer === "") {
1303
+ return options[0].model;
1304
+ }
1305
+ const index = Number(answer);
1306
+ if (Number.isInteger(index) && index >= 1 && index <= options.length) {
1307
+ return options[index - 1].model;
1308
+ }
1309
+ if (Number.isInteger(index) && index === options.length + 1) {
1310
+ break;
1311
+ }
1312
+ args.io.error(`Please enter a number between 1 and ${options.length + 1}.
1313
+ `);
1314
+ }
1315
+ }
1316
+ while (true) {
1317
+ const answer = (await args.io.ask("Custom model id: ")).trim();
1318
+ if (answer.toLowerCase() === "back" || answer.toLowerCase() === "b") {
1319
+ return PROMPT_BACK;
1320
+ }
1321
+ if (answer.length > 0) {
1322
+ return answer;
1323
+ }
1324
+ args.io.error("Model id cannot be empty.\n");
1325
+ }
1326
+ }
1327
+ function writeProviderDefaults(io, provider, selectedModel) {
942
1328
  const ui = getSetupPresenter(io);
1329
+ const options = getProviderModelOptions(provider);
943
1330
  if (provider === "openrouter") {
944
- io.write(`${ui.info("Using OpenRouter defaults for your first run.")}
1331
+ io.write(`${ui.info("OpenRouter fallback it is. Free is lovely right up until latency develops a personality.")}
945
1332
  `);
946
- io.write(`${ui.labelValue("Default model", "openrouter/free")}
1333
+ io.write(`${ui.labelValue("Default model", getDefaultProviderModel("openrouter"))}
947
1334
  `);
948
1335
  io.write(`${ui.labelValue("Default base URL", "https://openrouter.ai/api/v1")}
949
1336
  `);
950
1337
  } else {
951
- io.write(`${ui.info("Using OpenAI defaults for your first run.")}
1338
+ io.write(`${ui.info("OpenAI fallback it is. Start cheap, save the fancy stuff for when the logs deserve it.")}
952
1339
  `);
953
- io.write(`${ui.labelValue("Default model", "gpt-5-nano")}
1340
+ io.write(`${ui.labelValue("Default model", getDefaultProviderModel("openai"))}
954
1341
  `);
955
1342
  io.write(`${ui.labelValue("Default base URL", "https://api.openai.com/v1")}
1343
+ `);
1344
+ }
1345
+ io.write(`${ui.labelValue("Selected model", selectedModel)}
1346
+ `);
1347
+ io.write(
1348
+ `${ui.note("This fallback only wakes up when sift's own rules are not enough. The idea is fewer dead ends, not paying for a second opinion on every command.")}
1349
+ `
1350
+ );
1351
+ io.write(`${ui.note("Popular alternatives:")}
1352
+ `);
1353
+ for (const option of options.filter((option2) => option2.model !== selectedModel)) {
1354
+ io.write(` ${ui.command(option.label)}${ui.note(` # ${option.note}`)}
956
1355
  `);
957
1356
  }
958
1357
  io.write(
@@ -964,54 +1363,101 @@ function writeProviderDefaults(io, provider) {
964
1363
  `
965
1364
  );
966
1365
  }
967
- function materializeProfile(provider, profile, apiKey) {
1366
+ function materializeProfile(provider, profile, overrides = {}) {
968
1367
  return {
1368
+ ...profile,
969
1369
  ...getProfileProviderState(provider, profile),
970
- ...apiKey !== void 0 ? { apiKey } : {}
1370
+ ...overrides.model !== void 0 ? { model: overrides.model } : {},
1371
+ ...overrides.apiKey !== void 0 ? { apiKey: overrides.apiKey } : {}
971
1372
  };
972
1373
  }
973
1374
  function buildSetupConfig(args) {
974
1375
  const preservedConfig = preserveActiveNativeProviderProfile(args.config);
1376
+ if (args.mode !== "provider-assisted") {
1377
+ return {
1378
+ ...preservedConfig,
1379
+ runtime: {
1380
+ ...preservedConfig.runtime,
1381
+ operationMode: args.mode
1382
+ }
1383
+ };
1384
+ }
1385
+ if (!args.provider || !args.apiKeyChoice) {
1386
+ throw new Error("Provider-assisted setup requires provider and API key choice.");
1387
+ }
975
1388
  const storedProfile = getStoredProviderProfile(preservedConfig, args.provider);
976
1389
  if (args.apiKeyChoice === "saved") {
977
1390
  const profile2 = materializeProfile(
978
1391
  args.provider,
979
1392
  storedProfile,
980
- storedProfile?.apiKey ?? ""
1393
+ {
1394
+ apiKey: storedProfile?.apiKey ?? "",
1395
+ model: args.model
1396
+ }
981
1397
  );
982
1398
  const configWithProfile2 = setStoredProviderProfile(
983
1399
  preservedConfig,
984
1400
  args.provider,
985
1401
  profile2
986
1402
  );
987
- return applyActiveProvider(
1403
+ const applied2 = applyActiveProvider(
988
1404
  configWithProfile2,
989
1405
  args.provider,
990
1406
  profile2,
991
1407
  profile2.apiKey ?? ""
992
1408
  );
1409
+ return {
1410
+ ...applied2,
1411
+ runtime: {
1412
+ ...applied2.runtime,
1413
+ operationMode: args.mode
1414
+ }
1415
+ };
993
1416
  }
994
1417
  if (args.apiKeyChoice === "env") {
995
- const profile2 = storedProfile ? storedProfile : materializeProfile(args.provider, void 0);
996
- const configWithProfile2 = storedProfile ? preservedConfig : setStoredProviderProfile(preservedConfig, args.provider, profile2);
997
- return applyActiveProvider(configWithProfile2, args.provider, profile2, "");
1418
+ const profile2 = materializeProfile(args.provider, storedProfile, {
1419
+ model: args.model
1420
+ });
1421
+ const configWithProfile2 = setStoredProviderProfile(
1422
+ preservedConfig,
1423
+ args.provider,
1424
+ profile2
1425
+ );
1426
+ const applied2 = applyActiveProvider(configWithProfile2, args.provider, profile2, "");
1427
+ return {
1428
+ ...applied2,
1429
+ runtime: {
1430
+ ...applied2.runtime,
1431
+ operationMode: args.mode
1432
+ }
1433
+ };
998
1434
  }
999
1435
  const profile = materializeProfile(
1000
1436
  args.provider,
1001
1437
  storedProfile,
1002
- args.nextApiKey ?? ""
1438
+ {
1439
+ apiKey: args.nextApiKey ?? "",
1440
+ model: args.model
1441
+ }
1003
1442
  );
1004
1443
  const configWithProfile = setStoredProviderProfile(
1005
1444
  preservedConfig,
1006
1445
  args.provider,
1007
1446
  profile
1008
1447
  );
1009
- return applyActiveProvider(
1448
+ const applied = applyActiveProvider(
1010
1449
  configWithProfile,
1011
1450
  args.provider,
1012
1451
  profile,
1013
1452
  args.nextApiKey ?? ""
1014
1453
  );
1454
+ return {
1455
+ ...applied,
1456
+ runtime: {
1457
+ ...applied.runtime,
1458
+ operationMode: args.mode
1459
+ }
1460
+ };
1015
1461
  }
1016
1462
  async function configSetup(options = {}) {
1017
1463
  void options.global;
@@ -1025,29 +1471,119 @@ async function configSetup(options = {}) {
1025
1471
  );
1026
1472
  return 1;
1027
1473
  }
1028
- io.write(`${ui.welcome("Let's keep the expensive model for the interesting bits.")}
1474
+ if (!options.embedded) {
1475
+ io.write(`${ui.welcome("Let's keep the expensive model for the interesting bits.")}
1476
+ `);
1477
+ io.write(`${ui.note('"Sharp first, expensive later."')}
1478
+ `);
1479
+ } else {
1480
+ io.write(`${ui.info("Next: provider, model, and credentials. Press Esc any time if you want to step back.")}
1029
1481
  `);
1482
+ }
1030
1483
  const resolvedPath = resolveSetupPath(options.targetPath);
1031
1484
  const { config: existingConfig, existed } = loadEditableConfig(resolvedPath);
1032
1485
  if (existed) {
1033
1486
  io.write(`${ui.info(`Updating existing config at ${resolvedPath}.`)}
1034
1487
  `);
1035
1488
  }
1036
- const provider = await promptForProvider(io);
1037
- writeProviderDefaults(io, provider);
1038
- const storedProfile = getStoredProviderProfile(existingConfig, provider);
1039
- const envName = getNativeProviderApiKeyEnvName(provider);
1040
- const apiKeyChoice = await promptForApiKeyChoice({
1041
- io,
1042
- provider,
1043
- envName,
1044
- hasSavedKey: Boolean(storedProfile?.apiKey),
1045
- hasEnvKey: Boolean(env[envName])
1046
- });
1047
- const nextApiKey = apiKeyChoice === "override" ? await promptForApiKey(io, provider) : void 0;
1489
+ let mode = options.forcedMode ?? await promptForOperationMode(io);
1490
+ let provider;
1491
+ let model;
1492
+ let apiKeyChoice;
1493
+ let nextApiKey;
1494
+ let modeSummaryShown = false;
1495
+ while (true) {
1496
+ if (!modeSummaryShown) {
1497
+ writeModeSummary(io, mode);
1498
+ modeSummaryShown = true;
1499
+ }
1500
+ if (mode !== "provider-assisted") {
1501
+ io.write(
1502
+ `${ui.note("No provider credentials are required for this mode. You can switch later by running `sift config setup` again.")}
1503
+ `
1504
+ );
1505
+ break;
1506
+ }
1507
+ let providerStep = "provider";
1508
+ while (true) {
1509
+ if (providerStep === "provider") {
1510
+ const providerChoice = await promptForProvider(io);
1511
+ if (providerChoice === PROMPT_BACK) {
1512
+ if (options.forcedMode) {
1513
+ return options.embedded ? CONFIG_SETUP_BACK : 1;
1514
+ }
1515
+ mode = await promptForOperationMode(io);
1516
+ modeSummaryShown = false;
1517
+ provider = void 0;
1518
+ model = void 0;
1519
+ apiKeyChoice = void 0;
1520
+ nextApiKey = void 0;
1521
+ break;
1522
+ }
1523
+ provider = providerChoice;
1524
+ providerStep = "model";
1525
+ continue;
1526
+ }
1527
+ const storedProfile = getStoredProviderProfile(existingConfig, provider);
1528
+ if (providerStep === "model") {
1529
+ const modelChoice = await promptForProviderModel({
1530
+ io,
1531
+ provider,
1532
+ currentModel: storedProfile?.model
1533
+ });
1534
+ if (modelChoice === PROMPT_BACK) {
1535
+ providerStep = "provider";
1536
+ continue;
1537
+ }
1538
+ model = modelChoice;
1539
+ writeProviderDefaults(io, provider, model);
1540
+ providerStep = "api-key-choice";
1541
+ continue;
1542
+ }
1543
+ const envName = getNativeProviderApiKeyEnvName(provider);
1544
+ if (providerStep === "api-key-choice") {
1545
+ const keyChoice = await promptForApiKeyChoice({
1546
+ io,
1547
+ provider,
1548
+ envName,
1549
+ hasSavedKey: Boolean(storedProfile?.apiKey),
1550
+ hasEnvKey: Boolean(env[envName])
1551
+ });
1552
+ if (keyChoice === PROMPT_BACK) {
1553
+ providerStep = "model";
1554
+ continue;
1555
+ }
1556
+ apiKeyChoice = keyChoice;
1557
+ if (apiKeyChoice === "override") {
1558
+ io.write(`${ui.note("Press Esc if you want to go back instead of entering a key right now.")}
1559
+ `);
1560
+ providerStep = "api-key-entry";
1561
+ continue;
1562
+ }
1563
+ nextApiKey = void 0;
1564
+ break;
1565
+ }
1566
+ const apiKey = await promptForApiKey(io, provider);
1567
+ if (apiKey === PROMPT_BACK) {
1568
+ providerStep = "api-key-choice";
1569
+ continue;
1570
+ }
1571
+ nextApiKey = apiKey;
1572
+ break;
1573
+ }
1574
+ if (mode !== "provider-assisted") {
1575
+ continue;
1576
+ }
1577
+ if (!provider || !apiKeyChoice) {
1578
+ continue;
1579
+ }
1580
+ break;
1581
+ }
1048
1582
  const config = buildSetupConfig({
1049
1583
  config: existingConfig,
1584
+ mode,
1050
1585
  provider,
1586
+ model,
1051
1587
  apiKeyChoice,
1052
1588
  nextApiKey
1053
1589
  });
@@ -1056,18 +1592,19 @@ async function configSetup(options = {}) {
1056
1592
  config,
1057
1593
  overwrite: existed
1058
1594
  });
1059
- if (apiKeyChoice === "env") {
1595
+ if (mode === "provider-assisted" && provider && apiKeyChoice === "env") {
1596
+ const envName = getNativeProviderApiKeyEnvName(provider);
1060
1597
  io.write(
1061
1598
  `${ui.note(`Using ${envName} from the environment. No API key was written to config.`)}
1062
1599
  `
1063
1600
  );
1064
1601
  }
1065
- writeSetupSuccess(io, writtenPath);
1602
+ writeSetupSuccess(io, writtenPath, mode);
1066
1603
  const activeConfigPath = findConfigPath();
1067
1604
  if (activeConfigPath && path5.resolve(activeConfigPath) !== path5.resolve(writtenPath)) {
1068
1605
  writeOverrideWarning(io, activeConfigPath);
1069
1606
  }
1070
- writeNextSteps(io);
1607
+ writeNextSteps(io, mode);
1071
1608
  return 0;
1072
1609
  } finally {
1073
1610
  io.close?.();
@@ -1094,18 +1631,18 @@ function maskConfigSecrets(value) {
1094
1631
  return output;
1095
1632
  }
1096
1633
  function configInit(targetPath, global = false) {
1097
- const path8 = writeExampleConfig({
1634
+ const path9 = writeExampleConfig({
1098
1635
  targetPath,
1099
1636
  global
1100
1637
  });
1101
1638
  if (!process.stdout.isTTY) {
1102
- process.stdout.write(`${path8}
1639
+ process.stdout.write(`${path9}
1103
1640
  `);
1104
1641
  return;
1105
1642
  }
1106
1643
  const ui = createPresentation(true);
1107
1644
  process.stdout.write(
1108
- `${ui.success(`${global ? "Machine-wide" : "Template"} config written to ${path8}`)}
1645
+ `${ui.success(`${global ? "Machine-wide" : "Template"} config written to ${path9}`)}
1109
1646
  `
1110
1647
  );
1111
1648
  }
@@ -1268,16 +1805,34 @@ function getManagedBlockMarkers(agent) {
1268
1805
  end: `<!-- sift:end ${agent} -->`
1269
1806
  };
1270
1807
  }
1271
- function renderInstructionBody() {
1808
+ function inferOperationMode(args) {
1809
+ if (args.operationMode) {
1810
+ return args.operationMode;
1811
+ }
1812
+ try {
1813
+ const cwd = args.cwd ?? process.cwd();
1814
+ const homeDir = args.homeDir ?? os2.homedir();
1815
+ const repoConfigPath = path6.resolve(cwd, "sift.config.yaml");
1816
+ const globalConfigPath = getDefaultGlobalConfigPath(homeDir);
1817
+ const configPath = fs5.existsSync(repoConfigPath) ? repoConfigPath : fs5.existsSync(globalConfigPath) ? globalConfigPath : void 0;
1818
+ const config = resolveConfig(configPath ? { configPath } : {});
1819
+ return resolveEffectiveOperationMode(config);
1820
+ } catch {
1821
+ return "agent-escalation";
1822
+ }
1823
+ }
1824
+ function renderInstructionBody(mode) {
1272
1825
  return [
1273
- "Use `sift` as the default reduction layer for long, noisy, non-interactive command output.",
1274
- "The goal is to shrink that output before it spends context-window and token budget in the model.",
1826
+ "Use `sift` as the default local-first output guidance layer for long, noisy, non-interactive command output.",
1827
+ "The goal is to turn failure walls into grouped issues, likely root causes, and the next useful step before reading raw logs.",
1828
+ "",
1829
+ `Default operating mode: ${getOperationModeLabel(mode)}.`,
1830
+ describeOperationMode(mode),
1831
+ describeInsufficientBehavior(mode),
1275
1832
  "",
1276
1833
  "Start with:",
1277
1834
  '- `sift exec "question" -- <command> [args...]`',
1278
1835
  "- `sift exec --preset test-status -- <test command>`",
1279
- '- `sift watch "what changed between cycles?" < watcher-output.txt`',
1280
- '- `sift exec --watch "what changed between cycles?" -- <command> [args...]`',
1281
1836
  "- `sift exec --preset audit-critical -- npm audit`",
1282
1837
  "- `sift exec --preset infra-risk -- terraform plan`",
1283
1838
  "",
@@ -1316,9 +1871,9 @@ function renderInstructionBody() {
1316
1871
  "Do not pass API keys inline."
1317
1872
  ].join("\n");
1318
1873
  }
1319
- function renderManagedBlock(agent, eol = "\n") {
1874
+ function renderManagedBlock(agent, eol = "\n", mode = "agent-escalation") {
1320
1875
  const markers = getManagedBlockMarkers(agent);
1321
- return [markers.start, renderInstructionBody(), markers.end].join(eol);
1876
+ return [markers.start, renderInstructionBody(mode), markers.end].join(eol);
1322
1877
  }
1323
1878
  function inspectManagedBlock(content, agent) {
1324
1879
  const markers = getManagedBlockMarkers(agent);
@@ -1343,7 +1898,7 @@ function inspectManagedBlock(content, agent) {
1343
1898
  }
1344
1899
  function planManagedInstall(args) {
1345
1900
  const eol = args.existingContent?.includes("\r\n") ? "\r\n" : "\n";
1346
- const block = renderManagedBlock(args.agent, eol);
1901
+ const block = renderManagedBlock(args.agent, eol, args.operationMode ?? "agent-escalation");
1347
1902
  if (args.existingContent === void 0) {
1348
1903
  return {
1349
1904
  action: "create",
@@ -1433,6 +1988,7 @@ function showAgent(args, ioArg = createStdoutOnlyIO()) {
1433
1988
  const params = typeof args === "string" ? {
1434
1989
  agent: args,
1435
1990
  scope: "repo",
1991
+ operationMode: void 0,
1436
1992
  raw: false,
1437
1993
  targetPath: void 0,
1438
1994
  cwd: void 0,
@@ -1441,6 +1997,7 @@ function showAgent(args, ioArg = createStdoutOnlyIO()) {
1441
1997
  } : {
1442
1998
  agent: args.agent,
1443
1999
  scope: args.scope ?? "repo",
2000
+ operationMode: args.operationMode,
1444
2001
  raw: args.raw ?? false,
1445
2002
  targetPath: args.targetPath,
1446
2003
  cwd: args.cwd,
@@ -1449,8 +2006,13 @@ function showAgent(args, ioArg = createStdoutOnlyIO()) {
1449
2006
  };
1450
2007
  const agent = normalizeAgentName(params.agent);
1451
2008
  const io = params.io;
2009
+ const operationMode = inferOperationMode({
2010
+ cwd: params.cwd,
2011
+ homeDir: params.homeDir,
2012
+ operationMode: params.operationMode
2013
+ });
1452
2014
  if (params.raw) {
1453
- io.write(`${renderManagedBlock(agent)}
2015
+ io.write(`${renderManagedBlock(agent, "\n", operationMode)}
1454
2016
  `);
1455
2017
  return;
1456
2018
  }
@@ -1489,6 +2051,8 @@ function showAgent(args, ioArg = createStdoutOnlyIO()) {
1489
2051
  )}
1490
2052
  `
1491
2053
  );
2054
+ io.write(`${ui.labelValue("operation mode", getOperationModeLabel(operationMode))}
2055
+ `);
1492
2056
  if (currentInstalled) {
1493
2057
  io.write(`${ui.warning(`Already installed in ${params.scope} scope.`)}
1494
2058
  `);
@@ -1506,13 +2070,17 @@ function showAgent(args, ioArg = createStdoutOnlyIO()) {
1506
2070
  `
1507
2071
  );
1508
2072
  io.write(
1509
- `${ui.info("The point is to reduce long command output before it burns context-window and token budget.")}
2073
+ `${ui.info("The point is to narrow long command output before your agent burns time and tokens on the raw log wall.")}
1510
2074
  `
1511
2075
  );
1512
2076
  io.write(
1513
2077
  `${ui.info("The managed block teaches the agent to default to sift first, keep raw as the last resort, and treat standard as the usual stop point.")}
1514
2078
  `
1515
2079
  );
2080
+ io.write(`${ui.note(describeOperationMode(operationMode))}
2081
+ `);
2082
+ io.write(`${ui.note(describeInsufficientBehavior(operationMode))}
2083
+ `);
1516
2084
  io.write(` ${ui.command('sift exec "question" -- <command> [args...]')}
1517
2085
  `);
1518
2086
  io.write(` ${ui.command("sift exec --preset test-status -- <test command>")}
@@ -1522,7 +2090,7 @@ function showAgent(args, ioArg = createStdoutOnlyIO()) {
1522
2090
  io.write(` ${ui.command("sift exec --preset infra-risk -- terraform plan")}
1523
2091
  `);
1524
2092
  io.write(
1525
- `${ui.info("For test debugging, standard should usually be enough for first-pass triage.")}
2093
+ `${ui.info("For test debugging, standard should usually be enough for first-pass guidance.")}
1526
2094
  `
1527
2095
  );
1528
2096
  io.write(
@@ -1572,6 +2140,11 @@ async function installAgent(args) {
1572
2140
  homeDir: args.homeDir
1573
2141
  });
1574
2142
  const ui = createPresentation(io.stdoutIsTTY);
2143
+ const operationMode = inferOperationMode({
2144
+ cwd: args.cwd,
2145
+ homeDir: args.homeDir,
2146
+ operationMode: args.operationMode
2147
+ });
1575
2148
  try {
1576
2149
  const existingContent = readOptionalFile(targetPath);
1577
2150
  const fileExists = existingContent !== void 0;
@@ -1579,7 +2152,8 @@ async function installAgent(args) {
1579
2152
  const plan = planManagedInstall({
1580
2153
  agent,
1581
2154
  targetPath,
1582
- existingContent
2155
+ existingContent,
2156
+ operationMode
1583
2157
  });
1584
2158
  if (args.dryRun) {
1585
2159
  if (args.raw) {
@@ -1635,6 +2209,8 @@ async function installAgent(args) {
1635
2209
  io.write(`${ui.labelValue("scope", scope)}
1636
2210
  `);
1637
2211
  io.write(`${ui.labelValue("target", targetPath)}
2212
+ `);
2213
+ io.write(`${ui.labelValue("operation mode", getOperationModeLabel(operationMode))}
1638
2214
  `);
1639
2215
  io.write(`${ui.info("This will only manage the sift block.")}
1640
2216
  `);
@@ -1835,6 +2411,465 @@ function escapeRegExp(value) {
1835
2411
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1836
2412
  }
1837
2413
 
2414
+ // src/commands/install.ts
2415
+ import os3 from "os";
2416
+ import path7 from "path";
2417
+ import { emitKeypressEvents as emitKeypressEvents2 } from "readline";
2418
+ import { createInterface as createInterface3 } from "readline/promises";
2419
+ import {
2420
+ stderr as defaultStderr3,
2421
+ stdin as defaultStdin4,
2422
+ stdout as defaultStdout3
2423
+ } from "process";
2424
+ var INSTALL_TITLES = {
2425
+ codex: "Codex",
2426
+ claude: "Claude"
2427
+ };
2428
+ function createInstallTerminalIO() {
2429
+ let rl;
2430
+ function getInterface() {
2431
+ if (!rl) {
2432
+ rl = createInterface3({
2433
+ input: defaultStdin4,
2434
+ output: defaultStdout3,
2435
+ terminal: true
2436
+ });
2437
+ }
2438
+ return rl;
2439
+ }
2440
+ async function select(prompt, options, selectedLabel, allowBack) {
2441
+ emitKeypressEvents2(defaultStdin4);
2442
+ return await promptSelect({
2443
+ input: defaultStdin4,
2444
+ output: defaultStdout3,
2445
+ prompt,
2446
+ options,
2447
+ selectedLabel,
2448
+ allowBack
2449
+ });
2450
+ }
2451
+ return {
2452
+ stdinIsTTY: Boolean(defaultStdin4.isTTY),
2453
+ stdoutIsTTY: Boolean(defaultStdout3.isTTY),
2454
+ ask(prompt) {
2455
+ return getInterface().question(prompt);
2456
+ },
2457
+ select,
2458
+ write(message) {
2459
+ defaultStdout3.write(message);
2460
+ },
2461
+ error(message) {
2462
+ defaultStderr3.write(message);
2463
+ },
2464
+ close() {
2465
+ rl?.close();
2466
+ }
2467
+ };
2468
+ }
2469
+ function normalizeInstallRuntime(value) {
2470
+ if (value === void 0 || value === null || value === "") {
2471
+ return void 0;
2472
+ }
2473
+ if (value === "codex" || value === "claude" || value === "all") {
2474
+ return value;
2475
+ }
2476
+ throw new Error("Invalid runtime. Use codex, claude, or all.");
2477
+ }
2478
+ function normalizeInstallScope(value) {
2479
+ if (value === void 0 || value === null || value === "") {
2480
+ return void 0;
2481
+ }
2482
+ if (value === "global") {
2483
+ return "global";
2484
+ }
2485
+ if (value === "local" || value === "repo") {
2486
+ return "repo";
2487
+ }
2488
+ throw new Error("Invalid --scope value. Use local or global.");
2489
+ }
2490
+ function renderInstallBanner(version) {
2491
+ const teal = (text) => `\x1B[38;2;34;173;169m${text}\x1B[0m`;
2492
+ return [
2493
+ teal(" \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557"),
2494
+ teal(" \u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D"),
2495
+ teal(" \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551 "),
2496
+ teal(" \u255A\u2550\u2550\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u255D \u2588\u2588\u2551 "),
2497
+ teal(" \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551 "),
2498
+ teal(" \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D "),
2499
+ "",
2500
+ ` sift v${version}`,
2501
+ " Small, sharp, and mildly sarcastic output guidance.",
2502
+ ' "Loading the loading screen..." energy, minus the loading screen.'
2503
+ ].join("\n");
2504
+ }
2505
+ function getInstallTargets(runtime) {
2506
+ if (runtime === "all") {
2507
+ return ["codex", "claude"];
2508
+ }
2509
+ return [runtime];
2510
+ }
2511
+ function getGlobalTargetLabel(agent, homeDir = os3.homedir()) {
2512
+ return agent === "codex" ? getDefaultCodexGlobalInstructionsPath(homeDir) : getDefaultClaudeGlobalInstructionsPath(homeDir);
2513
+ }
2514
+ function getLocalTargetLabel(agent, cwd = process.cwd()) {
2515
+ return agent === "codex" ? path7.join(cwd, "AGENTS.md") : path7.join(cwd, "CLAUDE.md");
2516
+ }
2517
+ function describeScopeChoice(args) {
2518
+ const targets = getInstallTargets(args.runtime);
2519
+ const labels = targets.map(
2520
+ (agent) => args.scope === "global" ? getGlobalTargetLabel(agent, args.homeDir) : getLocalTargetLabel(agent, args.cwd)
2521
+ );
2522
+ return labels.join(" + ");
2523
+ }
2524
+ async function promptWithMenu(args) {
2525
+ const defaultIndex = args.defaultIndex ?? 0;
2526
+ if (args.io.select) {
2527
+ const labels = args.choices.map((choice) => choice.label);
2528
+ const selected = await args.io.select(args.prompt, labels, args.selectedLabel, args.allowBack);
2529
+ if (selected === PROMPT_BACK) {
2530
+ return PROMPT_BACK;
2531
+ }
2532
+ const match = args.choices.find((choice) => choice.label === selected);
2533
+ if (match) {
2534
+ return match.value;
2535
+ }
2536
+ }
2537
+ args.io.write(`
2538
+ ${args.prompt}
2539
+
2540
+ `);
2541
+ args.choices.forEach((choice, index) => {
2542
+ args.io.write(` ${index + 1}) ${choice.label}
2543
+ `);
2544
+ });
2545
+ if (args.allowBack) {
2546
+ args.io.write(` ${args.choices.length + 1}) Back
2547
+ `);
2548
+ }
2549
+ args.io.write("\n");
2550
+ while (true) {
2551
+ const answer = (await args.io.ask(`Choice [${defaultIndex + 1}]: `)).trim();
2552
+ if (args.allowBack && (answer.toLowerCase() === "back" || answer.toLowerCase() === "b")) {
2553
+ return PROMPT_BACK;
2554
+ }
2555
+ if (answer === "") {
2556
+ return args.choices[defaultIndex]?.value ?? args.choices[0].value;
2557
+ }
2558
+ const choiceIndex = Number(answer);
2559
+ if (Number.isInteger(choiceIndex) && choiceIndex >= 1 && choiceIndex <= args.choices.length) {
2560
+ return args.choices[choiceIndex - 1].value;
2561
+ }
2562
+ if (args.allowBack && Number.isInteger(choiceIndex) && choiceIndex === args.choices.length + 1) {
2563
+ return PROMPT_BACK;
2564
+ }
2565
+ const max = args.allowBack ? args.choices.length + 1 : args.choices.length;
2566
+ args.io.error(`Please enter a number between 1 and ${max}.
2567
+ `);
2568
+ }
2569
+ }
2570
+ async function promptForRuntime(io) {
2571
+ return await promptWithMenu({
2572
+ io,
2573
+ prompt: "Choose your runtime",
2574
+ selectedLabel: "Runtime",
2575
+ allowBack: true,
2576
+ choices: [
2577
+ {
2578
+ label: "Codex (AGENTS.md / ~/.codex/AGENTS.md) - first-class if you live in Codex",
2579
+ value: "codex"
2580
+ },
2581
+ {
2582
+ label: "Claude (CLAUDE.md / ~/.claude/CLAUDE.md) - same good manners, Claude-flavored",
2583
+ value: "claude"
2584
+ },
2585
+ {
2586
+ label: "All - if you refuse to pick favorites today",
2587
+ value: "all"
2588
+ }
2589
+ ]
2590
+ });
2591
+ }
2592
+ async function promptForScope(args) {
2593
+ return await promptWithMenu({
2594
+ io: args.io,
2595
+ prompt: "Choose where to install the runtime support",
2596
+ selectedLabel: "Location",
2597
+ allowBack: true,
2598
+ choices: [
2599
+ {
2600
+ label: `Global (${describeScopeChoice({
2601
+ runtime: args.runtime,
2602
+ scope: "global",
2603
+ cwd: args.cwd,
2604
+ homeDir: args.homeDir
2605
+ })}) - use this if you want sift ready everywhere`,
2606
+ value: "global"
2607
+ },
2608
+ {
2609
+ label: `Local (${describeScopeChoice({
2610
+ runtime: args.runtime,
2611
+ scope: "repo",
2612
+ cwd: args.cwd,
2613
+ homeDir: args.homeDir
2614
+ })}) - keep it here if this repo is the only one that matters`,
2615
+ value: "repo"
2616
+ }
2617
+ ]
2618
+ });
2619
+ }
2620
+ async function promptForOperationMode2(io) {
2621
+ return await promptWithMenu({
2622
+ io,
2623
+ prompt: "Choose how sift should work",
2624
+ selectedLabel: "Mode",
2625
+ allowBack: true,
2626
+ choices: [
2627
+ {
2628
+ label: "With an agent - recommended if Codex or Claude is already with you; sift does the fast local first pass, the agent only steps in when repo context is truly needed",
2629
+ value: "agent-escalation"
2630
+ },
2631
+ {
2632
+ label: "With provider fallback - recommended if you want sift to finish more ambiguous cases on its own before handing them back to you or your agent; needs an API key, cheap model only when needed",
2633
+ value: "provider-assisted"
2634
+ },
2635
+ {
2636
+ label: "Solo, local-only - recommended if you want zero model calls; great for supported presets, ambiguous cases stay with you",
2637
+ value: "local-only"
2638
+ }
2639
+ ]
2640
+ });
2641
+ }
2642
+ function createNestedInstallIO(parent) {
2643
+ return {
2644
+ stdinIsTTY: parent.stdinIsTTY,
2645
+ stdoutIsTTY: parent.stdoutIsTTY,
2646
+ ask: async () => "",
2647
+ write() {
2648
+ },
2649
+ error(message) {
2650
+ parent.error(message);
2651
+ }
2652
+ };
2653
+ }
2654
+ function writeSuccessSummary(args) {
2655
+ const ui = createPresentation(args.io.stdoutIsTTY);
2656
+ const targets = getInstallTargets(args.runtime);
2657
+ const scopeLabel = args.scope === "global" ? "global" : "local";
2658
+ const targetLabel = describeScopeChoice({
2659
+ runtime: args.runtime,
2660
+ scope: args.scope,
2661
+ cwd: args.cwd,
2662
+ homeDir: args.homeDir
2663
+ });
2664
+ if (args.io.stdoutIsTTY) {
2665
+ args.io.write(`
2666
+ ${ui.success("Installed runtime support.")}
2667
+ `);
2668
+ } else {
2669
+ args.io.write("Installed runtime support.\n");
2670
+ }
2671
+ args.io.write(
2672
+ `${ui.note(`sift v${args.version} now manages ${targets.map((target) => INSTALL_TITLES[target]).join(" + ")} in ${scopeLabel} scope.`)}
2673
+ `
2674
+ );
2675
+ args.io.write(`${ui.note(`Operating mode: ${getOperationModeLabel(args.operationMode)}`)}
2676
+ `);
2677
+ args.io.write(`${ui.note(describeOperationMode(args.operationMode))}
2678
+ `);
2679
+ args.io.write(`${ui.note(targetLabel)}
2680
+ `);
2681
+ args.io.write(`
2682
+ ${ui.section("Try next")}
2683
+ `);
2684
+ args.io.write(` ${ui.command("sift doctor")}
2685
+ `);
2686
+ if (args.operationMode === "provider-assisted") {
2687
+ args.io.write(` ${ui.command("sift config show --show-secrets")}
2688
+ `);
2689
+ } else {
2690
+ args.io.write(
2691
+ ` ${ui.command("sift config setup")}${ui.note(" # optional if you want provider-assisted fallback later")}
2692
+ `
2693
+ );
2694
+ }
2695
+ args.io.write(` ${ui.command("sift exec --preset test-status -- npm test")}
2696
+ `);
2697
+ }
2698
+ async function installRuntimeSupport(options) {
2699
+ const io = options.io ?? createInstallTerminalIO();
2700
+ const getPreviousEditableStep = (step) => {
2701
+ if (step === "runtime") {
2702
+ return void 0;
2703
+ }
2704
+ if (step === "mode") {
2705
+ return options.runtime ? void 0 : "runtime";
2706
+ }
2707
+ if (step === "scope") {
2708
+ if (!options.operationMode) {
2709
+ return "mode";
2710
+ }
2711
+ if (!options.runtime) {
2712
+ return "runtime";
2713
+ }
2714
+ return void 0;
2715
+ }
2716
+ if (step === "provider") {
2717
+ if (!options.scope) {
2718
+ return "scope";
2719
+ }
2720
+ if (!options.operationMode) {
2721
+ return "mode";
2722
+ }
2723
+ if (!options.runtime) {
2724
+ return "runtime";
2725
+ }
2726
+ return void 0;
2727
+ }
2728
+ return void 0;
2729
+ };
2730
+ try {
2731
+ if ((!io.stdinIsTTY || !io.stdoutIsTTY) && (!options.runtime || !options.scope || !options.yes)) {
2732
+ io.error(
2733
+ "sift install is interactive and requires a TTY. For non-interactive use `sift install codex --scope global --yes`.\n"
2734
+ );
2735
+ return 1;
2736
+ }
2737
+ if (io.stdoutIsTTY) {
2738
+ io.write(`${renderInstallBanner(options.version)}
2739
+ `);
2740
+ }
2741
+ let runtime = options.runtime;
2742
+ let operationMode = options.operationMode;
2743
+ let scope = options.scope;
2744
+ let step;
2745
+ if (!io.stdinIsTTY || !io.stdoutIsTTY) {
2746
+ runtime ??= options.runtime;
2747
+ operationMode ??= "agent-escalation";
2748
+ step = void 0;
2749
+ } else if (!runtime) {
2750
+ step = "runtime";
2751
+ } else if (!operationMode) {
2752
+ step = "mode";
2753
+ } else if (!scope) {
2754
+ step = "scope";
2755
+ } else if (operationMode === "provider-assisted") {
2756
+ step = "provider";
2757
+ }
2758
+ while (step) {
2759
+ if (step === "runtime") {
2760
+ const runtimeChoice = await promptForRuntime(io);
2761
+ if (runtimeChoice === PROMPT_BACK) {
2762
+ io.write(`
2763
+ ${createPresentation(io.stdoutIsTTY).note("Install canceled before we touched anything.")}
2764
+ `);
2765
+ return 0;
2766
+ }
2767
+ runtime = runtimeChoice;
2768
+ step = !operationMode ? "mode" : !scope ? "scope" : operationMode === "provider-assisted" ? "provider" : void 0;
2769
+ continue;
2770
+ }
2771
+ if (step === "mode") {
2772
+ const modeChoice = await promptForOperationMode2(io);
2773
+ if (modeChoice === PROMPT_BACK) {
2774
+ const previous = getPreviousEditableStep("mode");
2775
+ if (!previous) {
2776
+ io.write(`
2777
+ ${createPresentation(io.stdoutIsTTY).note("Install canceled before we touched anything.")}
2778
+ `);
2779
+ return 0;
2780
+ }
2781
+ step = previous;
2782
+ continue;
2783
+ }
2784
+ operationMode = modeChoice;
2785
+ step = !scope ? "scope" : operationMode === "provider-assisted" ? "provider" : void 0;
2786
+ continue;
2787
+ }
2788
+ if (step === "scope") {
2789
+ const scopeChoice = await promptForScope({
2790
+ io,
2791
+ runtime,
2792
+ cwd: options.cwd,
2793
+ homeDir: options.homeDir
2794
+ });
2795
+ if (scopeChoice === PROMPT_BACK) {
2796
+ const previous = getPreviousEditableStep("scope");
2797
+ if (!previous) {
2798
+ io.write(`
2799
+ ${createPresentation(io.stdoutIsTTY).note("Install canceled before we touched anything.")}
2800
+ `);
2801
+ return 0;
2802
+ }
2803
+ step = previous;
2804
+ continue;
2805
+ }
2806
+ scope = scopeChoice;
2807
+ step = operationMode === "provider-assisted" ? "provider" : void 0;
2808
+ continue;
2809
+ }
2810
+ if (scope === "repo") {
2811
+ io.write(
2812
+ `
2813
+ ${createPresentation(io.stdoutIsTTY).note("Local only applies to the runtime instructions in this repo. Provider fallback config is still machine-wide so sift can reuse it anywhere.")}
2814
+ `
2815
+ );
2816
+ }
2817
+ io.write(`
2818
+ ${createPresentation(io.stdoutIsTTY).info("Next: provider setup. Press Esc at any step to go back.")}
2819
+ `);
2820
+ const setupStatus = await configSetup({
2821
+ io,
2822
+ env: process.env,
2823
+ embedded: true,
2824
+ forcedMode: "provider-assisted",
2825
+ targetPath: getDefaultGlobalConfigPath(options.homeDir)
2826
+ });
2827
+ if (setupStatus === CONFIG_SETUP_BACK) {
2828
+ const previous = getPreviousEditableStep("provider");
2829
+ if (!previous) {
2830
+ io.write(`
2831
+ ${createPresentation(io.stdoutIsTTY).note("Install canceled before we touched anything.")}
2832
+ `);
2833
+ return 0;
2834
+ }
2835
+ step = previous;
2836
+ continue;
2837
+ }
2838
+ if (setupStatus !== 0) {
2839
+ return setupStatus;
2840
+ }
2841
+ step = void 0;
2842
+ }
2843
+ const nestedIo = createNestedInstallIO(io);
2844
+ for (const agent of getInstallTargets(runtime)) {
2845
+ const status = await installAgent({
2846
+ agent,
2847
+ scope,
2848
+ yes: true,
2849
+ io: nestedIo,
2850
+ operationMode,
2851
+ cwd: options.cwd,
2852
+ homeDir: options.homeDir
2853
+ });
2854
+ if (status !== 0) {
2855
+ return status;
2856
+ }
2857
+ }
2858
+ writeSuccessSummary({
2859
+ io,
2860
+ version: options.version,
2861
+ runtime,
2862
+ scope,
2863
+ operationMode,
2864
+ cwd: options.cwd,
2865
+ homeDir: options.homeDir
2866
+ });
2867
+ return 0;
2868
+ } finally {
2869
+ io.close?.();
2870
+ }
2871
+ }
2872
+
1838
2873
  // src/commands/doctor.ts
1839
2874
  var PLACEHOLDER_API_KEYS = [
1840
2875
  "YOUR_API_KEY",
@@ -1858,16 +2893,21 @@ function isRealApiKey(key) {
1858
2893
  }
1859
2894
  function runDoctor(config, configPath) {
1860
2895
  const ui = createPresentation(Boolean(process.stdout.isTTY));
2896
+ const effectiveMode = resolveEffectiveOperationMode(config);
1861
2897
  const apiKeyStatus = isRealApiKey(config.provider.apiKey) ? "set" : isPlaceholderApiKey(config.provider.apiKey) ? "placeholder (not a real key)" : "not set";
1862
2898
  const lines = [
1863
2899
  "sift doctor",
1864
2900
  "A quick check for your local setup.",
1865
- "mode: local config completeness check",
2901
+ "mode: operation-mode health check",
1866
2902
  ui.labelValue("configPath", configPath ?? "(defaults only)"),
2903
+ ui.labelValue("configuredMode", getOperationModeLabel(config.runtime.operationMode)),
2904
+ ui.labelValue("effectiveMode", getOperationModeLabel(effectiveMode)),
1867
2905
  ui.labelValue("provider", config.provider.provider),
1868
2906
  ui.labelValue("model", config.provider.model),
1869
2907
  ui.labelValue("baseUrl", config.provider.baseUrl),
1870
2908
  ui.labelValue("apiKey", apiKeyStatus),
2909
+ ui.labelValue("modeSummary", describeOperationMode(effectiveMode)),
2910
+ ui.labelValue("insufficientBehavior", describeInsufficientBehavior(effectiveMode)),
1871
2911
  ui.labelValue("maxCaptureChars", String(config.input.maxCaptureChars)),
1872
2912
  ui.labelValue("maxInputChars", String(config.input.maxInputChars)),
1873
2913
  ui.labelValue("rawFallback", String(config.runtime.rawFallback))
@@ -1875,24 +2915,31 @@ function runDoctor(config, configPath) {
1875
2915
  process.stdout.write(`${lines.join("\n")}
1876
2916
  `);
1877
2917
  const problems = [];
1878
- if (!config.provider.baseUrl) {
1879
- problems.push("Missing provider.baseUrl");
1880
- }
1881
- if (!config.provider.model) {
1882
- problems.push("Missing provider.model");
1883
- }
1884
- if ((config.provider.provider === "openai" || config.provider.provider === "openai-compatible" || config.provider.provider === "openrouter") && !isRealApiKey(config.provider.apiKey)) {
1885
- if (isPlaceholderApiKey(config.provider.apiKey)) {
1886
- problems.push(`provider.apiKey looks like a placeholder: "${config.provider.apiKey}"`);
1887
- } else {
1888
- problems.push("Missing provider.apiKey");
2918
+ if (config.runtime.operationMode === "provider-assisted") {
2919
+ if (!config.provider.baseUrl) {
2920
+ problems.push("Missing provider.baseUrl");
2921
+ }
2922
+ if (!config.provider.model) {
2923
+ problems.push("Missing provider.model");
2924
+ }
2925
+ if ((config.provider.provider === "openai" || config.provider.provider === "openai-compatible" || config.provider.provider === "openrouter") && !isRealApiKey(config.provider.apiKey)) {
2926
+ if (isPlaceholderApiKey(config.provider.apiKey)) {
2927
+ problems.push(`provider.apiKey looks like a placeholder: "${config.provider.apiKey}"`);
2928
+ } else {
2929
+ problems.push("Missing provider.apiKey");
2930
+ }
2931
+ problems.push(
2932
+ `Set one of: ${getProviderApiKeyEnvNames(
2933
+ config.provider.provider,
2934
+ config.provider.baseUrl
2935
+ ).join(", ")}`
2936
+ );
2937
+ }
2938
+ if (effectiveMode !== "provider-assisted") {
2939
+ problems.push(
2940
+ "Configured provider-assisted mode cannot activate yet, so sift will fall back to agent-escalation until provider credentials are usable."
2941
+ );
1889
2942
  }
1890
- problems.push(
1891
- `Set one of: ${getProviderApiKeyEnvNames(
1892
- config.provider.provider,
1893
- config.provider.baseUrl
1894
- ).join(", ")}`
1895
- );
1896
2943
  }
1897
2944
  if (problems.length > 0) {
1898
2945
  if (process.stderr.isTTY) {
@@ -2749,16 +3796,16 @@ function extractBucketPathCandidates(args) {
2749
3796
  }
2750
3797
  return [...candidates];
2751
3798
  }
2752
- function isConfigPathCandidate(path8) {
2753
- return /^\.github\/workflows\/.+\.(?:yml|yaml)$/i.test(path8) || /^(?:package\.json|pytest\.ini|pyproject\.toml|tox\.ini|(?:[A-Za-z0-9._/-]+\/)?conftest\.py)$/i.test(
2754
- path8
2755
- ) || /^(?:[A-Za-z0-9._/-]+\/)?(?:vitest|jest)\.config\.[A-Za-z0-9._-]+$/i.test(path8) || /^(?:[A-Za-z0-9._/-]+\/)?tsconfig(?:\.[A-Za-z0-9_-]+)?\.json$/i.test(path8) || /^[A-Za-z0-9._/-]*config[A-Za-z0-9._/-]*\.(?:json|yml|yaml)$/i.test(path8);
3799
+ function isConfigPathCandidate(path9) {
3800
+ return /^\.github\/workflows\/.+\.(?:yml|yaml)$/i.test(path9) || /^(?:package\.json|pytest\.ini|pyproject\.toml|tox\.ini|(?:[A-Za-z0-9._/-]+\/)?conftest\.py)$/i.test(
3801
+ path9
3802
+ ) || /^(?:[A-Za-z0-9._/-]+\/)?(?:vitest|jest)\.config\.[A-Za-z0-9._-]+$/i.test(path9) || /^(?:[A-Za-z0-9._/-]+\/)?tsconfig(?:\.[A-Za-z0-9_-]+)?\.json$/i.test(path9) || /^[A-Za-z0-9._/-]*config[A-Za-z0-9._/-]*\.(?:json|yml|yaml)$/i.test(path9);
2756
3803
  }
2757
- function isAppPathCandidate(path8) {
2758
- return path8.startsWith("src/");
3804
+ function isAppPathCandidate(path9) {
3805
+ return path9.startsWith("src/");
2759
3806
  }
2760
- function isTestPathCandidate(path8) {
2761
- return path8.startsWith("test/") || path8.startsWith("tests/");
3807
+ function isTestPathCandidate(path9) {
3808
+ return path9.startsWith("test/") || path9.startsWith("tests/");
2762
3809
  }
2763
3810
  function looksLikeMatcherLiteralComparison(detail) {
2764
3811
  return /\bexpected\b[\s\S]*\bto (?:be|contain)\b/i.test(detail);
@@ -3321,13 +4368,13 @@ function buildExtendedBucketSearchHint(bucket, anchor) {
3321
4368
  return detail.replace(/^of\s+/i, "") || anchor.label;
3322
4369
  }
3323
4370
  if (extended.type === "file_not_found_failure") {
3324
- const path8 = detail.match(/['"]([^'"]+)['"]/)?.[1];
3325
- return path8 ?? detail;
4371
+ const path9 = detail.match(/['"]([^'"]+)['"]/)?.[1];
4372
+ return path9 ?? detail;
3326
4373
  }
3327
4374
  if (extended.type === "permission_denied_failure") {
3328
- const path8 = detail.match(/['"]([^'"]+)['"]/)?.[1];
4375
+ const path9 = detail.match(/['"]([^'"]+)['"]/)?.[1];
3329
4376
  const port = detail.match(/\bport\s+(\d+)\b/i)?.[1];
3330
- return path8 ?? (port ? `port ${port}` : detail);
4377
+ return path9 ?? (port ? `port ${port}` : detail);
3331
4378
  }
3332
4379
  return detail;
3333
4380
  }
@@ -4435,7 +5482,7 @@ function buildPrompt(args) {
4435
5482
  "If per-test or per-module mapping is unclear, fall back to the focused grouped-cause view instead of guessing."
4436
5483
  ] : [];
4437
5484
  const prompt = [
4438
- "You are Sift, a CLI output reduction assistant for downstream agents and automation.",
5485
+ "You are Sift, a CLI output-guidance and reduction assistant for downstream agents and automation.",
4439
5486
  "Hard rules:",
4440
5487
  ...policy.sharedRules.map((rule) => `- ${rule}`),
4441
5488
  "",
@@ -6213,7 +7260,7 @@ function extractContractDriftEntities(input) {
6213
7260
  }
6214
7261
  function buildContractRepresentativeReason(args) {
6215
7262
  if (/openapi/i.test(args.label) && args.entities.apiPaths.length > 0) {
6216
- const nextPath = args.entities.apiPaths.find((path8) => !args.usedPaths.has(path8)) ?? args.entities.apiPaths[0];
7263
+ const nextPath = args.entities.apiPaths.find((path9) => !args.usedPaths.has(path9)) ?? args.entities.apiPaths[0];
6217
7264
  args.usedPaths.add(nextPath);
6218
7265
  return `added path: ${nextPath}`;
6219
7266
  }
@@ -8290,7 +9337,7 @@ function emitStatsFooter(args) {
8290
9337
 
8291
9338
  // src/core/testStatusState.ts
8292
9339
  import fs6 from "fs";
8293
- import path7 from "path";
9340
+ import path8 from "path";
8294
9341
  import { z as z3 } from "zod";
8295
9342
  var detailSchema = z3.enum(["standard", "focused", "verbose"]);
8296
9343
  var failureBucketTypeSchema = z3.enum([
@@ -8452,7 +9499,7 @@ function buildBucketSignature(bucket) {
8452
9499
  ]);
8453
9500
  }
8454
9501
  function basenameMatches(value, matcher) {
8455
- return matcher.test(path7.basename(value));
9502
+ return matcher.test(path8.basename(value));
8456
9503
  }
8457
9504
  function isPytestExecutable(value) {
8458
9505
  return basenameMatches(value, /^pytest(?:\.exe)?$/i);
@@ -8611,7 +9658,7 @@ function buildCachedRunnerState(args) {
8611
9658
  };
8612
9659
  }
8613
9660
  function normalizeCwd(value) {
8614
- return path7.resolve(value).replace(/\\/g, "/");
9661
+ return path8.resolve(value).replace(/\\/g, "/");
8615
9662
  }
8616
9663
  function buildTestStatusBaselineIdentity(args) {
8617
9664
  const cwd = normalizeCwd(args.cwd);
@@ -8760,7 +9807,7 @@ function tryReadCachedTestStatusRun(statePath = getDefaultTestStatusStatePath())
8760
9807
  }
8761
9808
  }
8762
9809
  function writeCachedTestStatusRun(state, statePath = getDefaultTestStatusStatePath()) {
8763
- fs6.mkdirSync(path7.dirname(statePath), {
9810
+ fs6.mkdirSync(path8.dirname(statePath), {
8764
9811
  recursive: true
8765
9812
  });
8766
9813
  fs6.writeFileSync(statePath, `${JSON.stringify(state, null, 2)}
@@ -9303,13 +10350,97 @@ function buildCommandPreview(request) {
9303
10350
  }
9304
10351
  return (request.command ?? []).join(" ");
9305
10352
  }
10353
+ function detectPackageManagerScriptKind(commandPreview) {
10354
+ const trimmed = commandPreview.trim();
10355
+ if (/^npm(?:\s+--?[^\s]+(?:[=\s][^\s]+)?)?\s+run\s+\S+/i.test(trimmed)) {
10356
+ return "npm";
10357
+ }
10358
+ if (/^pnpm(?:\s+--?[^\s]+(?:[=\s][^\s]+)?)?\s+run\s+\S+/i.test(trimmed)) {
10359
+ return "pnpm";
10360
+ }
10361
+ if (/^yarn(?:\s+--?[^\s]+(?:[=\s][^\s]+)?)?\s+run\s+\S+/i.test(trimmed)) {
10362
+ return "yarn";
10363
+ }
10364
+ if (/^bun(?:\s+--?[^\s]+(?:[=\s][^\s]+)?)?\s+run\s+\S+/i.test(trimmed)) {
10365
+ return "bun";
10366
+ }
10367
+ return null;
10368
+ }
10369
+ function normalizeScriptWrapperOutput(args) {
10370
+ const kind = detectPackageManagerScriptKind(args.commandPreview);
10371
+ if (!kind) {
10372
+ return args.capturedOutput;
10373
+ }
10374
+ const lines = args.capturedOutput.split(/\r?\n/);
10375
+ const trimBlankEdges = () => {
10376
+ while (lines.length > 0 && lines[0].trim() === "") {
10377
+ lines.shift();
10378
+ }
10379
+ while (lines.length > 0 && lines.at(-1).trim() === "") {
10380
+ lines.pop();
10381
+ }
10382
+ };
10383
+ const stripLeadingWrapperNoise = () => {
10384
+ let removed = false;
10385
+ while (lines.length > 0) {
10386
+ const line = lines[0];
10387
+ const trimmed = line.trim();
10388
+ if (trimmed === "") {
10389
+ lines.shift();
10390
+ removed = true;
10391
+ continue;
10392
+ }
10393
+ if (/^(?:npm|pnpm)\s+warn\s+unknown user config\b/i.test(trimmed) || /^(?:npm|pnpm)\s+warn\s+unknown env config\b/i.test(trimmed) || /^npm\s+warn\s+config\b/i.test(trimmed) || /^yarn\s+warning\b/i.test(trimmed) || /^bun\s+warn\b/i.test(trimmed)) {
10394
+ lines.shift();
10395
+ removed = true;
10396
+ continue;
10397
+ }
10398
+ break;
10399
+ }
10400
+ if (removed) {
10401
+ trimBlankEdges();
10402
+ }
10403
+ };
10404
+ trimBlankEdges();
10405
+ stripLeadingWrapperNoise();
10406
+ if (kind === "npm" || kind === "pnpm") {
10407
+ let removed = 0;
10408
+ while (lines.length > 0 && removed < 2 && /^\s*>\s+/.test(lines[0])) {
10409
+ lines.shift();
10410
+ removed += 1;
10411
+ }
10412
+ trimBlankEdges();
10413
+ }
10414
+ if (kind === "yarn") {
10415
+ if (lines[0] && /^\s*yarn run v/i.test(lines[0])) {
10416
+ lines.shift();
10417
+ }
10418
+ if (lines[0] && /^\s*\$\s+/.test(lines[0])) {
10419
+ lines.shift();
10420
+ }
10421
+ trimBlankEdges();
10422
+ if (lines.at(-1) && /^\s*Done in\b/i.test(lines.at(-1))) {
10423
+ lines.pop();
10424
+ }
10425
+ }
10426
+ if (kind === "bun") {
10427
+ if (lines[0] && /^\s*\$\s+/.test(lines[0])) {
10428
+ lines.shift();
10429
+ }
10430
+ }
10431
+ trimBlankEdges();
10432
+ return lines.join("\n");
10433
+ }
9306
10434
  function getExecSuccessShortcut(args) {
9307
10435
  if (args.exitCode !== 0) {
9308
10436
  return null;
9309
10437
  }
9310
- if (args.presetName === "typecheck-summary" && args.capturedOutput.trim() === "") {
10438
+ if (args.presetName === "typecheck-summary" && args.normalizedOutput.trim() === "") {
9311
10439
  return "No type errors.";
9312
10440
  }
10441
+ if (args.presetName === "lint-failures" && args.normalizedOutput.trim() === "") {
10442
+ return "No lint failures.";
10443
+ }
9313
10444
  return null;
9314
10445
  }
9315
10446
  async function runExec(request) {
@@ -9389,6 +10520,10 @@ async function runExec(request) {
9389
10520
  }
9390
10521
  const exitCode = normalizeChildExitCode(childStatus, childSignal);
9391
10522
  const capturedOutput = capture.render();
10523
+ const normalizedOutput = normalizeScriptWrapperOutput({
10524
+ commandPreview,
10525
+ capturedOutput
10526
+ });
9392
10527
  const autoWatchDetected = !request.watch && looksLikeWatchStream(capturedOutput);
9393
10528
  const useWatchFlow = Boolean(request.watch) || autoWatchDetected;
9394
10529
  const shouldBuildTestStatusState = isTestStatusPreset && !useWatchFlow;
@@ -9414,7 +10549,7 @@ async function runExec(request) {
9414
10549
  const execSuccessShortcut = useWatchFlow ? null : getExecSuccessShortcut({
9415
10550
  presetName: request.presetName,
9416
10551
  exitCode,
9417
- capturedOutput
10552
+ normalizedOutput
9418
10553
  });
9419
10554
  if (execSuccessShortcut && !request.dryRun) {
9420
10555
  if (request.config.runtime.verbose) {
@@ -9440,15 +10575,15 @@ async function runExec(request) {
9440
10575
  if (useWatchFlow) {
9441
10576
  let output2 = await runWatch({
9442
10577
  ...request,
9443
- stdin: capturedOutput
10578
+ stdin: normalizedOutput
9444
10579
  });
9445
10580
  if (isInsufficientSignalOutput(output2)) {
9446
10581
  output2 = buildInsufficientSignalOutput({
9447
10582
  presetName: request.presetName,
9448
- originalLength: capture.getTotalChars(),
10583
+ originalLength: normalizedOutput.length > 0 ? normalizedOutput.length : capture.getTotalChars(),
9449
10584
  truncatedApplied: capture.wasTruncated(),
9450
10585
  exitCode,
9451
- recognizedRunner: detectTestRunner(capturedOutput)
10586
+ recognizedRunner: detectTestRunner(normalizedOutput)
9452
10587
  });
9453
10588
  }
9454
10589
  process.stdout.write(`${output2}
@@ -9487,7 +10622,7 @@ async function runExec(request) {
9487
10622
  }) : null;
9488
10623
  const result = await runSiftWithStats({
9489
10624
  ...request,
9490
- stdin: capturedOutput,
10625
+ stdin: normalizedOutput,
9491
10626
  analysisContext: request.testStatusContext?.remainingMode && request.testStatusContext.remainingMode !== "none" && request.presetName === "test-status" ? [
9492
10627
  request.analysisContext,
9493
10628
  "Zoom context:",
@@ -9510,10 +10645,10 @@ async function runExec(request) {
9510
10645
  if (isInsufficientSignalOutput(output)) {
9511
10646
  output = buildInsufficientSignalOutput({
9512
10647
  presetName: request.presetName,
9513
- originalLength: capture.getTotalChars(),
10648
+ originalLength: normalizedOutput.length > 0 ? normalizedOutput.length : capture.getTotalChars(),
9514
10649
  truncatedApplied: capture.wasTruncated(),
9515
10650
  exitCode,
9516
- recognizedRunner: detectTestRunner(capturedOutput)
10651
+ recognizedRunner: detectTestRunner(normalizedOutput)
9517
10652
  });
9518
10653
  }
9519
10654
  if (request.diff && !request.dryRun && previousCachedRun && currentCachedRun) {
@@ -9540,10 +10675,10 @@ ${output}`;
9540
10675
  } else if (isInsufficientSignalOutput(output)) {
9541
10676
  output = buildInsufficientSignalOutput({
9542
10677
  presetName: request.presetName,
9543
- originalLength: capture.getTotalChars(),
10678
+ originalLength: normalizedOutput.length > 0 ? normalizedOutput.length : capture.getTotalChars(),
9544
10679
  truncatedApplied: capture.wasTruncated(),
9545
10680
  exitCode,
9546
- recognizedRunner: detectTestRunner(capturedOutput)
10681
+ recognizedRunner: detectTestRunner(normalizedOutput)
9547
10682
  });
9548
10683
  }
9549
10684
  process.stdout.write(`${output}
@@ -9654,6 +10789,7 @@ function getPreset(config, name) {
9654
10789
  var require2 = createRequire(import.meta.url);
9655
10790
  var pkg = require2("../package.json");
9656
10791
  var defaultCliDeps = {
10792
+ installRuntimeSupport,
9657
10793
  installAgent,
9658
10794
  removeAgent,
9659
10795
  showAgent,
@@ -10008,7 +11144,7 @@ function createCliApp(args = {}) {
10008
11144
  });
10009
11145
  });
10010
11146
  applySharedOptions(
10011
- cli.command("exec [question]", "Run a command and shrink its output for the model").allowUnknownOptions()
11147
+ cli.command("exec [question]", "Run a command and turn noisy output into a smaller first pass for the model").allowUnknownOptions()
10012
11148
  ).usage("exec [question] [options] -- <program> [args...]").example('exec "what changed?" -- git diff').example("exec --preset test-status -- npm test").example("exec --preset test-status --diff -- npm test").example('exec --watch "summarize the stream" -- node watcher.js').example('exec --preset infra-risk --shell "terraform plan"').option("--shell <command>", "Execute a shell command string instead of argv mode").option("--preset <name>", "Run a named preset in exec mode").option("--watch", "Treat the command output as a watch/change-summary stream").option("--diff", "Prepend material changes versus the previous matching test-status run").action(async (question, options) => {
10013
11149
  if (question === "preset") {
10014
11150
  throw new Error("Use 'sift exec --preset <name> -- <program> ...' instead.");
@@ -10287,6 +11423,14 @@ function createCliApp(args = {}) {
10287
11423
  stdout.write(`${output}
10288
11424
  `);
10289
11425
  });
11426
+ cli.command("install [runtime]", "Interactive runtime installer for Codex and Claude").usage("install [runtime] [options]").example("install").example("install codex").example("install codex --scope global --yes").example("install all --scope local --yes").option("--scope <scope>", "Install scope: local | global").option("--yes", "Skip prompts when runtime and scope are already provided").action(async (runtime, options) => {
11427
+ process.exitCode = await deps.installRuntimeSupport({
11428
+ runtime: normalizeInstallRuntime(runtime),
11429
+ scope: normalizeInstallScope(options.scope),
11430
+ yes: Boolean(options.yes),
11431
+ version
11432
+ });
11433
+ });
10290
11434
  cli.command("agent <action> [name]", "Agent commands: show | install | remove | status").usage("agent <show|install|remove|status> [name] [options]").example("agent show codex").example("agent show codex --raw").example("agent install codex").example("agent install claude --scope global").example("agent install codex --dry-run").example("agent install codex --dry-run --raw").example("agent status").example("agent remove codex --scope repo").option("--scope <scope>", "Install scope: repo | global").option("--dry-run", "Show a short plan without changing files").option("--raw", "Print the exact managed block or dry-run file content").option("--yes", "Skip confirmation prompts when writing").option("--path <path>", "Explicit target path for install or remove").action(async (action, name, options) => {
10291
11435
  const scope = normalizeAgentScope(options.scope);
10292
11436
  if (action === "show") {
@@ -10430,14 +11574,14 @@ function createCliApp(args = {}) {
10430
11574
  {
10431
11575
  title: ui.section("Quick start"),
10432
11576
  body: [
10433
- ` ${ui.command("sift config setup")}`,
11577
+ ` ${ui.command("sift install")}${ui.note(" # choose agent-escalation, provider-assisted, or local-only")}`,
10434
11578
  ` ${ui.command("sift exec --preset test-status -- npm test")}`,
10435
11579
  ` ${ui.command("sift exec --preset test-status -- npm test")}${ui.note(" # stop here if standard already shows the main buckets")}`,
10436
11580
  ` ${ui.command("sift rerun")}${ui.note(" # rerun the cached full suite after a fix")}`,
10437
11581
  ` ${ui.command("sift rerun --remaining --detail focused")}${ui.note(" # zoom into what is still failing")}`,
10438
11582
  ` ${ui.command("sift rerun --remaining --detail verbose --show-raw")}`,
10439
- ` ${ui.command('sift watch "what changed between cycles?" < watcher-output.txt')}`,
10440
- ` ${ui.command('sift exec --watch "what changed between cycles?" -- node watcher.js')}`,
11583
+ ` ${ui.command("sift config setup")}${ui.note(" # optional if you want provider-assisted fallback")}`,
11584
+ ` ${ui.command("sift install codex --scope global --yes")}`,
10441
11585
  ` ${ui.command("sift agent install codex --dry-run")}`,
10442
11586
  ` ${ui.command("sift agent install codex --dry-run --raw")}`,
10443
11587
  ` ${ui.command("sift agent status")}`,