24f213449e
Pure resolution logic combining a workspace's AgentSpec with the user-level default_agent into a Resolved value carrying Command, Args, and Env. No I/O — PATH lookup stays in shell.ExecAgent and ctask agents check, so Resolve is trivially testable and reusable. BuiltinProfiles enumerates claude and opencode; "custom" is the escape hatch and requires command. Keep BuiltinProfiles in sync with workspace.knownAgentTypes and workspace.IsBuiltinAgentType (Task 1).
82 lines
2.4 KiB
Go
82 lines
2.4 KiB
Go
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
|
|
}
|