@bilalimamoglu/sift 0.2.2 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // src/cli.ts
3
+ // src/cli-app.ts
4
4
  import { createRequire } from "module";
5
5
  import { cac } from "cac";
6
6
 
@@ -375,18 +375,211 @@ function writeConfigFile(options) {
375
375
  return resolved;
376
376
  }
377
377
 
378
+ // src/ui/presentation.ts
379
+ import pc from "picocolors";
380
+ function applyColor(enabled, formatter, value) {
381
+ return enabled ? formatter(value) : value;
382
+ }
383
+ function createPresentation(useColor) {
384
+ return {
385
+ useColor,
386
+ banner(_version) {
387
+ const mark = [
388
+ " \\\\ //",
389
+ " \\\\//",
390
+ " | |",
391
+ " o"
392
+ ].map((line) => applyColor(useColor, pc.cyan, line)).join("\n");
393
+ const tagline = applyColor(useColor, pc.dim, "Trim the noise. Keep the signal.");
394
+ return `${mark}
395
+ ${tagline}`;
396
+ },
397
+ welcome(text) {
398
+ return useColor ? `${pc.bold(pc.cyan("Welcome to sift."))} ${text}` : `Welcome to sift. ${text}`;
399
+ },
400
+ success(text) {
401
+ return useColor ? `${pc.green("\u2713")} ${text}` : text;
402
+ },
403
+ warning(text) {
404
+ return useColor ? `${pc.yellow("!")} ${text}` : text;
405
+ },
406
+ error(text) {
407
+ return useColor ? `${pc.red("x")} ${text}` : text;
408
+ },
409
+ info(text) {
410
+ return useColor ? `${pc.cyan("\u2022")} ${text}` : text;
411
+ },
412
+ note(text) {
413
+ return applyColor(useColor, pc.dim, text);
414
+ },
415
+ section(text) {
416
+ return applyColor(useColor, (value) => pc.bold(pc.yellow(value)), text);
417
+ },
418
+ labelValue(label, value) {
419
+ return `${applyColor(useColor, (entry) => pc.bold(pc.cyan(entry)), label)}: ${value}`;
420
+ },
421
+ command(text) {
422
+ return applyColor(useColor, (value) => pc.bold(pc.cyan(value)), text);
423
+ }
424
+ };
425
+ }
426
+
378
427
  // src/commands/config-setup.ts
379
428
  import fs3 from "fs";
380
429
  import path4 from "path";
430
+ import { emitKeypressEvents } from "readline";
381
431
  import { createInterface } from "readline/promises";
