ブランチ名生成戦略を設定可能に
デフォルトをローマ字化(高速)に変更し、AI生成が必要な場合は config.yaml で branchNameStrategy: ai を設定可能にした。これによりブランチ名生成の待ち時間を削減し、LLMコストも削減できる。 また、coder エージェントに「根本原因修正後の安全機構迂回は禁止」ルールを追加した。
This commit is contained in:
parent
7c928e0385
commit
163561a5b3
@ -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
|
||||
|
||||
|
||||
@ -24,6 +24,7 @@
|
||||
- 設計判断を勝手にする → 報告して判断を仰ぐ
|
||||
- レビュワーの指摘を軽視する → 禁止(あなたの認識が間違っている)
|
||||
- **後方互換・Legacy対応を勝手に追加する → 絶対禁止(フォールバック、古いAPI維持、移行期コードなど、明示的な指示がない限り不要)**
|
||||
- **根本原因を修正した上で安全機構を迂回するワークアラウンドを重ねる → 禁止(例: パス解決を直したのに `.gitignore` を無視する `git add -f` も追加する。根本修正が正しいなら追加の迂回は不要。安全機構は理由があって存在する)**
|
||||
|
||||
## 最重要ルール
|
||||
|
||||
|
||||
@ -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-]+$/);
|
||||
});
|
||||
});
|
||||
|
||||
@ -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 */
|
||||
|
||||
@ -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 */
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user