@@ -1503,6 +1503,69 @@ func configuredACPAgent(cfg *config.Config) *ACPAgent {
15031503 return resolved
15041504}
15051505
1506+ // applyCommandOverrides clones the agent and applies the configured
1507+ // command override from cfg. Returns the original agent unchanged when
1508+ // no override applies. Cloning avoids mutating global registry
1509+ // singletons that concurrent callers share.
1510+ func applyCommandOverrides (a Agent , cfg * config.Config ) Agent {
1511+ if cfg == nil {
1512+ return a
1513+ }
1514+ switch agent := a .(type ) {
1515+ case * CodexAgent :
1516+ if cfg .CodexCmd != "" {
1517+ clone := * agent
1518+ clone .Command = cfg .CodexCmd
1519+ return & clone
1520+ }
1521+ case * ClaudeAgent :
1522+ if cfg .ClaudeCodeCmd != "" {
1523+ clone := * agent
1524+ clone .Command = cfg .ClaudeCodeCmd
1525+ return & clone
1526+ }
1527+ case * CursorAgent :
1528+ if cfg .CursorCmd != "" {
1529+ clone := * agent
1530+ clone .Command = cfg .CursorCmd
1531+ return & clone
1532+ }
1533+ case * PiAgent :
1534+ if cfg .PiCmd != "" {
1535+ clone := * agent
1536+ clone .Command = cfg .PiCmd
1537+ return & clone
1538+ }
1539+ }
1540+ return a
1541+ }
1542+
1543+ // isAvailableWithConfig checks whether the named agent can be resolved
1544+ // to an executable command, considering config command overrides. If a
1545+ // config override points to an available binary, the agent is considered
1546+ // available even when the default command isn't in PATH.
1547+ func isAvailableWithConfig (name string , cfg * config.Config ) bool {
1548+ name = resolveAlias (name )
1549+ a , ok := registry [name ]
1550+ if ! ok {
1551+ return false
1552+ }
1553+ ca , ok := a .(CommandAgent )
1554+ if ! ok {
1555+ return true // non-command agents (e.g. test) are always available
1556+ }
1557+ // Check the configured command first — it takes priority.
1558+ overridden := applyCommandOverrides (a , cfg )
1559+ if oca , ok := overridden .(CommandAgent ); ok {
1560+ if _ , err := exec .LookPath (oca .CommandName ()); err == nil {
1561+ return true
1562+ }
1563+ }
1564+ // Fall back to the default (hardcoded) command.
1565+ _ , err := exec .LookPath (ca .CommandName ())
1566+ return err == nil
1567+ }
1568+
15061569// GetAvailableWithConfig resolves an available agent while honoring runtime ACP config.
15071570// It treats cfg.ACP.Name as an alias for "acp" and applies cfg.ACP command/mode/model
15081571// at resolution time instead of package-init time.
@@ -1528,10 +1591,58 @@ func GetAvailableWithConfig(preferred string, cfg *config.Config, backups ...str
15281591 }
15291592 }
15301593
1531- // Finally fall back to normal auto-selection (with backups).
1594+ // ACP unavailable — try backup agents with config-aware
1595+ // availability so *_cmd overrides are honored.
1596+ if cfg != nil {
1597+ for _ , b := range backups {
1598+ b = resolveAlias (b )
1599+ if b == "" {
1600+ continue
1601+ }
1602+ if _ , ok := registry [b ]; ok && isAvailableWithConfig (b , cfg ) {
1603+ a , _ := Get (b )
1604+ return applyCommandOverrides (a , cfg ), nil
1605+ }
1606+ }
1607+ }
1608+
1609+ // Finally fall back to normal auto-selection.
15321610 return GetAvailable ("" , backups ... )
15331611 }
15341612
1613+ // Check the preferred agent using config command overrides before
1614+ // falling back. GetAvailable only checks the hardcoded default
1615+ // command via IsAvailable, so a configured command (e.g.
1616+ // claude_code_cmd = "/usr/local/bin/claude-wrapper") would be
1617+ // missed when the default binary isn't in PATH.
1618+ if preferred != "" && cfg != nil {
1619+ if _ , ok := registry [preferred ]; ! ok {
1620+ // Unknown agent — let GetAvailable produce the error.
1621+ return GetAvailable (preferred , backups ... )
1622+ }
1623+ if isAvailableWithConfig (preferred , cfg ) {
1624+ a , _ := Get (preferred )
1625+ return applyCommandOverrides (a , cfg ), nil
1626+ }
1627+ }
1628+
1629+ // Try backup agents with config-aware availability before the
1630+ // fallback chain. This runs regardless of whether preferred is
1631+ // set so that backup-only configurations (preferred="" with a
1632+ // backup_agent) still honor *_cmd overrides.
1633+ if cfg != nil {
1634+ for _ , b := range backups {
1635+ b = resolveAlias (b )
1636+ if b == "" || b == preferred {
1637+ continue
1638+ }
1639+ if _ , ok := registry [b ]; ok && isAvailableWithConfig (b , cfg ) {
1640+ a , _ := Get (b )
1641+ return applyCommandOverrides (a , cfg ), nil
1642+ }
1643+ }
1644+ }
1645+
15351646 resolved , err := GetAvailable (preferred , backups ... )
15361647 if err != nil {
15371648 return nil , err
@@ -1544,39 +1655,7 @@ func GetAvailableWithConfig(preferred string, cfg *config.Config, backups ...str
15441655 return resolved , nil
15451656 }
15461657
1547- // Apply command overrides from config.
1548- // Clone agent instances before mutating to avoid contaminating the
1549- // global registry — concurrent callers share those singletons.
1550- if cfg != nil {
1551- switch agent := resolved .(type ) {
1552- case * CodexAgent :
1553- if cfg .CodexCmd != "" {
1554- clone := * agent
1555- clone .Command = cfg .CodexCmd
1556- resolved = & clone
1557- }
1558- case * ClaudeAgent :
1559- if cfg .ClaudeCodeCmd != "" {
1560- clone := * agent
1561- clone .Command = cfg .ClaudeCodeCmd
1562- resolved = & clone
1563- }
1564- case * CursorAgent :
1565- if cfg .CursorCmd != "" {
1566- clone := * agent
1567- clone .Command = cfg .CursorCmd
1568- resolved = & clone
1569- }
1570- case * PiAgent :
1571- if cfg .PiCmd != "" {
1572- clone := * agent
1573- clone .Command = cfg .PiCmd
1574- resolved = & clone
1575- }
1576- }
1577- }
1578-
1579- return resolved , nil
1658+ return applyCommandOverrides (resolved , cfg ), nil
15801659}
15811660
15821661func applyACPAgentConfigOverride (cfg * config.ACPAgentConfig , override * config.ACPAgentConfig ) {
0 commit comments