382
- import { clearLine, cursorTo, emitKeypressEvents, moveCursor } from "readline";
383
- import { stdin as defaultStdin, stdout as defaultStdout, stderr as defaultStderr } from "process";
432
+ import { stderr as defaultStderr, stdin as defaultStdin2, stdout as defaultStdout } from "process";
433
+
434
+ // src/ui/terminal.ts
435
+ import { execFileSync } from "child_process";
436
+ import { clearScreenDown, cursorTo, moveCursor } from "readline";
437
+ import { stdin as defaultStdin } from "process";
438
+ function setPosixEcho(enabled) {
439
+ const command = enabled ? "echo" : "-echo";
440
+ try {
441
+ execFileSync("sh", ["-c", `stty ${command} < /dev/tty`], {
442
+ stdio: ["inherit", "inherit", "ignore"]
443
+ });
444
+ return;
445
+ } catch {
446
+ }
447
+ try {
448
+ execFileSync("stty", [command], {
449
+ stdio: ["inherit", "inherit", "ignore"]
450
+ });
451
+ } catch {
452
+ }
453
+ }
454
+ function renderSelectionBlock(args) {
455
+ return [
456
+ `${args.prompt} (use \u2191/\u2193 and Enter)`,
457
+ ...args.options.map(
458
+ (option, index) => `${index === args.selectedIndex ? "\u203A" : " "} ${option}${index === args.selectedIndex ? " (selected)" : ""}`
459
+ )
460
+ ];
461
+ }
462
+ async function promptSelect(args) {
463
+ const { input, output, prompt, options } = args;
464
+ const stream = output;
465
+ const selectedLabel = args.selectedLabel ?? prompt;
466
+ let index = 0;
467
+ let previousLineCount = 0;
468
+ const render = () => {
469
+ if (previousLineCount > 0) {
470
+ moveCursor(stream, 0, -previousLineCount);
471
+ cursorTo(stream, 0);
472
+ clearScreenDown(stream);
473
+ }
474
+ const lines = renderSelectionBlock({
475
+ prompt,
476
+ options,
477
+ selectedIndex: index
478
+ });
479
+ output.write(`${lines.join("\n")}
480
+ `);
481
+ previousLineCount = lines.length;
482
+ };
483
+ const cleanup = (selected) => {
484
+ if (previousLineCount > 0) {
485
+ moveCursor(stream, 0, -previousLineCount);
486
+ cursorTo(stream, 0);
487
+ clearScreenDown(stream);
488
+ }
489
+ if (selected) {
490
+ output.write(`${selectedLabel}: ${selected}
491
+ `);
492
+ }
493
+ };
494
+ input.resume();
495
+ const wasRaw = Boolean(input.isRaw);
496
+ input.setRawMode?.(true);
497
+ render();
498
+ return await new Promise((resolve, reject) => {
499
+ const onKeypress = (_value, key) => {
500
+ if (key.ctrl && key.name === "c") {
501
+ input.off("keypress", onKeypress);
502
+ cleanup();
503
+ input.setRawMode?.(wasRaw);
504
+ input.pause?.();
505
+ reject(new Error("Aborted."));
506
+ return;
507
+ }
508
+ if (key.name === "up") {
509
+ index = index === 0 ? options.length - 1 : index - 1;
510
+ render();
511
+ return;
512
+ }
513
+ if (key.name === "down") {
514
+ index = (index + 1) % options.length;
515
+ render();
516
+ return;
517
+ }
518
+ if (key.name === "return" || key.name === "enter") {
519
+ const selected = options[index] ?? options[0] ?? "";
520
+ input.off("keypress", onKeypress);
521
+ cleanup(selected);
522
+ input.setRawMode?.(wasRaw);
523
+ input.pause?.();
524
+ resolve(selected);
525
+ }
526
+ };
527
+ input.on("keypress", onKeypress);
528
+ });
529
+ }
530
+ async function promptSecret(args) {
531
+ const { input, output, prompt } = args;
532
+ let value = "";
533
+ const shouldToggleEcho = process.platform !== "win32" && input === defaultStdin && Boolean(defaultStdin.isTTY);
534
+ output.write(prompt);
535
+ input.resume();
536
+ const wasRaw = Boolean(input.isRaw);
537
+ input.setRawMode?.(true);
538
+ if (shouldToggleEcho) {
539
+ setPosixEcho(false);
540
+ }
541
+ return await new Promise((resolve, reject) => {
542
+ const restoreInputState = () => {
543
+ input.setRawMode?.(wasRaw);
544
+ input.pause?.();
545
+ if (shouldToggleEcho) {
546
+ setPosixEcho(true);
547
+ }
548
+ };
549
+ const onKeypress = (chunk, key) => {
550
+ if (key.ctrl && key.name === "c") {
551
+ input.off("keypress", onKeypress);
552
+ restoreInputState();
553
+ output.write("\n");
554
+ reject(new Error("Aborted."));
555
+ return;
556
+ }
557
+ if (key.name === "return" || key.name === "enter") {
558
+ input.off("keypress", onKeypress);
559
+ restoreInputState();
560
+ output.write("\n");
561
+ resolve(value);
562
+ return;
563
+ }
564
+ if (key.name === "backspace" || key.name === "delete") {
565
+ value = value.slice(0, -1);
566
+ return;
567
+ }
568
+ if (!key.ctrl && chunk.length > 0) {
569
+ value += chunk;
570
+ }
571
+ };
572
+ input.on("keypress", onKeypress);
573
+ });
574
+ }
575
+
576
+ // src/commands/config-setup.ts
384
577
  function createTerminalIO() {
385
578
  let rl;
386
579
  function getInterface() {
387
580
  if (!rl) {
388
581
  rl = createInterface({
389
- input: defaultStdin,
582
+ input: defaultStdin2,
390
583
  output: defaultStdout,
391
584
  terminal: true
392
585
  });
@@ -394,69 +587,31 @@ function createTerminalIO() {
394
587
  return rl;
395
588
  }
396
589
  async function select(prompt, options) {
397
- const input = defaultStdin;
398
- const output = defaultStdout;
399
- const promptLine = `${prompt} (use \u2191/\u2193 and Enter)`;
400
- let index = 0;
401
- const lineCount = options.length + 1;
402
- emitKeypressEvents(input);
403
- input.resume();
404
- const wasRaw = input.isTTY ? input.isRaw : false;
405
- input.setRawMode?.(true);
406
- const render = () => {
407
- cursorTo(output, 0);
408
- clearLine(output, 0);
409
- output.write(`${promptLine}
410
- `);
411
- for (let optionIndex = 0; optionIndex < options.length; optionIndex += 1) {
412
- clearLine(output, 0);
413
- output.write(`${optionIndex === index ? "\u203A" : " "} ${options[optionIndex]}
414
- `);
415
- }
416
- moveCursor(output, 0, -lineCount);
417
- };
418
- render();
419
- return await new Promise((resolve, reject) => {
420
- const onKeypress = (_value, key) => {
421
- if (key.ctrl && key.name === "c") {
422
- cleanup();
423
- reject(new Error("Aborted."));
424
- return;
425
- }
426
- if (key.name === "up") {
427
- index = index === 0 ? options.length - 1 : index - 1;
428
- render();
429
- return;
430
- }
431
- if (key.name === "down") {
432
- index = (index + 1) % options.length;
433
- render();
434
- return;
435
- }
436
- if (key.name === "return" || key.name === "enter") {
437
- const selected = options[index] ?? options[0];
438
- cleanup();
439
- resolve(selected ?? "OpenAI");
440
- }
441
- };
442
- const cleanup = () => {
443
- input.off("keypress", onKeypress);
444
- moveCursor(output, 0, lineCount);
445
- cursorTo(output, 0);
446
- clearLine(output, 0);
447
- output.write("\n");
448
- input.setRawMode?.(Boolean(wasRaw));
449
- };
450
- input.on("keypress", onKeypress);
590
+ emitKeypressEvents(defaultStdin2);
591
+ return await promptSelect({
592
+ input: defaultStdin2,
593
+ output: defaultStdout,
594
+ prompt,
595
+ options,
596
+ selectedLabel: "Provider"
597
+ });
598
+ }
599
+ async function secret(prompt) {
600
+ emitKeypressEvents(defaultStdin2);
601
+ return await promptSecret({
602
+ input: defaultStdin2,
603
+ output: defaultStdout,
604
+ prompt
451
605
  });
452
606
  }
453
607
  return {
454
- stdinIsTTY: Boolean(defaultStdin.isTTY),
608
+ stdinIsTTY: Boolean(defaultStdin2.isTTY),
455
609
  stdoutIsTTY: Boolean(defaultStdout.isTTY),
456
610
  ask(prompt) {
457
611
  return getInterface().question(prompt);
458
612
  },
459
613
  select,
614
+ secret,
460
615
  write(message) {
461
616
  defaultStdout.write(message);
462
617
  },
@@ -483,9 +638,12 @@ function buildOpenAISetupConfig(apiKey) {
483
638
  }
484
639
  };
485
640
  }
641
+ function getSetupPresenter(io) {
642
+ return createPresentation(io.stdoutIsTTY);
643
+ }
486
644
  async function promptForProvider(io) {
487
645
  if (io.select) {
488
- const choice = await io.select("Select provider", ["OpenAI"]);
646
+ const choice = await io.select("Select provider for this machine", ["OpenAI"]);
489
647
  if (choice === "OpenAI") {
490
648
  return "openai";
491
649
  }
@@ -500,7 +658,7 @@ async function promptForProvider(io) {
500
658
  }
501
659
  async function promptForApiKey(io) {
502
660
  while (true) {
503
- const answer = (await io.ask("Enter your OpenAI API key: ")).trim();
661
+ const answer = (await (io.secret ? io.secret("Enter your OpenAI API key (input hidden): ") : io.ask("Enter your OpenAI API key: "))).trim();
504
662
  if (answer.length > 0) {
505
663
  return answer;
506
664
  }
@@ -521,9 +679,41 @@ async function promptForOverwrite(io, targetPath) {
521
679
  io.error("Please answer y or n.\n");
522
680
  }
523
681
  }
682
+ function writeSetupSuccess(io, writtenPath) {
683
+ const ui = getSetupPresenter(io);
684
+ io.write(`
685
+ ${ui.success("You're set.")}
686
+ `);
687
+ io.write(`${ui.info(`Machine-wide config: ${writtenPath}`)}
688
+ `);
689
+ io.write(`${ui.note("sift is ready to use from any terminal on this machine.")}
690
+ `);
691
+ io.write(
692
+ `${ui.note("A repo-local sift.config.yaml can still override it when a project needs its own settings.")}
693
+ `
694
+ );
695
+ }
696
+ function writeOverrideWarning(io, activeConfigPath) {
697
+ const ui = getSetupPresenter(io);
698
+ io.write(
699
+ `${ui.warning(`Heads-up: ${activeConfigPath} currently overrides this machine-wide config in this directory.`)}
700
+ `
701
+ );
702
+ }
703
+ function writeNextSteps(io) {
704
+ const ui = getSetupPresenter(io);
705
+ io.write(`
706
+ ${ui.section("Try next")}
707
+ `);
708
+ io.write(` ${ui.command("sift doctor")}
709
+ `);
710
+ io.write(` ${ui.command("sift exec --preset test-status -- pytest")}
711
+ `);
712
+ }
524
713
  async function configSetup(options = {}) {
525
714
  void options.global;
526
715
  const io = options.io ?? createTerminalIO();
716
+ const ui = getSetupPresenter(io);
527
717
  try {
528
718
  if (!io.stdinIsTTY || !io.stdoutIsTTY) {
529
719
  io.error(
@@ -531,24 +721,31 @@ async function configSetup(options = {}) {
531
721
  );
532
722
  return 1;
533
723
  }
724
+ io.write(`${ui.welcome("Let's keep the expensive model for the interesting bits.")}
725
+ `);
534
726
  const resolvedPath = resolveSetupPath(options.targetPath);
535
727
  if (fs3.existsSync(resolvedPath)) {
536
728
  const shouldOverwrite = await promptForOverwrite(io, resolvedPath);
537
729
  if (!shouldOverwrite) {
538
- io.write("Aborted.\n");
730
+ io.write(`${ui.note("Aborted.")}
731
+ `);
539
732
  return 1;
540
733
  }
541
734
  }
542
- const provider = await promptForProvider(io);
543
- if (provider !== "openai") {
544
- io.error("Unsupported provider selection.\n");
545
- return 1;
546
- }
547
- io.write("Using OpenAI defaults.\n");
548
- io.write("Default model: gpt-5-nano\n");
549
- io.write("Default base URL: https://api.openai.com/v1\n");
735
+ await promptForProvider(io);
736
+ io.write(`${ui.info("Using OpenAI defaults for your first run.")}
737
+ `);
738
+ io.write(`${ui.labelValue("Default model", "gpt-5-nano")}
739
+ `);
740
+ io.write(`${ui.labelValue("Default base URL", "https://api.openai.com/v1")}
741
+ `);
550
742
  io.write(
551
- "You can change these later by editing the config file or running 'sift config show --show-secrets'.\n"
743
+ `${ui.note(`Want to switch providers or tweak defaults later? Edit ${resolvedPath}.`)}
744
+ `
745
+ );
746
+ io.write(
747
+ `${ui.note("Want to inspect the active values first? Run 'sift config show --show-secrets'.")}
748
+ `
552
749
  );
553
750
  const apiKey = await promptForApiKey(io);
554
751
  const config = buildOpenAISetupConfig(apiKey);
@@ -557,21 +754,12 @@ async function configSetup(options = {}) {
557
754
  config,
558
755
  overwrite: true
559
756
  });
560
- io.write(`Wrote ${writtenPath}
561
- `);
562
- io.write(
563
- "This is your machine-wide default config. Repo-local sift.config.yaml can still override it later.\n"
564
- );
757
+ writeSetupSuccess(io, writtenPath);
565
758
  const activeConfigPath = findConfigPath();
566
759
  if (activeConfigPath && path4.resolve(activeConfigPath) !== path4.resolve(writtenPath)) {
567
- io.write(
568
- `Note: ${activeConfigPath} currently overrides this machine-wide config in the current directory.
569
- `
570
- );
760
+ writeOverrideWarning(io, activeConfigPath);
571
761
  }
572
- io.write("Try:\n");
573
- io.write(" sift doctor\n");
574
- io.write(" sift exec --preset test-status -- pytest\n");
762
+ writeNextSteps(io);
575
763
  return 0;
576
764
  } finally {
577
765
  io.close?.();
@@ -602,8 +790,16 @@ function configInit(targetPath, global = false) {
602
790
  targetPath,
603
791
  global
604
792
  });
605
- process.stdout.write(`${path5}
793
+ if (!process.stdout.isTTY) {
794
+ process.stdout.write(`${path5}
606
795
  `);
796
+ return;
797
+ }
798
+ const ui = createPresentation(true);
799
+ process.stdout.write(
800
+ `${ui.success(`${global ? "Machine-wide" : "Template"} config written to ${path5}`)}
801
+ `
802
+ );
607
803
  }
608
804
  function configShow(configPath, showSecrets = false) {
609
805
  const config = resolveConfig({
@@ -620,25 +816,32 @@ function configValidate(configPath) {
620
816
  env: process.env
621
817
  });
622
818
  const resolvedPath = findConfigPath(configPath);
623
- process.stdout.write(
624
- `Resolved config is valid${resolvedPath ? ` (${resolvedPath})` : " (using defaults)"}.
625
- `
626
- );
819
+ const message = `Resolved config is valid${resolvedPath ? ` (${resolvedPath})` : " (using defaults)"}.`;
820
+ if (!process.stdout.isTTY) {
821
+ process.stdout.write(`${message}
822
+ `);
823
+ return;
824
+ }
825
+ const ui = createPresentation(true);
826
+ process.stdout.write(`${ui.success(message)}
827
+ `);
627
828
  }
628
829
 
629
830
  // src/commands/doctor.ts
630
831
  function runDoctor(config, configPath) {
832
+ const ui = createPresentation(Boolean(process.stdout.isTTY));
631
833
  const lines = [
632
834
  "sift doctor",
835
+ "A quick check for your local setup.",
633
836
  "mode: local config completeness check",
634
- `configPath: ${configPath ?? "(defaults only)"}`,
635
- `provider: ${config.provider.provider}`,
636
- `model: ${config.provider.model}`,
637
- `baseUrl: ${config.provider.baseUrl}`,
638
- `apiKey: ${config.provider.apiKey ? "set" : "not set"}`,
639
- `maxCaptureChars: ${config.input.maxCaptureChars}`,
640
- `maxInputChars: ${config.input.maxInputChars}`,
641
- `rawFallback: ${config.runtime.rawFallback}`
837
+ ui.labelValue("configPath", configPath ?? "(defaults only)"),
838
+ ui.labelValue("provider", config.provider.provider),
839
+ ui.labelValue("model", config.provider.model),
840
+ ui.labelValue("baseUrl", config.provider.baseUrl),
841
+ ui.labelValue("apiKey", config.provider.apiKey ? "set" : "not set"),
842
+ ui.labelValue("maxCaptureChars", String(config.input.maxCaptureChars)),
843
+ ui.labelValue("maxInputChars", String(config.input.maxInputChars)),
844
+ ui.labelValue("rawFallback", String(config.runtime.rawFallback))
642
845
  ];
643
846
  process.stdout.write(`${lines.join("\n")}
644
847
  `);
@@ -659,8 +862,16 @@ function runDoctor(config, configPath) {
659
862
  );
660
863
  }
661
864
  if (problems.length > 0) {
662
- process.stderr.write(`${problems.join("\n")}
865
+ if (process.stderr.isTTY) {
866
+ const errorUi = createPresentation(true);
867
+ process.stderr.write(
868
+ `${problems.map((problem) => errorUi.error(problem)).join("\n")}
869
+ `
870
+ );
871
+ } else {
872
+ process.stderr.write(`${problems.join("\n")}
663
873
  `);
874
+ }
664
875
  return 1;
665
876
  }
666
877
  return 0;
@@ -689,7 +900,7 @@ function showPreset(config, name, includeInternal = false) {
689
900
  // src/core/exec.ts
690
901
  import { spawn } from "child_process";
691
902
  import { constants as osConstants } from "os";
692
- import pc2 from "picocolors";
903
+ import pc3 from "picocolors";
693
904
 
694
905
  // src/core/gate.ts
695
906
  var FAIL_ON_SUPPORTED_PRESETS = /* @__PURE__ */ new Set(["infra-risk", "audit-critical"]);
@@ -738,8 +949,31 @@ function evaluateGate(args) {
738
949
  return { shouldFail: false };
739
950
  }
740
951
 
952
+ // src/core/insufficient.ts
953
+ function isInsufficientSignalOutput(output) {
954
+ const trimmed = output.trim();
955
+ return trimmed === INSUFFICIENT_SIGNAL_TEXT || trimmed.startsWith(`${INSUFFICIENT_SIGNAL_TEXT}
956
+ Hint:`);
957
+ }
958
+ function buildInsufficientSignalOutput(input) {
959
+ let hint;
960
+ if (input.originalLength === 0) {
961
+ hint = "Hint: no command output was captured.";
962
+ } else if (input.truncatedApplied) {
963
+ hint = "Hint: captured output was truncated before a clear summary was found.";
964
+ } else if (input.presetName === "test-status" && input.exitCode === 0) {
965
+ hint = "Hint: command succeeded, but no recognizable test summary was found.";
966
+ } else if (input.presetName === "test-status" && typeof input.exitCode === "number") {
967
+ hint = "Hint: command failed, but the captured output did not include a recognizable test summary.";
968
+ } else {
969
+ hint = "Hint: the captured output did not contain a clear answer for this preset.";
970
+ }
971
+ return `${INSUFFICIENT_SIGNAL_TEXT}
972
+ ${hint}`;
973
+ }
974
+
741
975
  // src/core/run.ts
742
- import pc from "picocolors";
976
+ import pc2 from "picocolors";
743
977
 
744
978
  // src/providers/systemInstruction.ts
745
979
  var REDUCTION_SYSTEM_INSTRUCTION = "You reduce noisy command output into compact answers for agents and automation.";
@@ -815,7 +1049,7 @@ var OpenAIProvider = class {
815
1049
  if (!text) {
816
1050
  throw new Error("Provider returned an empty response");
817
1051
  }
818
- return {
1052
+ const result = {
819
1053
  text,
820
1054
  usage: data?.usage ? {
821
1055
  inputTokens: data.usage.input_tokens,
@@ -824,13 +1058,14 @@ var OpenAIProvider = class {
824
1058
  } : void 0,
825
1059
  raw: data
826
1060
  };
1061
+ clearTimeout(timeout);
1062
+ return result;
827
1063
  } catch (error) {
1064
+ clearTimeout(timeout);
828
1065
  if (error.name === "AbortError") {
829
1066
  throw new Error("Provider request timed out");
830
1067
  }
831
1068
  throw error;
832
- } finally {
833
- clearTimeout(timeout);
834
1069
  }
835
1070
  }
836
1071
  };
@@ -912,7 +1147,7 @@ var OpenAICompatibleProvider = class {
912
1147
  if (!text.trim()) {
913
1148
  throw new Error("Provider returned an empty response");
914
1149
  }
915
- return {
1150
+ const result = {
916
1151
  text,
917
1152
  usage: data?.usage ? {
918
1153
  inputTokens: data.usage.prompt_tokens,
@@ -921,13 +1156,14 @@ var OpenAICompatibleProvider = class {
921
1156
  } : void 0,
922
1157
  raw: data
923
1158
  };
1159
+ clearTimeout(timeout);
1160
+ return result;
924
1161
  } catch (error) {
1162
+ clearTimeout(timeout);
925
1163
  if (error.name === "AbortError") {
926
1164
  throw new Error("Provider request timed out");
927
1165
  }
928
1166
  throw error;
929
- } finally {
930
- clearTimeout(timeout);
931
1167
  }
932
1168
  }
933
1169
  };
@@ -1132,6 +1368,19 @@ function buildPrompt(args) {
1132
1368
  policyName: args.policyName,
1133
1369
  outputContract: args.outputContract
1134
1370
  });
1371
+ const detailRules = args.policyName === "test-status" && args.detail === "focused" ? [
1372
+ "Use a focused failure view.",
1373
+ "When the output clearly maps failures to specific tests or modules, group them by dominant error type first.",
1374
+ "Within each error group, prefer compact bullets in the form '- test-or-module -> dominant reason'.",
1375
+ "Cap focused entries at 6 per error group and end with '- and N more failing modules' if more clear mappings are visible.",
1376
+ "If per-test or per-module mapping is unclear, fall back to grouped root causes instead of guessing."
1377
+ ] : args.policyName === "test-status" && args.detail === "verbose" ? [
1378
+ "Use a verbose failure view.",
1379
+ "When the output clearly maps failures to specific tests or modules, list each visible failing test or module on its own line in the form '- test-or-module -> normalized reason'.",
1380
+ "Preserve the original file or module order when the mapping is visible.",
1381
+ "Prefer concrete normalized reasons such as missing modules or assertion failures over traceback plumbing.",
1382
+ "If per-test or per-module mapping is unclear, fall back to the focused grouped-cause view instead of guessing."
1383
+ ] : [];
1135
1384
  const prompt = [
1136
1385
  "You are Sift, a CLI output reduction assistant for downstream agents and automation.",
1137
1386
  "Hard rules:",
@@ -1139,6 +1388,7 @@ function buildPrompt(args) {
1139
1388
  "",
1140
1389
  `Task policy: ${policy.name}`,
1141
1390
  ...policy.taskRules.map((rule) => `- ${rule}`),
1391
+ ...detailRules.map((rule) => `- ${rule}`),
1142
1392
  ...policy.outputContract ? ["", `Output contract: ${policy.outputContract}`] : [],
1143
1393
  "",
1144
1394
  `Question: ${args.question}`,
@@ -1251,6 +1501,410 @@ function inferPackage(line) {
1251
1501
  function inferRemediation(pkg2) {
1252
1502
  return `Upgrade ${pkg2} to a patched version.`;
1253
1503
  }
1504
+ function getCount(input, label) {
1505
+ const matches = [...input.matchAll(new RegExp(`(\\d+)\\s+${label}`, "gi"))];
1506
+ const lastMatch = matches.at(-1);
1507
+ return lastMatch ? Number(lastMatch[1]) : 0;
1508
+ }
1509
+ function formatCount(count, singular, plural = `${singular}s`) {
1510
+ return `${count} ${count === 1 ? singular : plural}`;
1511
+ }
1512
+ function countPattern(input, matcher) {
1513
+ return [...input.matchAll(matcher)].length;
1514
+ }
1515
+ function collectUniqueMatches(input, matcher, limit = 6) {
1516
+ const values = [];
1517
+ for (const match of input.matchAll(matcher)) {
1518
+ const candidate = match[1]?.trim();
1519
+ if (!candidate || values.includes(candidate)) {
1520
+ continue;
1521
+ }
1522
+ values.push(candidate);
1523
+ if (values.length >= limit) {
1524
+ break;
1525
+ }
1526
+ }
1527
+ return values;
1528
+ }
1529
+ function cleanFailureLabel(label) {
1530
+ return label.trim().replace(/^['"]|['"]$/g, "");
1531
+ }
1532
+ function isLowValueInternalReason(normalized) {
1533
+ return /^Hint:\s+make sure your test modules\/packages have valid Python names\.?$/i.test(
1534
+ normalized
1535
+ ) || /^Traceback\b/i.test(normalized) || /^return _bootstrap\._gcd_import/i.test(normalized) || /(?:^|[/\\])(?:site-packages[/\\])?_pytest(?:[/\\]|$)/i.test(normalized) || /(?:^|[/\\])importlib[/\\]__init__\.py:\d+:\s+in\s+import_module\b/i.test(
1536
+ normalized
1537
+ ) || /\bpython\.py:\d+:\s+in\s+importtestmodule\b/i.test(normalized) || /\bpython\.py:\d+:\s+in\s+import_path\b/i.test(normalized);
1538
+ }
1539
+ function scoreFailureReason(reason) {
1540
+ if (reason.startsWith("missing module:")) {
1541
+ return 5;
1542
+ }
1543
+ if (reason.startsWith("assertion failed:")) {
1544
+ return 4;
1545
+ }
1546
+ if (/^[A-Z][A-Za-z]+(?:Error|Exception):/.test(reason)) {
1547
+ return 3;
1548
+ }
1549
+ if (reason === "import error during collection") {
1550
+ return 2;
1551
+ }
1552
+ return 1;
1553
+ }
1554
+ function classifyFailureReason(line, options) {
1555
+ const normalized = line.trim().replace(/^[A-Z]\s+/, "");
1556
+ if (normalized.length === 0) {
1557
+ return null;
1558
+ }
1559
+ if (isLowValueInternalReason(normalized)) {
1560
+ return null;
1561
+ }
1562
+ const pythonMissingModule = normalized.match(
1563
+ /ModuleNotFoundError:\s+No module named ['"]([^'"]+)['"]/i
1564
+ );
1565
+ if (pythonMissingModule) {
1566
+ return {
1567
+ reason: `missing module: ${pythonMissingModule[1]}`,
1568
+ group: options.duringCollection ? "import/dependency errors during collection" : "missing dependency/module errors"
1569
+ };
1570
+ }
1571
+ const nodeMissingModule = normalized.match(/Cannot find module ['"]([^'"]+)['"]/i);
1572
+ if (nodeMissingModule) {
1573
+ return {
1574
+ reason: `missing module: ${nodeMissingModule[1]}`,
1575
+ group: options.duringCollection ? "import/dependency errors during collection" : "missing dependency/module errors"
1576
+ };
1577
+ }
1578
+ const assertionFailure = normalized.match(/AssertionError:\s*(.+)$/i);
1579
+ if (assertionFailure) {
1580
+ return {
1581
+ reason: `assertion failed: ${assertionFailure[1]}`.slice(0, 120),
1582
+ group: "assertion failures"
1583
+ };
1584
+ }
1585
+ const genericError = normalized.match(/\b([A-Z][A-Za-z]+(?:Error|Exception)):\s*(.+)$/);
1586
+ if (genericError) {
1587
+ const errorType = genericError[1];
1588
+ return {
1589
+ reason: `${errorType}: ${genericError[2]}`.slice(0, 120),
1590
+ group: options.duringCollection && errorType === "ImportError" ? "import/dependency errors during collection" : `${errorType} failures`
1591
+ };
1592
+ }
1593
+ if (/ImportError while importing test module/i.test(normalized)) {
1594
+ return {
1595
+ reason: "import error during collection",
1596
+ group: "import/dependency errors during collection"
1597
+ };
1598
+ }
1599
+ if (!/[A-Za-z]/.test(normalized)) {
1600
+ return null;
1601
+ }
1602
+ return {
1603
+ reason: normalized.slice(0, 120),
1604
+ group: options.duringCollection ? "collection/import errors" : "other failures"
1605
+ };
1606
+ }
1607
+ function pushFocusedFailureItem(items, candidate) {
1608
+ if (items.some((item) => item.label === candidate.label && item.reason === candidate.reason)) {
1609
+ return;
1610
+ }
1611
+ items.push(candidate);
1612
+ }
1613
+ function chooseStrongestFailureItems(items) {
1614
+ const strongest = /* @__PURE__ */ new Map();
1615
+ const order = [];
1616
+ for (const item of items) {
1617
+ const existing = strongest.get(item.label);
1618
+ if (!existing) {
1619
+ strongest.set(item.label, item);
1620
+ order.push(item.label);
1621
+ continue;
1622
+ }
1623
+ if (scoreFailureReason(item.reason) > scoreFailureReason(existing.reason)) {
1624
+ strongest.set(item.label, item);
1625
+ }
1626
+ }
1627
+ return order.map((label) => strongest.get(label));
1628
+ }
1629
+ function collectCollectionFailureItems(input) {
1630
+ const items = [];
1631
+ const lines = input.split("\n");
1632
+ let currentLabel = null;
1633
+ let pendingGenericReason = null;
1634
+ for (const line of lines) {
1635
+ const collecting = line.match(/^_+\s+ERROR collecting\s+(.+?)\s+_+\s*$/);
1636
+ if (collecting) {
1637
+ if (currentLabel && pendingGenericReason) {
1638
+ pushFocusedFailureItem(
1639
+ items,
1640
+ {
1641
+ label: currentLabel,
1642
+ reason: pendingGenericReason.reason,
1643
+ group: pendingGenericReason.group
1644
+ }
1645
+ );
1646
+ }
1647
+ currentLabel = cleanFailureLabel(collecting[1]);
1648
+ pendingGenericReason = null;
1649
+ continue;
1650
+ }
1651
+ if (!currentLabel) {
1652
+ continue;
1653
+ }
1654
+ const classification = classifyFailureReason(line, {
1655
+ duringCollection: true
1656
+ });
1657
+ if (!classification) {
1658
+ continue;
1659
+ }
1660
+ if (classification.reason === "import error during collection") {
1661
+ pendingGenericReason = classification;
1662
+ continue;
1663
+ }
1664
+ pushFocusedFailureItem(
1665
+ items,
1666
+ {
1667
+ label: currentLabel,
1668
+ reason: classification.reason,
1669
+ group: classification.group
1670
+ }
1671
+ );
1672
+ currentLabel = null;
1673
+ pendingGenericReason = null;
1674
+ }
1675
+ if (currentLabel && pendingGenericReason) {
1676
+ pushFocusedFailureItem(
1677
+ items,
1678
+ {
1679
+ label: currentLabel,
1680
+ reason: pendingGenericReason.reason,
1681
+ group: pendingGenericReason.group
1682
+ }
1683
+ );
1684
+ }
1685
+ return items;
1686
+ }
1687
+ function collectInlineFailureItems(input) {
1688
+ const items = [];
1689
+ for (const line of input.split("\n")) {
1690
+ const inlineFailure = line.match(/^(FAILED|ERROR)\s+(.+?)\s+-\s+(.+)$/);
1691
+ if (!inlineFailure) {
1692
+ continue;
1693
+ }
1694
+ const classification = classifyFailureReason(inlineFailure[3], {
1695
+ duringCollection: false
1696
+ });
1697
+ if (!classification) {
1698
+ continue;
1699
+ }
1700
+ pushFocusedFailureItem(
1701
+ items,
1702
+ {
1703
+ label: cleanFailureLabel(inlineFailure[2]),
1704
+ reason: classification.reason,
1705
+ group: classification.group
1706
+ }
1707
+ );
1708
+ }
1709
+ return items;
1710
+ }
1711
+ function formatFocusedFailureGroups(args) {
1712
+ const maxGroups = args.maxGroups ?? 3;
1713
+ const maxPerGroup = args.maxPerGroup ?? 6;
1714
+ const grouped = /* @__PURE__ */ new Map();
1715
+ for (const item of args.items) {
1716
+ const entries = grouped.get(item.group) ?? [];
1717
+ entries.push(item);
1718
+ grouped.set(item.group, entries);
1719
+ }
1720
+ const lines = [];
1721
+ const visibleGroups = [...grouped.entries()].slice(0, maxGroups);
1722
+ for (const [group, entries] of visibleGroups) {
1723
+ lines.push(`- ${group}`);
1724
+ for (const item of entries.slice(0, maxPerGroup)) {
1725
+ lines.push(` - ${item.label} -> ${item.reason}`);
1726
+ }
1727
+ const remaining = entries.length - Math.min(entries.length, maxPerGroup);
1728
+ if (remaining > 0) {
1729
+ lines.push(` - and ${remaining} more failing ${args.remainderLabel}`);
1730
+ }
1731
+ }
1732
+ const hiddenGroups = grouped.size - visibleGroups.length;
1733
+ if (hiddenGroups > 0) {
1734
+ lines.push(`- and ${hiddenGroups} more error group${hiddenGroups === 1 ? "" : "s"}`);
1735
+ }
1736
+ return lines;
1737
+ }
1738
+ function formatVerboseFailureItems(args) {
1739
+ return chooseStrongestFailureItems(args.items).map(
1740
+ (item) => `- ${item.label} -> ${item.reason}`
1741
+ );
1742
+ }
1743
+ function summarizeRepeatedTestCauses(input, options) {
1744
+ const pythonMissingModules = collectUniqueMatches(
1745
+ input,
1746
+ /ModuleNotFoundError:\s+No module named ['"]([^'"]+)['"]/gi
1747
+ );
1748
+ const nodeMissingModules = collectUniqueMatches(
1749
+ input,
1750
+ /Cannot find module ['"]([^'"]+)['"]/gi
1751
+ );
1752
+ const missingModules = [...pythonMissingModules];
1753
+ for (const moduleName of nodeMissingModules) {
1754
+ if (!missingModules.includes(moduleName)) {
1755
+ missingModules.push(moduleName);
1756
+ }
1757
+ }
1758
+ const missingModuleHits = countPattern(
1759
+ input,
1760
+ /ModuleNotFoundError:\s+No module named ['"]([^'"]+)['"]/gi
1761
+ ) + countPattern(input, /Cannot find module ['"]([^'"]+)['"]/gi);
1762
+ const importCollectionHits = countPattern(input, /ImportError while importing test module/gi) + countPattern(input, /^\s*_+\s+ERROR collecting\b/gim);
1763
+ const genericErrorTypes = collectUniqueMatches(
1764
+ input,
1765
+ /\b((?:Assertion|Import|Type|Value|Runtime|Reference|Key|Attribute)[A-Za-z]*Error)\b/gi,
1766
+ 4
1767
+ );
1768
+ const bullets = [];
1769
+ if (options.duringCollection && (importCollectionHits >= 2 || missingModuleHits >= 2) || !options.duringCollection && missingModuleHits >= 2) {
1770
+ bullets.push(
1771
+ options.duringCollection ? "- Most failures are import/dependency errors during test collection." : "- Most failures are import/dependency errors."
1772
+ );
1773
+ }
1774
+ if (missingModules.length > 1) {
1775
+ bullets.push(`- Missing modules include ${missingModules.join(", ")}.`);
1776
+ } else if (missingModules.length === 1 && missingModuleHits >= 2) {
1777
+ bullets.push(`- Missing module repeated across failures: ${missingModules[0]}.`);
1778
+ }
1779
+ if (bullets.length < 2 && genericErrorTypes.length >= 2) {
1780
+ bullets.push(`- Repeated error types include ${genericErrorTypes.join(", ")}.`);
1781
+ }
1782
+ return bullets.slice(0, 2);
1783
+ }
1784
+ function testStatusHeuristic(input, detail = "standard") {
1785
+ const normalized = input.trim();
1786
+ if (normalized === "") {
1787
+ return null;
1788
+ }
1789
+ const passed = getCount(input, "passed");
1790
+ const failed = getCount(input, "failed");
1791
+ const errors = Math.max(
1792
+ getCount(input, "errors"),
1793
+ getCount(input, "error")
1794
+ );
1795
+ const skipped = getCount(input, "skipped");
1796
+ const collectionErrors = input.match(/(\d+)\s+errors?\s+during collection/i);
1797
+ const noTestsCollected = /\bcollected\s+0\s+items\b/i.test(input) || /\bno tests ran\b/i.test(input);
1798
+ const interrupted = /\binterrupted\b/i.test(input) || /\bKeyboardInterrupt\b/i.test(input);
1799
+ const inlineItems = collectInlineFailureItems(input);
1800
+ if (collectionErrors) {
1801
+ const count = Number(collectionErrors[1]);
1802
+ const items = chooseStrongestFailureItems(collectCollectionFailureItems(input));
1803
+ if (detail === "verbose") {
1804
+ if (items.length > 0) {
1805
+ return [
1806
+ "- Tests did not complete.",
1807
+ `- ${formatCount(count, "error")} occurred during collection.`,
1808
+ ...formatVerboseFailureItems({
1809
+ items
1810
+ })
1811
+ ].join("\n");
1812
+ }
1813
+ }
1814
+ if (detail === "focused") {
1815
+ if (items.length > 0) {
1816
+ const groupedLines = formatFocusedFailureGroups({
1817
+ items,
1818
+ remainderLabel: "modules"
1819
+ });
1820
+ if (groupedLines.length > 0) {
1821
+ return [
1822
+ "- Tests did not complete.",
1823
+ `- ${formatCount(count, "error")} occurred during collection.`,
1824
+ ...groupedLines
1825
+ ].join("\n");
1826
+ }
1827
+ }
1828
+ }
1829
+ const causes = summarizeRepeatedTestCauses(input, {
1830
+ duringCollection: true
1831
+ });
1832
+ return [
1833
+ "- Tests did not complete.",
1834
+ `- ${formatCount(count, "error")} occurred during collection.`,
1835
+ ...causes
1836
+ ].join("\n");
1837
+ }
1838
+ if (noTestsCollected) {
1839
+ return ["- Tests did not run.", "- Collected 0 items."].join("\n");
1840
+ }
1841
+ if (interrupted && failed === 0 && errors === 0) {
1842
+ return "- Test run was interrupted.";
1843
+ }
1844
+ if (failed === 0 && errors === 0 && passed > 0) {
1845
+ const details = [formatCount(passed, "test")];
1846
+ if (skipped > 0) {
1847
+ details.push(formatCount(skipped, "skip"));
1848
+ }
1849
+ return [
1850
+ "- Tests passed.",
1851
+ `- ${details.join(", ")}.`
1852
+ ].join("\n");
1853
+ }
1854
+ if (failed > 0 || errors > 0 || inlineItems.length > 0) {
1855
+ const summarizedInlineItems = chooseStrongestFailureItems(inlineItems);
1856
+ if (detail === "verbose") {
1857
+ if (summarizedInlineItems.length > 0) {
1858
+ const detailLines2 = [];
1859
+ if (failed > 0) {
1860
+ detailLines2.push(`- ${formatCount(failed, "test")} failed.`);
1861
+ }
1862
+ if (errors > 0) {
1863
+ detailLines2.push(`- ${formatCount(errors, "error")} occurred.`);
1864
+ }
1865
+ return [
1866
+ "- Tests did not pass.",
1867
+ ...detailLines2,
1868
+ ...formatVerboseFailureItems({
1869
+ items: summarizedInlineItems
1870
+ })
1871
+ ].join("\n");
1872
+ }
1873
+ }
1874
+ if (detail === "focused") {
1875
+ if (summarizedInlineItems.length > 0) {
1876
+ const detailLines2 = [];
1877
+ if (failed > 0) {
1878
+ detailLines2.push(`- ${formatCount(failed, "test")} failed.`);
1879
+ }
1880
+ if (errors > 0) {
1881
+ detailLines2.push(`- ${formatCount(errors, "error")} occurred.`);
1882
+ }
1883
+ return [
1884
+ "- Tests did not pass.",
1885
+ ...detailLines2,
1886
+ ...formatFocusedFailureGroups({
1887
+ items: summarizedInlineItems,
1888
+ remainderLabel: "tests or modules"
1889
+ })
1890
+ ].join("\n");
1891
+ }
1892
+ }
1893
+ const detailLines = [];
1894
+ const causes = summarizeRepeatedTestCauses(input, {
1895
+ duringCollection: false
1896
+ });
1897
+ if (failed > 0) {
1898
+ detailLines.push(`- ${formatCount(failed, "test")} failed.`);
1899
+ }
1900
+ if (errors > 0) {
1901
+ detailLines.push(`- ${formatCount(errors, "error")} occurred.`);
1902
+ }
1903
+ const evidence = input.split("\n").map((line) => line.trim()).filter((line) => /\b(FAILED|ERROR)\b/.test(line)).slice(0, 3).map((line) => `- ${line}`);
1904
+ return ["- Tests did not pass.", ...detailLines, ...causes, ...evidence].join("\n");
1905
+ }
1906
+ return null;
1907
+ }
1254
1908
  function auditCriticalHeuristic(input) {
1255
1909
  const vulnerabilities = input.split("\n").map((line) => line.trim()).filter(Boolean).map((line) => {
1256
1910
  if (!/\b(critical|high)\b/i.test(line)) {
@@ -1321,7 +1975,7 @@ function infraRiskHeuristic(input) {
1321
1975
  }
1322
1976
  return null;
1323
1977
  }
1324
- function applyHeuristicPolicy(policyName, input) {
1978
+ function applyHeuristicPolicy(policyName, input, detail) {
1325
1979
  if (!policyName) {
1326
1980
  return null;
1327
1981
  }
@@ -1331,6 +1985,9 @@ function applyHeuristicPolicy(policyName, input) {
1331
1985
  if (policyName === "infra-risk") {
1332
1986
  return infraRiskHeuristic(input);
1333
1987
  }
1988
+ if (policyName === "test-status") {
1989
+ return testStatusHeuristic(input, detail);
1990
+ }
1334
1991
  return null;
1335
1992
  }
1336
1993
 
@@ -1460,6 +2117,7 @@ function buildDryRunOutput(args) {
1460
2117
  },
1461
2118
  question: args.request.question,
1462
2119
  format: args.request.format,
2120
+ detail: args.request.detail ?? null,
1463
2121
  responseMode: args.responseMode,
1464
2122
  policy: args.request.policyName ?? null,
1465
2123
  heuristicOutput: args.heuristicOutput ?? null,
@@ -1479,35 +2137,42 @@ function buildDryRunOutput(args) {
1479
2137
  async function delay(ms) {
1480
2138
  await new Promise((resolve) => setTimeout(resolve, ms));
1481
2139
  }
2140
+ function withInsufficientHint(args) {
2141
+ if (!isInsufficientSignalOutput(args.output)) {
2142
+ return args.output;
2143
+ }
2144
+ return buildInsufficientSignalOutput({
2145
+ presetName: args.request.presetName,
2146
+ originalLength: args.prepared.meta.originalLength,
2147
+ truncatedApplied: args.prepared.meta.truncatedApplied
2148
+ });
2149
+ }
1482
2150
  async function generateWithRetry(args) {
1483
- let lastError;
1484
- for (let attempt = 0; attempt < 2; attempt += 1) {
1485
- try {
1486
- return await args.provider.generate({
1487
- model: args.request.config.provider.model,
1488
- prompt: args.prompt,
1489
- temperature: args.request.config.provider.temperature,
1490
- maxOutputTokens: args.request.config.provider.maxOutputTokens,
1491
- timeoutMs: args.request.config.provider.timeoutMs,
1492
- responseMode: args.responseMode,
1493
- jsonResponseFormat: args.request.config.provider.jsonResponseFormat
1494
- });
1495
- } catch (error) {
1496
- lastError = error;
1497
- const reason = error instanceof Error ? error.message : "unknown_error";
1498
- if (attempt > 0 || !isRetriableReason(reason)) {
1499
- throw error;
1500
- }
1501
- if (args.request.config.runtime.verbose) {
1502
- process.stderr.write(
1503
- `${pc.dim("sift")} retry=1 reason=${reason} delay_ms=${RETRY_DELAY_MS}
2151
+ const generate = () => args.provider.generate({
2152
+ model: args.request.config.provider.model,
2153
+ prompt: args.prompt,
2154
+ temperature: args.request.config.provider.temperature,
2155
+ maxOutputTokens: args.request.config.provider.maxOutputTokens,
2156
+ timeoutMs: args.request.config.provider.timeoutMs,
2157
+ responseMode: args.responseMode,
2158
+ jsonResponseFormat: args.request.config.provider.jsonResponseFormat
2159
+ });
2160
+ try {
2161
+ return await generate();
2162
+ } catch (error) {
2163
+ const reason = error instanceof Error ? error.message : "unknown_error";
2164
+ if (!isRetriableReason(reason)) {
2165
+ throw error;
2166
+ }
2167
+ if (args.request.config.runtime.verbose) {
2168
+ process.stderr.write(
2169
+ `${pc2.dim("sift")} retry=1 reason=${reason} delay_ms=${RETRY_DELAY_MS}
1504
2170
  `
1505
- );
1506
- }
1507
- await delay(RETRY_DELAY_MS);
2171
+ );
1508
2172
  }
2173
+ await delay(RETRY_DELAY_MS);
1509
2174
  }
1510
- throw lastError instanceof Error ? lastError : new Error("unknown_error");
2175
+ return generate();
1511
2176
  }
1512
2177
  async function runSift(request) {
1513
2178
  const prepared = prepareInput(request.stdin, request.config.input);
@@ -1515,23 +2180,25 @@ async function runSift(request) {
1515
2180
  question: request.question,
1516
2181
  format: request.format,
1517
2182
  input: prepared.truncated,
2183
+ detail: request.detail,
1518
2184
  policyName: request.policyName,
1519
2185
  outputContract: request.outputContract
1520
2186
  });
1521
2187
  const provider = createProvider(request.config);
1522
2188
  if (request.config.runtime.verbose) {
1523
2189
  process.stderr.write(
1524
- `${pc.dim("sift")} provider=${provider.name} model=${request.config.provider.model} base_url=${request.config.provider.baseUrl} input_chars=${prepared.meta.finalLength}
2190
+ `${pc2.dim("sift")} provider=${provider.name} model=${request.config.provider.model} base_url=${request.config.provider.baseUrl} input_chars=${prepared.meta.finalLength}
1525
2191
  `
1526
2192
  );
1527
2193
  }
1528
2194
  const heuristicOutput = applyHeuristicPolicy(
1529
2195
  request.policyName,
1530
- prepared.truncated
2196
+ prepared.truncated,
2197
+ request.detail
1531
2198
  );
1532
2199
  if (heuristicOutput) {
1533
2200
  if (request.config.runtime.verbose) {
1534
- process.stderr.write(`${pc.dim("sift")} heuristic=${request.policyName}
2201
+ process.stderr.write(`${pc2.dim("sift")} heuristic=${request.policyName}
1535
2202
  `);
1536
2203
  }
1537
2204
  if (request.dryRun) {
@@ -1544,7 +2211,11 @@ async function runSift(request) {
1544
2211
  heuristicOutput
1545
2212
  });
1546
2213
  }
1547
- return heuristicOutput;
2214
+ return withInsufficientHint({
2215
+ output: heuristicOutput,
2216
+ request,
2217
+ prepared
2218
+ });
1548
2219
  }
1549
2220
  if (request.dryRun) {
1550
2221
  return buildDryRunOutput({
@@ -1570,15 +2241,23 @@ async function runSift(request) {
1570
2241
  })) {
1571
2242
  throw new Error("Model output rejected by quality gate");
1572
2243
  }
1573
- return normalizeOutput(result.text, responseMode);
2244
+ return withInsufficientHint({
2245
+ output: normalizeOutput(result.text, responseMode),
2246
+ request,
2247
+ prepared
2248
+ });
1574
2249
  } catch (error) {
1575
2250
  const reason = error instanceof Error ? error.message : "unknown_error";
1576
- return buildFallbackOutput({
1577
- format: request.format,
1578
- reason,
1579
- rawInput: prepared.truncated,
1580
- rawFallback: request.config.runtime.rawFallback,
1581
- jsonFallback: request.fallbackJson
2251
+ return withInsufficientHint({
2252
+ output: buildFallbackOutput({
2253
+ format: request.format,
2254
+ reason,
2255
+ rawInput: prepared.truncated,
2256
+ rawFallback: request.config.runtime.rawFallback,
2257
+ jsonFallback: request.fallbackJson
2258
+ }),
2259
+ request,
2260
+ prepared
1582
2261
  });
1583
2262
  }
1584
2263
  }
@@ -1679,7 +2358,7 @@ async function runExec(request) {
1679
2358
  const shellPath = process.env.SHELL || "/bin/bash";
1680
2359
  if (request.config.runtime.verbose) {
1681
2360
  process.stderr.write(
1682
- `${pc2.dim("sift")} exec mode=${hasShellCommand ? "shell" : "argv"} command=${buildCommandPreview(request)}
2361
+ `${pc3.dim("sift")} exec mode=${hasShellCommand ? "shell" : "argv"} command=${buildCommandPreview(request)}
1683
2362
  `
1684
2363
  );
1685
2364
  }
@@ -1688,7 +2367,6 @@ async function runExec(request) {
1688
2367
  let bypassed = false;
1689
2368
  let childStatus = null;
1690
2369
  let childSignal = null;
1691
- let childSpawnError = null;
1692
2370
  const child = hasShellCommand ? spawn(shellPath, ["-lc", request.shellCommand], {
1693
2371
  stdio: ["inherit", "pipe", "pipe"]
1694
2372
  }) : spawn(request.command[0], request.command.slice(1), {
@@ -1707,7 +2385,7 @@ async function runExec(request) {
1707
2385
  }
1708
2386
  bypassed = true;
1709
2387
  if (request.config.runtime.verbose) {
1710
- process.stderr.write(`${pc2.dim("sift")} bypass=interactive-prompt
2388
+ process.stderr.write(`${pc3.dim("sift")} bypass=interactive-prompt
1711
2389
  `);
1712
2390
  }
1713
2391
  process.stderr.write(capture.render());
@@ -1716,7 +2394,6 @@ async function runExec(request) {
1716
2394
  child.stderr.on("data", handleChunk);
1717
2395
  await new Promise((resolve, reject) => {
1718
2396
  child.on("error", (error) => {
1719
- childSpawnError = error;
1720
2397
  reject(error);
1721
2398
  });
1722
2399
  child.on("close", (status, signal) => {
@@ -1730,18 +2407,21 @@ async function runExec(request) {
1730
2407
  }
1731
2408
  throw new Error("Failed to start child process.");
1732
2409
  });
1733
- if (childSpawnError) {
1734
- throw childSpawnError;
1735
- }
1736
2410
  const exitCode = normalizeChildExitCode(childStatus, childSignal);
1737
2411
  const capturedOutput = capture.render();
1738
2412
  if (request.config.runtime.verbose) {
1739
2413
  process.stderr.write(
1740
- `${pc2.dim("sift")} child_exit=${exitCode} captured_chars=${capture.getTotalChars()} capture_truncated=${capture.wasTruncated()}
2414
+ `${pc3.dim("sift")} child_exit=${exitCode} captured_chars=${capture.getTotalChars()} capture_truncated=${capture.wasTruncated()}
1741
2415
  `
1742
2416
  );
1743
2417
  }
1744
2418
  if (!bypassed) {
2419
+ if (request.showRaw && capturedOutput.length > 0) {
2420
+ process.stderr.write(capturedOutput);
2421
+ if (!capturedOutput.endsWith("\n")) {
2422
+ process.stderr.write("\n");
2423
+ }
2424
+ }
1745
2425
  const execSuccessShortcut = getExecSuccessShortcut({
1746
2426
  presetName: request.presetName,
1747
2427
  exitCode,
@@ -1750,7 +2430,7 @@ async function runExec(request) {
1750
2430
  if (execSuccessShortcut && !request.dryRun) {
1751
2431
  if (request.config.runtime.verbose) {
1752
2432
  process.stderr.write(
1753
- `${pc2.dim("sift")} exec_shortcut=${request.presetName}
2433
+ `${pc3.dim("sift")} exec_shortcut=${request.presetName}
1754
2434
  `
1755
2435
  );
1756
2436
  }
@@ -1758,10 +2438,18 @@ async function runExec(request) {
1758
2438
  `);
1759
2439
  return exitCode;
1760
2440
  }
1761
- const output = await runSift({
2441
+ let output = await runSift({
1762
2442
  ...request,
1763
2443
  stdin: capturedOutput
1764
2444
  });
2445
+ if (isInsufficientSignalOutput(output)) {
2446
+ output = buildInsufficientSignalOutput({
2447
+ presetName: request.presetName,
2448
+ originalLength: capture.getTotalChars(),
2449
+ truncatedApplied: capture.wasTruncated(),
2450
+ exitCode
2451
+ });
2452
+ }
1765
2453
  process.stdout.write(`${output}
1766
2454
  `);
1767
2455
  if (request.failOn && !request.dryRun && exitCode === 0 && supportsFailOnPreset(request.presetName) && evaluateGate({
@@ -1795,16 +2483,27 @@ function getPreset(config, name) {
1795
2483
  return preset;
1796
2484
  }
1797
2485
 
1798
- // src/cli.ts
2486
+ // src/cli-app.ts
1799
2487
  var require2 = createRequire(import.meta.url);
1800
2488
  var pkg = require2("../package.json");
1801
- var cli = cac("sift");
1802
- var HELP_BANNER = [
1803
- " \\\\ //",
1804
- " \\\\//",
1805
- " ||",
1806
- " o"
1807
- ].join("\n");
2489
+ var defaultCliDeps = {
2490
+ configInit,
2491
+ configSetup,
2492
+ configShow,
2493
+ configValidate,
2494
+ runDoctor,
2495
+ listPresets,
2496
+ showPreset,
2497
+ resolveConfig,
2498
+ findConfigPath,
2499
+ runExec,
2500
+ assertSupportedFailOnFormat,
2501
+ assertSupportedFailOnPreset,
2502
+ evaluateGate,
2503
+ readStdin,
2504
+ runSift,
2505
+ getPreset
2506
+ };
1808
2507
  function toNumber(value) {
1809
2508
  if (value === void 0 || value === null || value === "") {
1810
2509
  return void 0;
@@ -1849,44 +2548,32 @@ function applySharedOptions(command) {
1849
2548
  ).option(
1850
2549
  "--json-response-format <mode>",
1851
2550
  "JSON response format mode: auto | on | off"
1852
- ).option("--timeout-ms <ms>", "Request timeout in milliseconds").option("--format <format>", "brief | bullets | json | verdict").option("--max-capture-chars <n>", "Maximum raw child output chars kept in memory").option("--max-input-chars <n>", "Maximum input chars sent to the model").option("--head-chars <n>", "Head chars to preserve during truncation").option("--tail-chars <n>", "Tail chars to preserve during truncation").option("--strip-ansi", "Force ANSI stripping").option("--redact", "Enable standard redaction").option("--redact-strict", "Enable strict redaction").option("--raw-fallback", "Enable raw fallback text output").option("--dry-run", "Show the reduced input and prompt without calling the provider").option(
2551
+ ).option("--timeout-ms <ms>", "Request timeout in milliseconds").option("--format <format>", "brief | bullets | json | verdict").option(
2552
+ "--detail <mode>",
2553
+ "Detail level for supported presets: standard | focused | verbose"
2554
+ ).option("--max-capture-chars <n>", "Maximum raw child output chars kept in memory").option("--max-input-chars <n>", "Maximum input chars sent to the model").option("--head-chars <n>", "Head chars to preserve during truncation").option("--tail-chars <n>", "Tail chars to preserve during truncation").option("--strip-ansi", "Force ANSI stripping").option("--redact", "Enable standard redaction").option("--redact-strict", "Enable strict redaction").option("--raw-fallback", "Enable raw fallback text output").option("--dry-run", "Show the reduced input and prompt without calling the provider").option("--show-raw", "Print the captured raw input to stderr for debugging").option(
1853
2555
  "--fail-on",
1854
2556
  "Fail with exit code 1 when a supported built-in preset produces a blocking result"
1855
2557
  ).option("--config <path>", "Path to config file").option("--verbose", "Enable verbose stderr logging");
1856
2558
  }
1857
- async function executeRun(args) {
1858
- if (Boolean(args.options.failOn)) {
1859
- assertSupportedFailOnPreset(args.presetName);
1860
- assertSupportedFailOnFormat({
1861
- presetName: args.presetName,
1862
- format: args.format
1863
- });
2559
+ function normalizeDetail(value) {
2560
+ if (value === void 0 || value === null || value === "") {
2561
+ return void 0;
1864
2562
  }
1865
- const config = resolveConfig({
1866
- configPath: args.options.config,
1867
- env: process.env,
1868
- cliOverrides: buildCliOverrides(args.options)
1869
- });
1870
- const stdin = await readStdin();
1871
- const output = await runSift({
1872
- question: args.question,
1873
- format: args.format,
1874
- stdin,
1875
- config,
1876
- dryRun: Boolean(args.options.dryRun),
1877
- presetName: args.presetName,
1878
- policyName: args.policyName,
1879
- outputContract: args.outputContract,
1880
- fallbackJson: args.fallbackJson
1881
- });
1882
- process.stdout.write(`${output}
1883
- `);
1884
- if (Boolean(args.options.failOn) && !Boolean(args.options.dryRun) && args.presetName && evaluateGate({
1885
- presetName: args.presetName,
1886
- output
1887
- }).shouldFail) {
1888
- process.exitCode = 1;
2563
+ if (value === "standard" || value === "focused" || value === "verbose") {
2564
+ return value;
2565
+ }
2566
+ throw new Error("Invalid --detail value. Use standard, focused, or verbose.");
2567
+ }
2568
+ function resolveDetail(args) {
2569
+ const requested = normalizeDetail(args.options.detail);
2570
+ if (!requested) {
2571
+ return args.presetName === "test-status" ? "standard" : void 0;
2572
+ }
2573
+ if (args.presetName !== "test-status") {
2574
+ throw new Error("--detail is supported only with --preset test-status.");
1889
2575
  }
2576
+ return requested;
1890
2577
  }
1891
2578
  function extractExecCommand(options) {
1892
2579
  const passthrough = Array.isArray(options["--"]) ? options["--"].map((value) => String(value)) : [];
@@ -1902,180 +2589,278 @@ function extractExecCommand(options) {
1902
2589
  shellCommand
1903
2590
  };
1904
2591
  }
1905
- async function executeExec(args) {
1906
- if (Boolean(args.options.failOn)) {
1907
- assertSupportedFailOnPreset(args.presetName);
1908
- assertSupportedFailOnFormat({
1909
- presetName: args.presetName,
1910
- format: args.format
2592
+ function cleanHelpSectionBody(body, escapedVersion) {
2593
+ return body.replace(
2594
+ new RegExp(`(^|\\n)sift/${escapedVersion}\\n\\n?`, "g"),
2595
+ "\n"
2596
+ );
2597
+ }
2598
+ function createCliApp(args = {}) {
2599
+ const deps = {
2600
+ ...defaultCliDeps,
2601
+ ...args.deps
2602
+ };
2603
+ const env = args.env ?? process.env;
2604
+ const stdout = args.stdout ?? process.stdout;
2605
+ const stderr = args.stderr ?? process.stderr;
2606
+ const version = args.version ?? pkg.version;
2607
+ const cli = cac("sift");
2608
+ const ui = createPresentation(Boolean(stdout.isTTY));
2609
+ const escapedVersion = version.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2610
+ async function executeRun(input) {
2611
+ if (Boolean(input.options.failOn)) {
2612
+ deps.assertSupportedFailOnPreset(input.presetName);
2613
+ deps.assertSupportedFailOnFormat({
2614
+ presetName: input.presetName,
2615
+ format: input.format
2616
+ });
2617
+ }
2618
+ const config = deps.resolveConfig({
2619
+ configPath: input.options.config,
2620
+ env,
2621
+ cliOverrides: buildCliOverrides(input.options)
1911
2622
  });
2623
+ const stdin = await deps.readStdin();
2624
+ if (Boolean(input.options.showRaw) && stdin.length > 0) {
2625
+ stderr.write(stdin);
2626
+ if (!stdin.endsWith("\n")) {
2627
+ stderr.write("\n");
2628
+ }
2629
+ }
2630
+ const output = await deps.runSift({
2631
+ question: input.question,
2632
+ format: input.format,
2633
+ stdin,
2634
+ config,
2635
+ dryRun: Boolean(input.options.dryRun),
2636
+ showRaw: Boolean(input.options.showRaw),
2637
+ detail: input.detail,
2638
+ presetName: input.presetName,
2639
+ policyName: input.policyName,
2640
+ outputContract: input.outputContract,
2641
+ fallbackJson: input.fallbackJson
2642
+ });
2643
+ stdout.write(`${output}
2644
+ `);
2645
+ if (Boolean(input.options.failOn) && !Boolean(input.options.dryRun) && input.presetName && deps.evaluateGate({
2646
+ presetName: input.presetName,
2647
+ output
2648
+ }).shouldFail) {
2649
+ process.exitCode = 1;
2650
+ }
1912
2651
  }
1913
- const config = resolveConfig({
1914
- configPath: args.options.config,
1915
- env: process.env,
1916
- cliOverrides: buildCliOverrides(args.options)
1917
- });
1918
- const command = extractExecCommand(args.options);
1919
- process.exitCode = await runExec({
1920
- question: args.question,
1921
- format: args.format,
1922
- config,
1923
- dryRun: Boolean(args.options.dryRun),
1924
- failOn: Boolean(args.options.failOn),
1925
- presetName: args.presetName,
1926
- policyName: args.policyName,
1927
- outputContract: args.outputContract,
1928
- fallbackJson: args.fallbackJson,
1929
- ...command
1930
- });
1931
- }
1932
- applySharedOptions(
1933
- cli.command("preset <name>", "Run a named preset against piped CLI output")
1934
- ).usage("preset <name> [options]").example("preset test-status < test-output.txt").action(async (name, options) => {
1935
- const config = resolveConfig({
1936
- configPath: options.config,
1937
- env: process.env,
1938
- cliOverrides: buildCliOverrides(options)
1939
- });
1940
- const preset = getPreset(config, name);
1941
- await executeRun({
1942
- question: preset.question,
1943
- format: options.format ?? preset.format,
1944
- presetName: name,
1945
- policyName: options.format === void 0 || options.format === preset.format ? preset.policy : void 0,
1946
- options,
1947
- outputContract: preset.outputContract,
1948
- fallbackJson: preset.fallbackJson
1949
- });
1950
- });
1951
- applySharedOptions(
1952
- cli.command("exec [question]", "Run a command and reduce its output").allowUnknownOptions()
1953
- ).usage("exec [question] [options] -- <program> [args...]").example('exec "what changed?" -- git diff').example("exec --preset test-status -- pytest").example('exec --preset infra-risk --shell "terraform plan"').option("--shell <command>", "Execute a shell command string instead of argv mode").option("--preset <name>", "Run a named preset in exec mode").action(async (question, options) => {
1954
- if (question === "preset") {
1955
- throw new Error("Use 'sift exec --preset <name> -- <program> ...' instead.");
1956
- }
1957
- const presetName = typeof options.preset === "string" && options.preset.length > 0 ? options.preset : void 0;
1958
- if (presetName) {
1959
- if (question) {
1960
- throw new Error("Use either a freeform question or --preset <name>, not both.");
1961
- }
1962
- const preset = getPreset(
1963
- resolveConfig({
1964
- configPath: options.config,
1965
- env: process.env,
1966
- cliOverrides: buildCliOverrides(options)
1967
- }),
1968
- presetName
1969
- );
1970
- await executeExec({
2652
+ async function executeExec(input) {
2653
+ if (Boolean(input.options.failOn)) {
2654
+ deps.assertSupportedFailOnPreset(input.presetName);
2655
+ deps.assertSupportedFailOnFormat({
2656
+ presetName: input.presetName,
2657
+ format: input.format
2658
+ });
2659
+ }
2660
+ const config = deps.resolveConfig({
2661
+ configPath: input.options.config,
2662
+ env,
2663
+ cliOverrides: buildCliOverrides(input.options)
2664
+ });
2665
+ const command = extractExecCommand(input.options);
2666
+ process.exitCode = await deps.runExec({
2667
+ question: input.question,
2668
+ format: input.format,
2669
+ config,
2670
+ dryRun: Boolean(input.options.dryRun),
2671
+ failOn: Boolean(input.options.failOn),
2672
+ showRaw: Boolean(input.options.showRaw),
2673
+ detail: input.detail,
2674
+ presetName: input.presetName,
2675
+ policyName: input.policyName,
2676
+ outputContract: input.outputContract,
2677
+ fallbackJson: input.fallbackJson,
2678
+ ...command
2679
+ });
2680
+ }
2681
+ applySharedOptions(
2682
+ cli.command("preset <name>", "Run a named preset against piped output")
2683
+ ).usage("preset <name> [options]").example("preset test-status < test-output.txt").action(async (name, options) => {
2684
+ const config = deps.resolveConfig({
2685
+ configPath: options.config,
2686
+ env,
2687
+ cliOverrides: buildCliOverrides(options)
2688
+ });
2689
+ const preset = deps.getPreset(config, name);
2690
+ await executeRun({
1971
2691
  question: preset.question,
1972
2692
  format: options.format ?? preset.format,
1973
- presetName,
2693
+ presetName: name,
2694
+ detail: resolveDetail({
2695
+ presetName: name,
2696
+ options
2697
+ }),
1974
2698
  policyName: options.format === void 0 || options.format === preset.format ? preset.policy : void 0,
1975
2699
  options,
1976
2700
  outputContract: preset.outputContract,
1977
2701
  fallbackJson: preset.fallbackJson
1978
2702
  });
1979
- return;
1980
- }
1981
- if (!question) {
1982
- throw new Error("Missing question or preset.");
1983
- }
1984
- const format = options.format ?? "brief";
1985
- await executeExec({
1986
- question,
1987
- format,
1988
- options
1989
2703
  });
1990
- });
1991
- cli.command(
1992
- "config <action>",
1993
- "Config commands: setup | init | show | validate (show/validate use resolved runtime config)"
1994
- ).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(
1995
- "--global",
1996
- "Use the machine-wide config path (~/.config/sift/config.yaml) for init or setup"
1997
- ).option("--config <path>", "Path to config file").option("--show-secrets", "Show secret values in config show").action(async (action, options) => {
1998
- if (action === "setup") {
1999
- process.exitCode = await configSetup({
2000
- targetPath: options.path,
2001
- global: Boolean(options.global)
2704
+ applySharedOptions(
2705
+ cli.command("exec [question]", "Run a command and shrink its output for the model").allowUnknownOptions()
2706
+ ).usage("exec [question] [options] -- <program> [args...]").example('exec "what changed?" -- git diff').example("exec --preset test-status -- pytest").example('exec --preset infra-risk --shell "terraform plan"').option("--shell <command>", "Execute a shell command string instead of argv mode").option("--preset <name>", "Run a named preset in exec mode").action(async (question, options) => {
2707
+ if (question === "preset") {
2708
+ throw new Error("Use 'sift exec --preset <name> -- <program> ...' instead.");
2709
+ }
2710
+ const presetName = typeof options.preset === "string" && options.preset.length > 0 ? options.preset : void 0;
2711
+ if (presetName) {
2712
+ if (question) {
2713
+ throw new Error("Use either a freeform question or --preset <name>, not both.");
2714
+ }
2715
+ const preset = deps.getPreset(
2716
+ deps.resolveConfig({
2717
+ configPath: options.config,
2718
+ env,
2719
+ cliOverrides: buildCliOverrides(options)
2720
+ }),
2721
+ presetName
2722
+ );
2723
+ await executeExec({
2724
+ question: preset.question,
2725
+ format: options.format ?? preset.format,
2726
+ presetName,
2727
+ detail: resolveDetail({
2728
+ presetName,
2729
+ options
2730
+ }),
2731
+ policyName: options.format === void 0 || options.format === preset.format ? preset.policy : void 0,
2732
+ options,
2733
+ outputContract: preset.outputContract,
2734
+ fallbackJson: preset.fallbackJson
2735
+ });
2736
+ return;
2737
+ }
2738
+ if (!question) {
2739
+ throw new Error("Missing question or preset.");
2740
+ }
2741
+ const format = options.format ?? "brief";
2742
+ await executeExec({
2743
+ question,
2744
+ format,
2745
+ detail: resolveDetail({
2746
+ options
2747
+ }),
2748
+ options
2002
2749
  });
2003
- return;
2004
- }
2005
- if (action === "init") {
2006
- configInit(
2007
- options.path,
2008
- Boolean(options.global)
2009
- );
2010
- return;
2011
- }
2012
- if (action === "show") {
2013
- configShow(
2014
- options.config,
2015
- Boolean(options.showSecrets)
2016
- );
2017
- return;
2018
- }
2019
- if (action === "validate") {
2020
- configValidate(options.config);
2021
- return;
2022
- }
2023
- throw new Error(`Unknown config action: ${action}`);
2024
- });
2025
- cli.command("doctor", "Check local runtime config completeness").usage("doctor [options]").option("--config <path>", "Path to config file").action((options) => {
2026
- const configPath = findConfigPath(options.config);
2027
- const config = resolveConfig({
2028
- configPath: options.config,
2029
- env: process.env
2030
2750
  });
2031
- process.exitCode = runDoctor(config, configPath);
2032
- });
2033
- cli.command("presets <action> [name]", "Preset commands: list | show").usage("presets <list|show> [name] [options]").example("presets list").example("presets show infra-risk").option("--config <path>", "Path to config file").option("--internal", "Show internal preset fields in presets show").action((action, name, options) => {
2034
- const config = resolveConfig({
2035
- configPath: options.config,
2036
- env: process.env
2751
+ 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(
2752
+ "--global",
2753
+ "Use the machine-wide config path (~/.config/sift/config.yaml) for init or setup"
2754
+ ).option("--config <path>", "Path to config file").option("--show-secrets", "Show secret values in config show").action(async (action, options) => {
2755
+ if (action === "setup") {
2756
+ process.exitCode = await deps.configSetup({
2757
+ targetPath: options.path,
2758
+ global: Boolean(options.global)
2759
+ });
2760
+ return;
2761
+ }
2762
+ if (action === "init") {
2763
+ deps.configInit(options.path, Boolean(options.global));
2764
+ return;
2765
+ }
2766
+ if (action === "show") {
2767
+ deps.configShow(
2768
+ options.config,
2769
+ Boolean(options.showSecrets)
2770
+ );
2771
+ return;
2772
+ }
2773
+ if (action === "validate") {
2774
+ deps.configValidate(options.config);
2775
+ return;
2776
+ }
2777
+ throw new Error(`Unknown config action: ${action}`);
2037
2778
  });
2038
- if (action === "list") {
2039
- listPresets(config);
2040
- return;
2041
- }
2042
- if (action === "show") {
2043
- if (!name) {
2044
- throw new Error("Missing preset name.");
2779
+ 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) => {
2780
+ const configPath = deps.findConfigPath(options.config);
2781
+ const config = deps.resolveConfig({
2782
+ configPath: options.config,
2783
+ env
2784
+ });
2785
+ process.exitCode = deps.runDoctor(config, configPath);
2786
+ });
2787
+ cli.command("presets <action> [name]", "Preset commands: list | show").usage("presets <list|show> [name] [options]").example("presets list").example("presets show infra-risk").option("--config <path>", "Path to config file").option("--internal", "Show internal preset fields in presets show").action((action, name, options) => {
2788
+ const config = deps.resolveConfig({
2789
+ configPath: options.config,
2790
+ env
2791
+ });
2792
+ if (action === "list") {
2793
+ deps.listPresets(config);
2794
+ return;
2045
2795
  }
2046
- showPreset(config, name, Boolean(options.internal));
2047
- return;
2048
- }
2049
- throw new Error(`Unknown presets action: ${action}`);
2050
- });
2051
- applySharedOptions(
2052
- cli.command("[question]", "Ask a freeform question about piped CLI output")
2053
- ).action(async (question, options) => {
2054
- if (!question) {
2055
- throw new Error("Missing question.");
2056
- }
2057
- const format = options.format ?? "brief";
2058
- await executeRun({
2059
- question,
2060
- format,
2061
- options
2796
+ if (action === "show") {
2797
+ if (!name) {
2798
+ throw new Error("Missing preset name.");
2799
+ }
2800
+ deps.showPreset(config, name, Boolean(options.internal));
2801
+ return;
2802
+ }
2803
+ throw new Error(`Unknown presets action: ${action}`);
2062
2804
  });
2063
- });
2064
- cli.help((sections) => [
2065
- {
2066
- body: `${HELP_BANNER}
2805
+ applySharedOptions(cli.command("[question]", "Ask a question about piped output")).action(
2806
+ async (question, options) => {
2807
+ if (!question) {
2808
+ throw new Error("Missing question.");
2809
+ }
2810
+ const format = options.format ?? "brief";
2811
+ await executeRun({
2812
+ question,
2813
+ format,
2814
+ detail: resolveDetail({
2815
+ options
2816
+ }),
2817
+ options
2818
+ });
2819
+ }
2820
+ );
2821
+ cli.help((sections) => {
2822
+ const cleanedSections = sections.map((section) => ({
2823
+ ...section,
2824
+ body: cleanHelpSectionBody(section.body, escapedVersion)
2825
+ }));
2826
+ return [
2827
+ {
2828
+ body: `${ui.banner(version)}
2067
2829
  `
2068
- },
2069
- ...sections
2070
- ]);
2071
- cli.version(pkg.version);
2072
- async function main() {
2073
- cli.parse(process.argv, { run: false });
2830
+ },
2831
+ {
2832
+ title: ui.section("Quick start"),
2833
+ body: [
2834
+ ` ${ui.command("sift config setup")}`,
2835
+ ` ${ui.command("sift exec --preset test-status -- pytest")}`,
2836
+ ` ${ui.command("sift exec --preset test-status --show-raw -- pytest")}`,
2837
+ ` ${ui.command('sift exec "what changed?" -- git diff')}`
2838
+ ].join("\n")
2839
+ },
2840
+ ...cleanedSections
2841
+ ];
2842
+ });
2843
+ cli.version(version);
2844
+ return cli;
2845
+ }
2846
+ async function runCli(args = {}) {
2847
+ const cli = createCliApp(args);
2848
+ cli.parse(args.argv ?? process.argv, { run: false });
2074
2849
  await cli.runMatchedCommand();
2075
2850
  }
2076
- main().catch((error) => {
2851
+ function handleCliError(error, stderr = process.stderr) {
2077
2852
  const message = error instanceof Error ? error.message : "Unexpected error.";
2078
- process.stderr.write(`${message}
2853
+ if (stderr.isTTY) {
2854
+ stderr.write(`${createPresentation(true).error(message)}
2855
+ `);
2856
+ } else {
2857
+ stderr.write(`${message}
2079
2858
  `);
2859
+ }
2080
2860
  process.exitCode = 1;
2861
+ }
2862
+
2863
+ // src/cli.ts
2864
+ runCli().catch((error) => {
2865
+ handleCliError(error);
2081
2866
  });