diff --git a/src/cli.ts b/src/cli.ts index c31e504..09ba96c 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -66,6 +66,9 @@ let resolvedCwd = ''; /** Whether pipeline mode is active (--task specified, set in preAction) */ let pipelineMode = false; +/** Whether quiet mode is active (--quiet flag or config, set in preAction) */ +let quietMode = false; + export interface WorktreeConfirmationResult { execCwd: string; isWorktree: boolean; @@ -263,7 +266,8 @@ program .option('-t, --task ', 'Task content (as alternative to GitHub issue)') .option('--pipeline', 'Pipeline mode: non-interactive, no worktree, direct branch creation') .option('--skip-git', 'Skip branch creation, commit, and push (pipeline mode)') - .option('--create-worktree ', 'Skip the worktree prompt by explicitly specifying yes or no'); + .option('--create-worktree ', 'Skip the worktree prompt by explicitly specifying yes or no') + .option('-q, --quiet', 'Minimal output mode: suppress AI output (for CI)'); // Common initialization for all commands program.hook('preAction', async () => { @@ -285,17 +289,27 @@ program.hook('preAction', async () => { initDebugLogger(debugConfig, resolvedCwd); + // Load config once for both log level and quiet mode + const config = loadGlobalConfig(); + if (verbose) { setVerboseConsole(true); setLogLevel('debug'); } else { - const config = loadGlobalConfig(); setLogLevel(config.logLevel); } - log.info('TAKT CLI starting', { version: cliVersion, cwd: resolvedCwd, verbose, pipelineMode }); + // Quiet mode: CLI flag takes precedence over config + quietMode = rootOpts.quiet === true || config.minimalOutput === true; + + log.info('TAKT CLI starting', { version: cliVersion, cwd: resolvedCwd, verbose, pipelineMode, quietMode }); }); +/** Get whether quiet mode is active (CLI flag or config, resolved in preAction) */ +export function isQuietMode(): boolean { + return quietMode; +} + // --- Subcommands --- program diff --git a/src/commands/interactive.ts b/src/commands/interactive.ts index 6396ac4..610c46c 100644 --- a/src/commands/interactive.ts +++ b/src/commands/interactive.ts @@ -13,6 +13,7 @@ import * as readline from 'node:readline'; import chalk from 'chalk'; import { loadGlobalConfig } from '../config/globalConfig.js'; +import { isQuietMode } from '../cli.js'; import { loadAgentSessions, updateAgentSession } from '../config/paths.js'; import { getProvider, type ProviderType } from '../providers/index.js'; import { createLogger } from '../utils/debug.js'; @@ -151,14 +152,14 @@ export async function interactiveMode(cwd: string, initialInput?: string): Promi /** Call AI with automatic retry on session error (stale/invalid session ID). */ async function callAIWithRetry(prompt: string): Promise { - const display = new StreamDisplay('assistant'); + const display = new StreamDisplay('assistant', isQuietMode()); try { const result = await callAI(provider, prompt, cwd, model, sessionId, display); // If session failed, clear it and retry without session if (!result.success && sessionId) { log.info('Session invalid, retrying without session'); sessionId = undefined; - const retryDisplay = new StreamDisplay('assistant'); + const retryDisplay = new StreamDisplay('assistant', isQuietMode()); const retry = await callAI(provider, prompt, cwd, model, undefined, retryDisplay); if (retry.sessionId) { sessionId = retry.sessionId; diff --git a/src/commands/workflowExecution.ts b/src/commands/workflowExecution.ts index 3c718b9..6ca2dc7 100644 --- a/src/commands/workflowExecution.ts +++ b/src/commands/workflowExecution.ts @@ -14,6 +14,7 @@ import { updateWorktreeSession, } from '../config/paths.js'; import { loadGlobalConfig } from '../config/globalConfig.js'; +import { isQuietMode } from '../cli.js'; import { header, info, @@ -200,7 +201,8 @@ export async function executeWorkflow( log.debug('Step instruction', instruction); } - displayRef.current = new StreamDisplay(step.agentDisplayName); + // Use quiet mode from CLI (already resolved CLI flag + config in preAction) + displayRef.current = new StreamDisplay(step.agentDisplayName, isQuietMode()); // Write step_start record to NDJSON log const record: NdjsonStepStart = { diff --git a/src/config/globalConfig.ts b/src/config/globalConfig.ts index 83386aa..c672dfd 100644 --- a/src/config/globalConfig.ts +++ b/src/config/globalConfig.ts @@ -52,6 +52,7 @@ export function loadGlobalConfig(): GlobalConfig { commitMessageTemplate: parsed.pipeline.commit_message_template, prBodyTemplate: parsed.pipeline.pr_body_template, } : undefined, + minimalOutput: parsed.minimal_output, }; } @@ -95,6 +96,9 @@ export function saveGlobalConfig(config: GlobalConfig): void { raw.pipeline = pipelineRaw; } } + if (config.minimalOutput !== undefined) { + raw.minimal_output = config.minimalOutput; + } writeFileSync(configPath, stringifyYaml(raw), 'utf-8'); } diff --git a/src/models/schemas.ts b/src/models/schemas.ts index 1272032..4532da3 100644 --- a/src/models/schemas.ts +++ b/src/models/schemas.ts @@ -185,6 +185,8 @@ export const GlobalConfigSchema = z.object({ openai_api_key: z.string().optional(), /** Pipeline execution settings */ pipeline: PipelineConfigSchema.optional(), + /** Minimal output mode for CI - suppress AI output to prevent sensitive information leaks */ + minimal_output: z.boolean().optional().default(false), }); /** Project config schema */ diff --git a/src/models/types.ts b/src/models/types.ts index 13f7ef0..da890ae 100644 --- a/src/models/types.ts +++ b/src/models/types.ts @@ -207,6 +207,8 @@ export interface GlobalConfig { openaiApiKey?: string; /** Pipeline execution settings */ pipeline?: PipelineConfig; + /** Minimal output mode for CI - suppress AI output to prevent sensitive information leaks */ + minimalOutput?: boolean; } /** Project-level configuration */ diff --git a/src/utils/ui.ts b/src/utils/ui.ts index 9d83232..ff59a66 100644 --- a/src/utils/ui.ts +++ b/src/utils/ui.ts @@ -166,10 +166,14 @@ export class StreamDisplay { private spinnerFrames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']; private spinnerFrame = 0; - constructor(private agentName = 'Claude') {} + constructor( + private agentName = 'Claude', + private quiet = false, + ) {} /** Display initialization event */ showInit(model: string): void { + if (this.quiet) return; console.log(chalk.gray(`[${this.agentName}] Model: ${model}`)); } @@ -202,6 +206,8 @@ export class StreamDisplay { /** Display tool use event */ showToolUse(tool: string, input: Record): void { + if (this.quiet) return; + // Clear any buffered text first this.flushText(); @@ -216,6 +222,7 @@ export class StreamDisplay { /** Display tool output streaming */ showToolOutput(output: string, tool?: string): void { + if (this.quiet) return; if (!output) return; this.stopToolSpinner(); this.flushThinking(); @@ -238,9 +245,22 @@ export class StreamDisplay { /** Display tool result event */ showToolResult(content: string, isError: boolean): void { - // Stop the spinner first + // Stop the spinner first (always, even in quiet mode to prevent spinner artifacts) this.stopToolSpinner(); + if (this.quiet) { + // In quiet mode: show errors but suppress success messages + if (isError) { + const toolName = this.lastToolUse || 'Tool'; + const errorContent = content || 'Unknown error'; + console.log(chalk.red(` ✗ ${toolName}:`), chalk.red(truncate(errorContent, 70))); + } + this.lastToolUse = null; + this.currentToolInputPreview = null; + this.toolOutputPrinted = false; + return; + } + if (this.toolOutputBuffer) { this.printToolOutputLines([this.toolOutputBuffer], this.lastToolUse ?? undefined); this.toolOutputBuffer = ''; @@ -264,6 +284,8 @@ export class StreamDisplay { /** Display streaming thinking (Claude's internal reasoning) */ showThinking(thinking: string): void { + if (this.quiet) return; + // Stop spinner if running this.stopToolSpinner(); // Flush any regular text first @@ -292,6 +314,8 @@ export class StreamDisplay { /** Display streaming text (accumulated) */ showText(text: string): void { + if (this.quiet) return; + // Stop spinner if running this.stopToolSpinner(); // Flush any thinking first