feat(agentci): package dispatch system for multi-agent deployment
Config-driven agent targets replace hardcoded map so new agents
can be added via CLI instead of recompiling. Includes setup script
for bootstrapping agent machines and CLI commands for management.
- Add pkg/agentci with config types and CRUD (LoadAgents, SaveAgent, etc.)
- Add CLI: core ai agent {add,list,status,logs,setup,remove}
- Add scripts/agent-setup.sh (SSH bootstrap: dirs, cron, prereq check)
- Headless loads agents from ~/.core/config.yaml
- Dispatch ticket includes forgejo_user for dynamic clone URLs
- agent-runner.sh reads username from ticket JSON, not hardcoded
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 10:36:23 +00:00
|
|
|
package ai
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"fmt"
|
|
|
|
|
"os"
|
|
|
|
|
"os/exec"
|
|
|
|
|
"path/filepath"
|
|
|
|
|
"strings"
|
|
|
|
|
|
2026-02-16 14:24:37 +00:00
|
|
|
"forge.lthn.ai/core/go/pkg/agentci"
|
|
|
|
|
"forge.lthn.ai/core/go/pkg/cli"
|
|
|
|
|
"forge.lthn.ai/core/go/pkg/config"
|
feat(agentci): package dispatch system for multi-agent deployment
Config-driven agent targets replace hardcoded map so new agents
can be added via CLI instead of recompiling. Includes setup script
for bootstrapping agent machines and CLI commands for management.
- Add pkg/agentci with config types and CRUD (LoadAgents, SaveAgent, etc.)
- Add CLI: core ai agent {add,list,status,logs,setup,remove}
- Add scripts/agent-setup.sh (SSH bootstrap: dirs, cron, prereq check)
- Headless loads agents from ~/.core/config.yaml
- Dispatch ticket includes forgejo_user for dynamic clone URLs
- agent-runner.sh reads username from ticket JSON, not hardcoded
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 10:36:23 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// AddAgentCommands registers the 'agent' subcommand group under 'ai'.
|
|
|
|
|
func AddAgentCommands(parent *cli.Command) {
|
|
|
|
|
agentCmd := &cli.Command{
|
|
|
|
|
Use: "agent",
|
|
|
|
|
Short: "Manage AgentCI dispatch targets",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
agentCmd.AddCommand(agentAddCmd())
|
|
|
|
|
agentCmd.AddCommand(agentListCmd())
|
|
|
|
|
agentCmd.AddCommand(agentStatusCmd())
|
|
|
|
|
agentCmd.AddCommand(agentLogsCmd())
|
|
|
|
|
agentCmd.AddCommand(agentSetupCmd())
|
|
|
|
|
agentCmd.AddCommand(agentRemoveCmd())
|
|
|
|
|
|
|
|
|
|
parent.AddCommand(agentCmd)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func loadConfig() (*config.Config, error) {
|
|
|
|
|
return config.New()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func agentAddCmd() *cli.Command {
|
|
|
|
|
cmd := &cli.Command{
|
|
|
|
|
Use: "add <name> <user@host>",
|
2026-02-10 02:57:41 +00:00
|
|
|
Short: "Add an agent to the config and verify SSH",
|
feat(agentci): package dispatch system for multi-agent deployment
Config-driven agent targets replace hardcoded map so new agents
can be added via CLI instead of recompiling. Includes setup script
for bootstrapping agent machines and CLI commands for management.
- Add pkg/agentci with config types and CRUD (LoadAgents, SaveAgent, etc.)
- Add CLI: core ai agent {add,list,status,logs,setup,remove}
- Add scripts/agent-setup.sh (SSH bootstrap: dirs, cron, prereq check)
- Headless loads agents from ~/.core/config.yaml
- Dispatch ticket includes forgejo_user for dynamic clone URLs
- agent-runner.sh reads username from ticket JSON, not hardcoded
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 10:36:23 +00:00
|
|
|
Args: cli.ExactArgs(2),
|
|
|
|
|
RunE: func(cmd *cli.Command, args []string) error {
|
|
|
|
|
name := args[0]
|
|
|
|
|
host := args[1]
|
|
|
|
|
|
|
|
|
|
forgejoUser, _ := cmd.Flags().GetString("forgejo-user")
|
|
|
|
|
if forgejoUser == "" {
|
|
|
|
|
forgejoUser = name
|
|
|
|
|
}
|
|
|
|
|
queueDir, _ := cmd.Flags().GetString("queue-dir")
|
|
|
|
|
if queueDir == "" {
|
|
|
|
|
queueDir = "/home/claude/ai-work/queue"
|
|
|
|
|
}
|
2026-02-10 02:57:41 +00:00
|
|
|
model, _ := cmd.Flags().GetString("model")
|
|
|
|
|
dualRun, _ := cmd.Flags().GetBool("dual-run")
|
feat(agentci): package dispatch system for multi-agent deployment
Config-driven agent targets replace hardcoded map so new agents
can be added via CLI instead of recompiling. Includes setup script
for bootstrapping agent machines and CLI commands for management.
- Add pkg/agentci with config types and CRUD (LoadAgents, SaveAgent, etc.)
- Add CLI: core ai agent {add,list,status,logs,setup,remove}
- Add scripts/agent-setup.sh (SSH bootstrap: dirs, cron, prereq check)
- Headless loads agents from ~/.core/config.yaml
- Dispatch ticket includes forgejo_user for dynamic clone URLs
- agent-runner.sh reads username from ticket JSON, not hardcoded
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 10:36:23 +00:00
|
|
|
|
2026-02-10 02:57:41 +00:00
|
|
|
// Scan and add host key to known_hosts.
|
|
|
|
|
parts := strings.Split(host, "@")
|
|
|
|
|
hostname := parts[len(parts)-1]
|
|
|
|
|
|
|
|
|
|
fmt.Printf("Scanning host key for %s... ", hostname)
|
|
|
|
|
scanCmd := exec.Command("ssh-keyscan", "-H", hostname)
|
|
|
|
|
keys, err := scanCmd.Output()
|
|
|
|
|
if err != nil {
|
|
|
|
|
fmt.Println(errorStyle.Render("FAILED"))
|
|
|
|
|
return fmt.Errorf("failed to scan host keys: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
home, _ := os.UserHomeDir()
|
|
|
|
|
knownHostsPath := filepath.Join(home, ".ssh", "known_hosts")
|
|
|
|
|
f, err := os.OpenFile(knownHostsPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("failed to open known_hosts: %w", err)
|
|
|
|
|
}
|
|
|
|
|
if _, err := f.Write(keys); err != nil {
|
|
|
|
|
f.Close()
|
|
|
|
|
return fmt.Errorf("failed to write known_hosts: %w", err)
|
|
|
|
|
}
|
|
|
|
|
f.Close()
|
|
|
|
|
fmt.Println(successStyle.Render("OK"))
|
|
|
|
|
|
|
|
|
|
// Test SSH with strict host key checking.
|
feat(agentci): package dispatch system for multi-agent deployment
Config-driven agent targets replace hardcoded map so new agents
can be added via CLI instead of recompiling. Includes setup script
for bootstrapping agent machines and CLI commands for management.
- Add pkg/agentci with config types and CRUD (LoadAgents, SaveAgent, etc.)
- Add CLI: core ai agent {add,list,status,logs,setup,remove}
- Add scripts/agent-setup.sh (SSH bootstrap: dirs, cron, prereq check)
- Headless loads agents from ~/.core/config.yaml
- Dispatch ticket includes forgejo_user for dynamic clone URLs
- agent-runner.sh reads username from ticket JSON, not hardcoded
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 10:36:23 +00:00
|
|
|
fmt.Printf("Testing SSH to %s... ", host)
|
2026-02-10 02:57:41 +00:00
|
|
|
testCmd := agentci.SecureSSHCommand(host, "echo ok")
|
|
|
|
|
out, err := testCmd.CombinedOutput()
|
feat(agentci): package dispatch system for multi-agent deployment
Config-driven agent targets replace hardcoded map so new agents
can be added via CLI instead of recompiling. Includes setup script
for bootstrapping agent machines and CLI commands for management.
- Add pkg/agentci with config types and CRUD (LoadAgents, SaveAgent, etc.)
- Add CLI: core ai agent {add,list,status,logs,setup,remove}
- Add scripts/agent-setup.sh (SSH bootstrap: dirs, cron, prereq check)
- Headless loads agents from ~/.core/config.yaml
- Dispatch ticket includes forgejo_user for dynamic clone URLs
- agent-runner.sh reads username from ticket JSON, not hardcoded
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 10:36:23 +00:00
|
|
|
if err != nil {
|
|
|
|
|
fmt.Println(errorStyle.Render("FAILED"))
|
|
|
|
|
return fmt.Errorf("SSH failed: %s", strings.TrimSpace(string(out)))
|
|
|
|
|
}
|
|
|
|
|
fmt.Println(successStyle.Render("OK"))
|
|
|
|
|
|
|
|
|
|
cfg, err := loadConfig()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ac := agentci.AgentConfig{
|
|
|
|
|
Host: host,
|
|
|
|
|
QueueDir: queueDir,
|
|
|
|
|
ForgejoUser: forgejoUser,
|
2026-02-10 02:57:41 +00:00
|
|
|
Model: model,
|
|
|
|
|
DualRun: dualRun,
|
feat(agentci): package dispatch system for multi-agent deployment
Config-driven agent targets replace hardcoded map so new agents
can be added via CLI instead of recompiling. Includes setup script
for bootstrapping agent machines and CLI commands for management.
- Add pkg/agentci with config types and CRUD (LoadAgents, SaveAgent, etc.)
- Add CLI: core ai agent {add,list,status,logs,setup,remove}
- Add scripts/agent-setup.sh (SSH bootstrap: dirs, cron, prereq check)
- Headless loads agents from ~/.core/config.yaml
- Dispatch ticket includes forgejo_user for dynamic clone URLs
- agent-runner.sh reads username from ticket JSON, not hardcoded
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 10:36:23 +00:00
|
|
|
Active: true,
|
|
|
|
|
}
|
|
|
|
|
if err := agentci.SaveAgent(cfg, name, ac); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fmt.Printf("Agent %s added (%s)\n", successStyle.Render(name), host)
|
|
|
|
|
return nil
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
cmd.Flags().String("forgejo-user", "", "Forgejo username (defaults to agent name)")
|
|
|
|
|
cmd.Flags().String("queue-dir", "", "Remote queue directory (default: /home/claude/ai-work/queue)")
|
2026-02-10 02:57:41 +00:00
|
|
|
cmd.Flags().String("model", "sonnet", "Primary AI model")
|
|
|
|
|
cmd.Flags().Bool("dual-run", false, "Enable Clotho dual-run verification")
|
feat(agentci): package dispatch system for multi-agent deployment
Config-driven agent targets replace hardcoded map so new agents
can be added via CLI instead of recompiling. Includes setup script
for bootstrapping agent machines and CLI commands for management.
- Add pkg/agentci with config types and CRUD (LoadAgents, SaveAgent, etc.)
- Add CLI: core ai agent {add,list,status,logs,setup,remove}
- Add scripts/agent-setup.sh (SSH bootstrap: dirs, cron, prereq check)
- Headless loads agents from ~/.core/config.yaml
- Dispatch ticket includes forgejo_user for dynamic clone URLs
- agent-runner.sh reads username from ticket JSON, not hardcoded
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 10:36:23 +00:00
|
|
|
return cmd
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func agentListCmd() *cli.Command {
|
|
|
|
|
return &cli.Command{
|
|
|
|
|
Use: "list",
|
|
|
|
|
Short: "List configured agents",
|
|
|
|
|
RunE: func(cmd *cli.Command, args []string) error {
|
|
|
|
|
cfg, err := loadConfig()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
agents, err := agentci.ListAgents(cfg)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if len(agents) == 0 {
|
|
|
|
|
fmt.Println(dimStyle.Render("No agents configured. Use 'core ai agent add' to add one."))
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-10 02:57:41 +00:00
|
|
|
table := cli.NewTable("NAME", "HOST", "MODEL", "DUAL", "ACTIVE", "QUEUE")
|
feat(agentci): package dispatch system for multi-agent deployment
Config-driven agent targets replace hardcoded map so new agents
can be added via CLI instead of recompiling. Includes setup script
for bootstrapping agent machines and CLI commands for management.
- Add pkg/agentci with config types and CRUD (LoadAgents, SaveAgent, etc.)
- Add CLI: core ai agent {add,list,status,logs,setup,remove}
- Add scripts/agent-setup.sh (SSH bootstrap: dirs, cron, prereq check)
- Headless loads agents from ~/.core/config.yaml
- Dispatch ticket includes forgejo_user for dynamic clone URLs
- agent-runner.sh reads username from ticket JSON, not hardcoded
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 10:36:23 +00:00
|
|
|
for name, ac := range agents {
|
|
|
|
|
active := dimStyle.Render("no")
|
|
|
|
|
if ac.Active {
|
|
|
|
|
active = successStyle.Render("yes")
|
|
|
|
|
}
|
2026-02-10 02:57:41 +00:00
|
|
|
dual := dimStyle.Render("no")
|
|
|
|
|
if ac.DualRun {
|
|
|
|
|
dual = successStyle.Render("yes")
|
|
|
|
|
}
|
feat(agentci): package dispatch system for multi-agent deployment
Config-driven agent targets replace hardcoded map so new agents
can be added via CLI instead of recompiling. Includes setup script
for bootstrapping agent machines and CLI commands for management.
- Add pkg/agentci with config types and CRUD (LoadAgents, SaveAgent, etc.)
- Add CLI: core ai agent {add,list,status,logs,setup,remove}
- Add scripts/agent-setup.sh (SSH bootstrap: dirs, cron, prereq check)
- Headless loads agents from ~/.core/config.yaml
- Dispatch ticket includes forgejo_user for dynamic clone URLs
- agent-runner.sh reads username from ticket JSON, not hardcoded
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 10:36:23 +00:00
|
|
|
|
|
|
|
|
// Quick SSH check for queue depth.
|
|
|
|
|
queue := dimStyle.Render("-")
|
2026-02-10 02:57:41 +00:00
|
|
|
checkCmd := agentci.SecureSSHCommand(ac.Host, fmt.Sprintf("ls %s/ticket-*.json 2>/dev/null | wc -l", ac.QueueDir))
|
|
|
|
|
out, err := checkCmd.Output()
|
feat(agentci): package dispatch system for multi-agent deployment
Config-driven agent targets replace hardcoded map so new agents
can be added via CLI instead of recompiling. Includes setup script
for bootstrapping agent machines and CLI commands for management.
- Add pkg/agentci with config types and CRUD (LoadAgents, SaveAgent, etc.)
- Add CLI: core ai agent {add,list,status,logs,setup,remove}
- Add scripts/agent-setup.sh (SSH bootstrap: dirs, cron, prereq check)
- Headless loads agents from ~/.core/config.yaml
- Dispatch ticket includes forgejo_user for dynamic clone URLs
- agent-runner.sh reads username from ticket JSON, not hardcoded
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 10:36:23 +00:00
|
|
|
if err == nil {
|
|
|
|
|
n := strings.TrimSpace(string(out))
|
|
|
|
|
if n != "0" {
|
|
|
|
|
queue = n
|
|
|
|
|
} else {
|
|
|
|
|
queue = "0"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-10 02:57:41 +00:00
|
|
|
table.AddRow(name, ac.Host, ac.Model, dual, active, queue)
|
feat(agentci): package dispatch system for multi-agent deployment
Config-driven agent targets replace hardcoded map so new agents
can be added via CLI instead of recompiling. Includes setup script
for bootstrapping agent machines and CLI commands for management.
- Add pkg/agentci with config types and CRUD (LoadAgents, SaveAgent, etc.)
- Add CLI: core ai agent {add,list,status,logs,setup,remove}
- Add scripts/agent-setup.sh (SSH bootstrap: dirs, cron, prereq check)
- Headless loads agents from ~/.core/config.yaml
- Dispatch ticket includes forgejo_user for dynamic clone URLs
- agent-runner.sh reads username from ticket JSON, not hardcoded
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 10:36:23 +00:00
|
|
|
}
|
|
|
|
|
table.Render()
|
|
|
|
|
return nil
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func agentStatusCmd() *cli.Command {
|
|
|
|
|
return &cli.Command{
|
|
|
|
|
Use: "status <name>",
|
|
|
|
|
Short: "Check agent status via SSH",
|
|
|
|
|
Args: cli.ExactArgs(1),
|
|
|
|
|
RunE: func(cmd *cli.Command, args []string) error {
|
|
|
|
|
name := args[0]
|
|
|
|
|
cfg, err := loadConfig()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
agents, err := agentci.ListAgents(cfg)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
ac, ok := agents[name]
|
|
|
|
|
if !ok {
|
|
|
|
|
return fmt.Errorf("agent %q not found", name)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
script := `
|
|
|
|
|
echo "=== Queue ==="
|
|
|
|
|
ls ~/ai-work/queue/ticket-*.json 2>/dev/null | wc -l
|
|
|
|
|
echo "=== Active ==="
|
|
|
|
|
ls ~/ai-work/active/ticket-*.json 2>/dev/null || echo "none"
|
|
|
|
|
echo "=== Done ==="
|
|
|
|
|
ls ~/ai-work/done/ticket-*.json 2>/dev/null | wc -l
|
|
|
|
|
echo "=== Lock ==="
|
|
|
|
|
if [ -f ~/ai-work/.runner.lock ]; then
|
|
|
|
|
PID=$(cat ~/ai-work/.runner.lock)
|
|
|
|
|
if kill -0 "$PID" 2>/dev/null; then
|
|
|
|
|
echo "RUNNING (PID $PID)"
|
|
|
|
|
else
|
|
|
|
|
echo "STALE (PID $PID)"
|
|
|
|
|
fi
|
|
|
|
|
else
|
|
|
|
|
echo "IDLE"
|
|
|
|
|
fi
|
|
|
|
|
`
|
|
|
|
|
|
2026-02-10 02:57:41 +00:00
|
|
|
sshCmd := agentci.SecureSSHCommand(ac.Host, script)
|
feat(agentci): package dispatch system for multi-agent deployment
Config-driven agent targets replace hardcoded map so new agents
can be added via CLI instead of recompiling. Includes setup script
for bootstrapping agent machines and CLI commands for management.
- Add pkg/agentci with config types and CRUD (LoadAgents, SaveAgent, etc.)
- Add CLI: core ai agent {add,list,status,logs,setup,remove}
- Add scripts/agent-setup.sh (SSH bootstrap: dirs, cron, prereq check)
- Headless loads agents from ~/.core/config.yaml
- Dispatch ticket includes forgejo_user for dynamic clone URLs
- agent-runner.sh reads username from ticket JSON, not hardcoded
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 10:36:23 +00:00
|
|
|
sshCmd.Stdout = os.Stdout
|
|
|
|
|
sshCmd.Stderr = os.Stderr
|
|
|
|
|
return sshCmd.Run()
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func agentLogsCmd() *cli.Command {
|
|
|
|
|
cmd := &cli.Command{
|
|
|
|
|
Use: "logs <name>",
|
|
|
|
|
Short: "Stream agent runner logs",
|
|
|
|
|
Args: cli.ExactArgs(1),
|
|
|
|
|
RunE: func(cmd *cli.Command, args []string) error {
|
|
|
|
|
name := args[0]
|
|
|
|
|
follow, _ := cmd.Flags().GetBool("follow")
|
|
|
|
|
lines, _ := cmd.Flags().GetInt("lines")
|
|
|
|
|
|
|
|
|
|
cfg, err := loadConfig()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
agents, err := agentci.ListAgents(cfg)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
ac, ok := agents[name]
|
|
|
|
|
if !ok {
|
|
|
|
|
return fmt.Errorf("agent %q not found", name)
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-10 02:57:41 +00:00
|
|
|
remoteCmd := fmt.Sprintf("tail -n %d ~/ai-work/logs/runner.log", lines)
|
feat(agentci): package dispatch system for multi-agent deployment
Config-driven agent targets replace hardcoded map so new agents
can be added via CLI instead of recompiling. Includes setup script
for bootstrapping agent machines and CLI commands for management.
- Add pkg/agentci with config types and CRUD (LoadAgents, SaveAgent, etc.)
- Add CLI: core ai agent {add,list,status,logs,setup,remove}
- Add scripts/agent-setup.sh (SSH bootstrap: dirs, cron, prereq check)
- Headless loads agents from ~/.core/config.yaml
- Dispatch ticket includes forgejo_user for dynamic clone URLs
- agent-runner.sh reads username from ticket JSON, not hardcoded
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 10:36:23 +00:00
|
|
|
if follow {
|
2026-02-10 02:57:41 +00:00
|
|
|
remoteCmd = fmt.Sprintf("tail -f -n %d ~/ai-work/logs/runner.log", lines)
|
feat(agentci): package dispatch system for multi-agent deployment
Config-driven agent targets replace hardcoded map so new agents
can be added via CLI instead of recompiling. Includes setup script
for bootstrapping agent machines and CLI commands for management.
- Add pkg/agentci with config types and CRUD (LoadAgents, SaveAgent, etc.)
- Add CLI: core ai agent {add,list,status,logs,setup,remove}
- Add scripts/agent-setup.sh (SSH bootstrap: dirs, cron, prereq check)
- Headless loads agents from ~/.core/config.yaml
- Dispatch ticket includes forgejo_user for dynamic clone URLs
- agent-runner.sh reads username from ticket JSON, not hardcoded
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 10:36:23 +00:00
|
|
|
}
|
|
|
|
|
|
2026-02-10 02:57:41 +00:00
|
|
|
sshCmd := agentci.SecureSSHCommand(ac.Host, remoteCmd)
|
feat(agentci): package dispatch system for multi-agent deployment
Config-driven agent targets replace hardcoded map so new agents
can be added via CLI instead of recompiling. Includes setup script
for bootstrapping agent machines and CLI commands for management.
- Add pkg/agentci with config types and CRUD (LoadAgents, SaveAgent, etc.)
- Add CLI: core ai agent {add,list,status,logs,setup,remove}
- Add scripts/agent-setup.sh (SSH bootstrap: dirs, cron, prereq check)
- Headless loads agents from ~/.core/config.yaml
- Dispatch ticket includes forgejo_user for dynamic clone URLs
- agent-runner.sh reads username from ticket JSON, not hardcoded
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 10:36:23 +00:00
|
|
|
sshCmd.Stdout = os.Stdout
|
|
|
|
|
sshCmd.Stderr = os.Stderr
|
|
|
|
|
sshCmd.Stdin = os.Stdin
|
|
|
|
|
return sshCmd.Run()
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
cmd.Flags().BoolP("follow", "f", false, "Follow log output")
|
|
|
|
|
cmd.Flags().IntP("lines", "n", 50, "Number of lines to show")
|
|
|
|
|
return cmd
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func agentSetupCmd() *cli.Command {
|
|
|
|
|
return &cli.Command{
|
|
|
|
|
Use: "setup <name>",
|
|
|
|
|
Short: "Bootstrap agent machine (create dirs, copy runner, install cron)",
|
|
|
|
|
Args: cli.ExactArgs(1),
|
|
|
|
|
RunE: func(cmd *cli.Command, args []string) error {
|
|
|
|
|
name := args[0]
|
|
|
|
|
cfg, err := loadConfig()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
agents, err := agentci.ListAgents(cfg)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
ac, ok := agents[name]
|
|
|
|
|
if !ok {
|
|
|
|
|
return fmt.Errorf("agent %q not found — use 'core ai agent add' first", name)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Find the setup script relative to the binary or in known locations.
|
|
|
|
|
scriptPath := findSetupScript()
|
|
|
|
|
if scriptPath == "" {
|
|
|
|
|
return fmt.Errorf("agent-setup.sh not found — expected in scripts/ directory")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fmt.Printf("Setting up %s on %s...\n", name, ac.Host)
|
|
|
|
|
setupCmd := exec.Command("bash", scriptPath, ac.Host)
|
|
|
|
|
setupCmd.Stdout = os.Stdout
|
|
|
|
|
setupCmd.Stderr = os.Stderr
|
|
|
|
|
if err := setupCmd.Run(); err != nil {
|
|
|
|
|
return fmt.Errorf("setup failed: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fmt.Println(successStyle.Render("Setup complete!"))
|
|
|
|
|
return nil
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func agentRemoveCmd() *cli.Command {
|
|
|
|
|
return &cli.Command{
|
|
|
|
|
Use: "remove <name>",
|
|
|
|
|
Short: "Remove an agent from config",
|
|
|
|
|
Args: cli.ExactArgs(1),
|
|
|
|
|
RunE: func(cmd *cli.Command, args []string) error {
|
|
|
|
|
name := args[0]
|
|
|
|
|
cfg, err := loadConfig()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := agentci.RemoveAgent(cfg, name); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fmt.Printf("Agent %s removed.\n", name)
|
|
|
|
|
return nil
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// findSetupScript looks for agent-setup.sh in common locations.
|
|
|
|
|
func findSetupScript() string {
|
|
|
|
|
exe, _ := os.Executable()
|
|
|
|
|
if exe != "" {
|
|
|
|
|
dir := filepath.Dir(exe)
|
|
|
|
|
candidates := []string{
|
|
|
|
|
filepath.Join(dir, "scripts", "agent-setup.sh"),
|
|
|
|
|
filepath.Join(dir, "..", "scripts", "agent-setup.sh"),
|
|
|
|
|
}
|
|
|
|
|
for _, c := range candidates {
|
|
|
|
|
if _, err := os.Stat(c); err == nil {
|
|
|
|
|
return c
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cwd, _ := os.Getwd()
|
|
|
|
|
if cwd != "" {
|
|
|
|
|
p := filepath.Join(cwd, "scripts", "agent-setup.sh")
|
|
|
|
|
if _, err := os.Stat(p); err == nil {
|
|
|
|
|
return p
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return ""
|
|
|
|
|
}
|