@@ -0,0 +1,376 @@
1 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // Package shepherd implements a goal-directed agent coordination bot.
2 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ //
3 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // Shepherd monitors channels, tracks agent activity, assigns work from
4 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // configured goal sources, checks in on progress, and reports status.
5 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // It uses an LLM to reason about priorities, detect blockers, and
6 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // generate summaries.
7 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ //
8 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // Commands (via DM or channel mention):
9 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ //
10 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // GOAL <text> — set a goal for the current channel
11 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // STATUS — report progress on current goals
12 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // ASSIGN <nick> <task> — manually assign a task to an agent
13 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // CHECKIN — trigger a check-in round
14 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // PLAN — generate a work plan from current goals
15 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ package shepherd
16 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
17 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ import (
18 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ "context"
19 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ "fmt"
20 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ "log/slog"
21 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ "net"
22 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ "strconv"
23 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ "strings"
24 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ "sync"
25 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ "time"
26 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
27 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ "github.com/lrstanley/girc"
28 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
29 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ "github.com/conflicthq/scuttlebot/internal/bots/cmdparse"
30 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ )
31 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
32 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const defaultNick = "shepherd"
33 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
34 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // LLMProvider calls a language model for reasoning.
35 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ type LLMProvider interface {
36 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ Summarize(ctx context.Context, prompt string) (string, error)
37 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
38 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
39 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // Config controls shepherd's behaviour.
40 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ type Config struct {
41 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ IRCAddr string
42 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ Nick string
43 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ Password string
44 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
45 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // Channels to join and monitor.
46 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ Channels []string
47 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
48 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // ReportChannel is where status reports go (e.g. "#ops").
49 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ ReportChannel string
50 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
51 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // CheckinInterval is how often to check in on agents. 0 = disabled.
52 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ CheckinInterval time.Duration
53 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
54 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // GoalSource is an optional URL for seeding goals (e.g. GitHub milestone).
55 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ GoalSource string
56 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
57 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
58 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // Goal is a tracked objective.
59 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ type Goal struct {
60 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ ID string `json:"id"`
61 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ Channel string `json:"channel"`
62 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ Description string `json:"description"`
63 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ CreatedAt time.Time `json:"created_at"`
64 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ CreatedBy string `json:"created_by"`
65 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ Status string `json:"status"` // "active", "done", "blocked"
66 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
67 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
68 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // Assignment tracks which agent is working on what.
69 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ type Assignment struct {
70 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ Nick string `json:"nick"`
71 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ Task string `json:"task"`
72 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ Channel string `json:"channel"`
73 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ AssignedAt time.Time `json:"assigned_at"`
74 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ LastUpdate time.Time `json:"last_update"`
75 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
76 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
77 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // Bot is the shepherd bot.
78 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ type Bot struct {
79 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ cfg Config
80 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ llm LLMProvider
81 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ log *slog.Logger
82 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ client *girc.Client
83 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
84 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ mu sync.Mutex
85 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ goals map[string][]Goal // channel → goals
86 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ assignments map[string]*Assignment // nick → assignment
87 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ activity map[string]time.Time // nick → last message time
88 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ history map[string][]string // channel → recent messages for LLM context
89 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
90 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
91 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // New creates a shepherd bot.
92 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ func New(cfg Config, llm LLMProvider, log *slog.Logger) *Bot {
93 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if cfg.Nick == "" {
94 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ cfg.Nick = defaultNick
95 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
96 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ return &Bot{
97 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ cfg: cfg,
98 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ llm: llm,
99 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ log: log,
100 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ goals: make(map[string][]Goal),
101 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ assignments: make(map[string]*Assignment),
102 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ activity: make(map[string]time.Time),
103 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ history: make(map[string][]string),
104 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
105 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
106 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
107 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // Name returns the bot's IRC nick.
108 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ func (b *Bot) Name() string { return b.cfg.Nick }
109 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
110 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // Start connects to IRC and begins shepherding. Blocks until ctx is cancelled.
111 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ func (b *Bot) Start(ctx context.Context) error {
112 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ host, port, err := splitHostPort(b.cfg.IRCAddr)
113 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if err != nil {
114 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ return fmt.Errorf("shepherd: %w", err)
115 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
116 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
117 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ c := girc.New(girc.Config{
118 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ Server: host,
119 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ Port: port,
120 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ Nick: b.cfg.Nick,
121 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ User: b.cfg.Nick,
122 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ Name: "scuttlebot shepherd",
123 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ SASL: &girc.SASLPlain{User: b.cfg.Nick, Pass: b.cfg.Password},
124 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ PingDelay: 30 * time.Second,
125 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ PingTimeout: 30 * time.Second,
126 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ })
127 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
128 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ router := cmdparse.NewRouter(b.cfg.Nick)
129 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ router.Register(cmdparse.Command{
130 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ Name: "goal",
131 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ Usage: "GOAL <description>",
132 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ Description: "set a goal for the current channel",
133 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ Handler: func(cmdCtx *cmdparse.Context, args string) string {
134 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ return b.handleGoal(cmdCtx.Channel, cmdCtx.Nick, args)
135 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ },
136 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ })
137 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ router.Register(cmdparse.Command{
138 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ Name: "status",
139 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ Usage: "STATUS",
140 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ Description: "report progress on current goals",
141 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ Handler: func(cmdCtx *cmdparse.Context, args string) string {
142 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ return b.handleStatus(ctx, cmdCtx.Channel)
143 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ },
144 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ })
145 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ router.Register(cmdparse.Command{
146 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ Name: "assign",
147 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ Usage: "ASSIGN <nick> <task>",
148 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ Description: "manually assign a task to an agent",
149 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ Handler: func(cmdCtx *cmdparse.Context, args string) string {
150 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ return b.handleAssign(cmdCtx.Channel, args)
151 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ },
152 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ })
153 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ router.Register(cmdparse.Command{
154 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ Name: "checkin",
155 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ Usage: "CHECKIN",
156 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ Description: "trigger a check-in round with all assigned agents",
157 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ Handler: func(cmdCtx *cmdparse.Context, args string) string {
158 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ b.runCheckin(c)
159 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ return "check-in round started"
160 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ },
161 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ })
162 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ router.Register(cmdparse.Command{
163 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ Name: "plan",
164 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ Usage: "PLAN",
165 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ Description: "generate a work plan from current goals using LLM",
166 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ Handler: func(cmdCtx *cmdparse.Context, args string) string {
167 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ return b.handlePlan(ctx, cmdCtx.Channel)
168 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ },
169 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ })
170 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ router.Register(cmdparse.Command{
171 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ Name: "goals",
172 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ Usage: "GOALS",
173 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ Description: "list all active goals for this channel",
174 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ Handler: func(cmdCtx *cmdparse.Context, args string) string {
175 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ return b.handleListGoals(cmdCtx.Channel)
176 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ },
177 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ })
178 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ router.Register(cmdparse.Command{
179 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ Name: "done",
180 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ Usage: "DONE <goal-id>",
181 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ Description: "mark a goal as completed",
182 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ Handler: func(cmdCtx *cmdparse.Context, args string) string {
183 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ return b.handleDone(cmdCtx.Channel, args)
184 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ },
185 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ })
186 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
187 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ c.Handlers.AddBg(girc.CONNECTED, func(cl *girc.Client, _ girc.Event) {
188 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ cl.Cmd.Mode(cl.GetNick(), "+B")
189 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ for _, ch := range b.cfg.Channels eportChannel)
190 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
191 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }()
192 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if b.cfg.ReportChannel != "" {
193 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ cl.Cmd.Join(b.cfg.ReportChannel)
194 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
195 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if b.log != nil {
196 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ b.log.Info("shepherd connected", "channels", b.cfg.Channels)
197 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
198 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ })
199 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
200 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ c.Handlers.AddBg(girc.INVITE, func(cl *girc.Client, e girc.Event) {
201 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if ch := e.Last(); strings.HasPrefix(ch, "#") {
202 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ cl.Cmd.Join(ch)
203 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
204 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ })
205 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
206 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ c.Handlers.AddBg(girc.PRIVMSG, func(cl *girc.Client, e girc.Event) {
207 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if len(e.Params) < 1 || e.Source == nil {
208 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ return
209 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
210 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ nick := e.Source.Name
211 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ target := e.Params[0]
212 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ text := strings.TrimSpace(e.Last())
213 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
214 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // Track activity.
215 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ b.mu.Lock()
216 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ b.activity[nick] = time.Now()
217 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if strings.HasPrefix(target, "#") {
218 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ hist := b.history[target]
219 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ hist = append(hist, fmt.Sprintf("[%s] %s", nick, text))
220 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if len(hist) > 100 {
221 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ hist = hist[len(hist)-100:]
222 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
223 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ b.history[target] = hist
224 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
225 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ b.mu.Unlock()
226 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
227 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // Dispatch commands.
228 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if reply := router.Dispatch(nick, target, text); reply != nil {
229 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ cl.Cmd.Message(reply.Target, reply.Text)
230 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
231 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ })
232 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
233 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ b.client = c
234 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
235 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // Start periodic check-in if configured.
236 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if b.cfg.CheckinInterval > 0 {
237 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ go b.checkinLoop(ctx, c)
238 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
239 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
240 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ errCh := make(chan error, 1)
241 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ go func() {
242 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if err := c.Connect(); err != nil && ctx.Err() == nil {
243 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ errCh <- err
244 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
245 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }()
246 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
247 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ select {
248 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ case <-ctx.Done():
249 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ c.Close()
250 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ return nil
251 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ case err := <-errCh:
252 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ return fmt.Errorf("shepherd: irc: %w", err)
253 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
254 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
255 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
256 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // Stop disconnects the bot.
257 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ func (b *Bot) Stop() {
258 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if b.client != nil {
259 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ b.client.Close()
260 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
261 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
262 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
263 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // --- Command handlers ---
264 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
265 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ func (b *Bot) handleGoal(channel, nick, desc string) string {
266 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ desc = strings.TrimSpace(desc)
267 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if desc == "" {
268 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ return "usage: GOAL <description>"
269 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
270 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ b.mu.Lock()
271 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ defer b.mu.Unlock()
272 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ id := fmt.Sprintf("G%d", len(b.goals[channel])+1)
273 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ b.goals[channel] = append(b.goals[channel], Goal{
274 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ ID: id,
275 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ Channel: channel,
276 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ Description: desc,
277 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ CreatedAt: time.Now(),
278 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ CreatedBy: nick,
279 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ Status: "active",
280 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ })
281 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ return fmt.Sprintf("goal %s set: %s", id, desc)
282 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
283 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
284 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ func (b *Bot) handleListGoals(channel string) string {
285 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ b.mu.Lock()
286 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ defer b.mu.Unlock()
287 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ goals := b.goals[channel]
288 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if len(goals) == 0 {
289 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ return "no goals set for " + channel
290 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
291 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ var lines []string
292 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ for _, g := range goals {
293 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ lines = append(lines, fmt.Sprintf("[%s] %s (%s) — %s", g.ID, g.Description, g.Status, g.CreatedBy))
294 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
295 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ return strings.Join(lines, " | ")
296 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
297 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
298 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ func (b *Bot) handleDone(channel, goalID string) string {
299 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ goalID = strings.TrimSpace(goalID)
300 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ b.mu.Lock()
301 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ defer b.mu.Unlock()
302 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ for i, g := range b.goals[channel] {
303 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if strings.EqualFold(g.ID, goalID) {
304 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ b.goals[channel][i].Status = "done"
305 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ return fmt.Sprintf("goal %s marked done: %s", g.ID, g.Description)
306 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
307 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
308 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ return fmt.Sprintf("goal %q not found in %s", goalID, channel)
309 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
310 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
311 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ func (b *Bot) handleAssign(channel, args string) string {
312 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ parts := strings.SplitN(strings.TrimSpace(args), " ", 2)
313 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if len(parts) < 2 {
314 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ return "usage: ASSIGN <nick> <task>"
315 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
316 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ nick, task := parts[0], parts[1]
317 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ b.mu.Lock()
318 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ b.assignments[nick] = &Assignment{
319 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ Nick: nick,
320 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ Task: task,
321 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ Channel: channel,
322 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ AssignedAt: time.Now(),
323 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ LastUpdate: time.Now(),
324 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
325 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ b.mu.Unlock()
326 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ return fmt.Sprintf("assigned %s to %s", nick, task)
327 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
328 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
329 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ func (b *Bot) handleStatus(ctx context.Context, channel string) string {
330 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ b.mu.Lock()
331 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ goals := b.goals[channel]
332 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ var active, done int
333 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ for _, g := range goals {
334 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if g.Status == "done" {
335 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ done++
336 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ } else {
337 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ active++
338 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
339 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
340 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ hist := b.history[channel]
341 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ var assignments []string
342 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ for _, a := range b.assignments {
343 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if a.Channel == channel {
344 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ assignments = append(assignments, fmt.Sprintf("%s: %s", a.Nick, a.Task))
345 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
346 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
347 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ b.mu.Unlock()
348 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
349 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ summary := fmt.Sprintf("goals: %d active, %d done", active, done)
350 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if len(assignments) > 0 {
351 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ summary += " | assignments: " + strings.Join(assignments, ", ")
352 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
353 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
354 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // Use LLM for richer summary if available and there's context.
355 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if b.llm != nil && len(hist) > 5 {
356 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ prompt := fmt.Sprintf("Summarize the current status of work in %s. "+
357 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ "Goals: %d active, %d done. Assignments: %s. "+
358 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ "Recent conversation:\n%s\n\n"+
359 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ "Give a brief status report (2-3 sentences).",
360 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ channel, active, done, strings.Join(assignments, ", "),
361 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ strings.Join(hist[max(0, len(hist)-30):], "\n"))
362 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if llmSummary, err := b.llm.Summarize(ctx, prompt); err == nil {
363 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ return llmSummary
364 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
365 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
366 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
367 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ return summary
368 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
369 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
370 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ func (b *Bot) handlePlan(ctx context.Context, channel string) string {
371 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ b.mu.Lock()
372 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ goals := b.goals[channel]
373 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ var goalDescs []string
374 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ for _, g := range goals {
375 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if g.Status == "active" {
376 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ goalDescs =