@bilalimamoglu/sift 0.4.0 → 0.4.2

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 CHANGED
@@ -4,17 +4,19 @@
4
4
  [![license](https://img.shields.io/github/license/bilalimamoglu/sift)](LICENSE)
5
5
  [![CI](https://img.shields.io/github/actions/workflow/status/bilalimamoglu/sift/ci.yml?branch=main&label=CI)](https://github.com/bilalimamoglu/sift/actions/workflows/ci.yml)
6
6
 
7
- <img src="assets/brand/sift-logo-minimal-teal-default.svg" alt="sift logo" width="140" />
7
+ Turn 13,000 lines of test output into 2 root causes.
8
8
 
9
- Your AI agent should not be reading 13,000 lines of test output.
9
+ Your agent reads a diagnosis, not a log file.
10
10
 
11
- On the largest real fixture in the benchmark:
12
- **Before:** 128 failures, 198K raw-output tokens, agent reconstructs the failure shape from scratch.
13
- **After:** 6 lines, 129 `standard` tokens, agent acts on a grouped diagnosis immediately.
11
+ <p align="center">
12
+ <img src="assets/readme/test-status-demo.gif" alt="sift turning a pytest failure wall into a short diagnosis" width="960" />
13
+ </p>
14
14
 
15
- ```bash
16
- sift exec --preset test-status -- pytest -q
17
- ```
15
+ ## Before / After
16
+
17
+ 128 test failures. 13,000 lines of logs. The agent reads all of it.
18
+
19
+ With `sift`, it reads this instead:
18
20
 
19
21
  ```text
20
22
  - Tests did not pass.
@@ -28,13 +30,18 @@ sift exec --preset test-status -- pytest -q
28
30
  - Decision: stop and act.
29
31
  ```
30
32
 
31
- If 125 tests fail for one reason, the agent should pay for that reason once.
33
+ Same diagnosis. One run compressed from 198,000 tokens to 129.
32
34
 
33
- ## What it is
35
+ ## Not just tests
34
36
 
35
- Developers using coding agents Claude Code, Codex, Cursor, Windsurf, Copilot, or any LLM-driven workflow that runs shell commands and reads the output.
37
+ The same idea applies across noisy dev workflows:
36
38
 
37
- `sift` sits between the command and the agent. It captures noisy output, groups repeated failures into root-cause buckets, and returns a short diagnosis with an anchor, a likely fix, and a decision signal. The agent gets a map instead of a wall of text.
39
+ - **Type errors** grouped by error code, no model call
40
+ - **Lint output** → grouped by rule, no model call
41
+ - **Build failures** → first real error from webpack, esbuild/Vite, Cargo, Go, GCC/Clang
42
+ - **`npm audit`** → high/critical vulnerabilities only, no model call
43
+ - **`terraform plan`** → destructive risk detection, no model call
44
+ - **Diffs and logs** → compressed through a cheaper model before reaching your agent
38
45
 
39
46
  ## Install
40
47
 
@@ -44,255 +51,101 @@ npm install -g @bilalimamoglu/sift
44
51
 
45
52
  Requires Node.js 20+.
46
53
 
47
- ## Try it in 60 seconds
48
-
49
- If you already have an API key, you can try `sift` without any setup wizard:
54
+ ## Try it
50
55
 
51
56
  ```bash
52
- export OPENAI_API_KEY=your_openai_api_key
53
57
  sift exec --preset test-status -- pytest -q
58
+ sift exec --preset test-status -- npx vitest run
59
+ sift exec --preset test-status -- npx jest
54
60
  ```
55
61
 
56
- You can also use a freeform prompt for non-test output:
57
-
58
- ```bash
59
- sift exec "what changed?" -- git diff
60
- ```
61
-
62
- ## Set it up for daily use
63
-
64
- Guided setup writes a machine-wide config, verifies the provider, and makes the CLI easier to use day to day:
62
+ Other workflows:
65
63
 
66
64
  ```bash
67
- sift config setup
68
- sift doctor
69
- ```
70
-
71
- Config lives at `~/.config/sift/config.yaml`. A repo-local `sift.config.yaml` can override it later.
72
-
73
- If you want your coding agent to use `sift` automatically, install the managed instruction block too:
74
-
75
- ```bash
76
- sift agent install codex
77
- sift agent install claude
78
- ```
79
-
80
- Then run noisy commands through `sift`:
81
-
82
- ```bash
83
- sift exec --preset test-status -- <test command>
84
- sift exec "what changed?" -- git diff
65
+ sift exec --preset typecheck-summary -- npx tsc --noEmit
66
+ sift exec --preset lint-failures -- npx eslint src/
67
+ sift exec --preset build-failure -- npm run build
85
68
  sift exec --preset audit-critical -- npm audit
86
69
  sift exec --preset infra-risk -- terraform plan
70
+ sift exec "what changed?" -- git diff
87
71
  ```
88
72
 
89
- Useful flags:
90
- - `--dry-run` to preview the reduced input and prompt without calling a provider
91
- - `--show-raw` to print captured raw output to `stderr`
92
- - `--fail-on` to let reduced results fail CI for commands such as `npm audit` or `terraform plan`
93
-
94
- If you prefer environment variables instead of setup:
95
-
96
- ```bash
97
- # OpenAI
98
- export SIFT_PROVIDER=openai
99
- export SIFT_BASE_URL=https://api.openai.com/v1
100
- export SIFT_MODEL=gpt-5-nano
101
- export OPENAI_API_KEY=your_openai_api_key
102
-
103
- # OpenRouter
104
- export SIFT_PROVIDER=openrouter
105
- export OPENROUTER_API_KEY=your_openrouter_api_key
106
-
107
- # Any OpenAI-compatible endpoint
108
- export SIFT_PROVIDER=openai-compatible
109
- export SIFT_BASE_URL=https://your-endpoint/v1
110
- export SIFT_PROVIDER_API_KEY=your_api_key
111
- ```
112
-
113
- ## Why it helps
114
-
115
- The core abstraction is a **bucket**: one distinct root cause, no matter how many tests it affects.
116
-
117
- Instead of making an agent reason over 125 repeated tracebacks, `sift` compresses them into one actionable bucket with:
118
- - a label
119
- - an affected count
120
- - an anchor
121
- - a likely fix
122
- - a decision signal
123
-
124
- That changes the agent's job from "figure out what happened" to "act on the diagnosis."
125
-
126
73
  ## How it works
127
74
 
128
- `sift` follows a cheapest-first pipeline:
129
-
130
- 1. Capture command output.
131
- 2. Sanitize sensitive-looking material.
132
- 3. Apply local heuristics for known failure shapes.
133
- 4. Escalate to a cheaper provider only if needed.
134
- 5. Return a short diagnosis to the main agent.
75
+ `sift` sits between a noisy command and a coding agent.
135
76
 
136
- It also returns a decision signal:
137
- - `stop and act` when the diagnosis is already actionable
138
- - `zoom` when one deeper pass is justified
139
- - raw logs only as a last resort
77
+ 1. Capture output.
78
+ 2. Run local heuristics for known failure shapes.
79
+ 3. If heuristics are confident, return the diagnosis. No model call.
80
+ 4. If not, call a cheaper model — not your agent's.
140
81
 
141
- For recognized formats, local heuristics can fully handle the output and skip the provider entirely.
82
+ The agent gets the root cause, where it happens, and what to do next.
142
83
 
143
- The deepest local coverage today is test debugging, especially `pytest`, with growing support for `vitest` and `jest`. Other presets cover typecheck walls, lint failures, build errors, audit output, and Terraform risk detection.
84
+ So your agent spends tokens fixing, not reading.
144
85
 
145
86
  ## Built-in presets
146
87
 
147
- Every preset runs local heuristics first. When the heuristic confidently handles the output, the provider is never called.
148
-
149
- | Preset | Heuristic | What it does |
150
- |--------|-----------|-------------|
151
- | `test-status` | Deep | Bucket/anchor/decision system for pytest, vitest, jest. 30+ failure patterns, confidence-gated stop/zoom decisions. |
152
- | `typecheck-summary` | Deterministic | Parses `tsc` output (standard and pretty formats), groups by error code, returns max 5 bullets. |
153
- | `lint-failures` | Deterministic | Parses ESLint stylish output, groups by rule, distinguishes errors from warnings, detects fixable hints. |
154
- | `audit-critical` | Deterministic | Extracts high/critical vulnerabilities from `npm audit` or similar. |
155
- | `infra-risk` | Deterministic | Detects destructive signals in `terraform plan` output. Returns pass/fail verdict. |
156
- | `build-failure` | Deterministic-first | Extracts the first concrete build error for recognized webpack, esbuild/Vite, Cargo, Go, GCC/Clang, and `tsc --build` output; falls back to the provider for unsupported formats. |
157
- | `diff-summary` | Provider | Summarizes changes and risks in diff output. |
158
- | `log-errors` | Provider | Extracts top error signals from log output. |
159
-
160
- Presets marked **Deterministic** bypass the provider entirely for recognized output formats. Presets marked **Deterministic-first** try a local heuristic first and fall back to the provider only when the captured output is unsupported or ambiguous. Presets marked **Provider** always call the LLM but benefit from input sanitization and truncation.
88
+ Every preset runs local heuristics first. When the heuristic handles the output, the provider is never called.
161
89
 
162
- ```bash
163
- sift exec --preset typecheck-summary -- npx tsc --noEmit
164
- sift exec --preset lint-failures -- npx eslint src/
165
- sift exec --preset build-failure -- npm run build
166
- sift exec --preset audit-critical -- npm audit
167
- sift exec --preset infra-risk -- terraform plan
168
- ```
169
-
170
- On an interactive terminal, `sift` also shows a small stderr footer so humans can see whether the provider was skipped:
171
-
172
- ```text
173
- [sift: heuristic • LLM skipped • summary 47ms]
174
- [sift: provider • LLM used • 380 tokens • summary 1.2s]
175
- ```
90
+ | Preset | What it does |
91
+ |--------|-------------|
92
+ | `test-status` | Groups pytest, vitest, jest failures into root-cause buckets with anchors and fix suggestions. 30+ failure patterns. |
93
+ | `typecheck-summary` | Parses `tsc` output, groups by error code, returns max 5 bullets. No model call. |
94
+ | `lint-failures` | Parses ESLint output, groups by rule, detects fixable hints. No model call. |
95
+ | `build-failure` | Extracts first concrete error from webpack, esbuild/Vite, Cargo, Go, GCC/Clang, `tsc --build`. Falls back to model for unsupported formats. |
96
+ | `audit-critical` | Extracts high/critical vulnerabilities from `npm audit`. No model call. |
97
+ | `infra-risk` | Detects destructive signals in `terraform plan`. No model call. |
98
+ | `diff-summary` | Summarizes changes and risks in diff output. |
99
+ | `log-errors` | Extracts top error signals from log output. |
176
100
 
177
- Suppress the footer with `--quiet`:
178
-
179
- ```bash
180
- sift exec --preset typecheck-summary --quiet -- npx tsc --noEmit
181
- ```
101
+ ## Benchmark
182
102
 
183
- ## Strongest today
103
+ End-to-end debug loop on a real 640-test Python backend (125 repeated setup errors, 3 contract failures, 510 passing tests):
184
104
 
185
- `sift` is strongest when output is:
186
- - long
187
- - repetitive
188
- - triage-heavy
189
- - shaped by a small number of shared root causes
105
+ | Metric | Without sift | With sift | Reduction |
106
+ |--------|-------------|-----------|-----------|
107
+ | Tokens | 52,944 | 20,049 | 62% fewer |
108
+ | Tool calls | 40.8 | 12 | 71% fewer |
109
+ | Wall-clock time | 244s | 85s | 65% faster |
110
+ | Commands | 15.5 | 6 | 61% fewer |
111
+ | Diagnosis | Same | Same | — |
190
112
 
191
- Best fits today:
192
- - large `pytest`, `vitest`, or `jest` runs
193
- - `tsc` type errors and `eslint` lint failures
194
- - build failures from webpack, esbuild/Vite, Cargo, Go, GCC/Clang
195
- - `npm audit` and `terraform plan`
196
- - repeated CI blockers
197
- - noisy diffs and log streams
113
+ Methodology and caveats: [BENCHMARK_NOTES.md](BENCHMARK_NOTES.md)
198
114
 
199
115
  ## Test debugging workflow
200
116
 
201
- This is where `sift` is strongest today.
202
-
203
117
  Think of it like this:
204
118
  - `standard` = map
205
119
  - `focused` = zoom
206
120
  - raw traceback = last resort
207
121
 
208
- Typical loop:
209
-
210
122
  ```bash
211
123
  sift exec --preset test-status -- <test command>
212
124
  sift rerun
213
125
  sift rerun --remaining --detail focused
214
126
  ```
215
127
 
216
- If `standard` already gives you the root cause, anchor, and fix, stop there and act.
128
+ If `standard` already gives you the root cause, anchor, and fix stop and act.
217
129
 
218
- `sift rerun --remaining` narrows automatically for cached `pytest` runs.
130
+ `sift rerun --remaining` narrows automatically for cached `pytest` runs. For `vitest` and `jest`, it reruns the full command and keeps diagnosis focused on what still fails.
219
131
 
220
- For cached `vitest` and `jest` runs, it reruns the original full command and keeps the diagnosis focused on what still fails relative to the cached baseline.
132
+ ## Setup
221
133
 
222
- For other runners, rerun a narrowed command manually with `sift exec --preset test-status -- <narrowed command>`.
134
+ Guided setup writes a config, verifies the provider, and makes daily use easier:
223
135
 
224
136
  ```bash
225
- sift agent status
226
- sift agent show claude
227
- sift agent remove claude
228
- ```
229
-
230
- ## Where it helps less
231
-
232
- `sift` adds less value when:
233
- - the output is already short and obvious
234
- - the command is interactive or TUI-based
235
- - the exact raw log matters
236
- - the output does not expose enough evidence for reliable grouping
237
-
238
- When it cannot be confident, it tells you to zoom or read raw instead of pretending certainty.
239
-
240
- ## Benchmark
241
-
242
- On a real 640-test Python backend (125 repeated setup errors, 3 contract failures, 510 passing tests):
243
-
244
- | Metric | Raw agent | sift-first | Reduction |
245
- |--------|-----------|------------|-----------|
246
- | Tokens | 305K | 600 | 99.8% |
247
- | Tool calls | 16 | 7 | 56% |
248
- | Diagnosis | Same | Same | — |
249
-
250
- The table above is the single-fixture reduction story: the largest real test log in the benchmark shrank from `198026` raw tokens to `129` `standard` tokens.
251
-
252
- The end-to-end workflow benchmark is a different metric:
253
- - `62%` fewer total debugging tokens
254
- - `71%` fewer tool calls
255
- - `65%` faster wall-clock time
256
-
257
- Both matter. The table shows how aggressively `sift` can compress one large noisy run. The workflow numbers show how that compounds across a full debug loop.
258
-
259
- Methodology and caveats live in [BENCHMARK_NOTES.md](BENCHMARK_NOTES.md).
260
-
261
- ## Configuration
262
-
263
- Inspect and validate config with:
264
-
265
- ```bash
266
- sift config show
267
- sift config show --show-secrets
268
- sift config validate
137
+ sift config setup
138
+ sift doctor
269
139
  ```
270
140
 
271
- To switch between saved providers without editing files:
141
+ To wire `sift` into your coding agent automatically:
272
142
 
273
143
  ```bash
274
- sift config use openai
275
- sift config use openrouter
144
+ sift agent install claude
145
+ sift agent install codex
276
146
  ```
277
147
 
278
- Minimal YAML config:
279
-
280
- ```yaml
281
- provider:
282
- provider: openai
283
- model: gpt-5-nano
284
- baseUrl: https://api.openai.com/v1
285
- apiKey: YOUR_API_KEY
286
-
287
- input:
288
- stripAnsi: true
289
- redact: false
290
- maxCaptureChars: 400000
291
- maxInputChars: 60000
292
-
293
- runtime:
294
- rawFallback: true
295
- ```
148
+ Config details: [docs/cli-reference.md](docs/cli-reference.md)
296
149
 
297
150
  ## Docs
298
151
 
package/dist/cli.js CHANGED
@@ -7594,7 +7594,7 @@ function buildGenericRawSlice(args) {
7594
7594
 
7595
7595
  // src/core/run.ts
7596
7596
  var RETRY_DELAY_MS = 300;
7597
- var PROVIDER_PENDING_NOTICE_DELAY_MS = 150;
7597
+ var PENDING_NOTICE_DELAY_MS = 150;
7598
7598
  function estimateTokenCount(text) {
7599
7599
  return Math.max(1, Math.ceil(text.length / 4));
7600
7600
  }
@@ -7675,17 +7675,16 @@ function buildDryRunOutput(args) {
7675
7675
  async function delay(ms) {
7676
7676
  await new Promise((resolve) => setTimeout(resolve, ms));
7677
7677
  }
7678
- function startProviderPendingNotice() {
7679
- if (!process.stderr.isTTY) {
7678
+ function startPendingNotice(message, enabled) {
7679
+ if (!enabled) {
7680
7680
  return () => {
7681
7681
  };
7682
7682
  }
7683
- const message = "sift waiting for provider...";
7684
7683
  let shown = false;
7685
7684
  const timer = setTimeout(() => {
7686
7685
  shown = true;
7687
7686
  process.stderr.write(`${message}\r`);
7688
- }, PROVIDER_PENDING_NOTICE_DELAY_MS);
7687
+ }, PENDING_NOTICE_DELAY_MS);
7689
7688
  return () => {
7690
7689
  clearTimeout(timer);
7691
7690
  if (!shown) {
@@ -7715,7 +7714,10 @@ async function generateWithRetry(args) {
7715
7714
  responseMode: args.responseMode,
7716
7715
  jsonResponseFormat: args.request.config.provider.jsonResponseFormat
7717
7716
  });
7718
- const stopPendingNotice = startProviderPendingNotice();
7717
+ const stopPendingNotice = startPendingNotice(
7718
+ "sift waiting for provider...",
7719
+ Boolean(process.stderr.isTTY)
7720
+ );
7719
7721
  try {
7720
7722
  try {
7721
7723
  return await generate();
@@ -9281,6 +9283,10 @@ async function runExec(request) {
9281
9283
  cwd: commandCwd,
9282
9284
  stdio: ["inherit", "pipe", "pipe"]
9283
9285
  });
9286
+ const stopChildPendingNotice = startPendingNotice(
9287
+ "sift waiting for child command...",
9288
+ Boolean(process.stderr.isTTY) && !request.quiet
9289
+ );
9284
9290
  const handleChunk = (chunk) => {
9285
9291
  const text = Buffer.isBuffer(chunk) ? chunk.toString("utf8") : String(chunk);
9286
9292
  if (bypassed) {
@@ -9301,21 +9307,25 @@ async function runExec(request) {
9301
9307
  };
9302
9308
  child.stdout.on("data", handleChunk);
9303
9309
  child.stderr.on("data", handleChunk);
9304
- await new Promise((resolve, reject) => {
9305
- child.on("error", (error) => {
9306
- reject(error);
9307
- });
9308
- child.on("close", (status, signal) => {
9309
- childStatus = status;
9310
- childSignal = signal;
9311
- resolve();
9310
+ try {
9311
+ await new Promise((resolve, reject) => {
9312
+ child.on("error", (error) => {
9313
+ reject(error);
9314
+ });
9315
+ child.on("close", (status, signal) => {
9316
+ childStatus = status;
9317
+ childSignal = signal;
9318
+ resolve();
9319
+ });
9312
9320
  });
9313
- }).catch((error) => {
9321
+ } catch (error) {
9314
9322
  if (error instanceof Error) {
9315
9323
  throw error;
9316
9324
  }
9317
9325
  throw new Error("Failed to start child process.");
9318
- });
9326
+ } finally {
9327
+ stopChildPendingNotice();
9328
+ }
9319
9329
  const exitCode = normalizeChildExitCode(childStatus, childSignal);
9320
9330
  const capturedOutput = capture.render();
9321
9331
  const autoWatchDetected = !request.watch && looksLikeWatchStream(capturedOutput);
package/dist/index.d.ts CHANGED
@@ -162,6 +162,7 @@ interface RunResult {
162
162
  stats: RunStats | null;
163
163
  }
164
164
 
165
+ declare function startPendingNotice(message: string, enabled: boolean): () => void;
165
166
  declare function runSift(request: RunRequest): Promise<string>;
166
167
  declare function runSiftWithStats(request: RunRequest): Promise<RunResult>;
167
168
 
@@ -178,4 +179,4 @@ interface ResolveOptions {
178
179
  }
179
180
  declare function resolveConfig(options?: ResolveOptions): SiftConfig;
180
181
 
181
- export { BoundedCapture, type DetailLevel, type ExecRequest, type GenerateInput, type GenerateResult, type Goal, type InputConfig, type JsonResponseFormatMode, type LLMProvider, type NativeProviderName, type OutputFormat, type PartialSiftConfig, type PreparedInput, type PresetDefinition, type PromptPolicyName, type ProviderConfig, type ProviderName, type ProviderProfile, type ProviderProfiles, type RawSliceStrategy, type ResolveOptions, type ResponseMode, type RunRequest, type RuntimeConfig, type SiftConfig, type TestStatusRemainingMode, type UsageInfo, buildCommandPreview, getExecSuccessShortcut, looksInteractivePrompt, mergeDefined, normalizeChildExitCode, resolveConfig, runExec, runSift, runSiftWithStats };
182
+ export { BoundedCapture, type DetailLevel, type ExecRequest, type GenerateInput, type GenerateResult, type Goal, type InputConfig, type JsonResponseFormatMode, type LLMProvider, type NativeProviderName, type OutputFormat, type PartialSiftConfig, type PreparedInput, type PresetDefinition, type PromptPolicyName, type ProviderConfig, type ProviderName, type ProviderProfile, type ProviderProfiles, type RawSliceStrategy, type ResolveOptions, type ResponseMode, type RunRequest, type RuntimeConfig, type SiftConfig, type TestStatusRemainingMode, type UsageInfo, buildCommandPreview, getExecSuccessShortcut, looksInteractivePrompt, mergeDefined, normalizeChildExitCode, resolveConfig, runExec, runSift, runSiftWithStats, startPendingNotice };
package/dist/index.js CHANGED
@@ -5774,7 +5774,7 @@ function buildGenericRawSlice(args) {
5774
5774
 
5775
5775
  // src/core/run.ts
5776
5776
  var RETRY_DELAY_MS = 300;
5777
- var PROVIDER_PENDING_NOTICE_DELAY_MS = 150;
5777
+ var PENDING_NOTICE_DELAY_MS = 150;
5778
5778
  function estimateTokenCount(text) {
5779
5779
  return Math.max(1, Math.ceil(text.length / 4));
5780
5780
  }
@@ -5855,17 +5855,16 @@ function buildDryRunOutput(args) {
5855
5855
  async function delay(ms) {
5856
5856
  await new Promise((resolve) => setTimeout(resolve, ms));
5857
5857
  }
5858
- function startProviderPendingNotice() {
5859
- if (!process.stderr.isTTY) {
5858
+ function startPendingNotice(message, enabled) {
5859
+ if (!enabled) {
5860
5860
  return () => {
5861
5861
  };
5862
5862
  }
5863
- const message = "sift waiting for provider...";
5864
5863
  let shown = false;
5865
5864
  const timer = setTimeout(() => {
5866
5865
  shown = true;
5867
5866
  process.stderr.write(`${message}\r`);
5868
- }, PROVIDER_PENDING_NOTICE_DELAY_MS);
5867
+ }, PENDING_NOTICE_DELAY_MS);
5869
5868
  return () => {
5870
5869
  clearTimeout(timer);
5871
5870
  if (!shown) {
@@ -5895,7 +5894,10 @@ async function generateWithRetry(args) {
5895
5894
  responseMode: args.responseMode,
5896
5895
  jsonResponseFormat: args.request.config.provider.jsonResponseFormat
5897
5896
  });
5898
- const stopPendingNotice = startProviderPendingNotice();
5897
+ const stopPendingNotice = startPendingNotice(
5898
+ "sift waiting for provider...",
5899
+ Boolean(process.stderr.isTTY)
5900
+ );
5899
5901
  try {
5900
5902
  try {
5901
5903
  return await generate();
@@ -7293,6 +7295,10 @@ async function runExec(request) {
7293
7295
  cwd: commandCwd,
7294
7296
  stdio: ["inherit", "pipe", "pipe"]
7295
7297
  });
7298
+ const stopChildPendingNotice = startPendingNotice(
7299
+ "sift waiting for child command...",
7300
+ Boolean(process.stderr.isTTY) && !request.quiet
7301
+ );
7296
7302
  const handleChunk = (chunk) => {
7297
7303
  const text = Buffer.isBuffer(chunk) ? chunk.toString("utf8") : String(chunk);
7298
7304
  if (bypassed) {
@@ -7313,21 +7319,25 @@ async function runExec(request) {
7313
7319
  };
7314
7320
  child.stdout.on("data", handleChunk);
7315
7321
  child.stderr.on("data", handleChunk);
7316
- await new Promise((resolve, reject) => {
7317
- child.on("error", (error) => {
7318
- reject(error);
7319
- });
7320
- child.on("close", (status, signal) => {
7321
- childStatus = status;
7322
- childSignal = signal;
7323
- resolve();
7322
+ try {
7323
+ await new Promise((resolve, reject) => {
7324
+ child.on("error", (error) => {
7325
+ reject(error);
7326
+ });
7327
+ child.on("close", (status, signal) => {
7328
+ childStatus = status;
7329
+ childSignal = signal;
7330
+ resolve();
7331
+ });
7324
7332
  });
7325
- }).catch((error) => {
7333
+ } catch (error) {
7326
7334
  if (error instanceof Error) {
7327
7335
  throw error;
7328
7336
  }
7329
7337
  throw new Error("Failed to start child process.");
7330
- });
7338
+ } finally {
7339
+ stopChildPendingNotice();
7340
+ }
7331
7341
  const exitCode = normalizeChildExitCode(childStatus, childSignal);
7332
7342
  const capturedOutput = capture.render();
7333
7343
  const autoWatchDetected = !request.watch && looksLikeWatchStream(capturedOutput);
@@ -7853,5 +7863,6 @@ export {
7853
7863
  resolveConfig,
7854
7864
  runExec,
7855
7865
  runSift,
7856
- runSiftWithStats
7866
+ runSiftWithStats,
7867
+ startPendingNotice
7857
7868
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bilalimamoglu/sift",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "Agent-first command-output reduction layer for agents, CI, and automation.",
5
5
  "type": "module",
6
6
  "bin": {