11import type { AutoNameConfig } from "../domain/config" ;
22import { isValidBranchName } from "../domain/policies" ;
3+ import { generateFallbackBranchName } from "../lib/branch-name" ;
4+ import { log } from "../lib/log" ;
35
46interface SpawnResult {
57 exitCode : number ;
68 stdout : string ;
79 stderr : string ;
810}
911
10- type SpawnLike = ( args : string [ ] ) => Promise < SpawnResult > ;
12+ interface SpawnOptions {
13+ timeoutMs ?: number ;
14+ }
15+
16+ type SpawnLike = ( args : string [ ] , options ?: SpawnOptions ) => Promise < SpawnResult > ;
1117
1218const MAX_BRANCH_LENGTH = 40 ;
1319const DEFAULT_AUTO_NAME_MODEL = "claude-haiku-4-5-20251001" ;
20+ const AUTO_NAME_TIMEOUT_MS = 10_000 ;
1421
1522const DEFAULT_SYSTEM_PROMPT = [
1623 "Generate a concise git branch name from the task description." ,
@@ -46,17 +53,50 @@ function getSystemPrompt(config: AutoNameConfig): string {
4653 return config . systemPrompt ?. trim ( ) || DEFAULT_SYSTEM_PROMPT ;
4754}
4855
49- async function defaultSpawn ( args : string [ ] ) : Promise < SpawnResult > {
56+ class AutoNameTimeoutError extends Error {
57+ constructor ( readonly timeoutMs : number ) {
58+ super ( `Auto-name timed out after ${ timeoutMs } ms` ) ;
59+ }
60+ }
61+
62+ async function defaultSpawn ( args : string [ ] , options : SpawnOptions = { } ) : Promise < SpawnResult > {
5063 const proc = Bun . spawn ( args , {
5164 stdout : "pipe" ,
5265 stderr : "pipe" ,
5366 } ) ;
54- const [ stdout , stderr , exitCode ] = await Promise . all ( [
67+ const resultPromise = Promise . all ( [
5568 new Response ( proc . stdout ) . text ( ) ,
5669 new Response ( proc . stderr ) . text ( ) ,
5770 proc . exited ,
58- ] ) ;
59- return { exitCode, stdout, stderr } ;
71+ ] ) . then ( ( [ stdout , stderr , exitCode ] ) => ( { exitCode, stdout, stderr } ) ) ;
72+
73+ if ( options . timeoutMs === undefined ) {
74+ return await resultPromise ;
75+ }
76+
77+ return await new Promise < SpawnResult > ( ( resolve , reject ) => {
78+ let settled = false ;
79+ const timeoutId = setTimeout ( ( ) => {
80+ if ( settled ) return ;
81+ settled = true ;
82+ try {
83+ proc . kill ( "SIGKILL" ) ;
84+ } catch { }
85+ reject ( new AutoNameTimeoutError ( options . timeoutMs ! ) ) ;
86+ } , options . timeoutMs ) ;
87+
88+ void resultPromise . then ( ( result ) => {
89+ if ( settled ) return ;
90+ settled = true ;
91+ clearTimeout ( timeoutId ) ;
92+ resolve ( result ) ;
93+ } , ( error ) => {
94+ if ( settled ) return ;
95+ settled = true ;
96+ clearTimeout ( timeoutId ) ;
97+ reject ( error ) ;
98+ } ) ;
99+ } ) ;
60100}
61101
62102function buildClaudeArgs ( model : string | undefined , systemPrompt : string , prompt : string ) : string [ ] {
@@ -97,6 +137,7 @@ function buildCodexArgs(model: string | undefined, systemPrompt: string, prompt:
97137
98138export interface AutoNameServiceDependencies {
99139 spawnImpl ?: SpawnLike ;
140+ timeoutMs ?: number ;
100141}
101142
102143export interface AutoNameGenerator {
@@ -105,9 +146,11 @@ export interface AutoNameGenerator {
105146
106147export class AutoNameService implements AutoNameGenerator {
107148 private readonly spawnImpl : SpawnLike ;
149+ private readonly timeoutMs : number ;
108150
109151 constructor ( deps : AutoNameServiceDependencies = { } ) {
110152 this . spawnImpl = deps . spawnImpl ?? defaultSpawn ;
153+ this . timeoutMs = deps . timeoutMs ?? AUTO_NAME_TIMEOUT_MS ;
111154 }
112155
113156 async generateBranchName ( config : AutoNameConfig , task : string ) : Promise < string > {
@@ -127,8 +170,13 @@ export class AutoNameService implements AutoNameGenerator {
127170
128171 let result : SpawnResult ;
129172 try {
130- result = await this . spawnImpl ( args ) ;
131- } catch {
173+ result = await this . spawnImpl ( args , { timeoutMs : this . timeoutMs } ) ;
174+ } catch ( error ) {
175+ if ( error instanceof AutoNameTimeoutError ) {
176+ const fallback = generateFallbackBranchName ( ) ;
177+ log . warn ( `[auto-name] ${ cli } timed out after ${ this . timeoutMs } ms; using fallback branch ${ fallback } ` ) ;
178+ return fallback ;
179+ }
132180 throw new Error ( `'${ cli } ' CLI not found. Install it or check your PATH.` ) ;
133181 }
134182
0 commit comments