Merge pull request #80 from nrslib/takt/issue-70-1769924108

CI用にログ出力最小モードを導入する
This commit is contained in:
nrs 2026-02-01 17:47:21 +09:00 committed by GitHub
commit 29841f0c51
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 57 additions and 8 deletions

View File

@ -66,6 +66,9 @@ let resolvedCwd = '';
/** Whether pipeline mode is active (--task specified, set in preAction) */ /** Whether pipeline mode is active (--task specified, set in preAction) */
let pipelineMode = false; let pipelineMode = false;
/** Whether quiet mode is active (--quiet flag or config, set in preAction) */
let quietMode = false;
export interface WorktreeConfirmationResult { export interface WorktreeConfirmationResult {
execCwd: string; execCwd: string;
isWorktree: boolean; isWorktree: boolean;
@ -263,7 +266,8 @@ program
.option('-t, --task <string>', 'Task content (as alternative to GitHub issue)') .option('-t, --task <string>', 'Task content (as alternative to GitHub issue)')
.option('--pipeline', 'Pipeline mode: non-interactive, no worktree, direct branch creation') .option('--pipeline', 'Pipeline mode: non-interactive, no worktree, direct branch creation')
.option('--skip-git', 'Skip branch creation, commit, and push (pipeline mode)') .option('--skip-git', 'Skip branch creation, commit, and push (pipeline mode)')
.option('--create-worktree <yes|no>', 'Skip the worktree prompt by explicitly specifying yes or no'); .option('--create-worktree <yes|no>', '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 // Common initialization for all commands
program.hook('preAction', async () => { program.hook('preAction', async () => {
@ -285,17 +289,27 @@ program.hook('preAction', async () => {
initDebugLogger(debugConfig, resolvedCwd); initDebugLogger(debugConfig, resolvedCwd);
// Load config once for both log level and quiet mode
const config = loadGlobalConfig();
if (verbose) { if (verbose) {
setVerboseConsole(true); setVerboseConsole(true);
setLogLevel('debug'); setLogLevel('debug');
} else { } else {
const config = loadGlobalConfig();
setLogLevel(config.logLevel); 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 --- // --- Subcommands ---
program program

View File

@ -13,6 +13,7 @@
import * as readline from 'node:readline'; import * as readline from 'node:readline';
import chalk from 'chalk'; import chalk from 'chalk';
import { loadGlobalConfig } from '../config/globalConfig.js'; import { loadGlobalConfig } from '../config/globalConfig.js';
import { isQuietMode } from '../cli.js';
import { loadAgentSessions, updateAgentSession } from '../config/paths.js'; import { loadAgentSessions, updateAgentSession } from '../config/paths.js';
import { getProvider, type ProviderType } from '../providers/index.js'; import { getProvider, type ProviderType } from '../providers/index.js';
import { createLogger } from '../utils/debug.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). */ /** Call AI with automatic retry on session error (stale/invalid session ID). */
async function callAIWithRetry(prompt: string): Promise<CallAIResult | null> { async function callAIWithRetry(prompt: string): Promise<CallAIResult | null> {
const display = new StreamDisplay('assistant'); const display = new StreamDisplay('assistant', isQuietMode());
try { try {
const result = await callAI(provider, prompt, cwd, model, sessionId, display); const result = await callAI(provider, prompt, cwd, model, sessionId, display);
// If session failed, clear it and retry without session // If session failed, clear it and retry without session
if (!result.success && sessionId) { if (!result.success && sessionId) {
log.info('Session invalid, retrying without session'); log.info('Session invalid, retrying without session');
sessionId = undefined; sessionId = undefined;
const retryDisplay = new StreamDisplay('assistant'); const retryDisplay = new StreamDisplay('assistant', isQuietMode());
const retry = await callAI(provider, prompt, cwd, model, undefined, retryDisplay); const retry = await callAI(provider, prompt, cwd, model, undefined, retryDisplay);
if (retry.sessionId) { if (retry.sessionId) {
sessionId = retry.sessionId; sessionId = retry.sessionId;

View File

@ -14,6 +14,7 @@ import {
updateWorktreeSession, updateWorktreeSession,
} from '../config/paths.js'; } from '../config/paths.js';
import { loadGlobalConfig } from '../config/globalConfig.js'; import { loadGlobalConfig } from '../config/globalConfig.js';
import { isQuietMode } from '../cli.js';
import { import {
header, header,
info, info,
@ -200,7 +201,8 @@ export async function executeWorkflow(
log.debug('Step instruction', instruction); 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 // Write step_start record to NDJSON log
const record: NdjsonStepStart = { const record: NdjsonStepStart = {

View File

@ -52,6 +52,7 @@ export function loadGlobalConfig(): GlobalConfig {
commitMessageTemplate: parsed.pipeline.commit_message_template, commitMessageTemplate: parsed.pipeline.commit_message_template,
prBodyTemplate: parsed.pipeline.pr_body_template, prBodyTemplate: parsed.pipeline.pr_body_template,
} : undefined, } : undefined,
minimalOutput: parsed.minimal_output,
}; };
} }
@ -95,6 +96,9 @@ export function saveGlobalConfig(config: GlobalConfig): void {
raw.pipeline = pipelineRaw; raw.pipeline = pipelineRaw;
} }
} }
if (config.minimalOutput !== undefined) {
raw.minimal_output = config.minimalOutput;
}
writeFileSync(configPath, stringifyYaml(raw), 'utf-8'); writeFileSync(configPath, stringifyYaml(raw), 'utf-8');
} }

View File

@ -185,6 +185,8 @@ export const GlobalConfigSchema = z.object({
openai_api_key: z.string().optional(), openai_api_key: z.string().optional(),
/** Pipeline execution settings */ /** Pipeline execution settings */
pipeline: PipelineConfigSchema.optional(), 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 */ /** Project config schema */

View File

@ -207,6 +207,8 @@ export interface GlobalConfig {
openaiApiKey?: string; openaiApiKey?: string;
/** Pipeline execution settings */ /** Pipeline execution settings */
pipeline?: PipelineConfig; pipeline?: PipelineConfig;
/** Minimal output mode for CI - suppress AI output to prevent sensitive information leaks */
minimalOutput?: boolean;
} }
/** Project-level configuration */ /** Project-level configuration */

View File

@ -166,10 +166,14 @@ export class StreamDisplay {
private spinnerFrames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']; private spinnerFrames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
private spinnerFrame = 0; private spinnerFrame = 0;
constructor(private agentName = 'Claude') {} constructor(
private agentName = 'Claude',
private quiet = false,
) {}
/** Display initialization event */ /** Display initialization event */
showInit(model: string): void { showInit(model: string): void {
if (this.quiet) return;
console.log(chalk.gray(`[${this.agentName}] Model: ${model}`)); console.log(chalk.gray(`[${this.agentName}] Model: ${model}`));
} }
@ -202,6 +206,8 @@ export class StreamDisplay {
/** Display tool use event */ /** Display tool use event */
showToolUse(tool: string, input: Record<string, unknown>): void { showToolUse(tool: string, input: Record<string, unknown>): void {
if (this.quiet) return;
// Clear any buffered text first // Clear any buffered text first
this.flushText(); this.flushText();
@ -216,6 +222,7 @@ export class StreamDisplay {
/** Display tool output streaming */ /** Display tool output streaming */
showToolOutput(output: string, tool?: string): void { showToolOutput(output: string, tool?: string): void {
if (this.quiet) return;
if (!output) return; if (!output) return;
this.stopToolSpinner(); this.stopToolSpinner();
this.flushThinking(); this.flushThinking();
@ -238,9 +245,22 @@ export class StreamDisplay {
/** Display tool result event */ /** Display tool result event */
showToolResult(content: string, isError: boolean): void { 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(); 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) { if (this.toolOutputBuffer) {
this.printToolOutputLines([this.toolOutputBuffer], this.lastToolUse ?? undefined); this.printToolOutputLines([this.toolOutputBuffer], this.lastToolUse ?? undefined);
this.toolOutputBuffer = ''; this.toolOutputBuffer = '';
@ -264,6 +284,8 @@ export class StreamDisplay {
/** Display streaming thinking (Claude's internal reasoning) */ /** Display streaming thinking (Claude's internal reasoning) */
showThinking(thinking: string): void { showThinking(thinking: string): void {
if (this.quiet) return;
// Stop spinner if running // Stop spinner if running
this.stopToolSpinner(); this.stopToolSpinner();
// Flush any regular text first // Flush any regular text first
@ -292,6 +314,8 @@ export class StreamDisplay {
/** Display streaming text (accumulated) */ /** Display streaming text (accumulated) */
showText(text: string): void { showText(text: string): void {
if (this.quiet) return;
// Stop spinner if running // Stop spinner if running
this.stopToolSpinner(); this.stopToolSpinner();
// Flush any thinking first // Flush any thinking first