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).
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user