package agent import ( "fmt" "github.com/warrenronsiek/ctask/internal/workspace" ) // Profile describes a built-in agent. The Default command is what ctask // invokes when AgentSpec.Command is empty; Type is the canonical name. type Profile struct { Type string Default string } // BuiltinProfiles enumerates the v0.6 built-in agents. "custom" is NOT // in this map — it is the escape hatch with no defaults. Keep this in // sync with workspace.knownAgentTypes AND workspace.IsBuiltinAgentType // (update all three together when a new built-in lands). var BuiltinProfiles = map[string]Profile{ "claude": {Type: "claude", Default: "claude"}, "opencode": {Type: "opencode", Default: "opencode"}, } // IsKnownType reports whether t is "claude", "opencode", or "custom". func IsKnownType(t string) bool { if t == "custom" { return true } _, ok := BuiltinProfiles[t] return ok } // Resolved is the launch-ready agent value: a single executable name or // path, optional arguments, and an env-var map merged AFTER ctask's own // exported vars at launch time. type Resolved struct { Type string Command string Args []string Env map[string]string } // Resolve combines a workspace's AgentSpec with the user-level // default_agent into a launch-ready Resolved. Resolution rules (per // v0.6 spec §5): // // 1. If spec.Type is empty, fall through to defaultAgent. // 2. If the resolved type is "custom", spec.Command is required. // 3. Otherwise, the resolved command is spec.Command if set, // else BuiltinProfiles[type].Default. // 4. Args and Env are carried verbatim from the spec. // // Resolve does NOT call exec.LookPath. PATH validation is the launch // path's job (shell.ExecAgent fails fast with a diagnostic) and // `ctask agents check`'s job. Keeping Resolve I/O-free makes it // trivially testable and reusable. func Resolve(spec workspace.AgentSpec, defaultAgent string) (*Resolved, error) { typ := spec.Type if typ == "" { typ = defaultAgent } if !IsKnownType(typ) { return nil, fmt.Errorf("agent: unknown type %q (must be claude, opencode, or custom)", typ) } cmd := spec.Command if cmd == "" { if typ == "custom" { return nil, fmt.Errorf("agent: type \"custom\" requires command field") } cmd = BuiltinProfiles[typ].Default } return &Resolved{ Type: typ, Command: cmd, Args: spec.Args, Env: spec.Env, }, nil }