ブランチ名生成戦略を設定可能に

デフォルトをローマ字化(高速)に変更し、AI生成が必要な場合は config.yaml で branchNameStrategy: ai を設定可能にした。これによりブランチ名生成の待ち時間を削減し、LLMコストも削減できる。

また、coder エージェントに「根本原因修正後の安全機構迂回は禁止」ルールを追加した。
This commit is contained in:
nrslib 2026-02-06 16:30:45 +09:00
parent 7c928e0385
commit 163561a5b3
8 changed files with 128 additions and 26 deletions

View File

@ -24,6 +24,7 @@ You are the implementer. **Focus on implementation, not design decisions.**
- Making design decisions arbitrarily → Report and ask for guidance
- Dismissing reviewer feedback → Prohibited (your understanding is wrong)
- **Adding backward compatibility or legacy support without being asked → Absolutely prohibited (fallbacks, old API maintenance, migration code, etc. are unnecessary unless explicitly instructed)**
- **Layering workarounds that bypass safety mechanisms on top of a root cause fix → Prohibited (e.g., fixing path resolution AND adding `git add -f` to override `.gitignore`. If the root fix is correct, the bypass is unnecessary. Safety mechanisms exist for a reason)**
## Most Important Rule

View File

@ -24,6 +24,7 @@
- 設計判断を勝手にする → 報告して判断を仰ぐ
- レビュワーの指摘を軽視する → 禁止(あなたの認識が間違っている)
- **後方互換・Legacy対応を勝手に追加する → 絶対禁止フォールバック、古いAPI維持、移行期コードなど、明示的な指示がない限り不要**
- **根本原因を修正した上で安全機構を迂回するワークアラウンドを重ねる → 禁止(例: パス解決を直したのに `.gitignore` を無視する `git add -f` も追加する。根本修正が正しいなら追加の迂回は不要。安全機構は理由があって存在する)**
## 最重要ルール

View File

