@bilalimamoglu/sift 0.3.0 → 0.3.1

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
@@ -136,6 +136,78 @@ var defaultConfig = {
136
136
  }
137
137
  };
138
138
 
139
+ // src/config/native-provider.ts
140
+ function getNativeProviderDefaults(provider) {
141
+ if (provider === "openrouter") {
142
+ return {
143
+ provider,
144
+ model: "openrouter/free",
145
+ baseUrl: "https://openrouter.ai/api/v1"
146
+ };
147
+ }
148
+ return {
149
+ provider,
150
+ model: defaultConfig.provider.model,
151
+ baseUrl: defaultConfig.provider.baseUrl
152
+ };
153
+ }
154
+ function getProfileProviderState(provider, profile) {
155
+ const defaults = getNativeProviderDefaults(provider);
156
+ return {
157
+ provider,
158
+ model: profile?.model ?? defaults.model,
159
+ baseUrl: profile?.baseUrl ?? defaults.baseUrl
160
+ };
161
+ }
162
+ function getStoredProviderProfile(config, provider) {
163
+ const existingProfile = config.providerProfiles?.[provider];
164
+ if (existingProfile) {
165
+ return existingProfile;
166
+ }
167
+ if (config.provider.provider !== provider) {
168
+ return void 0;
169
+ }
170
+ return {
171
+ model: config.provider.model,
172
+ baseUrl: config.provider.baseUrl,
173
+ apiKey: config.provider.apiKey || void 0
174
+ };
175
+ }
176
+ function setStoredProviderProfile(config, provider, profile) {
177
+ const providerProfiles = {
178
+ ...config.providerProfiles ?? {},
179
+ [provider]: profile
180
+ };
181
+ return {
182
+ ...config,
183
+ providerProfiles
184
+ };
185
+ }
186
+ function preserveActiveNativeProviderProfile(config) {
187
+ const provider = config.provider.provider;
188
+ if (provider !== "openai" && provider !== "openrouter") {
189
+ return config;
190
+ }
191
+ if (config.providerProfiles?.[provider]) {
192
+ return config;
193
+ }
194
+ return setStoredProviderProfile(config, provider, {
195
+ model: config.provider.model,
196
+ baseUrl: config.provider.baseUrl,
197
+ apiKey: config.provider.apiKey || void 0
198
+ });
199
+ }
200
+ function applyActiveProvider(config, provider, profile, apiKey) {
201
+ return {
202
+ ...config,
203
+ provider: {
204
+ ...config.provider,
205
+ ...getProfileProviderState(provider, profile),
206
+ apiKey
207
+ }
208
+ };
209
+ }
210
+
139
211
  // src/config/provider-api-key.ts
