From 163561a5b3c1f113c08a462dc3f3f19ff62165dc Mon Sep 17 00:00:00 2001 From: nrslib <38722970+nrslib@users.noreply.github.com> Date: Fri, 6 Feb 2026 16:30:45 +0900 Subject: [PATCH] =?UTF-8?q?=E3=83=96=E3=83=A9=E3=83=B3=E3=83=81=E5=90=8D?= =?UTF-8?q?=E7=94=9F=E6=88=90=E6=88=A6=E7=95=A5=E3=82=92=E8=A8=AD=E5=AE=9A?= =?UTF-8?q?=E5=8F=AF=E8=83=BD=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit デフォルトをローマ字化(高速)に変更し、AI生成が必要な場合は config.yaml で branchNameStrategy: ai を設定可能にした。これによりブランチ名生成の待ち時間を削減し、LLMコストも削減できる。 また、coder エージェントに「根本原因修正後の安全機構迂回は禁止」ルールを追加した。 --- resources/global/en/agents/default/coder.md | 1 + resources/global/ja/agents/default/coder.md | 1 + src/__tests__/summarize.test.ts | 137 ++++++++++++++++---- src/core/models/global-config.ts | 2 + src/core/models/schemas.ts | 2 + src/infra/config/global/globalConfig.ts | 4 + src/infra/task/summarize.ts | 5 +- src/infra/task/types.ts | 2 +- 8 files changed, 128 insertions(+), 26 deletions(-) diff --git a/resources/global/en/agents/default/coder.md b/resources/global/en/agents/default/coder.md index b530686..6a1c287 100644 --- a/resources/global/en/agents/default/coder.md +++ b/resources/global/en/agents/default/coder.md @@ -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 diff --git a/resources/global/ja/agents/default/coder.md b/resources/global/ja/agents/default/coder.md index 35214bb..15f29e0 100644 --- a/resources/global/ja/agents/default/coder.md +++ b/resources/global/ja/agents/default/coder.md @@ -24,6 +24,7 @@ - 設計判断を勝手にする → 報告して判断を仰ぐ - レビュワーの指摘を軽視する → 禁止(あなたの認識が間違っている) - **後方互換・Legacy対応を勝手に追加する → 絶対禁止(フォールバック、古いAPI維持、移行期コードなど、明示的な指示がない限り不要)** +- **根本原因を修正した上で安全機構を迂回するワークアラウンドを重ねる → 禁止(例: パス解決を直したのに `.gitignore` を無視する `git add -f` も追加する。根本修正が正しいなら追加の迂回は不要。安全機構は理由があって存在する)** ## 最重要ルール diff --git a/src/__tests__/summarize.test.ts b/src/__tests__/summarize.test.ts index 2da9779..216741e 100644 --- a/src/__tests__/summarize.test.ts +++ b/src/__tests__/summarize.test.ts @@ -38,13 +38,14 @@ const mockProvider = { beforeEach(() => { vi.clearAllMocks(); mockGetProvider.mockReturnValue(mockProvider); - mockLoadGlobalConfig.mockReturnValue({ - language: 'ja', - defaultPiece: 'default', - logLevel: 'info', - provider: 'claude', - model: undefined, - }); + mockLoadGlobalConfig.mockReturnValue({ + language: 'ja', + defaultPiece: 'default', + logLevel: 'info', + provider: 'claude', + model: undefined, + branchNameStrategy: 'ai', + }); }); describe('summarizeTaskName', () => { @@ -63,15 +64,15 @@ describe('summarizeTaskName', () => { // Then expect(result).toBe('add-auth'); expect(mockGetProvider).toHaveBeenCalledWith('claude'); - expect(mockProviderCall).toHaveBeenCalledWith( - 'summarizer', - 'long task name for testing', - expect.objectContaining({ - cwd: '/project', - allowedTools: [], - }) - ); - }); + expect(mockProviderCall).toHaveBeenCalledWith( + 'summarizer', + 'long task name for testing', + expect.objectContaining({ + cwd: '/project', + allowedTools: [], + }) + ); + }); it('should return AI-generated slug for English task name', async () => { // Given @@ -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-]+$/); }); }); diff --git a/src/core/models/global-config.ts b/src/core/models/global-config.ts index c6b9cd8..e0c49d5 100644 --- a/src/core/models/global-config.ts +++ b/src/core/models/global-config.ts @@ -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 */ diff --git a/src/core/models/schemas.ts b/src/core/models/schemas.ts index 58db65b..8000ca9 100644 --- a/src/core/models/schemas.ts +++ b/src/core/models/schemas.ts @@ -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 */ diff --git a/src/infra/config/global/globalConfig.ts b/src/infra/config/global/globalConfig.ts index dbb8ccc..5fff55d 100644 --- a/src/infra/config/global/globalConfig.ts +++ b/src/infra/config/global/globalConfig.ts @@ -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(); } diff --git a/src/infra/task/summarize.ts b/src/infra/task/summarize.ts index 15c5215..9a7dd6e 100644 --- a/src/infra/task/summarize.ts +++ b/src/infra/task/summarize.ts @@ -53,7 +53,8 @@ export class TaskSummarizer { taskName: string, options: SummarizeOptions, ): Promise { - 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; diff --git a/src/infra/task/types.ts b/src/infra/task/types.ts index f142a02..523413a 100644 --- a/src/infra/task/types.ts +++ b/src/infra/task/types.ts @@ -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; }