@ -44,6 +44,7 @@ beforeEach(() => {
logLevel: 'info',
provider: 'claude',
model: undefined,
branchNameStrategy: 'ai',
});
});
@ -162,13 +163,14 @@ describe('summarizeTaskName', () => {
});
it('should use provider from config.yaml', async () => {
// Given: config has codex provider
// Given: config has codex provider with branchNameStrategy: 'ai'
mockLoadGlobalConfig.mockReturnValue({
language: 'ja',
defaultPiece: 'default',
logLevel: 'info',
provider: 'codex',
model: 'gpt-4',
branchNameStrategy: 'ai',
});
mockProviderCall.mockResolvedValue({
agent: 'summarizer',
@ -252,19 +254,110 @@ describe('summarizeTaskName', () => {
expect(result).not.toMatch(/^-|-$/); // No leading/trailing hyphens
});
it('should use LLM by default', async () => {
// Given
it('should use romaji by default', async () => {
// Given: branchNameStrategy is not set (undefined)
mockLoadGlobalConfig.mockReturnValue({
language: 'ja',
defaultPiece: 'default',
logLevel: 'info',
provider: 'claude',
model: undefined,
branchNameStrategy: undefined,
});
// When: useLLM not specified, branchNameStrategy not set
const result = await summarizeTaskName('test task', { cwd: '/project' });
// Then: should NOT call provider, should return romaji
expect(mockProviderCall).not.toHaveBeenCalled();
expect(result).toMatch(/^[a-z0-9-]+$/);
});
it('should use AI when branchNameStrategy is ai', async () => {
// Given: branchNameStrategy is 'ai'
mockLoadGlobalConfig.mockReturnValue({
language: 'ja',
defaultPiece: 'default',
logLevel: 'info',
provider: 'claude',
model: undefined,
branchNameStrategy: 'ai',
});
mockProviderCall.mockResolvedValue({
agent: 'summarizer',
status: 'done',
content: 'add-auth',
content: 'ai-generated-slug',
timestamp: new Date(),
});
// When: useLLM not specified (defaults to true)
await summarizeTaskName('test', { cwd: '/project' });
// When: useLLM not specified, branchNameStrategy is 'ai'
const result = await summarizeTaskName('test task', { cwd: '/project' });
// Then: should call provider
expect(mockProviderCall).toHaveBeenCalled();
expect(result).toBe('ai-generated-slug');
});
it('should use romaji when branchNameStrategy is romaji', async () => {
// Given: branchNameStrategy is 'romaji'
mockLoadGlobalConfig.mockReturnValue({
language: 'ja',
defaultPiece: 'default',
logLevel: 'info',
provider: 'claude',
model: undefined,
branchNameStrategy: 'romaji',
});
// When
const result = await summarizeTaskName('test task', { cwd: '/project' });
// Then: should NOT call provider
expect(mockProviderCall).not.toHaveBeenCalled();
expect(result).toMatch(/^[a-z0-9-]+$/);
});
it('should respect explicit useLLM option over config', async () => {
// Given: branchNameStrategy is 'romaji' but useLLM is explicitly true
mockLoadGlobalConfig.mockReturnValue({
language: 'ja',
defaultPiece: 'default',
logLevel: 'info',
provider: 'claude',
model: undefined,
branchNameStrategy: 'romaji',
});
mockProviderCall.mockResolvedValue({
agent: 'summarizer',
status: 'done',
content: 'explicit-ai-slug',
timestamp: new Date(),
});
// When: useLLM is explicitly true
const result = await summarizeTaskName('test task', { cwd: '/project', useLLM: true });
// Then: should call provider (explicit option overrides config)
expect(mockProviderCall).toHaveBeenCalled();
expect(result).toBe('explicit-ai-slug');
});
it('should respect explicit useLLM false over config with ai strategy', async () => {
// Given: branchNameStrategy is 'ai' but useLLM is explicitly false
mockLoadGlobalConfig.mockReturnValue({
language: 'ja',
defaultPiece: 'default',
logLevel: 'info',
provider: 'claude',
model: undefined,
branchNameStrategy: 'ai',
});
// When: useLLM is explicitly false
const result = await summarizeTaskName('test task', { cwd: '/project', useLLM: false });
// Then: should NOT call provider (explicit option overrides config)
expect(mockProviderCall).not.toHaveBeenCalled();
expect(result).toMatch(/^[a-z0-9-]+$/);
});
});

View File

@ -59,6 +59,8 @@ export interface GlobalConfig {
bookmarksFile?: string;
/** Path to piece categories file (default: ~/.takt/preferences/piece-categories.yaml) */
pieceCategoriesFile?: string;
/** Branch name generation strategy: 'romaji' (fast, default) or 'ai' (slow) */
branchNameStrategy?: 'romaji' | 'ai';
}
/** Project-level configuration */

View File

@ -268,6 +268,8 @@ export const GlobalConfigSchema = z.object({
bookmarks_file: z.string().optional(),
/** Path to piece categories file (default: ~/.takt/preferences/piece-categories.yaml) */
piece_categories_file: z.string().optional(),
/** Branch name generation strategy: 'romaji' (fast, default) or 'ai' (slow) */
branch_name_strategy: z.enum(['romaji', 'ai']).optional(),
});
/** Project config schema */

View File

@ -87,6 +87,7 @@ export class GlobalConfigManager {
minimalOutput: parsed.minimal_output,
bookmarksFile: parsed.bookmarks_file,
pieceCategoriesFile: parsed.piece_categories_file,
branchNameStrategy: parsed.branch_name_strategy,
};
this.cachedConfig = config;
return config;
@ -143,6 +144,9 @@ export class GlobalConfigManager {
if (config.pieceCategoriesFile) {
raw.piece_categories_file = config.pieceCategoriesFile;
}
if (config.branchNameStrategy) {
raw.branch_name_strategy = config.branchNameStrategy;
}
writeFileSync(configPath, stringifyYaml(raw), 'utf-8');
this.invalidateCache();
}

View File

@ -53,7 +53,8 @@ export class TaskSummarizer {
taskName: string,
options: SummarizeOptions,
): Promise<string> {
const useLLM = options.useLLM ?? true;
const globalConfig = loadGlobalConfig();
const useLLM = options.useLLM ?? (globalConfig.branchNameStrategy === 'ai');
log.info('Summarizing task name', { taskName, useLLM });
if (!useLLM) {
@ -61,8 +62,6 @@ export class TaskSummarizer {
log.info('Task name romanized', { original: taskName, slug });
return slug || 'task';
}
const globalConfig = loadGlobalConfig();
const providerType = (globalConfig.provider as ProviderType) ?? 'claude';
const model = options.model ?? globalConfig.model;

View File

@ -63,7 +63,7 @@ export interface SummarizeOptions {
cwd: string;
/** Model to use (optional, defaults to config or haiku) */
model?: string;
/** Use LLM for summarization (default: true). If false, uses romanization. */
/** Use LLM for summarization. Defaults to config.branchNameStrategy === 'ai'. If false, uses romanization. */
useLLM?: boolean;
}