ブランチ名生成戦略を設定可能に
デフォルトをローマ字化(高速)に変更し、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
|
- Making design decisions arbitrarily → Report and ask for guidance
|
||||||
- Dismissing reviewer feedback → Prohibited (your understanding is wrong)
|
- 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)**
|
- **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
|
## Most Important Rule
|
||||||
|
|
||||||
|
|||||||
@ -24,6 +24,7 @@
|
|||||||
- 設計判断を勝手にする → 報告して判断を仰ぐ
|
- 設計判断を勝手にする → 報告して判断を仰ぐ
|
||||||
- レビュワーの指摘を軽視する → 禁止(あなたの認識が間違っている)
|
- レビュワーの指摘を軽視する → 禁止(あなたの認識が間違っている)
|
||||||
- **後方互換・Legacy対応を勝手に追加する → 絶対禁止(フォールバック、古いAPI維持、移行期コードなど、明示的な指示がない限り不要)**
|
- **後方互換・Legacy対応を勝手に追加する → 絶対禁止(フォールバック、古いAPI維持、移行期コードなど、明示的な指示がない限り不要)**
|
||||||
|
- **根本原因を修正した上で安全機構を迂回するワークアラウンドを重ねる → 禁止(例: パス解決を直したのに `.gitignore` を無視する `git add -f` も追加する。根本修正が正しいなら追加の迂回は不要。安全機構は理由があって存在する)**
|
||||||
|
|
||||||
## 最重要ルール
|
## 最重要ルール
|
||||||
|
|
||||||
|
|||||||
@ -44,6 +44,7 @@ beforeEach(() => {
|
|||||||
logLevel: 'info',
|
logLevel: 'info',
|
||||||
provider: 'claude',
|
provider: 'claude',
|
||||||
model: undefined,
|
model: undefined,
|
||||||
|
branchNameStrategy: 'ai',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -162,13 +163,14 @@ describe('summarizeTaskName', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should use provider from config.yaml', async () => {
|
it('should use provider from config.yaml', async () => {
|
||||||
// Given: config has codex provider
|
// Given: config has codex provider with branchNameStrategy: 'ai'
|
||||||
mockLoadGlobalConfig.mockReturnValue({
|
mockLoadGlobalConfig.mockReturnValue({
|
||||||
language: 'ja',
|
language: 'ja',
|
||||||
defaultPiece: 'default',
|
defaultPiece: 'default',
|
||||||
logLevel: 'info',
|
logLevel: 'info',
|
||||||
provider: 'codex',
|
provider: 'codex',
|
||||||
model: 'gpt-4',
|
model: 'gpt-4',
|
||||||
|
branchNameStrategy: 'ai',
|
||||||
});
|
});
|
||||||
mockProviderCall.mockResolvedValue({
|
mockProviderCall.mockResolvedValue({
|
||||||
agent: 'summarizer',
|
agent: 'summarizer',
|
||||||
@ -252,19 +254,110 @@ describe('summarizeTaskName', () => {
|
|||||||
expect(result).not.toMatch(/^-|-$/); // No leading/trailing hyphens
|
expect(result).not.toMatch(/^-|-$/); // No leading/trailing hyphens
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should use LLM by default', async () => {
|
it('should use romaji by default', async () => {
|
||||||
// Given
|
// 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({
|
mockProviderCall.mockResolvedValue({
|
||||||
agent: 'summarizer',
|
agent: 'summarizer',
|
||||||
status: 'done',
|
status: 'done',
|
||||||
content: 'add-auth',
|
content: 'ai-generated-slug',
|
||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// When: useLLM not specified (defaults to true)
|
// When: useLLM not specified, branchNameStrategy is 'ai'
|
||||||
await summarizeTaskName('test', { cwd: '/project' });
|
const result = await summarizeTaskName('test task', { cwd: '/project' });
|
||||||
|
|
||||||
// Then: should call provider
|
// Then: should call provider
|
||||||
expect(mockProviderCall).toHaveBeenCalled();
|
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;
|
bookmarksFile?: string;
|
||||||
/** Path to piece categories file (default: ~/.takt/preferences/piece-categories.yaml) */
|
/** Path to piece categories file (default: ~/.takt/preferences/piece-categories.yaml) */
|
||||||
pieceCategoriesFile?: string;
|
pieceCategoriesFile?: string;
|
||||||
|
/** Branch name generation strategy: 'romaji' (fast, default) or 'ai' (slow) */
|
||||||
|
branchNameStrategy?: 'romaji' | 'ai';
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Project-level configuration */
|
/** Project-level configuration */
|
||||||
|
|||||||
@ -268,6 +268,8 @@ export const GlobalConfigSchema = z.object({
|
|||||||
bookmarks_file: z.string().optional(),
|
bookmarks_file: z.string().optional(),
|
||||||
/** Path to piece categories file (default: ~/.takt/preferences/piece-categories.yaml) */
|
/** Path to piece categories file (default: ~/.takt/preferences/piece-categories.yaml) */
|
||||||
piece_categories_file: z.string().optional(),
|
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 */
|
/** Project config schema */
|
||||||
|
|||||||
@ -87,6 +87,7 @@ export class GlobalConfigManager {
|
|||||||
minimalOutput: parsed.minimal_output,
|
minimalOutput: parsed.minimal_output,
|
||||||
bookmarksFile: parsed.bookmarks_file,
|
bookmarksFile: parsed.bookmarks_file,
|
||||||
pieceCategoriesFile: parsed.piece_categories_file,
|
pieceCategoriesFile: parsed.piece_categories_file,
|
||||||
|
branchNameStrategy: parsed.branch_name_strategy,
|
||||||
};
|
};
|
||||||
this.cachedConfig = config;
|
this.cachedConfig = config;
|
||||||
return config;
|
return config;
|
||||||
@ -143,6 +144,9 @@ export class GlobalConfigManager {
|
|||||||
if (config.pieceCategoriesFile) {
|
if (config.pieceCategoriesFile) {
|
||||||
raw.piece_categories_file = config.pieceCategoriesFile;
|
raw.piece_categories_file = config.pieceCategoriesFile;
|
||||||
}
|
}
|
||||||
|
if (config.branchNameStrategy) {
|
||||||
|
raw.branch_name_strategy = config.branchNameStrategy;
|
||||||
|
}
|
||||||
writeFileSync(configPath, stringifyYaml(raw), 'utf-8');
|
writeFileSync(configPath, stringifyYaml(raw), 'utf-8');
|
||||||
this.invalidateCache();
|
this.invalidateCache();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -53,7 +53,8 @@ export class TaskSummarizer {
|
|||||||
taskName: string,
|
taskName: string,
|
||||||
options: SummarizeOptions,
|
options: SummarizeOptions,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const useLLM = options.useLLM ?? true;
|
const globalConfig = loadGlobalConfig();
|
||||||
|
const useLLM = options.useLLM ?? (globalConfig.branchNameStrategy === 'ai');
|
||||||
log.info('Summarizing task name', { taskName, useLLM });
|
log.info('Summarizing task name', { taskName, useLLM });
|
||||||
|
|
||||||
if (!useLLM) {
|
if (!useLLM) {
|
||||||
@ -61,8 +62,6 @@ export class TaskSummarizer {
|
|||||||
log.info('Task name romanized', { original: taskName, slug });
|
log.info('Task name romanized', { original: taskName, slug });
|
||||||
return slug || 'task';
|
return slug || 'task';
|
||||||
}
|
}
|
||||||
|
|
||||||
const globalConfig = loadGlobalConfig();
|
|
||||||
const providerType = (globalConfig.provider as ProviderType) ?? 'claude';
|
const providerType = (globalConfig.provider as ProviderType) ?? 'claude';
|
||||||
const model = options.model ?? globalConfig.model;
|
const model = options.model ?? globalConfig.model;
|
||||||
|
|
||||||
|
|||||||
@ -63,7 +63,7 @@ export interface SummarizeOptions {
|
|||||||
cwd: string;
|
cwd: string;
|
||||||
/** Model to use (optional, defaults to config or haiku) */
|
/** Model to use (optional, defaults to config or haiku) */
|
||||||
model?: string;
|
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;
|
useLLM?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user