feat(v0.6): ctask agents check + doctor integration
ctask agents check [workspace] validates that a workspace's agent configuration can be launched without launching it: agent type known, command resolvable on PATH, launch_dir valid, AGENTS.md present, CLAUDE.md shim present (WARN only, claude type only). agent.env keys are displayed informationally, with a WARN line when any key shadows a ctask-exported CTASK_* var. Returns non-zero when any check FAILs. ctask doctor includes the same sweep when a workspace context is resolvable — the most-recently-active workspace via workspace.MostRecentActive. When no active workspace exists, doctor shows "Agent check: skipped (no workspace context)" without bumping the failure counter. runAgentsCheckOnWorkspace is shared between the standalone command and the doctor integration. TestCompletionSubcommandViaExecute is made order-independent: cobra's default completion command captures the root output writer on the first Execute() in the process, and the new agents-check tests now run an Execute() earlier in the suite.
This commit is contained in:
+158
@@ -0,0 +1,158 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/warrenronsiek/ctask/internal/agent"
|
||||
"github.com/warrenronsiek/ctask/internal/config"
|
||||
"github.com/warrenronsiek/ctask/internal/workspace"
|
||||
)
|
||||
|
||||
var agentsCmd = &cobra.Command{
|
||||
Use: "agents",
|
||||
Short: "Inspect and validate agent configuration",
|
||||
SilenceUsage: true,
|
||||
}
|
||||
|
||||
var agentsCheckCmd = &cobra.Command{
|
||||
Use: "check [workspace]",
|
||||
Short: "Validate the agent configuration for a workspace",
|
||||
Long: `Validate that the configured agent for a workspace can be launched.
|
||||
Does NOT launch the agent. Checks: agent type known, command resolvable
|
||||
on PATH, launch_dir valid, AGENTS.md present, CLAUDE.md shim present
|
||||
(WARN, claude type only). Displays agent.env keys informationally.
|
||||
|
||||
If [workspace] is omitted, checks the most-recently-active workspace.`,
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
SilenceUsage: true,
|
||||
RunE: runAgentsCheck,
|
||||
}
|
||||
|
||||
func init() {
|
||||
agentsCheckCmd.ValidArgsFunction = completeWorkspaces(completionAny)
|
||||
agentsCmd.AddCommand(agentsCheckCmd)
|
||||
rootCmd.AddCommand(agentsCmd)
|
||||
}
|
||||
|
||||
func runAgentsCheck(cmd *cobra.Command, args []string) error {
|
||||
roots := config.SearchRoots()
|
||||
var ws *workspace.QueryResult
|
||||
if len(args) == 1 {
|
||||
ws = resolveOne(roots, args[0], true)
|
||||
} else {
|
||||
best, err := workspace.MostRecentActive(roots)
|
||||
if err != nil || best == nil {
|
||||
return fmt.Errorf("agents check: no workspace specified and no active workspace found")
|
||||
}
|
||||
ws = best
|
||||
}
|
||||
|
||||
return runAgentsCheckOnWorkspace(cmd.OutOrStdout(), ws)
|
||||
}
|
||||
|
||||
// runAgentsCheckOnWorkspace performs the checks and prints results.
|
||||
// Returns a non-nil error iff any check is FAIL (so doctor can surface
|
||||
// the failure count). Extracted from runAgentsCheck so doctor can reuse.
|
||||
func runAgentsCheckOnWorkspace(out io.Writer, ws *workspace.QueryResult) error {
|
||||
fmt.Fprintf(out, "── Agent Check: %s ─────────────────\n", ws.Meta.Slug)
|
||||
|
||||
failed := 0
|
||||
spec := ws.Meta.Agent
|
||||
|
||||
// 1. Agent type known. (ValidateAgentSpec already enforced this on read,
|
||||
// but we re-display for symmetry. If type is empty, fall through to
|
||||
// default_agent and label as such.)
|
||||
typ := spec.Type
|
||||
typeLabel := typ
|
||||
if typ == "" {
|
||||
typ = config.LoadResolver().DefaultAgent().Value
|
||||
typeLabel = typ + " (from default_agent)"
|
||||
}
|
||||
fmt.Fprintf(out, "Agent type: %s\n", typeLabel)
|
||||
if !agent.IsKnownType(typ) {
|
||||
fmt.Fprintf(out, " [FAIL] unknown agent type %q\n", typ)
|
||||
failed++
|
||||
} else {
|
||||
fmt.Fprintln(out, " [PASS]")
|
||||
}
|
||||
|
||||
// 2. Command resolvable.
|
||||
resolved, rerr := agent.Resolve(spec, typ)
|
||||
if rerr != nil {
|
||||
fmt.Fprintf(out, "Command: [FAIL] %v\n", rerr)
|
||||
failed++
|
||||
} else {
|
||||
path, lerr := exec.LookPath(resolved.Command)
|
||||
fmt.Fprintf(out, "Command: %s", resolved.Command)
|
||||
if lerr != nil {
|
||||
fmt.Fprintf(out, "\n [FAIL] not found on PATH: %v\n", lerr)
|
||||
failed++
|
||||
} else {
|
||||
fmt.Fprintf(out, " (%s)\n", path)
|
||||
fmt.Fprintln(out, " [PASS]")
|
||||
}
|
||||
}
|
||||
|
||||
// 3. launch_dir valid.
|
||||
launchAbs, _, lderr := workspace.ResolveLaunch(ws.Path, ws.Meta.LaunchDir)
|
||||
fmt.Fprintf(out, "Launch dir: %s\n", launchAbs)
|
||||
if lderr != nil {
|
||||
fmt.Fprintf(out, " [FAIL] %v\n", lderr)
|
||||
failed++
|
||||
} else {
|
||||
fmt.Fprintln(out, " [PASS]")
|
||||
}
|
||||
|
||||
// 4. AGENTS.md present.
|
||||
if _, err := os.Stat(filepath.Join(ws.Path, "AGENTS.md")); err == nil {
|
||||
fmt.Fprintln(out, "AGENTS.md: found\n [PASS]")
|
||||
} else {
|
||||
fmt.Fprintln(out, "AGENTS.md: [FAIL] missing")
|
||||
failed++
|
||||
}
|
||||
|
||||
// 5. CLAUDE.md shim — WARN only, claude type only.
|
||||
if typ == "claude" {
|
||||
if _, err := os.Stat(filepath.Join(ws.Path, "CLAUDE.md")); err == nil {
|
||||
fmt.Fprintln(out, "CLAUDE.md: found\n [PASS]")
|
||||
} else {
|
||||
fmt.Fprintln(out, "CLAUDE.md: [WARN] missing (shim is optional)")
|
||||
}
|
||||
}
|
||||
|
||||
// 6. agent.env keys (informational + CTASK_* shadow WARN).
|
||||
if len(spec.Env) > 0 {
|
||||
keys := make([]string, 0, len(spec.Env))
|
||||
for k := range spec.Env {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
fmt.Fprintf(out, "Agent env: %d keys configured (%s)\n", len(keys), strings.Join(keys, ", "))
|
||||
// agent.env merges AFTER ctask's own exported vars, so a key
|
||||
// matching CTASK_* shadows what ctask exports. The user is allowed
|
||||
// to do this (spec §5); we surface the surprise. WARN does not
|
||||
// bump the failed counter.
|
||||
var shadowed []string
|
||||
for _, k := range keys {
|
||||
if strings.HasPrefix(k, "CTASK_") {
|
||||
shadowed = append(shadowed, k)
|
||||
}
|
||||
}
|
||||
if len(shadowed) > 0 {
|
||||
fmt.Fprintf(out, " [WARN] agent.env overrides ctask-exported vars: %s\n",
|
||||
strings.Join(shadowed, ", "))
|
||||
}
|
||||
}
|
||||
|
||||
if failed > 0 {
|
||||
return fmt.Errorf("agents check: %d failures", failed)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user