@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/README.md +29 -1
- package/dist/cli.js +888 -114
- package/dist/index.d.ts +11 -2
- package/dist/index.js +533 -44
- package/package.json +1 -1
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
|
-
|
|
151
|
-
|
|
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([
|
|
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(
|
|
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(
|
|
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
|
|
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 ?
|
|
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", [
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
684
|
-
|
|
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 === "
|
|
689
|
-
return
|
|
875
|
+
if (answer === "override") {
|
|
876
|
+
return "override";
|
|
690
877
|
}
|
|
691
|
-
io.error("Please answer
|
|
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
|
-
|
|
740
|
-
|
|
741
|
-
|
|
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
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
);
|
|
758
|
-
io
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
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:
|
|
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 &&
|
|
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
|
|
1068
|
+
const path8 = writeExampleConfig({
|
|
802
1069
|
targetPath,
|
|
803
1070
|
global
|
|
804
1071
|
});
|
|
805
1072
|
if (!process.stdout.isTTY) {
|
|
806
|
-
process.stdout.write(`${
|
|
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 ${
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
2592
|
-
const
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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)?|
|
|
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 >=
|
|
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((
|
|
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
|
-
|
|
4597
|
-
|
|
4598
|
-
|
|
4599
|
-
|
|
4600
|
-
|
|
4601
|
-
|
|
4602
|
-
|
|
4603
|
-
|
|
4604
|
-
|
|
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
|
|
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(
|
|
4667
|
-
const testStatusAnalysis = hasTestStatusSignal ? analyzeTestStatus(
|
|
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:
|
|
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,
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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) => {
|