140
212
  var OPENAI_COMPATIBLE_BASE_URL_ENV = [
141
213
  { prefix: "https://api.openai.com/", envName: "OPENAI_API_KEY" },
@@ -143,13 +215,16 @@ var OPENAI_COMPATIBLE_BASE_URL_ENV = [
143
215
  { prefix: "https://api.together.xyz/", envName: "TOGETHER_API_KEY" },
144
216
  { prefix: "https://api.groq.com/openai/", envName: "GROQ_API_KEY" }
145
217
  ];
218
+ var NATIVE_PROVIDER_API_KEY_ENV = {
219
+ openai: "OPENAI_API_KEY",
220
+ openrouter: "OPENROUTER_API_KEY"
221
+ };
146
222
  var PROVIDER_API_KEY_ENV = {
147
223
  anthropic: "ANTHROPIC_API_KEY",
148
224
  claude: "ANTHROPIC_API_KEY",
149
225
  groq: "GROQ_API_KEY",
150
- openai: "OPENAI_API_KEY",
151
- openrouter: "OPENROUTER_API_KEY",
152
- together: "TOGETHER_API_KEY"
226
+ together: "TOGETHER_API_KEY",
227
+ ...NATIVE_PROVIDER_API_KEY_ENV
153
228
  };
154
229
  function normalizeBaseUrl(baseUrl) {
155
230
  if (!baseUrl) {
@@ -184,6 +259,9 @@ function resolveProviderApiKey(provider, baseUrl, env) {
184
259
  }
185
260
  return env.SIFT_PROVIDER_API_KEY;
186
261
  }
262
+ function getNativeProviderApiKeyEnvName(provider) {
263
+ return NATIVE_PROVIDER_API_KEY_ENV[provider];
264
+ }
187
265
  function getProviderApiKeyEnvNames(provider, baseUrl) {
188
266
  const envNames = ["SIFT_PROVIDER_API_KEY"];
189
267
  if (provider === "openai-compatible") {
@@ -205,7 +283,11 @@ function getProviderApiKeyEnvNames(provider, baseUrl) {
205
283
 
206
284
  // src/config/schema.ts
207
285
  import { z } from "zod";
208
- var providerNameSchema = z.enum(["openai", "openai-compatible"]);
286
+ var providerNameSchema = z.enum([
287
+ "openai",
288
+ "openai-compatible",
289
+ "openrouter"
290
+ ]);
209
291
  var outputFormatSchema = z.enum([
210
292
  "brief",
211
293
  "bullets",
@@ -234,6 +316,15 @@ var providerConfigSchema = z.object({
234
316
  temperature: z.number().min(0).max(2),
235
317
  maxOutputTokens: z.number().int().positive()
236
318
  });
319
+ var providerProfileSchema = z.object({
320
+ model: z.string().min(1).optional(),
321
+ baseUrl: z.string().url().optional(),
322
+ apiKey: z.string().optional()
323
+ });
324
+ var providerProfilesSchema = z.object({
325
+ openai: providerProfileSchema.optional(),
326
+ openrouter: providerProfileSchema.optional()
327
+ }).optional();
237
328
  var inputConfigSchema = z.object({
238
329
  stripAnsi: z.boolean(),
239
330
  redact: z.boolean(),
@@ -258,10 +349,19 @@ var siftConfigSchema = z.object({
258
349
  provider: providerConfigSchema,
259
350
  input: inputConfigSchema,
260
351
  runtime: runtimeConfigSchema,
261
- presets: z.record(presetDefinitionSchema)
352
+ presets: z.record(presetDefinitionSchema),
353
+ providerProfiles: providerProfilesSchema
262
354
  });
263
355
 
264
356
  // src/config/resolve.ts
357
+ var PROVIDER_DEFAULT_OVERRIDES = {
358
+ openrouter: {
359
+ provider: {
360
+ model: "openrouter/free",
361
+ baseUrl: "https://openrouter.ai/api/v1"
362
+ }
363
+ }
364
+ };
265
365
  function isRecord(value) {
266
366
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
267
367
  }
@@ -324,13 +424,32 @@ function buildCredentialEnvOverrides(env, context) {
324
424
  }
325
425
  };
326
426
  }
427
+ function getBaseConfigForProvider(provider) {
428
+ return mergeDefined(defaultConfig, provider ? PROVIDER_DEFAULT_OVERRIDES[provider] : {});
429
+ }
430
+ function resolveProvisionalProvider(args) {
431
+ const provisional = mergeDefined(
432
+ mergeDefined(
433
+ mergeDefined(defaultConfig, args.fileConfig),
434
+ args.nonCredentialEnvConfig
435
+ ),
436
+ stripApiKey(args.cliOverrides) ?? {}
437
+ );
438
+ return provisional.provider.provider;
439
+ }
327
440
  function resolveConfig(options = {}) {
328
441
  const env = options.env ?? process.env;
329
442
  const fileConfig = loadRawConfig(options.configPath);
330
443
  const nonCredentialEnvConfig = buildNonCredentialEnvOverrides(env);
444
+ const provisionalProvider = resolveProvisionalProvider({
445
+ fileConfig,
446
+ nonCredentialEnvConfig,
447
+ cliOverrides: options.cliOverrides
448
+ });
449
+ const baseConfig = getBaseConfigForProvider(provisionalProvider);
331
450
  const contextConfig = mergeDefined(
332
451
  mergeDefined(
333
- mergeDefined(defaultConfig, fileConfig),
452
+ mergeDefined(baseConfig, fileConfig),
334
453
  nonCredentialEnvConfig
335
454
  ),
336
455
  stripApiKey(options.cliOverrides) ?? {}
@@ -342,7 +461,7 @@ function resolveConfig(options = {}) {
342
461
  const merged = mergeDefined(
343
462
  mergeDefined(
344
463
  mergeDefined(
345
- mergeDefined(defaultConfig, fileConfig),
464
+ mergeDefined(baseConfig, fileConfig),
346
465
  nonCredentialEnvConfig
347
466
  ),
348
467
  credentialEnvConfig
@@ -387,6 +506,32 @@ function writeConfigFile(options) {
387
506
  return resolved;
388
507
  }
389
508
 
509
+ // src/config/editable.ts
510
+ import fs3 from "fs";
511
+ import path4 from "path";
512
+ function isRecord2(value) {
513
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
514
+ }
515
+ function resolveEditableConfigPath(explicitPath) {
516
+ if (explicitPath) {
517
+ return path4.resolve(explicitPath);
518
+ }
519
+ return findConfigPath() ?? getDefaultGlobalConfigPath();
520
+ }
521
+ function loadEditableConfig(explicitPath) {
522
+ const resolvedPath = resolveEditableConfigPath(explicitPath);
523
+ const existed = fs3.existsSync(resolvedPath);
524
+ const rawConfig = existed ? loadRawConfig(resolvedPath) : {};
525
+ const config = siftConfigSchema.parse(
526
+ mergeDefined(defaultConfig, isRecord2(rawConfig) ? rawConfig : {})
527
+ );
528
+ return {
529
+ config,
530
+ existed,
531
+ resolvedPath
532
+ };
533
+ }
534
+
390
535
  // src/ui/presentation.ts
391
536
  import pc from "picocolors";
392
537
  function applyColor(enabled, formatter, value) {
@@ -437,8 +582,7 @@ ${tagline}`;
437
582
  }
438
583
 
439
584
  // src/commands/config-setup.ts
440
- import fs3 from "fs";
441
- import path4 from "path";
585
+ import path5 from "path";
442
586
  import { emitKeypressEvents } from "readline";
443
587
  import { createInterface } from "readline/promises";
444
588
  import { stderr as defaultStderr, stdin as defaultStdin2, stdout as defaultStdout } from "process";
@@ -636,59 +780,102 @@ function createTerminalIO() {
636
780
  };
637
781
  }
638
782
  function resolveSetupPath(targetPath) {
639
- return targetPath ? path4.resolve(targetPath) : getDefaultGlobalConfigPath();
640
- }
641
- function buildOpenAISetupConfig(apiKey) {
642
- return {
643
- ...defaultConfig,
644
- provider: {
645
- ...defaultConfig.provider,
646
- provider: "openai",
647
- model: "gpt-5-nano",
648
- baseUrl: "https://api.openai.com/v1",
649
- apiKey
650
- }
651
- };
783
+ return targetPath ? path5.resolve(targetPath) : getDefaultGlobalConfigPath();
652
784
  }
653
785
  function getSetupPresenter(io) {
654
786
  return createPresentation(io.stdoutIsTTY);
655
787
  }
788
+ function getProviderLabel(provider) {
789
+ return provider === "openrouter" ? "OpenRouter" : "OpenAI";
790
+ }
656
791
  async function promptForProvider(io) {
657
792
  if (io.select) {
658
- const choice = await io.select("Select provider for this machine", ["OpenAI"]);
793
+ const choice = await io.select("Select provider for this machine", [
794
+ "OpenAI",
795
+ "OpenRouter"
796
+ ]);
659
797
  if (choice === "OpenAI") {
660
798
  return "openai";
661
799
  }
800
+ if (choice === "OpenRouter") {
801
+ return "openrouter";
802
+ }
662
803
  }
663
804
  while (true) {
664
- const answer = (await io.ask("Provider [OpenAI]: ")).trim().toLowerCase();
805
+ const answer = (await io.ask("Provider [OpenAI/OpenRouter]: ")).trim().toLowerCase();
665
806
  if (answer === "" || answer === "openai") {
666
807
  return "openai";
667
808
  }
668
- io.error("Only OpenAI is supported in guided setup right now.\n");
809
+ if (answer === "openrouter") {
810
+ return "openrouter";
811
+ }
812
+ io.error("Only OpenAI and OpenRouter are supported in guided setup right now.\n");
669
813
  }
670
814
  }
671
- async function promptForApiKey(io) {
815
+ async function promptForApiKey(io, provider) {
816
+ const providerLabel = getProviderLabel(provider);
817
+ const promptText = `Enter your ${providerLabel} API key (input hidden): `;
818
+ const visiblePromptText = `Enter your ${providerLabel} API key: `;
672
819
  while (true) {
673
- const answer = (await (io.secret ? io.secret("Enter your OpenAI API key (input hidden): ") : io.ask("Enter your OpenAI API key: "))).trim();
820
+ const answer = (await (io.secret ? io.secret(promptText) : io.ask(visiblePromptText))).trim();
674
821
  if (answer.length > 0) {
675
822
  return answer;
676
823
  }
677
824
  io.error("API key cannot be empty.\n");
678
825
  }
679
826
  }
680
- async function promptForOverwrite(io, targetPath) {
827
+ async function promptForApiKeyChoice(args) {
828
+ const providerLabel = getProviderLabel(args.provider);
829
+ if (!args.hasSavedKey && !args.hasEnvKey) {
830
+ return "override";
831
+ }
832
+ if (args.hasSavedKey && args.hasEnvKey) {
833
+ if (args.io.select) {
834
+ const choice = await args.io.select(
835
+ `Found both a saved ${providerLabel} API key and ${args.envName} in your environment`,
836
+ ["Use saved key", "Use env key", "Override"]
837
+ );
838
+ if (choice === "Use saved key") {
839
+ return "saved";
840
+ }
841
+ if (choice === "Use env key") {
842
+ return "env";
843
+ }
844
+ }
845
+ while (true) {
846
+ const answer = (await args.io.ask("API key choice [saved/env/override]: ")).trim().toLowerCase();
847
+ if (answer === "" || answer === "saved") {
848
+ return "saved";
849
+ }
850
+ if (answer === "env") {
851
+ return "env";
852
+ }
853
+ if (answer === "override") {
854
+ return "override";
855
+ }
856
+ args.io.error("Please answer saved, env, or override.\n");
857
+ }
858
+ }
859
+ const sourceLabel = args.hasSavedKey ? "saved key" : `${args.envName} from your environment`;
860
+ if (args.io.select) {
861
+ const choice = await args.io.select(
862
+ `Found an existing ${providerLabel} API key via ${sourceLabel}`,
863
+ ["Use existing key", "Override"]
864
+ );
865
+ if (choice === "Override") {
866
+ return "override";
867
+ }
868
+ return args.hasSavedKey ? "saved" : "env";
869
+ }
681
870
  while (true) {
682
- const answer = (await io.ask(
683
- `Config file already exists at ${targetPath}. Overwrite? [y/N]: `
684
- )).trim().toLowerCase();
685
- if (answer === "" || answer === "n" || answer === "no") {
686
- return false;
871
+ const answer = (await args.io.ask("API key choice [existing/override]: ")).trim().toLowerCase();
872
+ if (answer === "" || answer === "existing") {
873
+ return args.hasSavedKey ? "saved" : "env";
687
874
  }
688
- if (answer === "y" || answer === "yes") {
689
- return true;
875
+ if (answer === "override") {
876
+ return "override";
690
877
  }
691
- io.error("Please answer y or n.\n");
878
+ args.io.error("Please answer existing or override.\n");
692
879
  }
693
880
  }
694
881
  function writeSetupSuccess(io, writtenPath) {
@@ -722,10 +909,86 @@ ${ui.section("Try next")}
722
909
  io.write(` ${ui.command("sift exec --preset test-status -- npm test")}
723
910
  `);
724
911
  }
912
+ function writeProviderDefaults(io, provider) {
913
+ const ui = getSetupPresenter(io);
914
+ if (provider === "openrouter") {
915
+ io.write(`${ui.info("Using OpenRouter defaults for your first run.")}
916
+ `);
917
+ io.write(`${ui.labelValue("Default model", "openrouter/free")}
918
+ `);
919
+ io.write(`${ui.labelValue("Default base URL", "https://openrouter.ai/api/v1")}
920
+ `);
921
+ } else {
922
+ io.write(`${ui.info("Using OpenAI defaults for your first run.")}
923
+ `);
924
+ io.write(`${ui.labelValue("Default model", "gpt-5-nano")}
925
+ `);
926
+ io.write(`${ui.labelValue("Default base URL", "https://api.openai.com/v1")}
927
+ `);
928
+ }
929
+ io.write(
930
+ `${ui.note("Want to switch providers later? Run 'sift config use openai' or 'sift config use openrouter'.")}
931
+ `
932
+ );
933
+ io.write(
934
+ `${ui.note("Want to inspect the active values first? Run 'sift config show --show-secrets'.")}
935
+ `
936
+ );
937
+ }
938
+ function materializeProfile(provider, profile, apiKey) {
939
+ return {
940
+ ...getProfileProviderState(provider, profile),
941
+ ...apiKey !== void 0 ? { apiKey } : {}
942
+ };
943
+ }
944
+ function buildSetupConfig(args) {
945
+ const preservedConfig = preserveActiveNativeProviderProfile(args.config);
946
+ const storedProfile = getStoredProviderProfile(preservedConfig, args.provider);
947
+ if (args.apiKeyChoice === "saved") {
948
+ const profile2 = materializeProfile(
949
+ args.provider,
950
+ storedProfile,
951
+ storedProfile?.apiKey ?? ""
952
+ );
953
+ const configWithProfile2 = setStoredProviderProfile(
954
+ preservedConfig,
955
+ args.provider,
956
+ profile2
957
+ );
958
+ return applyActiveProvider(
959
+ configWithProfile2,
960
+ args.provider,
961
+ profile2,
962
+ profile2.apiKey ?? ""
963
+ );
964
+ }
965
+ if (args.apiKeyChoice === "env") {
966
+ const profile2 = storedProfile ? storedProfile : materializeProfile(args.provider, void 0);
967
+ const configWithProfile2 = storedProfile ? preservedConfig : setStoredProviderProfile(preservedConfig, args.provider, profile2);
968
+ return applyActiveProvider(configWithProfile2, args.provider, profile2, "");
969
+ }
970
+ const profile = materializeProfile(
971
+ args.provider,
972
+ storedProfile,
973
+ args.nextApiKey ?? ""
974
+ );
975
+ const configWithProfile = setStoredProviderProfile(
976
+ preservedConfig,
977
+ args.provider,
978
+ profile
979
+ );
980
+ return applyActiveProvider(
981
+ configWithProfile,
982
+ args.provider,
983
+ profile,
984
+ args.nextApiKey ?? ""
985
+ );
986
+ }
725
987
  async function configSetup(options = {}) {
726
988
  void options.global;
727
989
  const io = options.io ?? createTerminalIO();
728
990
  const ui = getSetupPresenter(io);
991
+ const env = options.env ?? process.env;
729
992
  try {
730
993
  if (!io.stdinIsTTY || !io.stdoutIsTTY) {
731
994
  io.error(
@@ -736,39 +999,43 @@ async function configSetup(options = {}) {
736
999
  io.write(`${ui.welcome("Let's keep the expensive model for the interesting bits.")}
737
1000
  `);
738
1001
  const resolvedPath = resolveSetupPath(options.targetPath);
739
- if (fs3.existsSync(resolvedPath)) {
740
- const shouldOverwrite = await promptForOverwrite(io, resolvedPath);
741
- if (!shouldOverwrite) {
742
- io.write(`${ui.note("Aborted.")}
1002
+ const { config: existingConfig, existed } = loadEditableConfig(resolvedPath);
1003
+ if (existed) {
1004
+ io.write(`${ui.info(`Updating existing config at ${resolvedPath}.`)}
743
1005
  `);
744
- return 1;
745
- }
746
1006
  }
747
- await promptForProvider(io);
748
- io.write(`${ui.info("Using OpenAI defaults for your first run.")}
749
- `);
750
- io.write(`${ui.labelValue("Default model", "gpt-5-nano")}
751
- `);
752
- io.write(`${ui.labelValue("Default base URL", "https://api.openai.com/v1")}
753
- `);
754
- io.write(
755
- `${ui.note(`Want to switch providers or tweak defaults later? Edit ${resolvedPath}.`)}
756
- `
757
- );
758
- io.write(
759
- `${ui.note("Want to inspect the active values first? Run 'sift config show --show-secrets'.")}
760
- `
761
- );
762
- const apiKey = await promptForApiKey(io);
763
- const config = buildOpenAISetupConfig(apiKey);
1007
+ const provider = await promptForProvider(io);
1008
+ writeProviderDefaults(io, provider);
1009
+ const storedProfile = getStoredProviderProfile(existingConfig, provider);
1010
+ const envName = getNativeProviderApiKeyEnvName(provider);
1011
+ const apiKeyChoice = await promptForApiKeyChoice({
1012
+ io,
1013
+ provider,
1014
+ envName,
1015
+ hasSavedKey: Boolean(storedProfile?.apiKey),
1016
+ hasEnvKey: Boolean(env[envName])
1017
+ });
1018
+ const nextApiKey = apiKeyChoice === "override" ? await promptForApiKey(io, provider) : void 0;
1019
+ const config = buildSetupConfig({
1020
+ config: existingConfig,
1021
+ provider,
1022
+ apiKeyChoice,
1023
+ nextApiKey
1024
+ });
764
1025
  const writtenPath = writeConfigFile({
765
1026
  targetPath: resolvedPath,
766
1027
  config,
767
- overwrite: true
1028
+ overwrite: existed
768
1029
  });
1030
+ if (apiKeyChoice === "env") {
1031
+ io.write(
1032
+ `${ui.note(`Using ${envName} from the environment. No API key was written to config.`)}
1033
+ `
1034
+ );
1035
+ }
769
1036
  writeSetupSuccess(io, writtenPath);
770
1037
  const activeConfigPath = findConfigPath();
771
- if (activeConfigPath && path4.resolve(activeConfigPath) !== path4.resolve(writtenPath)) {
1038
+ if (activeConfigPath && path5.resolve(activeConfigPath) !== path5.resolve(writtenPath)) {
772
1039
  writeOverrideWarning(io, activeConfigPath);
773
1040
  }
774
1041
  writeNextSteps(io);
@@ -798,18 +1065,18 @@ function maskConfigSecrets(value) {
798
1065
  return output;
799
1066
  }
800
1067
  function configInit(targetPath, global = false) {
801
- const path7 = writeExampleConfig({
1068
+ const path8 = writeExampleConfig({
802
1069
  targetPath,
803
1070
  global
804
1071
  });
805
1072
  if (!process.stdout.isTTY) {
806
- process.stdout.write(`${path7}
1073
+ process.stdout.write(`${path8}
807
1074
  `);
808
1075
  return;
809
1076
  }
810
1077
  const ui = createPresentation(true);
811
1078
  process.stdout.write(
812
- `${ui.success(`${global ? "Machine-wide" : "Template"} config written to ${path7}`)}
1079
+ `${ui.success(`${global ? "Machine-wide" : "Template"} config written to ${path8}`)}
813
1080
  `
814
1081
  );
815
1082
  }
@@ -838,11 +1105,61 @@ function configValidate(configPath) {
838
1105
  process.stdout.write(`${ui.success(message)}
839
1106
  `);
840
1107
  }
1108
+ function isNativeProviderName(value) {
1109
+ return value === "openai" || value === "openrouter";
1110
+ }
1111
+ function configUse(provider, configPath, env = process.env) {
1112
+ if (!isNativeProviderName(provider)) {
1113
+ throw new Error(`Unsupported config provider: ${provider}`);
1114
+ }
1115
+ const { config, existed, resolvedPath } = loadEditableConfig(configPath);
1116
+ const preservedConfig = preserveActiveNativeProviderProfile(config);
1117
+ const storedProfile = getStoredProviderProfile(preservedConfig, provider);
1118
+ const envName = getNativeProviderApiKeyEnvName(provider);
1119
+ const envKey = env[envName];
1120
+ if (!storedProfile?.apiKey && !envKey) {
1121
+ throw new Error(
1122
+ `No saved ${provider} API key or ${envName} found. Run 'sift config setup' first.`
1123
+ );
1124
+ }
1125
+ const nextConfig = applyActiveProvider(
1126
+ preservedConfig,
1127
+ provider,
1128
+ storedProfile,
1129
+ storedProfile?.apiKey ?? ""
1130
+ );
1131
+ writeConfigFile({
1132
+ targetPath: resolvedPath,
1133
+ config: nextConfig,
1134
+ overwrite: existed
1135
+ });
1136
+ const message = `Switched active provider to ${provider} (${resolvedPath}).`;
1137
+ if (!process.stdout.isTTY) {
1138
+ process.stdout.write(`${message}
1139
+ `);
1140
+ if (!storedProfile?.apiKey && envKey) {
1141
+ process.stdout.write(
1142
+ `Using ${envName} from the environment. No API key was written to config.
1143
+ `
1144
+ );
1145
+ }
1146
+ return;
1147
+ }
1148
+ const ui = createPresentation(true);
1149
+ process.stdout.write(`${ui.success(message)}
1150
+ `);
1151
+ if (!storedProfile?.apiKey && envKey) {
1152
+ process.stdout.write(
1153
+ `${ui.note(`Using ${envName} from the environment. No API key was written to config.`)}
1154
+ `
1155
+ );
1156
+ }
1157
+ }
841
1158
 
842
1159
  // src/commands/agent.ts
843
1160
  import fs4 from "fs";
844
1161
  import os2 from "os";
845
- import path5 from "path";
1162
+ import path6 from "path";
846
1163
  import { createInterface as createInterface2 } from "readline/promises";
847
1164
  import { stderr as defaultStderr2, stdin as defaultStdin3, stdout as defaultStdout2 } from "process";
848
1165
  var AGENT_FILENAMES = {
@@ -907,14 +1224,14 @@ function normalizeAgentScope(value) {
907
1224
  }
908
1225
  function resolveAgentTargetPath(args) {
909
1226
  if (args.targetPath) {
910
- return path5.resolve(args.cwd ?? process.cwd(), args.targetPath);
1227
+ return path6.resolve(args.cwd ?? process.cwd(), args.targetPath);
911
1228
  }
912
1229
  const scope = args.scope ?? "repo";
913
1230
  if (scope === "global") {
914
1231
  const homeDir = args.homeDir ?? os2.homedir();
915
1232
  return args.agent === "codex" ? getDefaultCodexGlobalInstructionsPath(homeDir) : getDefaultClaudeGlobalInstructionsPath(homeDir);
916
1233
  }
917
- return path5.resolve(args.cwd ?? process.cwd(), AGENT_FILENAMES[args.agent]);
1234
+ return path6.resolve(args.cwd ?? process.cwd(), AGENT_FILENAMES[args.agent]);
918
1235
  }
919
1236
  function getManagedBlockMarkers(agent) {
920
1237
  return {
@@ -949,6 +1266,7 @@ function renderInstructionBody() {
949
1266
  "- Start with `standard` text. Use diagnose JSON only when automation or machine branching truly needs it.",
950
1267
  "- If `standard` already shows bucket-level root cause, anchor, and fix lines, trust it and report from it directly.",
951
1268
  "- In that case, do not re-verify the same bucket with raw pytest; at most do one targeted source read before you edit.",
1269
+ "- If `standard` still contains an unknown bucket or ends with `Decision: zoom`, do one deeper sift pass before raw traceback.",
952
1270
  "- If you need a machine-readable diagnosis, use `sift exec --preset test-status --goal diagnose --format json -- <test command>` or the same shape with `sift rerun` / `sift watch --preset test-status`.",
953
1271
  "- Diagnose JSON is summary-first by default. Add `--include-test-ids` only when you truly need the raw failing test IDs.",
954
1272
  "- If diagnose JSON returns `read_targets.context_hint.start_line/end_line`, read only that small line range first.",
@@ -1478,7 +1796,7 @@ function readOptionalFile(targetPath) {
1478
1796
  return fs4.readFileSync(targetPath, "utf8");
1479
1797
  }
1480
1798
  function writeTextFileAtomic(targetPath, content) {
1481
- fs4.mkdirSync(path5.dirname(targetPath), { recursive: true });
1799
+ fs4.mkdirSync(path6.dirname(targetPath), { recursive: true });
1482
1800
  const tempPath = `${targetPath}.tmp-${process.pid}-${Date.now()}`;
1483
1801
  fs4.writeFileSync(tempPath, content, "utf8");
1484
1802
  fs4.renameSync(tempPath, targetPath);
@@ -1512,7 +1830,7 @@ function runDoctor(config, configPath) {
1512
1830
  if (!config.provider.model) {
1513
1831
  problems.push("Missing provider.model");
1514
1832
  }
1515
- if ((config.provider.provider === "openai" || config.provider.provider === "openai-compatible") && !config.provider.apiKey) {
1833
+ if ((config.provider.provider === "openai" || config.provider.provider === "openai-compatible" || config.provider.provider === "openrouter") && !config.provider.apiKey) {
1516
1834
  problems.push("Missing provider.apiKey");
1517
1835
  problems.push(
1518
1836
  `Set one of: ${getProviderApiKeyEnvNames(
@@ -1714,10 +2032,11 @@ async function buildOpenAICompatibleError(response) {
1714
2032
  return new Error(detail);
1715
2033
  }
1716
2034
  var OpenAICompatibleProvider = class {
1717
- name = "openai-compatible";
2035
+ name;
1718
2036
  baseUrl;
1719
2037
  apiKey;
1720
2038
  constructor(options) {
2039
+ this.name = options.name ?? "openai-compatible";
1721
2040
  this.baseUrl = options.baseUrl.replace(/\/$/, "");
1722
2041
  this.apiKey = options.apiKey;
1723
2042
  }
@@ -1793,13 +2112,20 @@ function createProvider(config) {
1793
2112
  apiKey: config.provider.apiKey
1794
2113
  });
1795
2114
  }
2115
+ if (config.provider.provider === "openrouter") {
2116
+ return new OpenAICompatibleProvider({
2117
+ baseUrl: config.provider.baseUrl,
2118
+ apiKey: config.provider.apiKey,
2119
+ name: "openrouter"
2120
+ });
2121
+ }
1796
2122
  throw new Error(`Unsupported provider: ${config.provider.provider}`);
1797
2123
  }
1798
2124
 
1799
2125
  // src/core/testStatusDecision.ts
1800
2126
  import { z as z2 } from "zod";
1801
2127
  var TEST_STATUS_DIAGNOSE_JSON_CONTRACT = '{"status":"ok|insufficient","diagnosis_complete":boolean,"raw_needed":boolean,"additional_source_read_likely_low_value":boolean,"read_raw_only_if":string|null,"decision":"stop|zoom|read_source|read_raw","dominant_blocker_bucket_index":number|null,"provider_used":boolean,"provider_confidence":number|null,"provider_failed":boolean,"raw_slice_used":boolean,"raw_slice_strategy":"none|bucket_evidence|traceback_window|head_tail","resolved_summary":{"count":number,"families":[{"prefix":string,"count":number}]},"remaining_summary":{"count":number,"families":[{"prefix":string,"count":number}]},"remaining_subset_available":boolean,"main_buckets":[{"bucket_index":number,"label":string,"count":number,"root_cause":string,"evidence":string[],"bucket_confidence":number,"root_cause_confidence":number,"dominant":boolean,"secondary_visible_despite_blocker":boolean,"mini_diff":{"added_paths"?:number,"removed_models"?:number,"changed_task_mappings"?:number}|null}],"read_targets":[{"file":string,"line":number|null,"why":string,"bucket_index":number,"context_hint":{"start_line":number|null,"end_line":number|null,"search_hint":string|null}}],"next_best_action":{"code":"fix_dominant_blocker|read_source_for_bucket|read_raw_for_exact_traceback|insufficient_signal","bucket_index":number|null,"note":string},"resolved_tests"?:string[],"remaining_tests"?:string[]}';
1802
- var TEST_STATUS_PROVIDER_SUPPLEMENT_JSON_CONTRACT = '{"diagnosis_complete":boolean,"raw_needed":boolean,"additional_source_read_likely_low_value":boolean,"read_raw_only_if":string|null,"decision":"stop|zoom|read_source|read_raw","provider_confidence":number|null,"next_best_action":{"code":"fix_dominant_blocker|read_source_for_bucket|read_raw_for_exact_traceback|insufficient_signal","bucket_index":number|null,"note":string}}';
2128
+ var TEST_STATUS_PROVIDER_SUPPLEMENT_JSON_CONTRACT = '{"diagnosis_complete":boolean,"raw_needed":boolean,"additional_source_read_likely_low_value":boolean,"read_raw_only_if":string|null,"decision":"stop|zoom|read_source|read_raw","provider_confidence":number|null,"bucket_supplements":[{"label":string,"count":number,"root_cause":string,"anchor":{"file":string|null,"line":number|null,"search_hint":string|null},"fix_hint":string|null,"confidence":number}],"next_best_action":{"code":"fix_dominant_blocker|read_source_for_bucket|read_raw_for_exact_traceback|insufficient_signal","bucket_index":number|null,"note":string}}';
1803
2129
  var nextBestActionSchema = z2.object({
1804
2130
  code: z2.enum([
1805
2131
  "fix_dominant_blocker",
@@ -1817,6 +2143,20 @@ var testStatusProviderSupplementSchema = z2.object({
1817
2143
  read_raw_only_if: z2.string().nullable(),
1818
2144
  decision: z2.enum(["stop", "zoom", "read_source", "read_raw"]),
1819
2145
  provider_confidence: z2.number().min(0).max(1).nullable(),
2146
+ bucket_supplements: z2.array(
2147
+ z2.object({
2148
+ label: z2.string().min(1),
2149
+ count: z2.number().int().positive(),
2150
+ root_cause: z2.string().min(1),
2151
+ anchor: z2.object({
2152
+ file: z2.string().nullable(),
2153
+ line: z2.number().int().nullable(),
2154
+ search_hint: z2.string().nullable()
2155
+ }),
2156
+ fix_hint: z2.string().nullable(),
2157
+ confidence: z2.number().min(0).max(1)
2158
+ })
2159
+ ).max(2),
1820
2160
  next_best_action: nextBestActionSchema
1821
2161
  });
1822
2162
  var testStatusDiagnoseContractSchema = z2.object({
@@ -1968,14 +2308,73 @@ function classifyGenericBucketType(reason) {
1968
2308
  }
1969
2309
  return "unknown_failure";
1970
2310
  }
2311
+ function isUnknownBucket(bucket) {
2312
+ return bucket.source === "unknown" || bucket.reason.startsWith("unknown ");
2313
+ }
2314
+ function classifyVisibleStatusForLabel(args) {
2315
+ const isError = args.errorLabels.has(args.label);
2316
+ const isFailed = args.failedLabels.has(args.label);
2317
+ if (isError && isFailed) {
2318
+ return "mixed";
2319
+ }
2320
+ if (isError) {
2321
+ return "error";
2322
+ }
2323
+ if (isFailed) {
2324
+ return "failed";
2325
+ }
2326
+ return "unknown";
2327
+ }
2328
+ function inferCoverageFromReason(reason) {
2329
+ if (reason.startsWith("missing test env:") || reason.startsWith("fixture guard:") || reason.startsWith("service unavailable:") || reason.startsWith("db refused:") || reason.startsWith("auth bypass absent:") || reason.startsWith("missing module:")) {
2330
+ return "error";
2331
+ }
2332
+ if (reason.startsWith("assertion failed:")) {
2333
+ return "failed";
2334
+ }
2335
+ return "mixed";
2336
+ }
2337
+ function buildCoverageCounts(args) {
2338
+ if (args.coverageKind === "error") {
2339
+ return {
2340
+ error: args.count,
2341
+ failed: 0
2342
+ };
2343
+ }
2344
+ if (args.coverageKind === "failed") {
2345
+ return {
2346
+ error: 0,
2347
+ failed: args.count
2348
+ };
2349
+ }
2350
+ return {
2351
+ error: 0,
2352
+ failed: 0
2353
+ };
2354
+ }
1971
2355
  function buildGenericBuckets(analysis) {
1972
2356
  const buckets = [];
1973
2357
  const grouped = /* @__PURE__ */ new Map();
2358
+ const errorLabels = new Set(analysis.visibleErrorLabels);
2359
+ const failedLabels = new Set(analysis.visibleFailedLabels);
1974
2360
  const push = (reason, item) => {
1975
- const key = `${classifyGenericBucketType(reason)}:${reason}`;
2361
+ const coverageKind = (() => {
2362
+ const status = classifyVisibleStatusForLabel({
2363
+ label: item.label,
2364
+ errorLabels,
2365
+ failedLabels
2366
+ });
2367
+ return status === "unknown" ? inferCoverageFromReason(reason) : status;
2368
+ })();
2369
+ const key = `${classifyGenericBucketType(reason)}:${coverageKind}:${reason}`;
1976
2370
  const existing = grouped.get(key);
1977
2371
  if (existing) {
1978
2372
  existing.count += 1;
2373
+ if (coverageKind === "error") {
2374
+ existing.coverage.error += 1;
2375
+ } else if (coverageKind === "failed") {
2376
+ existing.coverage.failed += 1;
2377
+ }
1979
2378
  if (!existing.representativeItems.some((entry) => entry.label === item.label) && existing.representativeItems.length < 6) {
1980
2379
  existing.representativeItems.push(item);
1981
2380
  }
@@ -1992,7 +2391,12 @@ function buildGenericBuckets(analysis) {
1992
2391
  entities: [],
1993
2392
  hint: void 0,
1994
2393
  overflowCount: 0,
1995
- overflowLabel: "failing tests/modules"
2394
+ overflowLabel: "failing tests/modules",
2395
+ coverage: buildCoverageCounts({
2396
+ count: 1,
2397
+ coverageKind
2398
+ }),
2399
+ source: "heuristic"
1996
2400
  });
1997
2401
  };
1998
2402
  for (const item of [...analysis.collectionItems, ...analysis.inlineItems]) {
@@ -2045,10 +2449,51 @@ function mergeBucketDetails(existing, incoming) {
2045
2449
  incoming.overflowCount,
2046
2450
  count - representativeItems.length
2047
2451
  ),
2048
- overflowLabel: existing.overflowLabel || incoming.overflowLabel
2452
+ overflowLabel: existing.overflowLabel || incoming.overflowLabel,
2453
+ labelOverride: existing.labelOverride ?? incoming.labelOverride,
2454
+ coverage: {
2455
+ error: Math.max(existing.coverage.error, incoming.coverage.error),
2456
+ failed: Math.max(existing.coverage.failed, incoming.coverage.failed)
2457
+ },
2458
+ source: existing.source
2459
+ };
2460
+ }
2461
+ function inferFailureBucketCoverage(bucket, analysis) {
2462
+ const errorLabels = new Set(analysis.visibleErrorLabels);
2463
+ const failedLabels = new Set(analysis.visibleFailedLabels);
2464
+ let error = 0;
2465
+ let failed = 0;
2466
+ for (const item of bucket.representativeItems) {
2467
+ const status = classifyVisibleStatusForLabel({
2468
+ label: item.label,
2469
+ errorLabels,
2470
+ failedLabels
2471
+ });
2472
+ if (status === "error") {
2473
+ error += 1;
2474
+ } else if (status === "failed") {
2475
+ failed += 1;
2476
+ }
2477
+ }
2478
+ const claimed = bucket.countClaimed ?? bucket.countVisible;
2479
+ if (bucket.type === "contract_snapshot_drift" || bucket.type === "assertion_failure") {
2480
+ return {
2481
+ error,
2482
+ failed: Math.max(failed, claimed)
2483
+ };
2484
+ }
2485
+ if (bucket.type === "shared_environment_blocker" || bucket.type === "import_dependency_failure" || bucket.type === "collection_failure" || bucket.type === "fixture_guard_failure" || bucket.type === "service_unavailable" || bucket.type === "db_connection_failure" || bucket.type === "auth_bypass_absent") {
2486
+ return {
2487
+ error: Math.max(error, claimed),
2488
+ failed
2489
+ };
2490
+ }
2491
+ return {
2492
+ error,
2493
+ failed
2049
2494
  };
2050
2495
  }
2051
- function mergeBuckets(analysis) {
2496
+ function mergeBuckets(analysis, extraBuckets = []) {
2052
2497
  const mergedByIdentity = /* @__PURE__ */ new Map();
2053
2498
  const merged = [];
2054
2499
  const pushBucket = (bucket) => {
@@ -2077,7 +2522,9 @@ function mergeBuckets(analysis) {
2077
2522
  entities: [...bucket2.entities],
2078
2523
  hint: bucket2.hint,
2079
2524
  overflowCount: bucket2.overflowCount,
2080
- overflowLabel: bucket2.overflowLabel
2525
+ overflowLabel: bucket2.overflowLabel,
2526
+ coverage: inferFailureBucketCoverage(bucket2, analysis),
2527
+ source: "heuristic"
2081
2528
  }))) {
2082
2529
  pushBucket(bucket);
2083
2530
  }
@@ -2101,6 +2548,9 @@ function mergeBuckets(analysis) {
2101
2548
  coveredLabels.add(item.label);
2102
2549
  }
2103
2550
  }
2551
+ for (const bucket of extraBuckets) {
2552
+ pushBucket(bucket);
2553
+ }
2104
2554
  return merged;
2105
2555
  }
2106
2556
  function dominantBucketPriority(bucket) {
@@ -2116,6 +2566,9 @@ function dominantBucketPriority(bucket) {
2116
2566
  if (bucket.type === "collection_failure") {
2117
2567
  return 2;
2118
2568
  }
2569
+ if (isUnknownBucket(bucket)) {
2570
+ return 2;
2571
+ }
2119
2572
  if (bucket.type === "contract_snapshot_drift") {
2120
2573
  return 1;
2121
2574
  }
@@ -2140,6 +2593,9 @@ function isDominantBlockerType(type) {
2140
2593
  return type === "shared_environment_blocker" || type === "import_dependency_failure" || type === "collection_failure";
2141
2594
  }
2142
2595
  function labelForBucket(bucket) {
2596
+ if (bucket.labelOverride) {
2597
+ return bucket.labelOverride;
2598
+ }
2143
2599
  if (bucket.reason.startsWith("missing test env:")) {
2144
2600
  return "missing test env";
2145
2601
  }
@@ -2179,15 +2635,27 @@ function labelForBucket(bucket) {
2179
2635
  if (bucket.type === "runtime_failure") {
2180
2636
  return "runtime failure";
2181
2637
  }
2638
+ if (bucket.reason.startsWith("unknown setup blocker:")) {
2639
+ return "unknown setup blocker";
2640
+ }
2641
+ if (bucket.reason.startsWith("unknown failure family:")) {
2642
+ return "unknown failure family";
2643
+ }
2182
2644
  return "unknown failure";
2183
2645
  }
2184
2646
  function rootCauseConfidenceFor(bucket) {
2647
+ if (isUnknownBucket(bucket)) {
2648
+ return 0.52;
2649
+ }
2185
2650
  if (bucket.reason.startsWith("missing test env:") || bucket.reason.startsWith("missing module:") || bucket.reason.startsWith("db refused:") || bucket.reason.startsWith("service unavailable:") || bucket.reason.startsWith("auth bypass absent:")) {
2186
2651
  return 0.95;
2187
2652
  }
2188
2653
  if (bucket.type === "contract_snapshot_drift") {
2189
2654
  return bucket.entities.length > 0 ? 0.92 : 0.76;
2190
2655
  }
2656
+ if (bucket.source === "provider") {
2657
+ return Math.max(0.6, Math.min(bucket.confidence, 0.82));
2658
+ }
2191
2659
  return Math.max(0.6, Math.min(bucket.confidence, 0.88));
2192
2660
  }
2193
2661
  function buildBucketEvidence(bucket) {
@@ -2231,6 +2699,12 @@ function buildReadTargetWhy(args) {
2231
2699
  if (args.bucket.reason.startsWith("auth bypass absent:")) {
2232
2700
  return "it contains the auth bypass setup behind this bucket";
2233
2701
  }
2702
+ if (args.bucket.reason.startsWith("unknown setup blocker:")) {
2703
+ return "it is the first anchored setup failure in this unknown bucket";
2704
+ }
2705
+ if (args.bucket.reason.startsWith("unknown failure family:")) {
2706
+ return "it is the first anchored failing test in this unknown bucket";
2707
+ }
2234
2708
  if (args.bucket.type === "contract_snapshot_drift") {
2235
2709
  if (args.bucketLabel === "route drift") {
2236
2710
  return "it maps to the visible route drift bucket";
@@ -2280,6 +2754,9 @@ function buildReadTargetSearchHint(bucket, anchor) {
2280
2754
  if (assertionText) {
2281
2755
  return assertionText;
2282
2756
  }
2757
+ if (bucket.reason.startsWith("unknown ")) {
2758
+ return anchor.reason;
2759
+ }
2283
2760
  const fallbackLabel = anchor.label.split("::")[1]?.trim();
2284
2761
  return fallbackLabel || null;
2285
2762
  }
@@ -2339,6 +2816,12 @@ function buildConcreteNextNote(args) {
2339
2816
  if (args.nextBestAction.code === "read_source_for_bucket") {
2340
2817
  return lead;
2341
2818
  }
2819
+ if (args.nextBestAction.code === "insufficient_signal") {
2820
+ if (args.nextBestAction.note.startsWith("Provider follow-up failed")) {
2821
+ return args.nextBestAction.note;
2822
+ }
2823
+ return `${lead} Then take one deeper sift pass before raw traceback.`;
2824
+ }
2342
2825
  return args.nextBestAction.note;
2343
2826
  }
2344
2827
  function extractMiniDiff(input, bucket) {
@@ -2363,6 +2846,152 @@ function extractMiniDiff(input, bucket) {
2363
2846
  ...changedTaskMappings > 0 ? { changed_task_mappings: changedTaskMappings } : {}
2364
2847
  };
2365
2848
  }
2849
+ function inferSupplementCoverageKind(args) {
2850
+ const normalized = `${args.label} ${args.rootCause}`.toLowerCase();
2851
+ if (/env|setup|fixture|import|dependency|service|db|database|auth bypass|collection|connection refused/.test(
2852
+ normalized
2853
+ )) {
2854
+ return "error";
2855
+ }
2856
+ if (/snapshot|contract|drift|assertion|expected|actual|golden/.test(normalized)) {
2857
+ return "failed";
2858
+ }
2859
+ if (args.remainingErrors > 0 && args.remainingFailed === 0) {
2860
+ return "error";
2861
+ }
2862
+ return "failed";
2863
+ }
2864
+ function buildProviderSupplementBuckets(args) {
2865
+ let remainingErrors = args.remainingErrors;
2866
+ let remainingFailed = args.remainingFailed;
2867
+ return args.supplements.flatMap((supplement) => {
2868
+ const coverageKind = inferSupplementCoverageKind({
2869
+ label: supplement.label,
2870
+ rootCause: supplement.root_cause,
2871
+ remainingErrors,
2872
+ remainingFailed
2873
+ });
2874
+ const budget = coverageKind === "error" ? remainingErrors : remainingFailed;
2875
+ const count = Math.max(0, Math.min(supplement.count, budget));
2876
+ if (count === 0) {
2877
+ return [];
2878
+ }
2879
+ if (coverageKind === "error") {
2880
+ remainingErrors -= count;
2881
+ } else {
2882
+ remainingFailed -= count;
2883
+ }
2884
+ const representativeLabel = supplement.anchor.file ?? `${supplement.label} supplement`;
2885
+ const representativeItem = {
2886
+ label: representativeLabel,
2887
+ reason: supplement.root_cause,
2888
+ group: supplement.label,
2889
+ file: supplement.anchor.file,
2890
+ line: supplement.anchor.line,
2891
+ anchor_kind: supplement.anchor.file && supplement.anchor.line !== null ? "traceback" : supplement.anchor.file ? "test_label" : supplement.anchor.search_hint ? "entity" : "none",
2892
+ anchor_confidence: Math.max(0.4, Math.min(supplement.confidence, 0.82))
2893
+ };
2894
+ return [
2895
+ {
2896
+ type: classifyGenericBucketType(supplement.root_cause),
2897
+ headline: `${supplement.label}: ${formatCount(count, "visible failure")} share ${supplement.root_cause}.`,
2898
+ summaryLines: [
2899
+ `${supplement.label}: ${formatCount(count, "visible failure")} share ${supplement.root_cause}.`
2900
+ ],
2901
+ reason: supplement.root_cause,
2902
+ count,
2903
+ confidence: Math.max(0.4, Math.min(supplement.confidence, 0.82)),
2904
+ representativeItems: [representativeItem],
2905
+ entities: supplement.anchor.search_hint ? [supplement.anchor.search_hint] : [],
2906
+ hint: supplement.fix_hint ?? void 0,
2907
+ overflowCount: Math.max(count - 1, 0),
2908
+ overflowLabel: "failing tests/modules",
2909
+ labelOverride: supplement.label,
2910
+ coverage: buildCoverageCounts({
2911
+ count,
2912
+ coverageKind
2913
+ }),
2914
+ source: "provider"
2915
+ }
2916
+ ];
2917
+ });
2918
+ }
2919
+ function pickUnknownAnchor(args) {
2920
+ const fromStatusItems = args.kind === "error" ? args.analysis.visibleErrorItems[0] : null;
2921
+ if (fromStatusItems) {
2922
+ return {
2923
+ label: fromStatusItems.label,
2924
+ reason: fromStatusItems.reason,
2925
+ group: fromStatusItems.group,
2926
+ file: fromStatusItems.file,
2927
+ line: fromStatusItems.line,
2928
+ anchor_kind: fromStatusItems.anchor_kind,
2929
+ anchor_confidence: fromStatusItems.anchor_confidence
2930
+ };
2931
+ }
2932
+ const label = args.kind === "error" ? args.analysis.visibleErrorLabels[0] : args.analysis.visibleFailedLabels[0];
2933
+ if (label) {
2934
+ const normalizedLabel = normalizeTestId(label);
2935
+ const fileMatch = normalizedLabel.match(/^([A-Za-z0-9_./-]+\.[A-Za-z0-9]+)\b/);
2936
+ const file = fileMatch?.[1] ?? normalizedLabel.split("::")[0] ?? null;
2937
+ return {
2938
+ label,
2939
+ reason: args.kind === "error" ? "setup failures share a repeated but unclassified pattern" : "failing tests share a repeated but unclassified pattern",
2940
+ group: args.kind === "error" ? "unknown setup blocker" : "unknown failure family",
2941
+ file: file && file !== label ? file : null,
2942
+ line: null,
2943
+ anchor_kind: file && file !== label ? "test_label" : "none",
2944
+ anchor_confidence: file && file !== label ? 0.6 : 0
2945
+ };
2946
+ }
2947
+ return null;
2948
+ }
2949
+ function buildUnknownBucket(args) {
2950
+ if (args.count <= 0) {
2951
+ return null;
2952
+ }
2953
+ const anchor = pickUnknownAnchor(args);
2954
+ const isError = args.kind === "error";
2955
+ const label = isError ? "unknown setup blocker" : "unknown failure family";
2956
+ const reason = isError ? "unknown setup blocker: setup failures share a repeated but unclassified pattern" : "unknown failure family: failing tests share a repeated but unclassified pattern";
2957
+ return {
2958
+ type: "unknown_failure",
2959
+ headline: `${label}: ${formatCount(args.count, "visible failure")} share a repeated but unclassified pattern.`,
2960
+ summaryLines: [
2961
+ `${label}: ${formatCount(args.count, "visible failure")} share a repeated but unclassified pattern.`
2962
+ ],
2963
+ reason,
2964
+ count: args.count,
2965
+ confidence: 0.45,
2966
+ representativeItems: anchor ? [anchor] : [],
2967
+ entities: [],
2968
+ hint: isError ? "Take one deeper sift pass or inspect the first anchored setup failure." : "Take one deeper sift pass or inspect the first anchored failing test.",
2969
+ overflowCount: Math.max(args.count - (anchor ? 1 : 0), 0),
2970
+ overflowLabel: "failing tests/modules",
2971
+ labelOverride: label,
2972
+ coverage: buildCoverageCounts({
2973
+ count: args.count,
2974
+ coverageKind: isError ? "error" : "failed"
2975
+ }),
2976
+ source: "unknown"
2977
+ };
2978
+ }
2979
+ function buildCoverageResiduals(args) {
2980
+ const covered = args.buckets.reduce(
2981
+ (totals, bucket) => ({
2982
+ error: totals.error + bucket.coverage.error,
2983
+ failed: totals.failed + bucket.coverage.failed
2984
+ }),
2985
+ {
2986
+ error: 0,
2987
+ failed: 0
2988
+ }
2989
+ );
2990
+ return {
2991
+ remainingErrors: Math.max(args.analysis.errors - Math.min(args.analysis.errors, covered.error), 0),
2992
+ remainingFailed: Math.max(args.analysis.failed - Math.min(args.analysis.failed, covered.failed), 0)
2993
+ };
2994
+ }
2366
2995
  function buildOutcomeLines(analysis) {
2367
2996
  if (analysis.noTestsCollected) {
2368
2997
  return ["- Tests did not run.", "- Collected 0 items."];
@@ -2481,6 +3110,12 @@ function buildStandardFixText(args) {
2481
3110
  if (args.bucket.reason.startsWith("auth bypass absent:")) {
2482
3111
  return "Restore the test auth bypass setup and rerun the full suite at standard.";
2483
3112
  }
3113
+ if (args.bucket.reason.startsWith("unknown setup blocker:")) {
3114
+ return "Take one deeper sift pass or inspect the first anchored setup failure before rerunning.";
3115
+ }
3116
+ if (args.bucket.reason.startsWith("unknown failure family:")) {
3117
+ return "Take one deeper sift pass or inspect the first anchored failing test before rerunning.";
3118
+ }
2484
3119
  if (args.bucket.type === "contract_snapshot_drift") {
2485
3120
  return "Review the visible drift and regenerate the contract snapshots if the changes are intentional.";
2486
3121
  }
@@ -2577,7 +3212,35 @@ function renderVerbose(args) {
2577
3212
  return lines.join("\n");
2578
3213
  }
2579
3214
  function buildTestStatusDiagnoseContract(args) {
2580
- const buckets = prioritizeBuckets(mergeBuckets(args.analysis)).slice(0, 3);
3215
+ const heuristicBuckets = mergeBuckets(args.analysis);
3216
+ const preUnknownSimpleCollectionFailure = args.analysis.collectionErrorCount !== void 0 && args.analysis.collectionItems.length === 0 && heuristicBuckets.length === 0 && (args.providerBucketSupplements?.length ?? 0) === 0;
3217
+ const heuristicResiduals = buildCoverageResiduals({
3218
+ analysis: args.analysis,
3219
+ buckets: heuristicBuckets
3220
+ });
3221
+ const providerSupplementBuckets = buildProviderSupplementBuckets({
3222
+ supplements: args.providerBucketSupplements ?? [],
3223
+ remainingErrors: heuristicResiduals.remainingErrors,
3224
+ remainingFailed: heuristicResiduals.remainingFailed
3225
+ });
3226
+ const combinedBuckets = mergeBuckets(args.analysis, providerSupplementBuckets);
3227
+ const residuals = buildCoverageResiduals({
3228
+ analysis: args.analysis,
3229
+ buckets: combinedBuckets
3230
+ });
3231
+ const unknownBuckets = preUnknownSimpleCollectionFailure ? [] : [
3232
+ buildUnknownBucket({
3233
+ analysis: args.analysis,
3234
+ kind: "error",
3235
+ count: residuals.remainingErrors
3236
+ }),
3237
+ buildUnknownBucket({
3238
+ analysis: args.analysis,
3239
+ kind: "failed",
3240
+ count: residuals.remainingFailed
3241
+ })
3242
+ ].filter((bucket) => Boolean(bucket));
3243
+ const buckets = prioritizeBuckets([...combinedBuckets, ...unknownBuckets]).slice(0, 3);
2581
3244
  const simpleCollectionFailure = args.analysis.collectionErrorCount !== void 0 && args.analysis.collectionItems.length === 0 && buckets.length === 0;
2582
3245
  const dominantBucket = buckets.map((bucket, index) => ({
2583
3246
  bucket,
@@ -2588,8 +3251,10 @@ function buildTestStatusDiagnoseContract(args) {
2588
3251
  }
2589
3252
  return right.bucket.confidence - left.bucket.confidence;
2590
3253
  })[0] ?? null;
2591
- const diagnosisComplete = args.analysis.failed === 0 && args.analysis.errors === 0 && args.analysis.passed > 0 || simpleCollectionFailure || buckets.length > 0 && (dominantBucket?.bucket.confidence ?? 0) >= 0.7;
2592
- const rawNeeded = buckets.length > 0 ? buckets.every((bucket) => bucket.confidence < 0.7) : !(args.analysis.failed === 0 && args.analysis.errors === 0 && args.analysis.passed > 0 || simpleCollectionFailure);
3254
+ const hasUnknownBucket = buckets.some((bucket) => isUnknownBucket(bucket));
3255
+ const hasConcreteCoverage = args.analysis.failed === 0 && args.analysis.errors === 0 ? true : residuals.remainingErrors === 0 && residuals.remainingFailed === 0;
3256
+ const diagnosisComplete = args.analysis.failed === 0 && args.analysis.errors === 0 && args.analysis.passed > 0 || simpleCollectionFailure || buckets.length > 0 && hasConcreteCoverage && !hasUnknownBucket && (dominantBucket?.bucket.confidence ?? 0) >= 0.6;
3257
+ const rawNeeded = buckets.length === 0 ? !(args.analysis.failed === 0 && args.analysis.errors === 0 && args.analysis.passed > 0 || simpleCollectionFailure) : !diagnosisComplete && !hasUnknownBucket && buckets.every((bucket) => bucket.confidence < 0.7);
2593
3258
  const dominantBlockerBucketIndex = dominantBucket && isDominantBlockerType(dominantBucket.bucket.type) ? dominantBucket.index + 1 : null;
2594
3259
  const readTargets = buildReadTargets({
2595
3260
  buckets,
@@ -2624,6 +3289,12 @@ function buildTestStatusDiagnoseContract(args) {
2624
3289
  bucket_index: null,
2625
3290
  note: "Inspect the collection traceback or setup code next; the run failed before tests executed."
2626
3291
  };
3292
+ } else if (hasUnknownBucket) {
3293
+ nextBestAction = {
3294
+ code: "insufficient_signal",
3295
+ bucket_index: dominantBucket ? dominantBucket.index + 1 : null,
3296
+ note: "Take one deeper sift pass or inspect the first anchored failure before falling back to raw traceback."
3297
+ };
2627
3298
  } else if (!diagnosisComplete) {
2628
3299
  nextBestAction = {
2629
3300
  code: rawNeeded ? "read_raw_for_exact_traceback" : "insufficient_signal",
@@ -2661,11 +3332,15 @@ function buildTestStatusDiagnoseContract(args) {
2661
3332
  read_targets: readTargets,
2662
3333
  next_best_action: nextBestAction
2663
3334
  };
3335
+ const effectiveDiagnosisComplete = Boolean(args.contractOverrides?.diagnosis_complete ?? diagnosisComplete) && !hasUnknownBucket;
3336
+ const requestedDecision = args.contractOverrides?.decision;
3337
+ const effectiveDecision = hasUnknownBucket && requestedDecision && (requestedDecision === "stop" || requestedDecision === "read_source") ? "zoom" : requestedDecision;
2664
3338
  const effectiveNextBestAction = args.contractOverrides?.next_best_action ?? baseContract.next_best_action;
2665
3339
  const mergedContractWithoutDecision = {
2666
3340
  ...baseContract,
2667
3341
  ...args.contractOverrides,
2668
- status: args.contractOverrides?.diagnosis_complete ?? diagnosisComplete ? "ok" : "insufficient",
3342
+ diagnosis_complete: effectiveDiagnosisComplete,
3343
+ status: effectiveDiagnosisComplete ? "ok" : "insufficient",
2669
3344
  next_best_action: {
2670
3345
  ...effectiveNextBestAction,
2671
3346
  note: buildConcreteNextNote({
@@ -2679,7 +3354,7 @@ function buildTestStatusDiagnoseContract(args) {
2679
3354
  };
2680
3355
  const contract = testStatusDiagnoseContractSchema.parse({
2681
3356
  ...mergedContractWithoutDecision,
2682
- decision: args.contractOverrides?.decision ?? deriveDecision(mergedContractWithoutDecision)
3357
+ decision: effectiveDecision ?? deriveDecision(mergedContractWithoutDecision)
2683
3358
  });
2684
3359
  return {
2685
3360
  contract,
@@ -2916,9 +3591,12 @@ function resolvePromptPolicy(args) {
2916
3591
  "Return only valid JSON.",
2917
3592
  `Use this exact contract: ${args.outputContract ?? TEST_STATUS_DIAGNOSE_JSON_CONTRACT}.`,
2918
3593
  "Treat the heuristic context as extraction guidance, but do not invent hidden failures.",
2919
- "Use the heuristic extract as the bucket truth unless the visible command output clearly disproves it.",
3594
+ "Use the heuristic extract as the base bucket truth unless the visible command output clearly disproves it.",
3595
+ "If some visible failure or error families remain unexplained, add at most 2 bucket_supplements for the residual families only.",
3596
+ "Do not rewrite or delete heuristic buckets; only supplement missing residual coverage.",
3597
+ "Keep bucket_supplement counts within the unexplained residual failures or errors.",
2920
3598
  "Identify the dominant blocker, remaining visible failure buckets, the decision, and the next best action.",
2921
- "Set diagnosis_complete to true only when the visible output is already sufficient to stop and act.",
3599
+ "Set diagnosis_complete to true only when the visible output is already sufficient to stop and act and no unknown residual family remains.",
2922
3600
  "Set raw_needed to true only when exact traceback lines are still required.",
2923
3601
  "Set provider_confidence to a number between 0 and 1, or null only when confidence cannot be estimated."
2924
3602
  ] : [
@@ -3229,7 +3907,25 @@ function extractEnvBlockerName(normalized) {
3229
3907
  const fallbackMatch = normalized.match(
3230
3908
  /\b([A-Z][A-Z0-9_]{2,})\b(?=[^.\n]*DB-isolated tests)/
3231
3909
  );
3232
- return fallbackMatch?.[1] ?? null;
3910
+ if (fallbackMatch) {
3911
+ return fallbackMatch[1];
3912
+ }
3913
+ const leadingEnvMatch = normalized.match(
3914
+ /\b([A-Z][A-Z0-9_]{2,})\b(?=[^.\n]{0,80}\b(?:is\s+)?(?:missing|unset|not set|not configured|required)\b)/
3915
+ );
3916
+ if (leadingEnvMatch) {
3917
+ return leadingEnvMatch[1];
3918
+ }
3919
+ const trailingEnvMatch = normalized.match(
3920
+ /\b(?:missing|unset|not set|not configured|required)\b[^.\n]{0,80}\b([A-Z][A-Z0-9_]{2,})\b/
3921
+ );
3922
+ if (trailingEnvMatch) {
3923
+ return trailingEnvMatch[1];
3924
+ }
3925
+ const validationEnvMatch = normalized.match(
3926
+ /\bValidationError\b[^.\n]{0,120}\b([A-Z][A-Z0-9_]{2,})\b/
3927
+ );
3928
+ return validationEnvMatch?.[1] ?? null;
3233
3929
  }
3234
3930
  function classifyFailureReason(line, options) {
3235
3931
  const normalized = line.trim().replace(/^[A-Z]\s+/, "");
@@ -3250,7 +3946,7 @@ function classifyFailureReason(line, options) {
3250
3946
  };
3251
3947
  }
3252
3948
  const missingEnv = normalized.match(
3253
- /\b(?:environment variable|env(?:ironment)? var(?:iable)?|Missing required env(?:ironment)? variable)\s+([A-Z][A-Z0-9_]{2,})\b/i
3949
+ /\b(?:environment variable|env(?:ironment)? var(?:iable)?|missing required env(?:ironment)? variable)\s+([A-Z][A-Z0-9_]{2,})\b/
3254
3950
  );
3255
3951
  if (missingEnv) {
3256
3952
  return {
@@ -3282,6 +3978,12 @@ function classifyFailureReason(line, options) {
3282
3978
  group: "database connectivity failures"
3283
3979
  };
3284
3980
  }
3981
+ if (/(ECONNREFUSED|ConnectionRefusedError|connection refused)/i.test(normalized)) {
3982
+ return {
3983
+ reason: "service unavailable: dependency connection was refused",
3984
+ group: "service availability failures"
3985
+ };
3986
+ }
3285
3987
  if (/(503\b|service unavailable|temporarily unavailable)/i.test(normalized)) {
3286
3988
  return {
3287
3989
  reason: "service unavailable: dependency service is unavailable",
@@ -3766,7 +4468,7 @@ function synthesizeImportDependencyBucket(args) {
3766
4468
  return null;
3767
4469
  }
3768
4470
  const allVisibleErrorsAreImportRelated = args.visibleErrorItems.length > 0 && args.visibleErrorItems.every((item) => item.reason.startsWith("missing module:"));
3769
- const countClaimed = allVisibleErrorsAreImportRelated && importItems.length >= 3 && args.errors >= importItems.length ? args.errors : void 0;
4471
+ const countClaimed = allVisibleErrorsAreImportRelated && importItems.length >= 2 && args.errors >= importItems.length ? args.errors : void 0;
3770
4472
  const modules = Array.from(
3771
4473
  new Set(
3772
4474
  importItems.map((item) => item.reason.replace("missing module:", "").trim()).filter(Boolean)
@@ -3802,7 +4504,7 @@ function synthesizeImportDependencyBucket(args) {
3802
4504
  };
3803
4505
  }
3804
4506
  function isContractDriftLabel(label) {
3805
- return /(freeze|snapshot|contract|manifest|openapi)/i.test(label);
4507
+ return /(freeze|snapshot|contract|manifest|openapi|golden)/i.test(label);
3806
4508
  }
3807
4509
  function looksLikeTaskKey(value) {
3808
4510
  return /^[a-z]+(?:_[a-z0-9]+)+$/i.test(value) && !value.startsWith("/api/");
@@ -3864,7 +4566,7 @@ function extractContractDriftEntities(input) {
3864
4566
  }
3865
4567
  function buildContractRepresentativeReason(args) {
3866
4568
  if (/openapi/i.test(args.label) && args.entities.apiPaths.length > 0) {
3867
- const nextPath = args.entities.apiPaths.find((path7) => !args.usedPaths.has(path7)) ?? args.entities.apiPaths[0];
4569
+ const nextPath = args.entities.apiPaths.find((path8) => !args.usedPaths.has(path8)) ?? args.entities.apiPaths[0];
3868
4570
  args.usedPaths.add(nextPath);
3869
4571
  return `added path: ${nextPath}`;
3870
4572
  }
@@ -4495,6 +5197,7 @@ function buildGenericRawSlice(args) {
4495
5197
 
4496
5198
  // src/core/run.ts
4497
5199
  var RETRY_DELAY_MS = 300;
5200
+ var PROVIDER_PENDING_NOTICE_DELAY_MS = 150;
4498
5201
  function estimateTokenCount(text) {
4499
5202
  return Math.max(1, Math.ceil(text.length / 4));
4500
5203
  }
@@ -4515,6 +5218,8 @@ function logVerboseTestStatusTelemetry(args) {
4515
5218
  `${pc2.dim("sift")} diagnosis_complete_at_layer=${getDiagnosisCompleteAtLayer(args.contract)}`,
4516
5219
  `${pc2.dim("sift")} heuristic_short_circuit=${!args.contract.provider_used && args.contract.diagnosis_complete && !args.contract.raw_needed && !args.contract.provider_failed}`,
4517
5220
  `${pc2.dim("sift")} raw_input_chars=${args.request.stdin.length}`,
5221
+ `${pc2.dim("sift")} heuristic_input_chars=${args.heuristicInputChars}`,
5222
+ `${pc2.dim("sift")} heuristic_input_truncated=${args.heuristicInputTruncated}`,
4518
5223
  `${pc2.dim("sift")} prepared_input_chars=${args.prepared.meta.finalLength}`,
4519
5224
  `${pc2.dim("sift")} raw_slice_chars=${args.rawSliceChars ?? 0}`,
4520
5225
  `${pc2.dim("sift")} provider_input_chars=${args.providerInputChars ?? 0}`,
@@ -4556,6 +5261,7 @@ function buildDryRunOutput(args) {
4556
5261
  responseMode: args.responseMode,
4557
5262
  policy: args.request.policyName ?? null,
4558
5263
  heuristicOutput: args.heuristicOutput ?? null,
5264
+ heuristicInput: args.heuristicInput,
4559
5265
  input: {
4560
5266
  originalLength: args.prepared.meta.originalLength,
4561
5267
  finalLength: args.prepared.meta.finalLength,
@@ -4572,6 +5278,25 @@ function buildDryRunOutput(args) {
4572
5278
  async function delay(ms) {
4573
5279
  await new Promise((resolve) => setTimeout(resolve, ms));
4574
5280
  }
5281
+ function startProviderPendingNotice() {
5282
+ if (!process.stderr.isTTY) {
5283
+ return () => {
5284
+ };
5285
+ }
5286
+ const message = "sift waiting for provider...";
5287
+ let shown = false;
5288
+ const timer = setTimeout(() => {
5289
+ shown = true;
5290
+ process.stderr.write(`${message}\r`);
5291
+ }, PROVIDER_PENDING_NOTICE_DELAY_MS);
5292
+ return () => {
5293
+ clearTimeout(timer);
5294
+ if (!shown) {
5295
+ return;
5296
+ }
5297
+ process.stderr.write(`\r${" ".repeat(message.length)}\r`);
5298
+ };
5299
+ }
4575
5300
  function withInsufficientHint(args) {
4576
5301
  if (!isInsufficientSignalOutput(args.output)) {
4577
5302
  return args.output;
@@ -4592,22 +5317,27 @@ async function generateWithRetry(args) {
4592
5317
  responseMode: args.responseMode,
4593
5318
  jsonResponseFormat: args.request.config.provider.jsonResponseFormat
4594
5319
  });
5320
+ const stopPendingNotice = startProviderPendingNotice();
4595
5321
  try {
4596
- return await generate();
4597
- } catch (error) {
4598
- const reason = error instanceof Error ? error.message : "unknown_error";
4599
- if (!isRetriableReason(reason)) {
4600
- throw error;
4601
- }
4602
- if (args.request.config.runtime.verbose) {
4603
- process.stderr.write(
4604
- `${pc2.dim("sift")} retry=1 reason=${reason} delay_ms=${RETRY_DELAY_MS}
5322
+ try {
5323
+ return await generate();
5324
+ } catch (error) {
5325
+ const reason = error instanceof Error ? error.message : "unknown_error";
5326
+ if (!isRetriableReason(reason)) {
5327
+ throw error;
5328
+ }
5329
+ if (args.request.config.runtime.verbose) {
5330
+ process.stderr.write(
5331
+ `${pc2.dim("sift")} retry=1 reason=${reason} delay_ms=${RETRY_DELAY_MS}
4605
5332
  `
4606
- );
5333
+ );
5334
+ }
5335
+ await delay(RETRY_DELAY_MS);
4607
5336
  }
4608
- await delay(RETRY_DELAY_MS);
5337
+ return await generate();
5338
+ } finally {
5339
+ stopPendingNotice();
4609
5340
  }
4610
- return generate();
4611
5341
  }
4612
5342
  function hasRecognizableTestStatusSignal(input) {
4613
5343
  const analysis = analyzeTestStatus(input);
@@ -4662,11 +5392,22 @@ function buildTestStatusProviderFailureDecision(args) {
4662
5392
  }
4663
5393
  async function runSift(request) {
4664
5394
  const prepared = prepareInput(request.stdin, request.config.input);
5395
+ const heuristicInput = prepared.redacted;
5396
+ const heuristicInputTruncated = false;
5397
+ const heuristicPrepared = {
5398
+ ...prepared,
5399
+ truncated: heuristicInput,
5400
+ meta: {
5401
+ ...prepared.meta,
5402
+ finalLength: heuristicInput.length,
5403
+ truncatedApplied: heuristicInputTruncated
5404
+ }
5405
+ };
4665
5406
  const provider = createProvider(request.config);
4666
- const hasTestStatusSignal = request.policyName === "test-status" && hasRecognizableTestStatusSignal(prepared.truncated);
4667
- const testStatusAnalysis = hasTestStatusSignal ? analyzeTestStatus(prepared.truncated) : null;
5407
+ const hasTestStatusSignal = request.policyName === "test-status" && hasRecognizableTestStatusSignal(heuristicInput);
5408
+ const testStatusAnalysis = hasTestStatusSignal ? analyzeTestStatus(heuristicInput) : null;
4668
5409
  const testStatusDecision = hasTestStatusSignal && testStatusAnalysis ? buildTestStatusDiagnoseContract({
4669
- input: prepared.truncated,
5410
+ input: heuristicInput,
4670
5411
  analysis: testStatusAnalysis,
4671
5412
  resolvedTests: request.testStatusContext?.resolvedTests,
4672
5413
  remainingTests: request.testStatusContext?.remainingTests
@@ -4681,7 +5422,7 @@ async function runSift(request) {
4681
5422
  `
4682
5423
  );
4683
5424
  }
4684
- const heuristicOutput = request.policyName === "test-status" ? testStatusDecision?.contract.diagnosis_complete ? testStatusHeuristicOutput : null : applyHeuristicPolicy(request.policyName, prepared.truncated, request.detail);
5425
+ const heuristicOutput = request.policyName === "test-status" ? testStatusDecision?.contract.diagnosis_complete ? testStatusHeuristicOutput : null : applyHeuristicPolicy(request.policyName, heuristicInput, request.detail);
4685
5426
  if (heuristicOutput) {
4686
5427
  if (request.config.runtime.verbose) {
4687
5428
  process.stderr.write(`${pc2.dim("sift")} heuristic=${request.policyName}
@@ -4691,7 +5432,7 @@ async function runSift(request) {
4691
5432
  question: request.question,
4692
5433
  format: request.format,
4693
5434
  goal: request.goal,
4694
- input: prepared.truncated,
5435
+ input: heuristicInput,
4695
5436
  detail: request.detail,
4696
5437
  policyName: request.policyName,
4697
5438
  outputContract: request.policyName === "test-status" && request.goal === "diagnose" && request.format === "json" ? request.outputContract ?? TEST_STATUS_DIAGNOSE_JSON_CONTRACT : request.outputContract,
@@ -4711,6 +5452,11 @@ async function runSift(request) {
4711
5452
  prompt: heuristicPrompt.prompt,
4712
5453
  responseMode: heuristicPrompt.responseMode,
4713
5454
  prepared,
5455
+ heuristicInput: {
5456
+ length: heuristicInput.length,
5457
+ truncatedApplied: heuristicInputTruncated,
5458
+ strategy: "full-redacted"
5459
+ },
4714
5460
  heuristicOutput,
4715
5461
  strategy: "heuristic"
4716
5462
  });
@@ -4724,6 +5470,8 @@ async function runSift(request) {
4724
5470
  logVerboseTestStatusTelemetry({
4725
5471
  request,
4726
5472
  prepared,
5473
+ heuristicInputChars: heuristicInput.length,
5474
+ heuristicInputTruncated,
4727
5475
  contract: testStatusDecision.contract,
4728
5476
  finalOutput
4729
5477
  });
@@ -4775,6 +5523,11 @@ async function runSift(request) {
4775
5523
  prompt: prompt.prompt,
4776
5524
  responseMode: prompt.responseMode,
4777
5525
  prepared: providerPrepared2,
5526
+ heuristicInput: {
5527
+ length: heuristicInput.length,
5528
+ truncatedApplied: heuristicInputTruncated,
5529
+ strategy: "full-redacted"
5530
+ },
4778
5531
  heuristicOutput: testStatusHeuristicOutput,
4779
5532
  strategy: "hybrid"
4780
5533
  });
@@ -4788,10 +5541,11 @@ async function runSift(request) {
4788
5541
  });
4789
5542
  const supplement = parseTestStatusProviderSupplement(result.text);
4790
5543
  const mergedDecision = buildTestStatusDiagnoseContract({
4791
- input: prepared.truncated,
5544
+ input: heuristicInput,
4792
5545
  analysis: testStatusAnalysis,
4793
5546
  resolvedTests: request.testStatusContext?.resolvedTests,
4794
5547
  remainingTests: request.testStatusContext?.remainingTests,
5548
+ providerBucketSupplements: supplement.bucket_supplements,
4795
5549
  contractOverrides: {
4796
5550
  diagnosis_complete: supplement.diagnosis_complete,
4797
5551
  raw_needed: supplement.raw_needed,
@@ -4813,6 +5567,8 @@ async function runSift(request) {
4813
5567
  logVerboseTestStatusTelemetry({
4814
5568
  request,
4815
5569
  prepared,
5570
+ heuristicInputChars: heuristicInput.length,
5571
+ heuristicInputTruncated,
4816
5572
  contract: mergedDecision.contract,
4817
5573
  finalOutput,
4818
5574
  rawSliceChars: rawSlice.text.length,
@@ -4825,7 +5581,7 @@ async function runSift(request) {
4825
5581
  const failureDecision = buildTestStatusProviderFailureDecision({
4826
5582
  request,
4827
5583
  baseDecision: testStatusDecision,
4828
- input: prepared.truncated,
5584
+ input: heuristicInput,
4829
5585
  analysis: testStatusAnalysis,
4830
5586
  reason,
4831
5587
  rawSliceUsed: rawSlice.used,
@@ -4846,6 +5602,8 @@ async function runSift(request) {
4846
5602
  logVerboseTestStatusTelemetry({
4847
5603
  request,
4848
5604
  prepared,
5605
+ heuristicInputChars: heuristicInput.length,
5606
+ heuristicInputTruncated,
4849
5607
  contract: failureDecision.contract,
4850
5608
  finalOutput,
4851
5609
  rawSliceChars: rawSlice.text.length,
@@ -4884,6 +5642,11 @@ async function runSift(request) {
4884
5642
  prompt: providerPrompt.prompt,
4885
5643
  responseMode: providerPrompt.responseMode,
4886
5644
  prepared: providerPrepared,
5645
+ heuristicInput: {
5646
+ length: heuristicInput.length,
5647
+ truncatedApplied: heuristicInputTruncated,
5648
+ strategy: "full-redacted"
5649
+ },
4887
5650
  heuristicOutput: testStatusDecision ? testStatusHeuristicOutput : null,
4888
5651
  strategy: testStatusDecision ? "hybrid" : "provider"
4889
5652
  });
@@ -4925,7 +5688,7 @@ async function runSift(request) {
4925
5688
 
4926
5689
  // src/core/testStatusState.ts
4927
5690
  import fs5 from "fs";
4928
- import path6 from "path";
5691
+ import path7 from "path";
4929
5692
  import { z as z3 } from "zod";
4930
5693
  var detailSchema = z3.enum(["standard", "focused", "verbose"]);
4931
5694
  var failureBucketTypeSchema = z3.enum([
@@ -5036,7 +5799,7 @@ function buildBucketSignature(bucket) {
5036
5799
  ]);
5037
5800
  }
5038
5801
  function basenameMatches(value, matcher) {
5039
- return matcher.test(path6.basename(value));
5802
+ return matcher.test(path7.basename(value));
5040
5803
  }
5041
5804
  function isPytestExecutable(value) {
5042
5805
  return basenameMatches(value, /^pytest(?:\.exe)?$/i);
@@ -5230,7 +5993,7 @@ function tryReadCachedTestStatusRun(statePath = getDefaultTestStatusStatePath())
5230
5993
  }
5231
5994
  }
5232
5995
  function writeCachedTestStatusRun(state, statePath = getDefaultTestStatusStatePath()) {
5233
- fs5.mkdirSync(path6.dirname(statePath), {
5996
+ fs5.mkdirSync(path7.dirname(statePath), {
5234
5997
  recursive: true
5235
5998
  });
5236
5999
  fs5.writeFileSync(statePath, `${JSON.stringify(state, null, 2)}
@@ -6039,6 +6802,7 @@ var defaultCliDeps = {
6039
6802
  configInit,
6040
6803
  configSetup,
6041
6804
  configShow,
6805
+ configUse,
6042
6806
  configValidate,
6043
6807
  runDoctor,
6044
6808
  listPresets,
@@ -6143,9 +6907,12 @@ function shouldKeepPresetPolicy(args) {
6143
6907
  return args.requestedFormat === void 0 || args.requestedFormat === args.presetFormat;
6144
6908
  }
6145
6909
  function applySharedOptions(command) {
6146
- return command.option("--provider <provider>", "Provider: openai | openai-compatible").option("--model <model>", "Model name").option("--base-url <url>", "Provider base URL").option(
6910
+ return command.option(
6911
+ "--provider <provider>",
6912
+ "Provider: openai | openai-compatible | openrouter"
6913
+ ).option("--model <model>", "Model name").option("--base-url <url>", "Provider base URL").option(
6147
6914
  "--api-key <key>",
6148
- "Provider API key (or set OPENAI_API_KEY for provider=openai; use SIFT_PROVIDER_API_KEY or endpoint-native envs for openai-compatible)"
6915
+ "Provider API key (or set OPENAI_API_KEY for provider=openai, OPENROUTER_API_KEY for provider=openrouter; use SIFT_PROVIDER_API_KEY or endpoint-native envs for openai-compatible)"
6149
6916
  ).option(
6150
6917
  "--json-response-format <mode>",
6151
6918
  "JSON response format mode: auto | on | off"
@@ -6690,10 +7457,10 @@ function createCliApp(args = {}) {
6690
7457
  }
6691
7458
  throw new Error(`Unknown agent action: ${action}`);
6692
7459
  });
6693
- cli.command("config <action>", "Config commands: setup | init | show | validate").usage("config <setup|init|show|validate> [options]").example("config setup").example("config setup --global").example("config setup --path ~/.config/sift/config.yaml").example("config init").example("config init --global").example("config show").example("config validate --config ./sift.config.yaml").option("--path <path>", "Target config path for init or setup").option(
7460
+ cli.command("config <action> [provider]", "Config commands: setup | init | show | validate | use").usage("config <setup|init|show|validate|use> [provider] [options]").example("config setup").example("config setup --global").example("config setup --path ~/.config/sift/config.yaml").example("config init").example("config init --global").example("config use openrouter").example("config show").example("config validate --config ./sift.config.yaml").option("--path <path>", "Target config path for init or setup").option(
6694
7461
  "--global",
6695
7462
  "Use the machine-wide config path (~/.config/sift/config.yaml) for init or setup"
6696
- ).option("--config <path>", "Path to config file").option("--show-secrets", "Show secret values in config show").action(async (action, options) => {
7463
+ ).option("--config <path>", "Path to config file").option("--show-secrets", "Show secret values in config show").action(async (action, provider, options) => {
6697
7464
  if (action === "setup") {
6698
7465
  process.exitCode = await deps.configSetup({
6699
7466
  targetPath: options.path,
@@ -6716,6 +7483,13 @@ function createCliApp(args = {}) {
6716
7483
  deps.configValidate(options.config);
6717
7484
  return;
6718
7485
  }
7486
+ if (action === "use") {
7487
+ if (!provider) {
7488
+ throw new Error("Missing provider name.");
7489
+ }
7490
+ deps.configUse(provider, options.config, env);
7491
+ return;
7492
+ }
6719
7493
  throw new Error(`Unknown config action: ${action}`);
6720
7494
  });
6721
7495
  cli.command("doctor", "Check which config is active and whether local setup looks complete").usage("doctor [options]").option("--config <path>", "Path to config file").action((options) => {