@bilalimamoglu/sift 0.4.3 → 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
@@ -5,13 +5,15 @@ import { createRequire } from "module";
5
5
  import { cac } from "cac";
6
6
 
7
7
  // src/config/load.ts
8
- import fs from "fs";
8
+ import fs2 from "fs";
9
9
  import path2 from "path";
10
10
  import YAML from "yaml";
11
11
 
12
12
  // src/constants.ts
13
+ import fs from "fs";
13
14
  import os from "os";
14
15
  import path from "path";
16
+ import crypto from "crypto";
15
17
  var DEFAULT_CONFIG_FILENAME = "sift.config.yaml";
16
18
  function getDefaultCodexGlobalInstructionsPath(homeDir = os.homedir()) {
17
19
  return path.join(homeDir, ".codex", "AGENTS.md");
@@ -28,6 +30,26 @@ function getDefaultGlobalStateDir(homeDir = os.homedir()) {
28
30
  function getDefaultTestStatusStatePath(homeDir = os.homedir()) {
29
31
  return path.join(getDefaultGlobalStateDir(homeDir), "last-test-status.json");
30
32
  }
33
+ function getDefaultScopedTestStatusStateDir(homeDir = os.homedir()) {
34
+ return path.join(getDefaultGlobalStateDir(homeDir), "test-status", "by-cwd");
35
+ }
36
+ function getScopedTestStatusStatePath(cwd, homeDir = os.homedir()) {
37
+ const normalizedCwd = normalizeScopedCacheCwd(cwd);
38
+ const baseName = slugCachePathSegment(path.basename(normalizedCwd)) || "root";
39
+ const shortHash = crypto.createHash("sha256").update(normalizedCwd).digest("hex").slice(0, 10);
40
+ return path.join(getDefaultScopedTestStatusStateDir(homeDir), `${baseName}-${shortHash}.json`);
41
+ }
42
+ function slugCachePathSegment(value) {
43
+ return value.trim().toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
44
+ }
45
+ function normalizeScopedCacheCwd(cwd) {
46
+ const absoluteCwd = path.resolve(cwd);
47
+ try {
48
+ return fs.realpathSync.native(absoluteCwd);
49
+ } catch {
50
+ return absoluteCwd;
51
+ }
52
+ }
31
53
  function getDefaultConfigSearchPaths() {
32
54
  return [
33
55
  path.resolve(process.cwd(), "sift.config.yaml"),
@@ -44,13 +66,13 @@ var CAPTURE_OMITTED_MARKER = "\n...[captured output omitted]...\n";
44
66
  function findConfigPath(explicitPath) {
45
67
  if (explicitPath) {
46
68
  const resolved = path2.resolve(explicitPath);
47
- if (!fs.existsSync(resolved)) {
69
+ if (!fs2.existsSync(resolved)) {
48
70
  throw new Error(`Config file not found: ${resolved}`);
49
71
  }
50
72
  return resolved;
51
73
  }
52
74
  for (const candidate of getDefaultConfigSearchPaths()) {
53
- if (fs.existsSync(candidate)) {
75
+ if (fs2.existsSync(candidate)) {
54
76
  return candidate;
55
77
  }
56
78
  }
@@ -61,7 +83,7 @@ function loadRawConfig(explicitPath) {
61
83
  if (!configPath) {
62
84
  return {};
63
85
  }
64
- const content = fs.readFileSync(configPath, "utf8");
86
+ const content = fs2.readFileSync(configPath, "utf8");
65
87
  return YAML.parse(content) ?? {};
66
88
  }
67
89
 
@@ -87,6 +109,7 @@ var defaultConfig = {
87
109
  tailChars: 2e4
88
110
  },
89
111
  runtime: {
112
+ operationMode: "agent-escalation",
90
113
  rawFallback: true,
91
114
  verbose: false
92
115
  },
@@ -136,12 +159,62 @@ var defaultConfig = {
136
159
  }
137
160
  };
138
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
+
139
212
  // src/config/native-provider.ts
140
213
  function getNativeProviderDefaults(provider) {
141
214
  if (provider === "openrouter") {
142
215
  return {
143
216
  provider,
144
- model: "openrouter/free",
217
+ model: getDefaultProviderModel("openrouter"),
145
218
  baseUrl: "https://openrouter.ai/api/v1"
146
219
  };
147
220
  }
@@ -283,6 +356,11 @@ function getProviderApiKeyEnvNames(provider, baseUrl) {
283
356
 
284
357
  // src/config/schema.ts
285
358
  import { z } from "zod";
359
+ var operationModeSchema = z.enum([
360
+ "agent-escalation",
361
+ "provider-assisted",
362
+ "local-only"
363
+ ]);
286
364
  var providerNameSchema = z.enum([
287
365
  "openai",
288
366
  "openai-compatible",
@@ -335,6 +413,7 @@ var inputConfigSchema = z.object({
335
413
  tailChars: z.number().int().positive()
336
414
  });
337
415
  var runtimeConfigSchema = z.object({
416
+ operationMode: operationModeSchema,
338
417
  rawFallback: z.boolean(),
339
418
  verbose: z.boolean()
340
419
  });
@@ -357,7 +436,7 @@ var siftConfigSchema = z.object({
357
436
  var PROVIDER_DEFAULT_OVERRIDES = {
358
437
  openrouter: {
359
438
  provider: {
360
- model: "openrouter/free",
439
+ model: getDefaultProviderModel("openrouter"),
361
440
  baseUrl: "https://openrouter.ai/api/v1"
362
441
  }
363
442
  }
@@ -397,13 +476,16 @@ function stripApiKey(overrides) {
397
476
  }
398
477
  function buildNonCredentialEnvOverrides(env) {
399
478
  const overrides = {};
400
- 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) {
401
480
  overrides.provider = {
402
481
  provider: env.SIFT_PROVIDER,
403
482
  model: env.SIFT_MODEL,
404
483
  baseUrl: env.SIFT_BASE_URL,
405
484
  timeoutMs: env.SIFT_TIMEOUT_MS ? Number(env.SIFT_TIMEOUT_MS) : void 0
406
485
  };
486
+ overrides.runtime = {
487
+ operationMode: env.SIFT_OPERATION_MODE
488
+ };
407
489
  }
408
490
  if (env.SIFT_MAX_INPUT_CHARS || env.SIFT_MAX_CAPTURE_CHARS) {
409
491
  overrides.input = {
@@ -470,9 +552,18 @@ function resolveConfig(options = {}) {
470
552
  );
471
553
  return siftConfigSchema.parse(merged);
472
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
+ }
473
564
 
474
565
  // src/config/write.ts
475
- import fs2 from "fs";
566
+ import fs3 from "fs";
476
567
  import path3 from "path";
477
568
  import YAML2 from "yaml";
478
569
  function writeExampleConfig(options = {}) {
@@ -480,41 +571,41 @@ function writeExampleConfig(options = {}) {
480
571
  throw new Error("Use either --path <path> or --global, not both.");
481
572
  }
482
573
  const resolved = options.global ? getDefaultGlobalConfigPath() : path3.resolve(options.targetPath ?? DEFAULT_CONFIG_FILENAME);
483
- if (fs2.existsSync(resolved)) {
574
+ if (fs3.existsSync(resolved)) {
484
575
  throw new Error(`Config file already exists at ${resolved}`);
485
576
  }
486
577
  const yaml = YAML2.stringify(defaultConfig);
487
- fs2.mkdirSync(path3.dirname(resolved), { recursive: true });
488
- fs2.writeFileSync(resolved, yaml, {
578
+ fs3.mkdirSync(path3.dirname(resolved), { recursive: true });
579
+ fs3.writeFileSync(resolved, yaml, {
489
580
  encoding: "utf8",
490
581
  mode: 384
491
582
  });
492
583
  try {
493
- fs2.chmodSync(resolved, 384);
584
+ fs3.chmodSync(resolved, 384);
494
585
  } catch {
495
586
  }
496
587
  return resolved;
497
588
  }
498
589
  function writeConfigFile(options) {
499
590
  const resolved = path3.resolve(options.targetPath);
500
- if (!options.overwrite && fs2.existsSync(resolved)) {
591
+ if (!options.overwrite && fs3.existsSync(resolved)) {
501
592
  throw new Error(`Config file already exists at ${resolved}`);
502
593
  }
503
594
  const yaml = YAML2.stringify(options.config);
504
- fs2.mkdirSync(path3.dirname(resolved), { recursive: true });
505
- fs2.writeFileSync(resolved, yaml, {
595
+ fs3.mkdirSync(path3.dirname(resolved), { recursive: true });
596
+ fs3.writeFileSync(resolved, yaml, {
506
597
  encoding: "utf8",
507
598
  mode: 384
508
599
  });
509
600
  try {
510
- fs2.chmodSync(resolved, 384);
601
+ fs3.chmodSync(resolved, 384);
511
602
  } catch {
512
603
  }
513
604
  return resolved;
514
605
  }
515
606
 
516
607
  // src/config/editable.ts
517
- import fs3 from "fs";
608
+ import fs4 from "fs";
518
609
  import path4 from "path";
519
610
  function isRecord2(value) {
520
611
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
@@ -527,7 +618,7 @@ function resolveEditableConfigPath(explicitPath) {
527
618
  }
528
619
  function loadEditableConfig(explicitPath) {
529
620
  const resolvedPath = resolveEditableConfigPath(explicitPath);
530
- const existed = fs3.existsSync(resolvedPath);
621
+ const existed = fs4.existsSync(resolvedPath);
531
622
  const rawConfig = existed ? loadRawConfig(resolvedPath) : {};
532
623
  const config = siftConfigSchema.parse(
533
624
  mergeDefined(defaultConfig, isRecord2(rawConfig) ? rawConfig : {})
@@ -594,10 +685,129 @@ import { emitKeypressEvents } from "readline";
594
685
  import { createInterface } from "readline/promises";
595
686
  import { stderr as defaultStderr, stdin as defaultStdin2, stdout as defaultStdout } from "process";
596
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
+
597
720
  // src/ui/terminal.ts
598
721
  import { execFileSync } from "child_process";
599
722
  import { clearScreenDown, cursorTo, moveCursor } from "readline";
600
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
+ }
601
811
  function setPosixEcho(enabled) {
602
812
  const command = enabled ? "echo" : "-echo";
603
813
  try {
@@ -615,10 +825,15 @@ function setPosixEcho(enabled) {
615
825
  }
616
826
  }
617
827
  function renderSelectionBlock(args) {
828
+ const options = args.allowBack ? [...args.options, args.backLabel ?? PROMPT_BACK_LABEL] : args.options;
618
829
  return [
619
- `${args.prompt} (use \u2191/\u2193 and Enter)`,
620
- ...args.options.map(
621
- (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)" : ""}`
622
837
  )
623
838
  ];
624
839
  }
@@ -626,6 +841,8 @@ async function promptSelect(args) {
626
841
  const { input, output, prompt, options } = args;
627
842
  const stream = output;
628
843
  const selectedLabel = args.selectedLabel ?? prompt;
844
+ const backLabel = args.backLabel ?? PROMPT_BACK_LABEL;
845
+ const allOptions = args.allowBack ? [...options, backLabel] : options;
629
846
  let index = 0;
630
847
  let previousLineCount = 0;
631
848
  const render = () => {
@@ -637,7 +854,10 @@ async function promptSelect(args) {
637
854
  const lines = renderSelectionBlock({
638
855
  prompt,
639
856
  options,
640
- selectedIndex: index
857
+ selectedIndex: index,
858
+ allowBack: args.allowBack,
859
+ backLabel,
860
+ colorize: Boolean(stream?.isTTY)
641
861
  });
642
862
  output.write(`${lines.join("\n")}
643
863
  `);
@@ -669,22 +889,30 @@ async function promptSelect(args) {
669
889
  return;
670
890
  }
671
891
  if (key.name === "up") {
672
- index = index === 0 ? options.length - 1 : index - 1;
892
+ index = index === 0 ? allOptions.length - 1 : index - 1;
673
893
  render();
674
894
  return;
675
895
  }
676
896
  if (key.name === "down") {
677
- index = (index + 1) % options.length;
897
+ index = (index + 1) % allOptions.length;
678
898
  render();
679
899
  return;
680
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
+ }
681
909
  if (key.name === "return" || key.name === "enter") {
682
- const selected = options[index] ?? options[0] ?? "";
910
+ const selected = allOptions[index] ?? allOptions[0] ?? "";
683
911
  input.off("keypress", onKeypress);
684
- cleanup(selected);
912
+ cleanup(selected === backLabel ? void 0 : selected);
685
913
  input.setRawMode?.(wasRaw);
686
914
  input.pause?.();
687
- resolve(selected);
915
+ resolve(selected === backLabel ? PROMPT_BACK : selected);
688
916
  }
689
917
  };
690
918
  input.on("keypress", onKeypress);
@@ -717,6 +945,13 @@ async function promptSecret(args) {
717
945
  reject(new Error("Aborted."));
718
946
  return;
719
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
+ }
720
955
  if (key.name === "return" || key.name === "enter") {
721
956
  input.off("keypress", onKeypress);
722
957
  restoreInputState();
@@ -737,6 +972,7 @@ async function promptSecret(args) {
737
972
  }
738
973
 
739
974
  // src/commands/config-setup.ts
975
+ var CONFIG_SETUP_BACK = 2;
740
976
  function createTerminalIO() {
741
977
  let rl;
742
978
  function getInterface() {
@@ -749,22 +985,24 @@ function createTerminalIO() {
749
985
  }
750
986
  return rl;
751
987
  }
752
- async function select(prompt, options) {
988
+ async function select(prompt, options, selectedLabel, allowBack) {
753
989
  emitKeypressEvents(defaultStdin2);
754
990
  return await promptSelect({
755
991
  input: defaultStdin2,
756
992
  output: defaultStdout,
757
993
  prompt,
758
994
  options,
759
- selectedLabel: "Provider"
995
+ selectedLabel,
996
+ allowBack
760
997
  });
761
998
  }
762
- async function secret(prompt) {
999
+ async function secret(prompt, allowBack) {
763
1000
  emitKeypressEvents(defaultStdin2);
764
1001
  return await promptSecret({
765
1002
  input: defaultStdin2,
766
1003
  output: defaultStdout,
767
- prompt
1004
+ prompt,
1005
+ allowBack
768
1006
  });
769
1007
  }
770
1008
  return {
@@ -795,12 +1033,58 @@ function getSetupPresenter(io) {
795
1033
  function getProviderLabel(provider) {
796
1034
  return provider === "openrouter" ? "OpenRouter" : "OpenAI";
797
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
+ }
798
1074
  async function promptForProvider(io) {
799
1075
  if (io.select) {
800
- const choice = await io.select("Select provider for this machine", [
801
- "OpenAI",
802
- "OpenRouter"
803
- ]);
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
+ }
804
1088
  if (choice === "OpenAI") {
805
1089
  return "openai";
806
1090
  }
@@ -810,6 +1094,9 @@ async function promptForProvider(io) {
810
1094
  }
811
1095
  while (true) {
812
1096
  const answer = (await io.ask("Provider [OpenAI/OpenRouter]: ")).trim().toLowerCase();
1097
+ if (answer === "back" || answer === "b") {
1098
+ return PROMPT_BACK;
1099
+ }
813
1100
  if (answer === "" || answer === "openai") {
814
1101
  return "openai";
815
1102
  }
@@ -824,7 +1111,13 @@ async function promptForApiKey(io, provider) {
824
1111
  const promptText = `Enter your ${providerLabel} API key (input hidden): `;
825
1112
  const visiblePromptText = `Enter your ${providerLabel} API key: `;
826
1113
  while (true) {
827
- 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
+ }
828
1121
  if (answer.length > 0) {
829
1122
  return answer;
830
1123
  }
@@ -840,17 +1133,25 @@ async function promptForApiKeyChoice(args) {
840
1133
  if (args.io.select) {
841
1134
  const choice = await args.io.select(
842
1135
  `Found both a saved ${providerLabel} API key and ${args.envName} in your environment`,
843
- ["Use saved key", "Use env key", "Override"]
1136
+ ["Use saved key", "Use environment key", "Enter a different key"],
1137
+ "API key",
1138
+ true
844
1139
  );
1140
+ if (isBackSelection(choice)) {
1141
+ return PROMPT_BACK;
1142
+ }
845
1143
  if (choice === "Use saved key") {
846
1144
  return "saved";
847
1145
  }
848
- if (choice === "Use env key") {
1146
+ if (choice === "Use environment key") {
849
1147
  return "env";
850
1148
  }
851
1149
  }
852
1150
  while (true) {
853
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
+ }
854
1155
  if (answer === "" || answer === "saved") {
855
1156
  return "saved";
856
1157
  }
@@ -867,15 +1168,23 @@ async function promptForApiKeyChoice(args) {
867
1168
  if (args.io.select) {
868
1169
  const choice = await args.io.select(
869
1170
  `Found an existing ${providerLabel} API key via ${sourceLabel}`,
870
- ["Use existing key", "Override"]
1171
+ ["Use saved key", "Enter a different key"],
1172
+ "API key",
1173
+ true
871
1174
  );
872
- if (choice === "Override") {
1175
+ if (isBackSelection(choice)) {
1176
+ return PROMPT_BACK;
1177
+ }
1178
+ if (choice === "Enter a different key") {
873
1179
  return "override";
874
1180
  }
875
1181
  return args.hasSavedKey ? "saved" : "env";
876
1182
  }
877
1183
  while (true) {
878
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
+ }
879
1188
  if (answer === "" || answer === "existing") {
880
1189
  return args.hasSavedKey ? "saved" : "env";
881
1190
  }
@@ -885,12 +1194,41 @@ async function promptForApiKeyChoice(args) {
885
1194
  args.io.error("Please answer existing or override.\n");
886
1195
  }
887
1196
  }
888
- 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) {
889
1225
  const ui = getSetupPresenter(io);
890
1226
  io.write(`
891
1227
  ${ui.success("You're set.")}
892
1228
  `);
893
1229
  io.write(`${ui.info(`Machine-wide config: ${writtenPath}`)}
1230
+ `);
1231
+ io.write(`${ui.labelValue("operation mode", getOperationModeLabel(mode))}
894
1232
  `);
895
1233
  io.write(`${ui.note("sift is ready to use from any terminal on this machine.")}
896
1234
  `);
@@ -906,31 +1244,114 @@ function writeOverrideWarning(io, activeConfigPath) {
906
1244
  `
907
1245
  );
908
1246
  }
909
- function writeNextSteps(io) {
1247
+ function writeNextSteps(io, mode) {
910
1248
  const ui = getSetupPresenter(io);
911
1249
  io.write(`
912
1250
  ${ui.section("Try next")}
913
1251
  `);
914
1252
  io.write(` ${ui.command("sift doctor")}
915
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
+ }
916
1263
  io.write(` ${ui.command("sift exec --preset test-status -- npm test")}
917
1264
  `);
918
1265
  }
919
- 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) {
920
1328
  const ui = getSetupPresenter(io);
1329
+ const options = getProviderModelOptions(provider);
921
1330
  if (provider === "openrouter") {
922
- 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.")}
923
1332
  `);
924
- io.write(`${ui.labelValue("Default model", "openrouter/free")}
1333
+ io.write(`${ui.labelValue("Default model", getDefaultProviderModel("openrouter"))}
925
1334
  `);
926
1335
  io.write(`${ui.labelValue("Default base URL", "https://openrouter.ai/api/v1")}
927
1336
  `);
928
1337
  } else {
929
- 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.")}
930
1339
  `);
931
- io.write(`${ui.labelValue("Default model", "gpt-5-nano")}
1340
+ io.write(`${ui.labelValue("Default model", getDefaultProviderModel("openai"))}
932
1341
  `);
933
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}`)}
934
1355
  `);
935
1356
  }
936
1357
  io.write(
@@ -942,54 +1363,101 @@ function writeProviderDefaults(io, provider) {
942
1363
  `
943
1364
  );
944
1365
  }
945
- function materializeProfile(provider, profile, apiKey) {
1366
+ function materializeProfile(provider, profile, overrides = {}) {
946
1367
  return {
1368
+ ...profile,
947
1369
  ...getProfileProviderState(provider, profile),
948
- ...apiKey !== void 0 ? { apiKey } : {}
1370
+ ...overrides.model !== void 0 ? { model: overrides.model } : {},
1371
+ ...overrides.apiKey !== void 0 ? { apiKey: overrides.apiKey } : {}
949
1372
  };
950
1373
  }
951
1374
  function buildSetupConfig(args) {
952
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
+ }
953
1388
  const storedProfile = getStoredProviderProfile(preservedConfig, args.provider);
954
1389
  if (args.apiKeyChoice === "saved") {
955
1390
  const profile2 = materializeProfile(
956
1391
  args.provider,
957
1392
  storedProfile,
958
- storedProfile?.apiKey ?? ""
1393
+ {
1394
+ apiKey: storedProfile?.apiKey ?? "",
1395
+ model: args.model
1396
+ }
959
1397
  );
960
1398
  const configWithProfile2 = setStoredProviderProfile(
961
1399
  preservedConfig,
962
1400
  args.provider,
963
1401
  profile2
964
1402
  );
965
- return applyActiveProvider(
1403
+ const applied2 = applyActiveProvider(
966
1404
  configWithProfile2,
967
1405
  args.provider,
968
1406
  profile2,
969
1407
  profile2.apiKey ?? ""
970
1408
  );
1409
+ return {
1410
+ ...applied2,
1411
+ runtime: {
1412
+ ...applied2.runtime,
1413
+ operationMode: args.mode
1414
+ }
1415
+ };
971
1416
  }
972
1417
  if (args.apiKeyChoice === "env") {
973
- const profile2 = storedProfile ? storedProfile : materializeProfile(args.provider, void 0);
974
- const configWithProfile2 = storedProfile ? preservedConfig : setStoredProviderProfile(preservedConfig, args.provider, profile2);
975
- 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
+ };
976
1434
  }
977
1435
  const profile = materializeProfile(
978
1436
  args.provider,
979
1437
  storedProfile,
980
- args.nextApiKey ?? ""
1438
+ {
1439
+ apiKey: args.nextApiKey ?? "",
1440
+ model: args.model
1441
+ }
981
1442
  );
982
1443
  const configWithProfile = setStoredProviderProfile(
983
1444
  preservedConfig,
984
1445
  args.provider,
985
1446
  profile
986
1447
  );
987
- return applyActiveProvider(
1448
+ const applied = applyActiveProvider(
988
1449
  configWithProfile,
989
1450
  args.provider,
990
1451
  profile,
991
1452
  args.nextApiKey ?? ""
992
1453
  );
1454
+ return {
1455
+ ...applied,
1456
+ runtime: {
1457
+ ...applied.runtime,
1458
+ operationMode: args.mode
1459
+ }
1460
+ };
993
1461
  }
994
1462
  async function configSetup(options = {}) {
995
1463
  void options.global;
@@ -1003,29 +1471,119 @@ async function configSetup(options = {}) {
1003
1471
  );
1004
1472
  return 1;
1005
1473
  }
1006
- 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.")}
1007
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.")}
1481
+ `);
1482
+ }
1008
1483
  const resolvedPath = resolveSetupPath(options.targetPath);
1009
1484
  const { config: existingConfig, existed } = loadEditableConfig(resolvedPath);
1010
1485
  if (existed) {
1011
1486
  io.write(`${ui.info(`Updating existing config at ${resolvedPath}.`)}
1012
1487
  `);
1013
1488
  }
1014
- const provider = await promptForProvider(io);
1015
- writeProviderDefaults(io, provider);
1016
- const storedProfile = getStoredProviderProfile(existingConfig, provider);
1017
- const envName = getNativeProviderApiKeyEnvName(provider);
1018
- const apiKeyChoice = await promptForApiKeyChoice({
1019
- io,
1020
- provider,
1021
- envName,
1022
- hasSavedKey: Boolean(storedProfile?.apiKey),
1023
- hasEnvKey: Boolean(env[envName])
1024
- });
1025
- 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
+ }
1026
1582
  const config = buildSetupConfig({
1027
1583
  config: existingConfig,
1584
+ mode,
1028
1585
  provider,
1586
+ model,
1029
1587
  apiKeyChoice,
1030
1588
  nextApiKey
1031
1589
  });
@@ -1034,18 +1592,19 @@ async function configSetup(options = {}) {
1034
1592
  config,
1035
1593
  overwrite: existed
1036
1594
  });
1037
- if (apiKeyChoice === "env") {
1595
+ if (mode === "provider-assisted" && provider && apiKeyChoice === "env") {
1596
+ const envName = getNativeProviderApiKeyEnvName(provider);
1038
1597
  io.write(
1039
1598
  `${ui.note(`Using ${envName} from the environment. No API key was written to config.`)}
1040
1599
  `
1041
1600
  );
1042
1601
  }
1043
- writeSetupSuccess(io, writtenPath);
1602
+ writeSetupSuccess(io, writtenPath, mode);
1044
1603
  const activeConfigPath = findConfigPath();
1045
1604
  if (activeConfigPath && path5.resolve(activeConfigPath) !== path5.resolve(writtenPath)) {
1046
1605
  writeOverrideWarning(io, activeConfigPath);
1047
1606
  }
1048
- writeNextSteps(io);
1607
+ writeNextSteps(io, mode);
1049
1608
  return 0;
1050
1609
  } finally {
1051
1610
  io.close?.();
@@ -1072,18 +1631,18 @@ function maskConfigSecrets(value) {
1072
1631
  return output;
1073
1632
  }
1074
1633
  function configInit(targetPath, global = false) {
1075
- const path8 = writeExampleConfig({
1634
+ const path9 = writeExampleConfig({
1076
1635
  targetPath,
1077
1636
  global
1078
1637
  });
1079
1638
  if (!process.stdout.isTTY) {
1080
- process.stdout.write(`${path8}
1639
+ process.stdout.write(`${path9}
1081
1640
  `);
1082
1641
  return;
1083
1642
  }
1084
1643
  const ui = createPresentation(true);
1085
1644
  process.stdout.write(
1086
- `${ui.success(`${global ? "Machine-wide" : "Template"} config written to ${path8}`)}
1645
+ `${ui.success(`${global ? "Machine-wide" : "Template"} config written to ${path9}`)}
1087
1646
  `
1088
1647
  );
1089
1648
  }
@@ -1164,7 +1723,7 @@ function configUse(provider, configPath, env = process.env) {
1164
1723
  }
1165
1724
 
1166
1725
  // src/commands/agent.ts
1167
- import fs4 from "fs";
1726
+ import fs5 from "fs";
1168
1727
  import os2 from "os";
1169
1728
  import path6 from "path";
1170
1729
  import { createInterface as createInterface2 } from "readline/promises";
@@ -1246,16 +1805,34 @@ function getManagedBlockMarkers(agent) {
1246
1805
  end: `<!-- sift:end ${agent} -->`
1247
1806
  };
1248
1807
  }
1249
- 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) {
1250
1825
  return [
1251
- "Use `sift` as the default reduction layer for long, noisy, non-interactive command output.",
1252
- "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),
1253
1832
  "",
1254
1833
  "Start with:",
1255
1834
  '- `sift exec "question" -- <command> [args...]`',
1256
1835
  "- `sift exec --preset test-status -- <test command>`",
1257
- '- `sift watch "what changed between cycles?" < watcher-output.txt`',
1258
- '- `sift exec --watch "what changed between cycles?" -- <command> [args...]`',
1259
1836
  "- `sift exec --preset audit-critical -- npm audit`",
1260
1837
  "- `sift exec --preset infra-risk -- terraform plan`",
1261
1838
  "",
@@ -1294,9 +1871,9 @@ function renderInstructionBody() {
1294
1871
  "Do not pass API keys inline."
1295
1872
  ].join("\n");
1296
1873
  }
1297
- function renderManagedBlock(agent, eol = "\n") {
1874
+ function renderManagedBlock(agent, eol = "\n", mode = "agent-escalation") {
1298
1875
  const markers = getManagedBlockMarkers(agent);
1299
- return [markers.start, renderInstructionBody(), markers.end].join(eol);
1876
+ return [markers.start, renderInstructionBody(mode), markers.end].join(eol);
1300
1877
  }
1301
1878
  function inspectManagedBlock(content, agent) {
1302
1879
  const markers = getManagedBlockMarkers(agent);
@@ -1321,7 +1898,7 @@ function inspectManagedBlock(content, agent) {
1321
1898
  }
1322
1899
  function planManagedInstall(args) {
1323
1900
  const eol = args.existingContent?.includes("\r\n") ? "\r\n" : "\n";
1324
- const block = renderManagedBlock(args.agent, eol);
1901
+ const block = renderManagedBlock(args.agent, eol, args.operationMode ?? "agent-escalation");
1325
1902
  if (args.existingContent === void 0) {
1326
1903
  return {
1327
1904
  action: "create",
@@ -1411,6 +1988,7 @@ function showAgent(args, ioArg = createStdoutOnlyIO()) {
1411
1988
  const params = typeof args === "string" ? {
1412
1989
  agent: args,
1413
1990
  scope: "repo",
1991
+ operationMode: void 0,
1414
1992
  raw: false,
1415
1993
  targetPath: void 0,
1416
1994
  cwd: void 0,
@@ -1419,6 +1997,7 @@ function showAgent(args, ioArg = createStdoutOnlyIO()) {
1419
1997
  } : {
1420
1998
  agent: args.agent,
1421
1999
  scope: args.scope ?? "repo",
2000
+ operationMode: args.operationMode,
1422
2001
  raw: args.raw ?? false,
1423
2002
  targetPath: args.targetPath,
1424
2003
  cwd: args.cwd,
@@ -1427,8 +2006,13 @@ function showAgent(args, ioArg = createStdoutOnlyIO()) {
1427
2006
  };
1428
2007
  const agent = normalizeAgentName(params.agent);
1429
2008
  const io = params.io;
2009
+ const operationMode = inferOperationMode({
2010
+ cwd: params.cwd,
2011
+ homeDir: params.homeDir,
2012
+ operationMode: params.operationMode
2013
+ });
1430
2014
  if (params.raw) {
1431
- io.write(`${renderManagedBlock(agent)}
2015
+ io.write(`${renderManagedBlock(agent, "\n", operationMode)}
1432
2016
  `);
1433
2017
  return;
1434
2018
  }
@@ -1467,6 +2051,8 @@ function showAgent(args, ioArg = createStdoutOnlyIO()) {
1467
2051
  )}
1468
2052
  `
1469
2053
  );
2054
+ io.write(`${ui.labelValue("operation mode", getOperationModeLabel(operationMode))}
2055
+ `);
1470
2056
  if (currentInstalled) {
1471
2057
  io.write(`${ui.warning(`Already installed in ${params.scope} scope.`)}
1472
2058
  `);
@@ -1484,13 +2070,17 @@ function showAgent(args, ioArg = createStdoutOnlyIO()) {
1484
2070
  `
1485
2071
  );
1486
2072
  io.write(
1487
- `${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.")}
1488
2074
  `
1489
2075
  );
1490
2076
  io.write(
1491
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.")}
1492
2078
  `
1493
2079
  );
2080
+ io.write(`${ui.note(describeOperationMode(operationMode))}
2081
+ `);
2082
+ io.write(`${ui.note(describeInsufficientBehavior(operationMode))}
2083
+ `);
1494
2084
  io.write(` ${ui.command('sift exec "question" -- <command> [args...]')}
1495
2085
  `);
1496
2086
  io.write(` ${ui.command("sift exec --preset test-status -- <test command>")}
@@ -1500,7 +2090,7 @@ function showAgent(args, ioArg = createStdoutOnlyIO()) {
1500
2090
  io.write(` ${ui.command("sift exec --preset infra-risk -- terraform plan")}
1501
2091
  `);
1502
2092
  io.write(
1503
- `${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.")}
1504
2094
  `
1505
2095
  );
1506
2096
  io.write(
@@ -1550,6 +2140,11 @@ async function installAgent(args) {
1550
2140
  homeDir: args.homeDir
1551
2141
  });
1552
2142
  const ui = createPresentation(io.stdoutIsTTY);
2143
+ const operationMode = inferOperationMode({
2144
+ cwd: args.cwd,
2145
+ homeDir: args.homeDir,
2146
+ operationMode: args.operationMode
2147
+ });
1553
2148
  try {
1554
2149
  const existingContent = readOptionalFile(targetPath);
1555
2150
  const fileExists = existingContent !== void 0;
@@ -1557,7 +2152,8 @@ async function installAgent(args) {
1557
2152
  const plan = planManagedInstall({
1558
2153
  agent,
1559
2154
  targetPath,
1560
- existingContent
2155
+ existingContent,
2156
+ operationMode
1561
2157
  });
1562
2158
  if (args.dryRun) {
1563
2159
  if (args.raw) {
@@ -1613,6 +2209,8 @@ async function installAgent(args) {
1613
2209
  io.write(`${ui.labelValue("scope", scope)}
1614
2210
  `);
1615
2211
  io.write(`${ui.labelValue("target", targetPath)}
2212
+ `);
2213
+ io.write(`${ui.labelValue("operation mode", getOperationModeLabel(operationMode))}
1616
2214
  `);
1617
2215
  io.write(`${ui.info("This will only manage the sift block.")}
1618
2216
  `);
@@ -1794,25 +2392,484 @@ function joinAroundRemoval(before, after, eol) {
1794
2392
  return `${left}${eol}${eol}${right}`;
1795
2393
  }
1796
2394
  function readOptionalFile(targetPath) {
1797
- if (!fs4.existsSync(targetPath)) {
2395
+ if (!fs5.existsSync(targetPath)) {
1798
2396
  return void 0;
1799
2397
  }
1800
- const stats = fs4.statSync(targetPath);
2398
+ const stats = fs5.statSync(targetPath);
1801
2399
  if (!stats.isFile()) {
1802
2400
  throw new Error(`${targetPath} exists but is not a file.`);
1803
2401
  }
1804
- return fs4.readFileSync(targetPath, "utf8");
2402
+ return fs5.readFileSync(targetPath, "utf8");
1805
2403
  }
1806
2404
  function writeTextFileAtomic(targetPath, content) {
1807
- fs4.mkdirSync(path6.dirname(targetPath), { recursive: true });
2405
+ fs5.mkdirSync(path6.dirname(targetPath), { recursive: true });
1808
2406
  const tempPath = `${targetPath}.tmp-${process.pid}-${Date.now()}`;
1809
- fs4.writeFileSync(tempPath, content, "utf8");
1810
- fs4.renameSync(tempPath, targetPath);
2407
+ fs5.writeFileSync(tempPath, content, "utf8");
2408
+ fs5.renameSync(tempPath, targetPath);
1811
2409
  }
1812
2410
  function escapeRegExp(value) {
1813
2411
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1814
2412
  }
1815
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
+
1816
2873
  // src/commands/doctor.ts
1817
2874
  var PLACEHOLDER_API_KEYS = [
1818
2875
  "YOUR_API_KEY",
@@ -1836,16 +2893,21 @@ function isRealApiKey(key) {
1836
2893
  }
1837
2894
  function runDoctor(config, configPath) {
1838
2895
  const ui = createPresentation(Boolean(process.stdout.isTTY));
2896
+ const effectiveMode = resolveEffectiveOperationMode(config);
1839
2897
  const apiKeyStatus = isRealApiKey(config.provider.apiKey) ? "set" : isPlaceholderApiKey(config.provider.apiKey) ? "placeholder (not a real key)" : "not set";
1840
2898
  const lines = [
1841
2899
  "sift doctor",
1842
2900
  "A quick check for your local setup.",
1843
- "mode: local config completeness check",
2901
+ "mode: operation-mode health check",
1844
2902
  ui.labelValue("configPath", configPath ?? "(defaults only)"),
2903
+ ui.labelValue("configuredMode", getOperationModeLabel(config.runtime.operationMode)),
2904
+ ui.labelValue("effectiveMode", getOperationModeLabel(effectiveMode)),
1845
2905
  ui.labelValue("provider", config.provider.provider),
1846
2906
  ui.labelValue("model", config.provider.model),
1847
2907
  ui.labelValue("baseUrl", config.provider.baseUrl),
1848
2908
  ui.labelValue("apiKey", apiKeyStatus),
2909
+ ui.labelValue("modeSummary", describeOperationMode(effectiveMode)),
2910
+ ui.labelValue("insufficientBehavior", describeInsufficientBehavior(effectiveMode)),
1849
2911
  ui.labelValue("maxCaptureChars", String(config.input.maxCaptureChars)),
1850
2912
  ui.labelValue("maxInputChars", String(config.input.maxInputChars)),
1851
2913
  ui.labelValue("rawFallback", String(config.runtime.rawFallback))
@@ -1853,24 +2915,31 @@ function runDoctor(config, configPath) {
1853
2915
  process.stdout.write(`${lines.join("\n")}
1854
2916
  `);
1855
2917
  const problems = [];
1856
- if (!config.provider.baseUrl) {
1857
- problems.push("Missing provider.baseUrl");
1858
- }
1859
- if (!config.provider.model) {
1860
- problems.push("Missing provider.model");
1861
- }
1862
- if ((config.provider.provider === "openai" || config.provider.provider === "openai-compatible" || config.provider.provider === "openrouter") && !isRealApiKey(config.provider.apiKey)) {
1863
- if (isPlaceholderApiKey(config.provider.apiKey)) {
1864
- problems.push(`provider.apiKey looks like a placeholder: "${config.provider.apiKey}"`);
1865
- } else {
1866
- 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
+ );
1867
2942
  }
1868
- problems.push(
1869
- `Set one of: ${getProviderApiKeyEnvNames(
1870
- config.provider.provider,
1871
- config.provider.baseUrl
1872
- ).join(", ")}`
1873
- );
1874
2943
  }
1875
2944
  if (problems.length > 0) {
1876
2945
  if (process.stderr.isTTY) {
@@ -1983,6 +3052,7 @@ var OpenAIProvider = class {
1983
3052
  signal: controller.signal,
1984
3053
  headers: {
1985
3054
  "content-type": "application/json",
3055
+ connection: "close",
1986
3056
  ...this.apiKey ? { authorization: `Bearer ${this.apiKey}` } : {}
1987
3057
  },
1988
3058
  body: JSON.stringify({
@@ -2083,6 +3153,7 @@ var OpenAICompatibleProvider = class {
2083
3153
  signal: controller.signal,
2084
3154
  headers: {
2085
3155
  "content-type": "application/json",
3156
+ connection: "close",
2086
3157
  ...this.apiKey ? { authorization: `Bearer ${this.apiKey}` } : {}
2087
3158
  },
2088
3159
  body: JSON.stringify({
@@ -2725,16 +3796,16 @@ function extractBucketPathCandidates(args) {
2725
3796
  }
2726
3797
  return [...candidates];
2727
3798
  }
2728
- function isConfigPathCandidate(path8) {
2729
- return /^\.github\/workflows\/.+\.(?:yml|yaml)$/i.test(path8) || /^(?:package\.json|pytest\.ini|pyproject\.toml|tox\.ini|(?:[A-Za-z0-9._/-]+\/)?conftest\.py)$/i.test(
2730
- path8
2731
- ) || /^(?:[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);
2732
3803
  }
2733
- function isAppPathCandidate(path8) {
2734
- return path8.startsWith("src/");
3804
+ function isAppPathCandidate(path9) {
3805
+ return path9.startsWith("src/");
2735
3806
  }
2736
- function isTestPathCandidate(path8) {
2737
- return path8.startsWith("test/") || path8.startsWith("tests/");
3807
+ function isTestPathCandidate(path9) {
3808
+ return path9.startsWith("test/") || path9.startsWith("tests/");
2738
3809
  }
2739
3810
  function looksLikeMatcherLiteralComparison(detail) {
2740
3811
  return /\bexpected\b[\s\S]*\bto (?:be|contain)\b/i.test(detail);
@@ -3297,13 +4368,13 @@ function buildExtendedBucketSearchHint(bucket, anchor) {
3297
4368
  return detail.replace(/^of\s+/i, "") || anchor.label;
3298
4369
  }
3299
4370
  if (extended.type === "file_not_found_failure") {
3300
- const path8 = detail.match(/['"]([^'"]+)['"]/)?.[1];
3301
- return path8 ?? detail;
4371
+ const path9 = detail.match(/['"]([^'"]+)['"]/)?.[1];
4372
+ return path9 ?? detail;
3302
4373
  }
3303
4374
  if (extended.type === "permission_denied_failure") {
3304
- const path8 = detail.match(/['"]([^'"]+)['"]/)?.[1];
4375
+ const path9 = detail.match(/['"]([^'"]+)['"]/)?.[1];
3305
4376
  const port = detail.match(/\bport\s+(\d+)\b/i)?.[1];
3306
- return path8 ?? (port ? `port ${port}` : detail);
4377
+ return path9 ?? (port ? `port ${port}` : detail);
3307
4378
  }
3308
4379
  return detail;
3309
4380
  }
@@ -4411,7 +5482,7 @@ function buildPrompt(args) {
4411
5482
  "If per-test or per-module mapping is unclear, fall back to the focused grouped-cause view instead of guessing."
4412
5483
  ] : [];
4413
5484
  const prompt = [
4414
- "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.",
4415
5486
  "Hard rules:",
4416
5487
  ...policy.sharedRules.map((rule) => `- ${rule}`),
4417
5488
  "",
@@ -6189,7 +7260,7 @@ function extractContractDriftEntities(input) {
6189
7260
  }
6190
7261
  function buildContractRepresentativeReason(args) {
6191
7262
  if (/openapi/i.test(args.label) && args.entities.apiPaths.length > 0) {
6192
- 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];
6193
7264
  args.usedPaths.add(nextPath);
6194
7265
  return `added path: ${nextPath}`;
6195
7266
  }
@@ -8265,8 +9336,8 @@ function emitStatsFooter(args) {
8265
9336
  }
8266
9337
 
8267
9338
  // src/core/testStatusState.ts
8268
- import fs5 from "fs";
8269
- import path7 from "path";
9339
+ import fs6 from "fs";
9340
+ import path8 from "path";
8270
9341
  import { z as z3 } from "zod";
8271
9342
  var detailSchema = z3.enum(["standard", "focused", "verbose"]);
8272
9343
  var failureBucketTypeSchema = z3.enum([
@@ -8428,7 +9499,7 @@ function buildBucketSignature(bucket) {
8428
9499
  ]);
8429
9500
  }
8430
9501
  function basenameMatches(value, matcher) {
8431
- return matcher.test(path7.basename(value));
9502
+ return matcher.test(path8.basename(value));
8432
9503
  }
8433
9504
  function isPytestExecutable(value) {
8434
9505
  return basenameMatches(value, /^pytest(?:\.exe)?$/i);
@@ -8587,7 +9658,7 @@ function buildCachedRunnerState(args) {
8587
9658
  };
8588
9659
  }
8589
9660
  function normalizeCwd(value) {
8590
- return path7.resolve(value).replace(/\\/g, "/");
9661
+ return path8.resolve(value).replace(/\\/g, "/");
8591
9662
  }
8592
9663
  function buildTestStatusBaselineIdentity(args) {
8593
9664
  const cwd = normalizeCwd(args.cwd);
@@ -8715,7 +9786,7 @@ function migrateCachedTestStatusRun(state) {
8715
9786
  function readCachedTestStatusRun(statePath = getDefaultTestStatusStatePath()) {
8716
9787
  let raw = "";
8717
9788
  try {
8718
- raw = fs5.readFileSync(statePath, "utf8");
9789
+ raw = fs6.readFileSync(statePath, "utf8");
8719
9790
  } catch (error) {
8720
9791
  if (error.code === "ENOENT") {
8721
9792
  throw new MissingCachedTestStatusRunError();
@@ -8736,10 +9807,10 @@ function tryReadCachedTestStatusRun(statePath = getDefaultTestStatusStatePath())
8736
9807
  }
8737
9808
  }
8738
9809
  function writeCachedTestStatusRun(state, statePath = getDefaultTestStatusStatePath()) {
8739
- fs5.mkdirSync(path7.dirname(statePath), {
9810
+ fs6.mkdirSync(path8.dirname(statePath), {
8740
9811
  recursive: true
8741
9812
  });
8742
- fs5.writeFileSync(statePath, `${JSON.stringify(state, null, 2)}
9813
+ fs6.writeFileSync(statePath, `${JSON.stringify(state, null, 2)}
8743
9814
  `, "utf8");
8744
9815
  }
8745
9816
  function getNextEscalationDetail(detail) {
@@ -8901,7 +9972,8 @@ function resolveEscalationDetail(state, requested, showRaw = false) {
8901
9972
  return nextDetail;
8902
9973
  }
8903
9974
  async function runEscalate(request) {
8904
- const state = readCachedTestStatusRun();
9975
+ const scopedStatePath = getScopedTestStatusStatePath(process.cwd());
9976
+ const state = readCachedTestStatusRun(scopedStatePath);
8905
9977
  const detail = resolveEscalationDetail(state, request.detail, request.showRaw);
8906
9978
  if (request.verbose) {
8907
9979
  process.stderr.write(
@@ -8949,10 +10021,13 @@ async function runEscalate(request) {
8949
10021
  quiet: Boolean(request.quiet)
8950
10022
  });
8951
10023
  try {
8952
- writeCachedTestStatusRun({
8953
- ...state,
8954
- detail
8955
- });
10024
+ writeCachedTestStatusRun(
10025
+ {
10026
+ ...state,
10027
+ detail
10028
+ },
10029
+ scopedStatePath
10030
+ );
8956
10031
  } catch (error) {
8957
10032
  if (request.verbose) {
8958
10033
  const reason = error instanceof Error ? error.message : "unknown_error";
@@ -9275,13 +10350,97 @@ function buildCommandPreview(request) {
9275
10350
  }
9276
10351
  return (request.command ?? []).join(" ");
9277
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
+ }
9278
10434
  function getExecSuccessShortcut(args) {
9279
10435
  if (args.exitCode !== 0) {
9280
10436
  return null;
9281
10437
  }
9282
- if (args.presetName === "typecheck-summary" && args.capturedOutput.trim() === "") {
10438
+ if (args.presetName === "typecheck-summary" && args.normalizedOutput.trim() === "") {
9283
10439
  return "No type errors.";
9284
10440
  }
10441
+ if (args.presetName === "lint-failures" && args.normalizedOutput.trim() === "") {
10442
+ return "No lint failures.";
10443
+ }
9285
10444
  return null;
9286
10445
  }
9287
10446
  async function runExec(request) {
@@ -9293,10 +10452,11 @@ async function runExec(request) {
9293
10452
  const shellPath = process.env.SHELL || "/bin/bash";
9294
10453
  const commandPreview = buildCommandPreview(request);
9295
10454
  const commandCwd = request.cwd ?? process.cwd();
10455
+ const scopedStatePath = getScopedTestStatusStatePath(commandCwd);
9296
10456
  const isTestStatusPreset = request.presetName === "test-status";
9297
10457
  const readCachedBaseline = isTestStatusPreset && (request.readCachedBaseline ?? true);
9298
10458
  const writeCachedBaselineRequested = isTestStatusPreset && (request.writeCachedBaseline ?? (request.skipCacheWrite ? false : true));
9299
- const previousCachedRun = readCachedBaseline ? tryReadCachedTestStatusRun() : null;
10459
+ const previousCachedRun = readCachedBaseline ? tryReadCachedTestStatusRun(scopedStatePath) : null;
9300
10460
  if (request.config.runtime.verbose) {
9301
10461
  process.stderr.write(
9302
10462
  `${pc5.dim("sift")} exec mode=${hasShellCommand ? "shell" : "argv"} command=${commandPreview}
@@ -9360,6 +10520,10 @@ async function runExec(request) {
9360
10520
  }
9361
10521
  const exitCode = normalizeChildExitCode(childStatus, childSignal);
9362
10522
  const capturedOutput = capture.render();
10523
+ const normalizedOutput = normalizeScriptWrapperOutput({
10524
+ commandPreview,
10525
+ capturedOutput
10526
+ });
9363
10527
  const autoWatchDetected = !request.watch && looksLikeWatchStream(capturedOutput);
9364
10528
  const useWatchFlow = Boolean(request.watch) || autoWatchDetected;
9365
10529
  const shouldBuildTestStatusState = isTestStatusPreset && !useWatchFlow;
@@ -9385,7 +10549,7 @@ async function runExec(request) {
9385
10549
  const execSuccessShortcut = useWatchFlow ? null : getExecSuccessShortcut({
9386
10550
  presetName: request.presetName,
9387
10551
  exitCode,
9388
- capturedOutput
10552
+ normalizedOutput
9389
10553
  });
9390
10554
  if (execSuccessShortcut && !request.dryRun) {
9391
10555
  if (request.config.runtime.verbose) {
@@ -9411,15 +10575,15 @@ async function runExec(request) {
9411
10575
  if (useWatchFlow) {
9412
10576
  let output2 = await runWatch({
9413
10577
  ...request,
9414
- stdin: capturedOutput
10578
+ stdin: normalizedOutput
9415
10579
  });
9416
10580
  if (isInsufficientSignalOutput(output2)) {
9417
10581
  output2 = buildInsufficientSignalOutput({
9418
10582
  presetName: request.presetName,
9419
- originalLength: capture.getTotalChars(),
10583
+ originalLength: normalizedOutput.length > 0 ? normalizedOutput.length : capture.getTotalChars(),
9420
10584
  truncatedApplied: capture.wasTruncated(),
9421
10585
  exitCode,
9422
- recognizedRunner: detectTestRunner(capturedOutput)
10586
+ recognizedRunner: detectTestRunner(normalizedOutput)
9423
10587
  });
9424
10588
  }
9425
10589
  process.stdout.write(`${output2}
@@ -9458,7 +10622,7 @@ async function runExec(request) {
9458
10622
  }) : null;
9459
10623
  const result = await runSiftWithStats({
9460
10624
  ...request,
9461
- stdin: capturedOutput,
10625
+ stdin: normalizedOutput,
9462
10626
  analysisContext: request.testStatusContext?.remainingMode && request.testStatusContext.remainingMode !== "none" && request.presetName === "test-status" ? [
9463
10627
  request.analysisContext,
9464
10628
  "Zoom context:",
@@ -9481,10 +10645,10 @@ async function runExec(request) {
9481
10645
  if (isInsufficientSignalOutput(output)) {
9482
10646
  output = buildInsufficientSignalOutput({
9483
10647
  presetName: request.presetName,
9484
- originalLength: capture.getTotalChars(),
10648
+ originalLength: normalizedOutput.length > 0 ? normalizedOutput.length : capture.getTotalChars(),
9485
10649
  truncatedApplied: capture.wasTruncated(),
9486
10650
  exitCode,
9487
- recognizedRunner: detectTestRunner(capturedOutput)
10651
+ recognizedRunner: detectTestRunner(normalizedOutput)
9488
10652
  });
9489
10653
  }
9490
10654
  if (request.diff && !request.dryRun && previousCachedRun && currentCachedRun) {
@@ -9499,7 +10663,7 @@ ${output}`;
9499
10663
  }
9500
10664
  if (currentCachedRun && shouldWriteCachedBaseline) {
9501
10665
  try {
9502
- writeCachedTestStatusRun(currentCachedRun);
10666
+ writeCachedTestStatusRun(currentCachedRun, scopedStatePath);
9503
10667
  } catch (error) {
9504
10668
  if (request.config.runtime.verbose) {
9505
10669
  const reason = error instanceof Error ? error.message : "unknown_error";
@@ -9511,10 +10675,10 @@ ${output}`;
9511
10675
  } else if (isInsufficientSignalOutput(output)) {
9512
10676
  output = buildInsufficientSignalOutput({
9513
10677
  presetName: request.presetName,
9514
- originalLength: capture.getTotalChars(),
10678
+ originalLength: normalizedOutput.length > 0 ? normalizedOutput.length : capture.getTotalChars(),
9515
10679
  truncatedApplied: capture.wasTruncated(),
9516
10680
  exitCode,
9517
- recognizedRunner: detectTestRunner(capturedOutput)
10681
+ recognizedRunner: detectTestRunner(normalizedOutput)
9518
10682
  });
9519
10683
  }
9520
10684
  process.stdout.write(`${output}
@@ -9535,7 +10699,7 @@ ${output}`;
9535
10699
 
9536
10700
  // src/core/rerun.ts
9537
10701
  async function runRerun(request) {
9538
- const state = readCachedTestStatusRun();
10702
+ const state = readCachedTestStatusRun(getScopedTestStatusStatePath(process.cwd()));
9539
10703
  if (!request.remaining) {
9540
10704
  return runExec({
9541
10705
  ...request,
@@ -9625,6 +10789,7 @@ function getPreset(config, name) {
9625
10789
  var require2 = createRequire(import.meta.url);
9626
10790
  var pkg = require2("../package.json");
9627
10791
  var defaultCliDeps = {
10792
+ installRuntimeSupport,
9628
10793
  installAgent,
9629
10794
  removeAgent,
9630
10795
  showAgent,
@@ -9979,7 +11144,7 @@ function createCliApp(args = {}) {
9979
11144
  });
9980
11145
  });
9981
11146
  applySharedOptions(
9982
- 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()
9983
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) => {
9984
11149
  if (question === "preset") {
9985
11150
  throw new Error("Use 'sift exec --preset <name> -- <program> ...' instead.");
@@ -10258,6 +11423,14 @@ function createCliApp(args = {}) {
10258
11423
  stdout.write(`${output}
10259
11424
  `);
10260
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
+ });
10261
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) => {
10262
11435
  const scope = normalizeAgentScope(options.scope);
10263
11436
  if (action === "show") {
@@ -10401,14 +11574,14 @@ function createCliApp(args = {}) {
10401
11574
  {
10402
11575
  title: ui.section("Quick start"),
10403
11576
  body: [
10404
- ` ${ui.command("sift config setup")}`,
11577
+ ` ${ui.command("sift install")}${ui.note(" # choose agent-escalation, provider-assisted, or local-only")}`,
10405
11578
  ` ${ui.command("sift exec --preset test-status -- npm test")}`,
10406
11579
  ` ${ui.command("sift exec --preset test-status -- npm test")}${ui.note(" # stop here if standard already shows the main buckets")}`,
10407
11580
  ` ${ui.command("sift rerun")}${ui.note(" # rerun the cached full suite after a fix")}`,
10408
11581
  ` ${ui.command("sift rerun --remaining --detail focused")}${ui.note(" # zoom into what is still failing")}`,
10409
11582
  ` ${ui.command("sift rerun --remaining --detail verbose --show-raw")}`,
10410
- ` ${ui.command('sift watch "what changed between cycles?" < watcher-output.txt')}`,
10411
- ` ${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")}`,
10412
11585
  ` ${ui.command("sift agent install codex --dry-run")}`,
10413
11586
  ` ${ui.command("sift agent install codex --dry-run --raw")}`,
10414
11587
  ` ${ui.command("sift agent status")}`,