@@ -35,6 +35,7 @@ export interface AskOptions extends CliOverrides {
3535 file ?: string ;
3636 json ?: boolean ;
3737 noColor ?: boolean ;
38+ quiet ?: boolean ;
3839 system ?: string ;
3940 /** Enable read-only tool use so the model can discover and read files.
4041 * Defaults to true when stdout is a TTY and the provider supports chatWithTools. */
@@ -62,6 +63,7 @@ async function renderFinalAnswer(
6263 options : AskOptions ,
6364 profile : { model ?: string } ,
6465 noColor : boolean ,
66+ stderrLog : ( msg : string ) => void ,
6567) : Promise < void > {
6668 if ( options . json ) {
6769 printJsonResult ( { response : text , usage, model : profile . model } ) ;
@@ -76,7 +78,7 @@ async function renderFinalAnswer(
7678
7779 if ( usage && ! options . json ) {
7880 const u = usage ;
79- process . stderr . write ( `\n${ formatUsage ( u . promptTokens , u . completionTokens , u . totalTokens , noColor ) } \n` ) ;
81+ stderrLog ( `\n${ formatUsage ( u . promptTokens , u . completionTokens , u . totalTokens , noColor ) } \n` ) ;
8082 }
8183}
8284
@@ -113,6 +115,9 @@ export async function runAsk(inlinePrompt: string | undefined, options: AskOptio
113115 chalk . default . level = 0 ;
114116 }
115117
118+ // Quiet mode: suppress all non-essential stderr output
119+ const stderrLog = options . quiet ? ( _msg : string ) => { } : ( msg : string ) => process . stderr . write ( msg ) ;
120+
116121 // Load config with CLI overrides
117122 const cliOverrides : CliOverrides = {
118123 profile : options . profile ,
@@ -164,7 +169,7 @@ export async function runAsk(inlinePrompt: string | undefined, options: AskOptio
164169 } catch { /* non-fatal */ }
165170
166171 // ── Planning phase: deep reasoning about what to search for ─────────
167- process . stderr . write ( formatSeparator ( 'Planning' , noColor ) ) ;
172+ stderrLog ( formatSeparator ( 'Planning' , noColor ) ) ;
168173 const projectCtxForPlan = [ jamContext ?? workspaceCtx , symbolHint , pastContext ] . filter ( Boolean ) . join ( '\n\n' ) ;
169174 const searchPlan = await generateSearchPlan ( adapter , prompt , projectCtxForPlan , {
170175 model : profile . model ,
@@ -173,9 +178,9 @@ export async function runAsk(inlinePrompt: string | undefined, options: AskOptio
173178 } ) ;
174179
175180 if ( searchPlan ) {
176- process . stderr . write ( formatPlanBlock ( searchPlan , noColor ) + '\n' ) ;
181+ stderrLog ( formatPlanBlock ( searchPlan , noColor ) + '\n' ) ;
177182 } else {
178- process . stderr . write ( formatInternalStatus ( 'planning skipped — using generic search strategy' , noColor ) + '\n' ) ;
183+ stderrLog ( formatInternalStatus ( 'planning skipped — using generic search strategy' , noColor ) + '\n' ) ;
179184 }
180185
181186 // Enrich the user's prompt with the search plan
@@ -185,12 +190,12 @@ export async function runAsk(inlinePrompt: string | undefined, options: AskOptio
185190 const MAX_TOOL_ROUNDS = 15 ;
186191 let synthesisInjected = false ;
187192
188- process . stderr . write ( formatSeparator ( 'Searching codebase' , noColor ) ) ;
193+ stderrLog ( formatSeparator ( 'Searching codebase' , noColor ) ) ;
189194
190195 for ( let round = 0 ; round < MAX_TOOL_ROUNDS ; round ++ ) {
191196 // ── Context window management: compact if approaching limit ───────
192197 if ( memory . shouldCompact ( messages ) ) {
193- process . stderr . write ( formatInternalStatus ( 'Compacting context…' , noColor ) + '\n' ) ;
198+ stderrLog ( formatInternalStatus ( 'Compacting context…' , noColor ) + '\n' ) ;
194199 messages = await memory . compact ( messages ) ;
195200 }
196201
@@ -211,25 +216,25 @@ export async function runAsk(inlinePrompt: string | undefined, options: AskOptio
211216
212217 // If the model already produced an answer, run critic evaluation
213218 if ( finalText . trim ( ) . length > 0 ) {
214- process . stderr . write ( formatInternalStatus ( 'Evaluating answer quality…' , noColor ) + '\n' ) ;
219+ stderrLog ( formatInternalStatus ( 'Evaluating answer quality…' , noColor ) + '\n' ) ;
215220 const verdict = await criticEvaluate ( adapter , prompt , finalText , { model : profile . model } ) ;
216221 if ( verdict . pass ) {
217- process . stderr . write ( formatSeparator ( 'Answer' , noColor ) ) ;
218- await renderFinalAnswer ( finalText , response . usage , options , profile , noColor ) ;
222+ stderrLog ( formatSeparator ( 'Answer' , noColor ) ) ;
223+ await renderFinalAnswer ( finalText , response . usage , options , profile , noColor , stderrLog ) ;
219224 // Auto-update JAM.md with usage patterns
220225 const log = memory . getAccessLog ( ) ;
221226 updateContextWithUsage ( workspaceRoot , log . readFiles , log . searchQueries ) . catch ( ( ) => { } ) ;
222227 return ;
223228 }
224229 // Critic rejected — use its specific feedback
225- process . stderr . write ( formatInternalStatus ( `Critic: ${ verdict . reason } ` , noColor ) + '\n' ) ;
230+ stderrLog ( formatInternalStatus ( `Critic: ${ verdict . reason } ` , noColor ) + '\n' ) ;
226231 messages . push ( { role : 'assistant' , content : finalText } ) ;
227232 messages . push ( { role : 'user' , content : buildCriticCorrection ( verdict , prompt ) } ) ;
228233 continue ;
229234 }
230235
231236 // No answer yet — inject synthesis reminder
232- process . stderr . write ( formatInternalStatus ( 'Grounding answer to your question…' , noColor ) + '\n' ) ;
237+ stderrLog ( formatInternalStatus ( 'Grounding answer to your question…' , noColor ) + '\n' ) ;
233238 messages . push ( { role : 'assistant' , content : finalText } ) ;
234239 messages . push ( { role : 'user' , content : buildSynthesisReminder ( prompt ) } ) ;
235240 continue ;
@@ -239,15 +244,15 @@ export async function runAsk(inlinePrompt: string | undefined, options: AskOptio
239244 if ( tracker . totalCalls > 0 && finalText . trim ( ) . length > 30 && round < MAX_TOOL_ROUNDS - 2 ) {
240245 const verdict = await criticEvaluate ( adapter , prompt , finalText , { model : profile . model } ) ;
241246 if ( ! verdict . pass ) {
242- process . stderr . write ( formatInternalStatus ( `Critic: ${ verdict . reason } ` , noColor ) + '\n' ) ;
247+ stderrLog ( formatInternalStatus ( `Critic: ${ verdict . reason } ` , noColor ) + '\n' ) ;
243248 messages . push ( { role : 'assistant' , content : finalText } ) ;
244249 messages . push ( { role : 'user' , content : buildCriticCorrection ( verdict , prompt ) } ) ;
245250 continue ;
246251 }
247252 }
248253
249- process . stderr . write ( formatSeparator ( 'Answer' , noColor ) ) ;
250- await renderFinalAnswer ( finalText , response . usage , options , profile , noColor ) ;
254+ stderrLog ( formatSeparator ( 'Answer' , noColor ) ) ;
255+ await renderFinalAnswer ( finalText , response . usage , options , profile , noColor , stderrLog ) ;
251256 // Auto-update JAM.md with usage patterns
252257 const log = memory . getAccessLog ( ) ;
253258 updateContextWithUsage ( workspaceRoot , log . readFiles , log . searchQueries ) . catch ( ( ) => { } ) ;
@@ -260,7 +265,7 @@ export async function runAsk(inlinePrompt: string | undefined, options: AskOptio
260265 for ( const tc of response . toolCalls ) {
261266 // Duplicate detection — skip and inject guidance
262267 if ( tracker . isDuplicate ( tc . name , tc . arguments ) ) {
263- process . stderr . write ( formatDuplicateSkip ( tc . name , noColor ) + '\n' ) ;
268+ stderrLog ( formatDuplicateSkip ( tc . name , noColor ) + '\n' ) ;
264269 messages . push ( {
265270 role : 'user' ,
266271 content : `[Tool result: ${ tc . name } ]\nYou already made this exact call. The result was the same as before. Try a DIFFERENT search query or tool.` ,
@@ -272,14 +277,14 @@ export async function runAsk(inlinePrompt: string | undefined, options: AskOptio
272277 // Check cache first
273278 const cached = cache . get ( tc . name , tc . arguments ) ;
274279 if ( cached !== null ) {
275- process . stderr . write ( formatToolCall ( tc . name , tc . arguments , noColor ) + ' (cached)\n' ) ;
280+ stderrLog ( formatToolCall ( tc . name , tc . arguments , noColor ) + ' (cached)\n' ) ;
276281 const capped = memory . processToolResult ( tc . name , tc . arguments , cached ) ;
277282 messages . push ( { role : 'user' , content : `[Tool result: ${ tc . name } ]\n${ capped } ` } ) ;
278283 tracker . record ( tc . name , tc . arguments , false ) ;
279284 continue ;
280285 }
281286
282- process . stderr . write ( formatToolCall ( tc . name , tc . arguments , noColor ) + '\n' ) ;
287+ stderrLog ( formatToolCall ( tc . name , tc . arguments , noColor ) + '\n' ) ;
283288
284289 let toolOutput : string ;
285290 let wasError = false ;
@@ -296,28 +301,28 @@ export async function runAsk(inlinePrompt: string | undefined, options: AskOptio
296301 // Cap the output before injecting into messages
297302 const cappedOutput = memory . processToolResult ( tc . name , tc . arguments , toolOutput ) ;
298303
299- process . stderr . write ( formatToolResult ( cappedOutput , noColor ) + '\n' ) ;
304+ stderrLog ( formatToolResult ( cappedOutput , noColor ) + '\n' ) ;
300305 tracker . record ( tc . name , tc . arguments , wasError ) ;
301306
302307 messages . push ( { role : 'user' , content : `[Tool result: ${ tc . name } ]\n${ cappedOutput } ` } ) ;
303308 }
304309
305310 // ── Scratchpad: periodic working memory checkpoint ─────────────────
306311 if ( memory . shouldScratchpad ( round ) ) {
307- process . stderr . write ( formatInternalStatus ( 'Working memory checkpoint…' , noColor ) + '\n' ) ;
312+ stderrLog ( formatInternalStatus ( 'Working memory checkpoint…' , noColor ) + '\n' ) ;
308313 messages . push ( memory . scratchpadPrompt ( ) ) ;
309314 }
310315
311316 // ── Inject correction hints if stuck ──────────────────────────────────
312317 const hint = tracker . getCorrectionHint ( ) ;
313318 if ( hint ) {
314- process . stderr . write ( formatHintInjection ( noColor ) + '\n' ) ;
319+ stderrLog ( formatHintInjection ( noColor ) + '\n' ) ;
315320 messages . push ( { role : 'user' , content : hint } ) ;
316321 }
317322 }
318323
319324 // Exceeded round limit — fall through to streaming
320- process . stderr . write ( formatSeparator ( 'Max tool rounds reached, generating answer' , noColor ) ) ;
325+ stderrLog ( formatSeparator ( 'Max tool rounds reached, generating answer' , noColor ) ) ;
321326 }
322327
323328 // ── Standard streaming response ───────────────────────────────────────────
0 commit comments