refactor(commands): provision slash commands at town-level only (gt-7x274)
- gt install now creates ~/gt/.claude/commands/ with all commands - Removed per-workspace provisioning from crew/polecat managers - Updated bd doctor to check town-level instead of per-workspace - All agents inherit via Claude directory traversal This eliminates duplicate /handoff skills in the picker. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -224,6 +224,14 @@ func runInstall(cmd *cobra.Command, args []string) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Provision town-level slash commands (.claude/commands/)
|
||||||
|
// All agents inherit these via Claude's directory traversal - no per-workspace copies needed.
|
||||||
|
if err := templates.ProvisionCommands(absPath); err != nil {
|
||||||
|
fmt.Printf(" %s Could not provision slash commands: %v\n", style.Dim.Render("⚠"), err)
|
||||||
|
} else {
|
||||||
|
fmt.Printf(" ✓ Created .claude/commands/ (slash commands for all agents)\n")
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize git if requested (--git or --github implies --git)
|
// Initialize git if requested (--git or --github implies --git)
|
||||||
if installGit || installGitHub != "" {
|
if installGit || installGitHub != "" {
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import (
|
|||||||
|
|
||||||
"github.com/steveyegge/gastown/internal/git"
|
"github.com/steveyegge/gastown/internal/git"
|
||||||
"github.com/steveyegge/gastown/internal/rig"
|
"github.com/steveyegge/gastown/internal/rig"
|
||||||
"github.com/steveyegge/gastown/internal/templates"
|
|
||||||
"github.com/steveyegge/gastown/internal/util"
|
"github.com/steveyegge/gastown/internal/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -114,12 +113,8 @@ func (m *Manager) Add(name string, createBranch bool) (*CrewWorker, error) {
|
|||||||
fmt.Printf("Warning: could not set up shared beads: %v\n", err)
|
fmt.Printf("Warning: could not set up shared beads: %v\n", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Provision .claude/commands/ with standard slash commands (e.g., /handoff)
|
// NOTE: Slash commands (.claude/commands/) are provisioned at town level by gt install.
|
||||||
// This ensures crew workers have Gas Town utilities even if source repo lacks them.
|
// All agents inherit them via Claude's directory traversal - no per-workspace copies needed.
|
||||||
if err := templates.ProvisionCommands(crewPath); err != nil {
|
|
||||||
// Non-fatal - crew can still work, warn but don't fail
|
|
||||||
fmt.Printf("Warning: could not provision slash commands: %v\n", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NOTE: We intentionally do NOT write to CLAUDE.md here.
|
// NOTE: We intentionally do NOT write to CLAUDE.md here.
|
||||||
// Gas Town context is injected ephemerally via SessionStart hook (gt prime).
|
// Gas Town context is injected ephemerally via SessionStart hook (gt prime).
|
||||||
|
|||||||
@@ -2,26 +2,17 @@ package doctor
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/steveyegge/gastown/internal/templates"
|
"github.com/steveyegge/gastown/internal/templates"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CommandsCheck validates that crew/polecat workspaces have .claude/commands/ provisioned.
|
// CommandsCheck validates that town-level .claude/commands/ is provisioned.
|
||||||
// This ensures all agents have access to slash commands like /handoff.
|
// All agents inherit these via Claude's directory traversal - no per-workspace copies needed.
|
||||||
type CommandsCheck struct {
|
type CommandsCheck struct {
|
||||||
FixableCheck
|
FixableCheck
|
||||||
missingWorkspaces []workspaceWithMissingCommands // Cached during Run for use in Fix
|
townRoot string // Cached for Fix
|
||||||
}
|
missingCommands []string // 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.
|
// NewCommandsCheck creates a new commands check.
|
||||||
@@ -30,140 +21,55 @@ func NewCommandsCheck() *CommandsCheck {
|
|||||||
FixableCheck: FixableCheck{
|
FixableCheck: FixableCheck{
|
||||||
BaseCheck: BaseCheck{
|
BaseCheck: BaseCheck{
|
||||||
CheckName: "commands-provisioned",
|
CheckName: "commands-provisioned",
|
||||||
CheckDescription: "Check .claude/commands/ is provisioned in crew/polecat workspaces",
|
CheckDescription: "Check .claude/commands/ is provisioned at town level",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run checks all crew and polecat workspaces for missing slash commands.
|
// Run checks if town-level slash commands are provisioned.
|
||||||
func (c *CommandsCheck) Run(ctx *CheckContext) *CheckResult {
|
func (c *CommandsCheck) Run(ctx *CheckContext) *CheckResult {
|
||||||
c.missingWorkspaces = nil
|
c.townRoot = ctx.TownRoot
|
||||||
|
c.missingCommands = nil
|
||||||
|
|
||||||
workspaces := c.findAllWorkerDirs(ctx.TownRoot)
|
// Check town-level commands
|
||||||
if len(workspaces) == 0 {
|
missing, err := templates.MissingCommands(ctx.TownRoot)
|
||||||
|
if err != nil {
|
||||||
|
return &CheckResult{
|
||||||
|
Name: c.Name(),
|
||||||
|
Status: StatusWarning,
|
||||||
|
Message: fmt.Sprintf("Error checking town-level commands: %v", err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(missing) == 0 {
|
||||||
|
// Get command names for the success message
|
||||||
|
names, _ := templates.CommandNames()
|
||||||
return &CheckResult{
|
return &CheckResult{
|
||||||
Name: c.Name(),
|
Name: c.Name(),
|
||||||
Status: StatusOK,
|
Status: StatusOK,
|
||||||
Message: "No crew/polecat workspaces found",
|
Message: fmt.Sprintf("Town-level slash commands provisioned (%s)", strings.Join(names, ", ")),
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
c.missingCommands = missing
|
||||||
return &CheckResult{
|
return &CheckResult{
|
||||||
Name: c.Name(),
|
Name: c.Name(),
|
||||||
Status: StatusWarning,
|
Status: StatusWarning,
|
||||||
Message: fmt.Sprintf("%d workspace(s) missing slash commands (e.g., /handoff)", len(c.missingWorkspaces)),
|
Message: fmt.Sprintf("Missing town-level slash commands: %s", strings.Join(missing, ", ")),
|
||||||
Details: details,
|
Details: []string{
|
||||||
|
fmt.Sprintf("Expected at: %s/.claude/commands/", ctx.TownRoot),
|
||||||
|
"All agents inherit town-level commands via directory traversal",
|
||||||
|
},
|
||||||
FixHint: "Run 'gt doctor --fix' to provision missing commands",
|
FixHint: "Run 'gt doctor --fix' to provision missing commands",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fix provisions missing slash commands to workspaces.
|
// Fix provisions missing slash commands at town level.
|
||||||
func (c *CommandsCheck) Fix(ctx *CheckContext) error {
|
func (c *CommandsCheck) Fix(ctx *CheckContext) error {
|
||||||
if len(c.missingWorkspaces) == 0 {
|
if len(c.missingCommands) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var lastErr error
|
return templates.ProvisionCommands(c.townRoot)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import (
|
|||||||
"github.com/steveyegge/gastown/internal/config"
|
"github.com/steveyegge/gastown/internal/config"
|
||||||
"github.com/steveyegge/gastown/internal/git"
|
"github.com/steveyegge/gastown/internal/git"
|
||||||
"github.com/steveyegge/gastown/internal/rig"
|
"github.com/steveyegge/gastown/internal/rig"
|
||||||
"github.com/steveyegge/gastown/internal/templates"
|
|
||||||
"github.com/steveyegge/gastown/internal/workspace"
|
"github.com/steveyegge/gastown/internal/workspace"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -245,12 +244,8 @@ func (m *Manager) AddWithOptions(name string, opts AddOptions) (*Polecat, error)
|
|||||||
fmt.Printf("Warning: could not set up shared beads: %v\n", err)
|
fmt.Printf("Warning: could not set up shared beads: %v\n", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Provision .claude/commands/ with standard slash commands (e.g., /handoff)
|
// NOTE: Slash commands (.claude/commands/) are provisioned at town level by gt install.
|
||||||
// This ensures polecats have Gas Town utilities even if source repo lacks them.
|
// All agents inherit them via Claude's directory traversal - no per-workspace copies needed.
|
||||||
if err := templates.ProvisionCommands(polecatPath); err != nil {
|
|
||||||
// Non-fatal - polecat can still work, warn but don't fail
|
|
||||||
fmt.Printf("Warning: could not provision slash commands: %v\n", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create agent bead for ZFC compliance (self-report state).
|
// Create agent bead for ZFC compliance (self-report state).
|
||||||
// State starts as "spawning" - will be updated to "working" when Claude starts.
|
// State starts as "spawning" - will be updated to "working" when Claude starts.
|
||||||
@@ -468,10 +463,7 @@ func (m *Manager) RecreateWithOptions(name string, force bool, opts AddOptions)
|
|||||||
fmt.Printf("Warning: could not set up shared beads: %v\n", err)
|
fmt.Printf("Warning: could not set up shared beads: %v\n", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Provision .claude/commands/ with standard slash commands (e.g., /handoff)
|
// NOTE: Slash commands inherited from town level - no per-workspace copies needed.
|
||||||
if err := templates.ProvisionCommands(polecatPath); err != nil {
|
|
||||||
fmt.Printf("Warning: could not provision slash commands: %v\n", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create fresh agent bead for ZFC compliance
|
// Create fresh agent bead for ZFC compliance
|
||||||
// HookBead is set atomically at recreation time if provided.
|
// HookBead is set atomically at recreation time if provided.
|
||||||
|
|||||||
Reference in New Issue
Block a user