Files
typebasedio 24f213449e feat(v0.6): internal/agent package — Resolve + BuiltinProfiles
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).
2026-05-15 10:58:55 -04:00

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
}