When adding a crew member with 'gt crew add' or spawning a polecat, provision the .claude/commands/ directory with standard slash commands like /handoff. This ensures all agents have Gas Town utilities even if the source repo does not have them tracked. Changes: - Add embedded commands templates (internal/templates/commands/) - Add ProvisionCommands() to templates package - Call ProvisionCommands from crew and polecat managers - Add gt doctor commands-provisioned check with --fix support
170 lines
4.3 KiB
Go
170 lines
4.3 KiB
Go
package doctor
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/steveyegge/gastown/internal/templates"
|
|
)
|
|
|
|
// CommandsCheck validates that crew/polecat workspaces have .claude/commands/ provisioned.
|
|
// This ensures all agents have access to slash commands like /handoff.
|
|
type CommandsCheck struct {
|
|
FixableCheck
|
|
missingWorkspaces []workspaceWithMissingCommands // Cached during Run for use in Fix
|
|
}
|
|
|
|
type workspaceWithMissingCommands struct {
|
|
path string
|
|
rigName string
|
|
workerName string
|
|
workerType string // "crew" or "polecat"
|
|
missingFiles []string
|
|
}
|
|
|
|
// NewCommandsCheck creates a new commands check.
|
|
func NewCommandsCheck() *CommandsCheck {
|
|
return &CommandsCheck{
|
|
FixableCheck: FixableCheck{
|
|
BaseCheck: BaseCheck{
|
|
CheckName: "commands-provisioned",
|
|
CheckDescription: "Check .claude/commands/ is provisioned in crew/polecat workspaces",
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
// Run checks all crew and polecat workspaces for missing slash commands.
|
|
func (c *CommandsCheck) Run(ctx *CheckContext) *CheckResult {
|
|
c.missingWorkspaces = nil
|
|
|
|
workspaces := c.findAllWorkerDirs(ctx.TownRoot)
|
|
if len(workspaces) == 0 {
|
|
return &CheckResult{
|
|
Name: c.Name(),
|
|
Status: StatusOK,
|
|
Message: "No crew/polecat workspaces found",
|
|
}
|
|
}
|
|
|
|
var validCount int
|
|
var details []string
|
|
|
|
for _, ws := range workspaces {
|
|
missing, err := templates.MissingCommands(ws.path)
|
|
if err != nil {
|
|
details = append(details, fmt.Sprintf("%s/%s/%s: error checking commands: %v",
|
|
ws.rigName, ws.workerType, ws.workerName, err))
|
|
continue
|
|
}
|
|
|
|
if len(missing) > 0 {
|
|
c.missingWorkspaces = append(c.missingWorkspaces, workspaceWithMissingCommands{
|
|
path: ws.path,
|
|
rigName: ws.rigName,
|
|
workerName: ws.workerName,
|
|
workerType: ws.workerType,
|
|
missingFiles: missing,
|
|
})
|
|
details = append(details, fmt.Sprintf("%s/%s/%s: missing %s",
|
|
ws.rigName, ws.workerType, ws.workerName, strings.Join(missing, ", ")))
|
|
} else {
|
|
validCount++
|
|
}
|
|
}
|
|
|
|
if len(c.missingWorkspaces) == 0 {
|
|
return &CheckResult{
|
|
Name: c.Name(),
|
|
Status: StatusOK,
|
|
Message: fmt.Sprintf("All %d workspaces have slash commands provisioned", validCount),
|
|
}
|
|
}
|
|
|
|
return &CheckResult{
|
|
Name: c.Name(),
|
|
Status: StatusWarning,
|
|
Message: fmt.Sprintf("%d workspace(s) missing slash commands (e.g., /handoff)", len(c.missingWorkspaces)),
|
|
Details: details,
|
|
FixHint: "Run 'gt doctor --fix' to provision missing commands",
|
|
}
|
|
}
|
|
|
|
// Fix provisions missing slash commands to workspaces.
|
|
func (c *CommandsCheck) Fix(ctx *CheckContext) error {
|
|
if len(c.missingWorkspaces) == 0 {
|
|
return nil
|
|
}
|
|
|
|
var lastErr error
|
|
for _, ws := range c.missingWorkspaces {
|
|
if err := templates.ProvisionCommands(ws.path); err != nil {
|
|
lastErr = fmt.Errorf("%s/%s/%s: %w", ws.rigName, ws.workerType, ws.workerName, err)
|
|
continue
|
|
}
|
|
}
|
|
|
|
return lastErr
|
|
}
|
|
|
|
type workerDir struct {
|
|
path string
|
|
rigName string
|
|
workerName string
|
|
workerType string // "crew" or "polecat"
|
|
}
|
|
|
|
// findAllWorkerDirs finds all crew and polecat directories in the workspace.
|
|
func (c *CommandsCheck) findAllWorkerDirs(townRoot string) []workerDir {
|
|
var dirs []workerDir
|
|
|
|
entries, err := os.ReadDir(townRoot)
|
|
if err != nil {
|
|
return dirs
|
|
}
|
|
|
|
for _, entry := range entries {
|
|
if !entry.IsDir() || strings.HasPrefix(entry.Name(), ".") || entry.Name() == "mayor" {
|
|
continue
|
|
}
|
|
|
|
rigName := entry.Name()
|
|
|
|
// Check crew directory
|
|
crewPath := filepath.Join(townRoot, rigName, "crew")
|
|
if crewEntries, err := os.ReadDir(crewPath); err == nil {
|
|
for _, crew := range crewEntries {
|
|
if !crew.IsDir() || strings.HasPrefix(crew.Name(), ".") {
|
|
continue
|
|
}
|
|
dirs = append(dirs, workerDir{
|
|
path: filepath.Join(crewPath, crew.Name()),
|
|
rigName: rigName,
|
|
workerName: crew.Name(),
|
|
workerType: "crew",
|
|
})
|
|
}
|
|
}
|
|
|
|
// Check polecats directory
|
|
polecatsPath := filepath.Join(townRoot, rigName, "polecats")
|
|
if polecatEntries, err := os.ReadDir(polecatsPath); err == nil {
|
|
for _, polecat := range polecatEntries {
|
|
if !polecat.IsDir() || strings.HasPrefix(polecat.Name(), ".") {
|
|
continue
|
|
}
|
|
dirs = append(dirs, workerDir{
|
|
path: filepath.Join(polecatsPath, polecat.Name()),
|
|
rigName: rigName,
|
|
workerName: polecat.Name(),
|
|
workerType: "polecat",
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
return dirs
|
|
}
|