fix: Address golangci-lint errors (errcheck, gosec) (#76)
Apply PR #76 from dannomayernotabot: - Add golangci exclusions for internal package false positives - Tighten file permissions (0644 -> 0600) for sensitive files - Add ReadHeaderTimeout to HTTP server (slowloris prevention) - Explicit error ignoring with _ = for intentional cases - Add //nolint comments with justifications - Spelling: cancelled -> canceled (US locale) Co-Authored-By: dannomayernotabot <noreply@github.com> 🤖 Generated with Claude Code
This commit is contained in:
+20
-2
@@ -29,6 +29,11 @@ linters:
|
||||
- (os).Chdir
|
||||
- (os).MkdirAll
|
||||
- (fmt).Sscanf
|
||||
# fmt.Fprintf/Fprintln errors are typically safe to ignore for logging
|
||||
- fmt.Fprintf
|
||||
- fmt.Fprintln
|
||||
- (fmt).Fprintf
|
||||
- (fmt).Fprintln
|
||||
misspell:
|
||||
locale: US
|
||||
|
||||
@@ -39,17 +44,30 @@ linters:
|
||||
linters:
|
||||
- gosec
|
||||
text: "G304"
|
||||
# G304: Config/state file loading uses constructed paths, not user input
|
||||
# All internal packages read files from constructed paths, not user input
|
||||
- path: 'internal/'
|
||||
linters:
|
||||
- gosec
|
||||
text: "G304"
|
||||
# G306: File permissions 0644 in tests are acceptable (test fixtures)
|
||||
- path: '_test\.go'
|
||||
linters:
|
||||
- gosec
|
||||
text: "G306"
|
||||
# G302/G306: Non-sensitive operational files (state, config, logs) can use 0644
|
||||
# Internal packages write non-sensitive operational data files
|
||||
- path: 'internal/'
|
||||
linters:
|
||||
- gosec
|
||||
text: "G306|G302"
|
||||
# G302/G306: Directory/file permissions 0700/0750 are acceptable
|
||||
- linters:
|
||||
- gosec
|
||||
text: "G302.*0700|G301.*0750"
|
||||
# G204: Safe subprocess launches with validated arguments (tmux, git, etc.)
|
||||
- path: 'internal/tmux/|internal/git/|internal/cmd/'
|
||||
# G204: Safe subprocess launches with validated arguments (internal tools)
|
||||
# All internal packages use subprocess calls for trusted internal tools
|
||||
- path: 'internal/'
|
||||
linters:
|
||||
- gosec
|
||||
text: 'G204'
|
||||
|
||||
@@ -84,7 +84,7 @@ func (b *Beads) LogDetachAudit(entry DetachAuditEntry) error {
|
||||
}
|
||||
|
||||
// Append to audit log file
|
||||
f, err := os.OpenFile(auditPath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
|
||||
f, err := os.OpenFile(auditPath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600) //nolint:gosec // G304: path is constructed internally
|
||||
if err != nil {
|
||||
return fmt.Errorf("opening audit log: %w", err)
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ func ResolveBeadsDir(workDir string) string {
|
||||
redirectPath := filepath.Join(beadsDir, "redirect")
|
||||
|
||||
// Check for redirect file
|
||||
data, err := os.ReadFile(redirectPath)
|
||||
data, err := os.ReadFile(redirectPath) //nolint:gosec // G304: path is constructed internally
|
||||
if err != nil {
|
||||
// No redirect, use local .beads
|
||||
return beadsDir
|
||||
@@ -229,7 +229,7 @@ func (b *Beads) run(args ...string) ([]byte, error) {
|
||||
// Use --no-daemon for faster read operations (avoids daemon IPC overhead)
|
||||
// The daemon is primarily useful for write coalescing, not reads
|
||||
fullArgs := append([]string{"--no-daemon"}, args...)
|
||||
cmd := exec.Command("bd", fullArgs...)
|
||||
cmd := exec.Command("bd", fullArgs...) //nolint:gosec // G204: bd is a trusted internal tool
|
||||
cmd.Dir = b.workDir
|
||||
|
||||
// Set BEADS_DIR if specified (enables cross-database access)
|
||||
|
||||
@@ -113,7 +113,7 @@ func (c *MoleculeCatalog) Count() int {
|
||||
// Each line should be a JSON object with id, title, and description fields.
|
||||
// The source parameter is added to each loaded molecule.
|
||||
func (c *MoleculeCatalog) LoadFromFile(path, source string) error {
|
||||
file, err := os.Open(path)
|
||||
file, err := os.Open(path) //nolint:gosec // G304: path is from trusted molecule catalog locations
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -111,10 +111,10 @@ func EnsureBdDaemonHealth(workDir string) string {
|
||||
}
|
||||
|
||||
// restartBdDaemons restarts all bd daemons.
|
||||
func restartBdDaemons() error {
|
||||
func restartBdDaemons() error { //nolint:unparam // error return kept for future use
|
||||
// Stop all daemons first
|
||||
stopCmd := exec.Command("bd", "daemon", "killall")
|
||||
stopCmd.Run() // Ignore errors - daemons might not be running
|
||||
_ = stopCmd.Run() // Ignore errors - daemons might not be running
|
||||
|
||||
// Give time for cleanup
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
@@ -136,7 +136,7 @@ func (b *Boot) SaveStatus(status *Status) error {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.WriteFile(b.statusPath(), data, 0644)
|
||||
return os.WriteFile(b.statusPath(), data, 0644) //nolint:gosec // G306: boot status is non-sensitive operational data
|
||||
}
|
||||
|
||||
// LoadStatus loads Boot's last execution status.
|
||||
|
||||
@@ -59,7 +59,7 @@ func Path(polecatDir string) string {
|
||||
func Read(polecatDir string) (*Checkpoint, error) {
|
||||
path := Path(polecatDir)
|
||||
|
||||
data, err := os.ReadFile(path)
|
||||
data, err := os.ReadFile(path) //nolint:gosec // G304: path is constructed from trusted polecatDir
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, nil
|
||||
@@ -96,7 +96,7 @@ func Write(polecatDir string, cp *Checkpoint) error {
|
||||
}
|
||||
|
||||
path := Path(polecatDir)
|
||||
if err := os.WriteFile(path, data, 0644); err != nil {
|
||||
if err := os.WriteFile(path, data, 0600); err != nil {
|
||||
return fmt.Errorf("writing checkpoint: %w", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ func EnsureSettings(workDir string, roleType RoleType) error {
|
||||
}
|
||||
|
||||
// Write settings file
|
||||
if err := os.WriteFile(settingsPath, content, 0644); err != nil {
|
||||
if err := os.WriteFile(settingsPath, content, 0600); err != nil {
|
||||
return fmt.Errorf("writing settings: %w", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -159,7 +159,7 @@ func parseDuration(s string) (time.Duration, error) {
|
||||
}
|
||||
|
||||
// collectGitCommits queries git log for commits by the actor.
|
||||
func collectGitCommits(townRoot, actor string, since time.Time) ([]AuditEntry, error) {
|
||||
func collectGitCommits(townRoot, actor string, since time.Time) ([]AuditEntry, error) { //nolint:unparam // error return kept for future use
|
||||
var entries []AuditEntry
|
||||
|
||||
// Build git log command
|
||||
|
||||
@@ -277,10 +277,7 @@ func runDegradedTriage(b *boot.Boot) (action, target string, err error) {
|
||||
tm := b.Tmux()
|
||||
|
||||
// Check if Deacon session exists
|
||||
deaconSession, err := getDeaconSessionName()
|
||||
if err != nil {
|
||||
return "error", "deacon", fmt.Errorf("getting deacon session name: %w", err)
|
||||
}
|
||||
deaconSession := getDeaconSessionName()
|
||||
hasDeacon, err := tm.HasSession(deaconSession)
|
||||
if err != nil {
|
||||
return "error", "deacon", fmt.Errorf("checking deacon session: %w", err)
|
||||
|
||||
@@ -274,7 +274,7 @@ func classifyCallback(subject string) CallbackType {
|
||||
|
||||
// handlePolecatDone processes a POLECAT_DONE callback.
|
||||
// These come from Witnesses forwarding polecat completion notices.
|
||||
func handlePolecatDone(townRoot string, msg *mail.Message, dryRun bool) (string, error) {
|
||||
func handlePolecatDone(townRoot string, msg *mail.Message, dryRun bool) (string, error) { //nolint:unparam // error return kept for consistency with callback interface
|
||||
matches := patternPolecatDone.FindStringSubmatch(msg.Subject)
|
||||
polecatName := ""
|
||||
if len(matches) > 1 {
|
||||
@@ -306,7 +306,7 @@ func handlePolecatDone(townRoot string, msg *mail.Message, dryRun bool) (string,
|
||||
}
|
||||
|
||||
// handleMergeCompleted processes a merge completion callback from Refinery.
|
||||
func handleMergeCompleted(townRoot string, msg *mail.Message, dryRun bool) (string, error) {
|
||||
func handleMergeCompleted(townRoot string, msg *mail.Message, dryRun bool) (string, error) { //nolint:unparam // error return kept for consistency with callback interface
|
||||
matches := patternMergeCompleted.FindStringSubmatch(msg.Subject)
|
||||
branch := ""
|
||||
if len(matches) > 1 {
|
||||
@@ -353,7 +353,7 @@ func handleMergeCompleted(townRoot string, msg *mail.Message, dryRun bool) (stri
|
||||
}
|
||||
|
||||
// handleMergeRejected processes a merge rejection callback from Refinery.
|
||||
func handleMergeRejected(townRoot string, msg *mail.Message, dryRun bool) (string, error) {
|
||||
func handleMergeRejected(townRoot string, msg *mail.Message, dryRun bool) (string, error) { //nolint:unparam // error return kept for consistency with callback interface
|
||||
matches := patternMergeRejected.FindStringSubmatch(msg.Subject)
|
||||
branch := ""
|
||||
if len(matches) > 1 {
|
||||
|
||||
@@ -23,7 +23,7 @@ import (
|
||||
// generateShortID generates a short random ID (5 lowercase chars).
|
||||
func generateShortID() string {
|
||||
b := make([]byte, 3)
|
||||
rand.Read(b)
|
||||
_, _ = rand.Read(b)
|
||||
return strings.ToLower(base32.StdEncoding.EncodeToString(b)[:5])
|
||||
}
|
||||
|
||||
|
||||
@@ -202,7 +202,7 @@ func attachToTmuxSession(sessionID string) error {
|
||||
// ensureMainBranch checks if a git directory is on main branch.
|
||||
// If not, warns the user and offers to switch.
|
||||
// Returns true if on main (or switched to main), false if user declined.
|
||||
func ensureMainBranch(dir, roleName string) bool {
|
||||
func ensureMainBranch(dir, roleName string) bool { //nolint:unparam // bool return kept for future callers to check
|
||||
g := git.NewGit(dir)
|
||||
|
||||
branch, err := g.CurrentBranch()
|
||||
@@ -271,7 +271,7 @@ func parseCrewSessionName(sessionName string) (rigName, crewName string, ok bool
|
||||
|
||||
// findRigCrewSessions returns all crew sessions for a given rig, sorted alphabetically.
|
||||
// Uses tmux list-sessions to find sessions matching gt-<rig>-crew-* pattern.
|
||||
func findRigCrewSessions(rigName string) ([]string, error) {
|
||||
func findRigCrewSessions(rigName string) ([]string, error) { //nolint:unparam // error return kept for future use
|
||||
cmd := exec.Command("tmux", "list-sessions", "-F", "#{session_name}")
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
|
||||
@@ -337,7 +337,7 @@ func runCrewRestart(cmd *cobra.Command, args []string) error {
|
||||
}
|
||||
|
||||
// Set environment
|
||||
t.SetEnvironment(sessionID, "GT_ROLE", "crew")
|
||||
_ = t.SetEnvironment(sessionID, "GT_ROLE", "crew")
|
||||
// Apply rig-based theming (non-fatal: theming failure doesn't affect operation)
|
||||
theme := getThemeForRig(r.Name)
|
||||
_ = t.ConfigureGasTownSession(sessionID, theme, r.Name, name, "crew")
|
||||
@@ -596,7 +596,7 @@ func runCrewStop(cmd *cobra.Command, args []string) error {
|
||||
if townRoot != "" {
|
||||
agent := fmt.Sprintf("%s/crew/%s", r.Name, name)
|
||||
logger := townlog.NewLogger(townRoot)
|
||||
logger.Log(townlog.EventKill, agent, "gt crew stop")
|
||||
_ = logger.Log(townlog.EventKill, agent, "gt crew stop")
|
||||
}
|
||||
|
||||
// Log captured output (truncated)
|
||||
@@ -682,7 +682,7 @@ func runCrewStopAll() error {
|
||||
townRoot, _ := workspace.FindFromCwd()
|
||||
if townRoot != "" {
|
||||
logger := townlog.NewLogger(townRoot)
|
||||
logger.Log(townlog.EventKill, agentName, "gt crew stop --all")
|
||||
_ = logger.Log(townlog.EventKill, agentName, "gt crew stop --all")
|
||||
}
|
||||
|
||||
// Log captured output (truncated)
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"net/http"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/steveyegge/gastown/internal/web"
|
||||
@@ -67,11 +68,16 @@ func runDashboard(cmd *cobra.Command, args []string) error {
|
||||
go openBrowser(url)
|
||||
}
|
||||
|
||||
// Start the server
|
||||
// Start the server with timeouts
|
||||
fmt.Printf("🚚 Gas Town Dashboard starting at %s\n", url)
|
||||
fmt.Printf(" Press Ctrl+C to stop\n")
|
||||
|
||||
return http.ListenAndServe(fmt.Sprintf(":%d", dashboardPort), handler)
|
||||
server := &http.Server{
|
||||
Addr: fmt.Sprintf(":%d", dashboardPort),
|
||||
Handler: handler,
|
||||
ReadHeaderTimeout: 10 * time.Second,
|
||||
}
|
||||
return server.ListenAndServe()
|
||||
}
|
||||
|
||||
// openBrowser opens the specified URL in the default browser.
|
||||
|
||||
+9
-24
@@ -22,8 +22,8 @@ import (
|
||||
)
|
||||
|
||||
// getDeaconSessionName returns the Deacon session name.
|
||||
func getDeaconSessionName() (string, error) {
|
||||
return session.DeaconSessionName(), nil
|
||||
func getDeaconSessionName() string {
|
||||
return session.DeaconSessionName()
|
||||
}
|
||||
|
||||
var deaconCmd = &cobra.Command{
|
||||
@@ -276,10 +276,7 @@ func init() {
|
||||
func runDeaconStart(cmd *cobra.Command, args []string) error {
|
||||
t := tmux.NewTmux()
|
||||
|
||||
sessionName, err := getDeaconSessionName()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sessionName := getDeaconSessionName()
|
||||
|
||||
// Check if session already exists
|
||||
running, err := t.HasSession(sessionName)
|
||||
@@ -370,10 +367,7 @@ func startDeaconSession(t *tmux.Tmux, sessionName string) error {
|
||||
func runDeaconStop(cmd *cobra.Command, args []string) error {
|
||||
t := tmux.NewTmux()
|
||||
|
||||
sessionName, err := getDeaconSessionName()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sessionName := getDeaconSessionName()
|
||||
|
||||
// Check if session exists
|
||||
running, err := t.HasSession(sessionName)
|
||||
@@ -402,10 +396,7 @@ func runDeaconStop(cmd *cobra.Command, args []string) error {
|
||||
func runDeaconAttach(cmd *cobra.Command, args []string) error {
|
||||
t := tmux.NewTmux()
|
||||
|
||||
sessionName, err := getDeaconSessionName()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sessionName := getDeaconSessionName()
|
||||
|
||||
// Check if session exists
|
||||
running, err := t.HasSession(sessionName)
|
||||
@@ -428,10 +419,7 @@ func runDeaconAttach(cmd *cobra.Command, args []string) error {
|
||||
func runDeaconStatus(cmd *cobra.Command, args []string) error {
|
||||
t := tmux.NewTmux()
|
||||
|
||||
sessionName, err := getDeaconSessionName()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sessionName := getDeaconSessionName()
|
||||
|
||||
running, err := t.HasSession(sessionName)
|
||||
if err != nil {
|
||||
@@ -470,10 +458,7 @@ func runDeaconStatus(cmd *cobra.Command, args []string) error {
|
||||
func runDeaconRestart(cmd *cobra.Command, args []string) error {
|
||||
t := tmux.NewTmux()
|
||||
|
||||
sessionName, err := getDeaconSessionName()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sessionName := getDeaconSessionName()
|
||||
|
||||
running, err := t.HasSession(sessionName)
|
||||
if err != nil {
|
||||
@@ -1092,7 +1077,7 @@ func getPolecatStaleness(polecatPath string) time.Duration {
|
||||
}
|
||||
|
||||
// nukeZombie cleans up a zombie polecat.
|
||||
func nukeZombie(townRoot string, z zombieInfo, t *tmux.Tmux) error {
|
||||
func nukeZombie(townRoot string, z zombieInfo, t *tmux.Tmux) error { //nolint:unparam // error return kept for future use
|
||||
// Step 1: Kill tmux session if somehow still exists
|
||||
if exists, _ := t.HasSession(z.sessionName); exists {
|
||||
_ = t.KillSession(z.sessionName)
|
||||
@@ -1204,7 +1189,7 @@ func sendMail(townRoot, to, subject, body string) {
|
||||
}
|
||||
|
||||
// updateAgentBeadState updates an agent bead's state.
|
||||
func updateAgentBeadState(townRoot, agent, state, reason string) {
|
||||
func updateAgentBeadState(townRoot, agent, state, _ string) { // reason unused but kept for API consistency
|
||||
beadID, _, err := agentAddressToIDs(agent)
|
||||
if err != nil {
|
||||
return
|
||||
|
||||
@@ -326,7 +326,7 @@ func runDone(cmd *cobra.Command, args []string) error {
|
||||
}
|
||||
|
||||
// Log done event (townlog and activity feed)
|
||||
LogDone(townRoot, sender, issueID)
|
||||
_ = LogDone(townRoot, sender, issueID)
|
||||
_ = events.LogFeed(events.TypeDone, sender, events.DonePayload(issueID, branch))
|
||||
|
||||
// Update agent bead state (ZFC: self-report completion)
|
||||
@@ -352,7 +352,7 @@ func runDone(cmd *cobra.Command, args []string) error {
|
||||
// - PHASE_COMPLETE → "awaiting-gate"
|
||||
//
|
||||
// Also self-reports cleanup_status for ZFC compliance (#10).
|
||||
func updateAgentStateOnDone(cwd, townRoot, exitType, issueID string) {
|
||||
func updateAgentStateOnDone(cwd, townRoot, exitType, _ string) { // issueID unused but kept for future audit logging
|
||||
// Get role context
|
||||
roleInfo, err := GetRoleWithContext(cwd, townRoot)
|
||||
if err != nil {
|
||||
|
||||
@@ -74,8 +74,8 @@ func runDown(cmd *cobra.Command, args []string) error {
|
||||
}
|
||||
|
||||
// Get session names
|
||||
mayorSession, _ := getMayorSessionName()
|
||||
deaconSession, _ := getDeaconSessionName()
|
||||
mayorSession := getMayorSessionName()
|
||||
deaconSession := getDeaconSessionName()
|
||||
|
||||
// 2. Stop Mayor
|
||||
if err := stopSession(t, mayorSession); err != nil {
|
||||
|
||||
@@ -226,7 +226,7 @@ func runFeedTUI(workDir string) error {
|
||||
|
||||
// Combine all sources
|
||||
multiSource := feed.NewMultiSource(sources...)
|
||||
defer multiSource.Close()
|
||||
defer func() { _ = multiSource.Close() }()
|
||||
|
||||
// Create model and connect event source
|
||||
m := feed.NewModel()
|
||||
@@ -305,7 +305,7 @@ func runFeedInWindow(workDir string, bdArgs []string) error {
|
||||
|
||||
// windowExists checks if a window with the given name exists in the session.
|
||||
// Note: getCurrentTmuxSession is defined in handoff.go
|
||||
func windowExists(t *tmux.Tmux, session, windowName string) (bool, error) {
|
||||
func windowExists(_ *tmux.Tmux, session, windowName string) (bool, error) { // t unused: direct exec for simplicity
|
||||
cmd := exec.Command("tmux", "list-windows", "-t", session, "-F", "#{window_name}")
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
@@ -321,14 +321,14 @@ func windowExists(t *tmux.Tmux, session, windowName string) (bool, error) {
|
||||
}
|
||||
|
||||
// createWindow creates a new tmux window with the given name and command.
|
||||
func createWindow(t *tmux.Tmux, session, windowName, workDir, command string) error {
|
||||
func createWindow(_ *tmux.Tmux, session, windowName, workDir, command string) error { // t unused: direct exec for simplicity
|
||||
args := []string{"new-window", "-t", session, "-n", windowName, "-c", workDir, command}
|
||||
cmd := exec.Command("tmux", args...)
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
// selectWindow switches to the specified window.
|
||||
func selectWindow(t *tmux.Tmux, target string) error {
|
||||
func selectWindow(_ *tmux.Tmux, target string) error { // t unused: direct exec for simplicity
|
||||
cmd := exec.Command("tmux", "select-window", "-t", target)
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
@@ -683,7 +683,7 @@ func extractPrompts(content string) map[string]string {
|
||||
// generateFormulaShortID generates a short random ID (5 lowercase chars)
|
||||
func generateFormulaShortID() string {
|
||||
b := make([]byte, 3)
|
||||
rand.Read(b)
|
||||
_, _ = rand.Read(b)
|
||||
return strings.ToLower(base32.StdEncoding.EncodeToString(b)[:5])
|
||||
}
|
||||
|
||||
|
||||
+5
-13
@@ -155,7 +155,7 @@ func runHandoff(cmd *cobra.Command, args []string) error {
|
||||
if agent == "" {
|
||||
agent = currentSession
|
||||
}
|
||||
LogHandoff(townRoot, agent, handoffSubject)
|
||||
_ = LogHandoff(townRoot, agent, handoffSubject)
|
||||
// Also log to activity feed
|
||||
_ = events.LogFeed(events.TypeHandoff, agent, events.HandoffPayload(handoffSubject, true))
|
||||
}
|
||||
@@ -230,18 +230,10 @@ func resolveRoleToSession(role string) (string, error) {
|
||||
|
||||
switch strings.ToLower(role) {
|
||||
case "mayor", "may":
|
||||
mayorSession, err := getMayorSessionName()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cannot determine mayor session name: %w", err)
|
||||
}
|
||||
return mayorSession, nil
|
||||
return getMayorSessionName(), nil
|
||||
|
||||
case "deacon", "dea":
|
||||
deaconSession, err := getDeaconSessionName()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cannot determine deacon session name: %w", err)
|
||||
}
|
||||
return deaconSession, nil
|
||||
return getDeaconSessionName(), nil
|
||||
|
||||
case "crew":
|
||||
// Try to get rig and crew name from environment or cwd
|
||||
@@ -369,8 +361,8 @@ func buildRestartCommand(sessionName string) (string, error) {
|
||||
// This is the canonical home for each role type.
|
||||
func sessionWorkDir(sessionName, townRoot string) (string, error) {
|
||||
// Get session names for comparison
|
||||
mayorSession, _ := getMayorSessionName()
|
||||
deaconSession, _ := getDeaconSessionName()
|
||||
mayorSession := getMayorSessionName()
|
||||
deaconSession := getDeaconSessionName()
|
||||
|
||||
switch {
|
||||
case sessionName == mayorSession:
|
||||
|
||||
@@ -114,7 +114,7 @@ func runHookOrStatus(cmd *cobra.Command, args []string) error {
|
||||
return runHook(cmd, args)
|
||||
}
|
||||
|
||||
func runHook(cmd *cobra.Command, args []string) error {
|
||||
func runHook(_ *cobra.Command, args []string) error {
|
||||
beadID := args[0]
|
||||
|
||||
// Polecats cannot hook - they use gt done for lifecycle
|
||||
|
||||
@@ -122,10 +122,7 @@ func detectCurrentSession() string {
|
||||
|
||||
// Check if we're mayor
|
||||
if os.Getenv("GT_ROLE") == "mayor" {
|
||||
mayorSession, err := getMayorSessionName()
|
||||
if err == nil {
|
||||
return mayorSession
|
||||
}
|
||||
return getMayorSessionName()
|
||||
}
|
||||
|
||||
return ""
|
||||
|
||||
@@ -433,7 +433,7 @@ func init() {
|
||||
// Reply flags
|
||||
mailReplyCmd.Flags().StringVarP(&mailReplySubject, "subject", "s", "", "Override reply subject (default: Re: <original>)")
|
||||
mailReplyCmd.Flags().StringVarP(&mailReplyMessage, "message", "m", "", "Reply message body (required)")
|
||||
mailReplyCmd.MarkFlagRequired("message")
|
||||
_ = mailReplyCmd.MarkFlagRequired("message")
|
||||
|
||||
// Search flags
|
||||
mailSearchCmd.Flags().StringVar(&mailSearchFrom, "from", "", "Filter by sender address")
|
||||
|
||||
+7
-22
@@ -15,8 +15,8 @@ import (
|
||||
)
|
||||
|
||||
// getMayorSessionName returns the Mayor session name.
|
||||
func getMayorSessionName() (string, error) {
|
||||
return session.MayorSessionName(), nil
|
||||
func getMayorSessionName() string {
|
||||
return session.MayorSessionName()
|
||||
}
|
||||
|
||||
var mayorCmd = &cobra.Command{
|
||||
@@ -90,10 +90,7 @@ func init() {
|
||||
func runMayorStart(cmd *cobra.Command, args []string) error {
|
||||
t := tmux.NewTmux()
|
||||
|
||||
sessionName, err := getMayorSessionName()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sessionName := getMayorSessionName()
|
||||
|
||||
// Check if session already exists
|
||||
running, err := t.HasSession(sessionName)
|
||||
@@ -172,10 +169,7 @@ func startMayorSession(t *tmux.Tmux, sessionName string) error {
|
||||
func runMayorStop(cmd *cobra.Command, args []string) error {
|
||||
t := tmux.NewTmux()
|
||||
|
||||
sessionName, err := getMayorSessionName()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sessionName := getMayorSessionName()
|
||||
|
||||
// Check if session exists
|
||||
running, err := t.HasSession(sessionName)
|
||||
@@ -204,10 +198,7 @@ func runMayorStop(cmd *cobra.Command, args []string) error {
|
||||
func runMayorAttach(cmd *cobra.Command, args []string) error {
|
||||
t := tmux.NewTmux()
|
||||
|
||||
sessionName, err := getMayorSessionName()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sessionName := getMayorSessionName()
|
||||
|
||||
// Check if session exists
|
||||
running, err := t.HasSession(sessionName)
|
||||
@@ -229,10 +220,7 @@ func runMayorAttach(cmd *cobra.Command, args []string) error {
|
||||
func runMayorStatus(cmd *cobra.Command, args []string) error {
|
||||
t := tmux.NewTmux()
|
||||
|
||||
sessionName, err := getMayorSessionName()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sessionName := getMayorSessionName()
|
||||
|
||||
running, err := t.HasSession(sessionName)
|
||||
if err != nil {
|
||||
@@ -271,10 +259,7 @@ func runMayorStatus(cmd *cobra.Command, args []string) error {
|
||||
func runMayorRestart(cmd *cobra.Command, args []string) error {
|
||||
t := tmux.NewTmux()
|
||||
|
||||
sessionName, err := getMayorSessionName()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sessionName := getMayorSessionName()
|
||||
|
||||
running, err := t.HasSession(sessionName)
|
||||
if err != nil {
|
||||
|
||||
@@ -235,7 +235,7 @@ func calculateEffectiveTimeout(idleCycles int) (time.Duration, error) {
|
||||
}
|
||||
|
||||
// waitForActivitySignal starts bd activity --follow and waits for any output.
|
||||
// Returns immediately when a line is received, or when context is cancelled.
|
||||
// Returns immediately when a line is received, or when context is canceled.
|
||||
func waitForActivitySignal(ctx context.Context, workDir string) (*AwaitSignalResult, error) {
|
||||
// Start bd activity --follow
|
||||
cmd := exec.CommandContext(ctx, "bd", "activity", "--follow")
|
||||
|
||||
@@ -250,7 +250,7 @@ func findNextReadyStep(b *beads.Beads, moleculeID string) (*beads.Issue, bool, e
|
||||
}
|
||||
|
||||
// handleStepContinue handles continuing to the next step.
|
||||
func handleStepContinue(cwd, townRoot, workDir string, nextStep *beads.Issue, dryRun bool) error {
|
||||
func handleStepContinue(cwd, townRoot, _ string, nextStep *beads.Issue, dryRun bool) error { // workDir unused but kept for signature consistency
|
||||
fmt.Printf("\n%s Next step: %s\n", style.Bold.Render("→"), nextStep.ID)
|
||||
fmt.Printf(" %s\n", nextStep.Title)
|
||||
|
||||
|
||||
@@ -163,7 +163,7 @@ func runNudge(cmd *cobra.Command, args []string) error {
|
||||
|
||||
// Log nudge event
|
||||
if townRoot, err := workspace.FindFromCwd(); err == nil && townRoot != "" {
|
||||
LogNudge(townRoot, "deacon", message)
|
||||
_ = LogNudge(townRoot, "deacon", message)
|
||||
}
|
||||
_ = events.LogFeed(events.TypeNudge, sender, events.NudgePayload("", "deacon", message))
|
||||
return nil
|
||||
@@ -202,7 +202,7 @@ func runNudge(cmd *cobra.Command, args []string) error {
|
||||
|
||||
// Log nudge event
|
||||
if townRoot, err := workspace.FindFromCwd(); err == nil && townRoot != "" {
|
||||
LogNudge(townRoot, target, message)
|
||||
_ = LogNudge(townRoot, target, message)
|
||||
}
|
||||
_ = events.LogFeed(events.TypeNudge, sender, events.NudgePayload(rigName, target, message))
|
||||
} else {
|
||||
@@ -223,7 +223,7 @@ func runNudge(cmd *cobra.Command, args []string) error {
|
||||
|
||||
// Log nudge event
|
||||
if townRoot, err := workspace.FindFromCwd(); err == nil && townRoot != "" {
|
||||
LogNudge(townRoot, target, message)
|
||||
_ = LogNudge(townRoot, target, message)
|
||||
}
|
||||
_ = events.LogFeed(events.TypeNudge, sender, events.NudgePayload("", target, message))
|
||||
}
|
||||
@@ -424,7 +424,7 @@ func resolveNudgePattern(pattern string, agents []*AgentSession) []string {
|
||||
// Returns (shouldSend bool, level string, err error).
|
||||
// If force is true, always returns true.
|
||||
// If the agent bead cannot be found, returns true (fail-open for backward compatibility).
|
||||
func shouldNudgeTarget(townRoot, targetAddress string, force bool) (bool, string, error) {
|
||||
func shouldNudgeTarget(townRoot, targetAddress string, force bool) (bool, string, error) { //nolint:unparam // error return kept for future use
|
||||
if force {
|
||||
return true, "", nil
|
||||
}
|
||||
|
||||
@@ -82,15 +82,15 @@ func cyclePolecatSession(direction int, sessionOverride string) error {
|
||||
// parsePolecatSessionName extracts rig and polecat name from a tmux session name.
|
||||
// Format: gt-<rig>-<name> where name is NOT crew-*, witness, or refinery.
|
||||
// Returns empty strings and false if the format doesn't match.
|
||||
func parsePolecatSessionName(sessionName string) (rigName, polecatName string, ok bool) {
|
||||
func parsePolecatSessionName(sessionName string) (rigName, polecatName string, ok bool) { //nolint:unparam // polecatName kept for API consistency
|
||||
// Must start with "gt-"
|
||||
if !strings.HasPrefix(sessionName, "gt-") {
|
||||
return "", "", false
|
||||
}
|
||||
|
||||
// Exclude town-level sessions by exact match
|
||||
mayorSession, _ := getMayorSessionName()
|
||||
deaconSession, _ := getDeaconSessionName()
|
||||
mayorSession := getMayorSessionName()
|
||||
deaconSession := getDeaconSessionName()
|
||||
if sessionName == mayorSession || sessionName == deaconSession {
|
||||
return "", "", false
|
||||
}
|
||||
@@ -133,7 +133,7 @@ func parsePolecatSessionName(sessionName string) (rigName, polecatName string, o
|
||||
// findRigPolecatSessions returns all polecat sessions for a given rig.
|
||||
// Uses tmux list-sessions to find sessions matching gt-<rig>-<name> pattern,
|
||||
// excluding crew, witness, and refinery sessions.
|
||||
func findRigPolecatSessions(rigName string) ([]string, error) {
|
||||
func findRigPolecatSessions(rigName string) ([]string, error) { //nolint:unparam // error return kept for future use
|
||||
cmd := exec.Command("tmux", "list-sessions", "-F", "#{session_name}")
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
|
||||
@@ -101,8 +101,8 @@ func runPrime(cmd *cobra.Command, args []string) error {
|
||||
persistSessionID(cwd, sessionID)
|
||||
}
|
||||
// Set environment for this process (affects event emission below)
|
||||
os.Setenv("GT_SESSION_ID", sessionID)
|
||||
os.Setenv("CLAUDE_SESSION_ID", sessionID) // Legacy compatibility
|
||||
_ = os.Setenv("GT_SESSION_ID", sessionID)
|
||||
_ = os.Setenv("CLAUDE_SESSION_ID", sessionID) // Legacy compatibility
|
||||
// Output session beacon
|
||||
fmt.Printf("[session:%s]\n", sessionID)
|
||||
if source != "" {
|
||||
@@ -1564,7 +1564,7 @@ func emitSessionEvent(ctx RoleContext) {
|
||||
|
||||
// Emit the event
|
||||
payload := events.SessionPayload(sessionID, actor, topic, ctx.WorkDir)
|
||||
events.LogFeed(events.TypeSessionStart, actor, payload)
|
||||
_ = events.LogFeed(events.TypeSessionStart, actor, payload)
|
||||
}
|
||||
|
||||
// outputSessionMetadata prints a structured metadata line for seance discovery.
|
||||
|
||||
@@ -278,7 +278,7 @@ func runSessionStart(cmd *cobra.Command, args []string) error {
|
||||
if townRoot, err := workspace.FindFromCwd(); err == nil && townRoot != "" {
|
||||
agent := fmt.Sprintf("%s/%s", rigName, polecatName)
|
||||
logger := townlog.NewLogger(townRoot)
|
||||
logger.Log(townlog.EventWake, agent, sessionIssue)
|
||||
_ = logger.Log(townlog.EventWake, agent, sessionIssue)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -314,7 +314,7 @@ func runSessionStop(cmd *cobra.Command, args []string) error {
|
||||
reason = "gt session stop --force"
|
||||
}
|
||||
logger := townlog.NewLogger(townRoot)
|
||||
logger.Log(townlog.EventKill, agent, reason)
|
||||
_ = logger.Log(townlog.EventKill, agent, reason)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -904,7 +904,7 @@ func runSlingFormula(args []string) error {
|
||||
// requires cross-database access (agent in rig db, hook bead in town db), but
|
||||
// bd slot set has a bug where it doesn't support this. See BD_BUG_AGENT_STATE_ROUTING.md.
|
||||
// The work is still correctly attached via `bd update <bead> --assignee=<agent>`.
|
||||
func updateAgentHookBead(agentID, beadID, workDir, townBeadsDir string) {
|
||||
func updateAgentHookBead(agentID, _, workDir, townBeadsDir string) { // beadID unused due to BD_BUG_AGENT_STATE_ROUTING
|
||||
_ = townBeadsDir // Not used - BEADS_DIR breaks redirect mechanism
|
||||
|
||||
// Convert agent ID to agent bead ID
|
||||
@@ -1166,7 +1166,7 @@ func generateDogName(mgr *dog.Manager) string {
|
||||
// slingGenerateShortID generates a short random ID (5 lowercase chars).
|
||||
func slingGenerateShortID() string {
|
||||
b := make([]byte, 3)
|
||||
rand.Read(b)
|
||||
_, _ = rand.Read(b)
|
||||
return strings.ToLower(base32.StdEncoding.EncodeToString(b)[:5])
|
||||
}
|
||||
|
||||
|
||||
+15
-21
@@ -179,14 +179,8 @@ func runStart(cmd *cobra.Command, args []string) error {
|
||||
// startCoreAgents starts Mayor and Deacon sessions.
|
||||
func startCoreAgents(t *tmux.Tmux) error {
|
||||
// Get session names
|
||||
mayorSession, err := getMayorSessionName()
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting Mayor session name: %w", err)
|
||||
}
|
||||
deaconSession, err := getDeaconSessionName()
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting Deacon session name: %w", err)
|
||||
}
|
||||
mayorSession := getMayorSessionName()
|
||||
deaconSession := getDeaconSessionName()
|
||||
|
||||
// Start Mayor first (so Deacon sees it as up)
|
||||
mayorRunning, _ := t.HasSession(mayorSession)
|
||||
@@ -335,15 +329,15 @@ func ensureRefinerySession(rigName string, r *rig.Rig) (bool, error) {
|
||||
|
||||
// Set environment
|
||||
bdActor := fmt.Sprintf("%s/refinery", rigName)
|
||||
t.SetEnvironment(sessionName, "GT_ROLE", "refinery")
|
||||
t.SetEnvironment(sessionName, "GT_RIG", rigName)
|
||||
t.SetEnvironment(sessionName, "BD_ACTOR", bdActor)
|
||||
_ = t.SetEnvironment(sessionName, "GT_ROLE", "refinery")
|
||||
_ = t.SetEnvironment(sessionName, "GT_RIG", rigName)
|
||||
_ = t.SetEnvironment(sessionName, "BD_ACTOR", bdActor)
|
||||
|
||||
// Set beads environment
|
||||
beadsDir := filepath.Join(r.Path, "mayor", "rig", ".beads")
|
||||
t.SetEnvironment(sessionName, "BEADS_DIR", beadsDir)
|
||||
t.SetEnvironment(sessionName, "BEADS_NO_DAEMON", "1")
|
||||
t.SetEnvironment(sessionName, "BEADS_AGENT_NAME", fmt.Sprintf("%s/refinery", rigName))
|
||||
_ = t.SetEnvironment(sessionName, "BEADS_DIR", beadsDir)
|
||||
_ = t.SetEnvironment(sessionName, "BEADS_NO_DAEMON", "1")
|
||||
_ = t.SetEnvironment(sessionName, "BEADS_AGENT_NAME", fmt.Sprintf("%s/refinery", rigName))
|
||||
|
||||
// Apply Gas Town theming (non-fatal: theming failure doesn't affect operation)
|
||||
theme := tmux.AssignTheme(rigName)
|
||||
@@ -391,8 +385,8 @@ func runShutdown(cmd *cobra.Command, args []string) error {
|
||||
}
|
||||
|
||||
// Get session names for categorization
|
||||
mayorSession, _ := getMayorSessionName()
|
||||
deaconSession, _ := getDeaconSessionName()
|
||||
mayorSession := getMayorSessionName()
|
||||
deaconSession := getDeaconSessionName()
|
||||
toStop, preserved := categorizeSessions(sessions, mayorSession, deaconSession)
|
||||
|
||||
if len(toStop) == 0 {
|
||||
@@ -421,7 +415,7 @@ func runShutdown(cmd *cobra.Command, args []string) error {
|
||||
response, _ := reader.ReadString('\n')
|
||||
response = strings.TrimSpace(strings.ToLower(response))
|
||||
if response != "y" && response != "yes" {
|
||||
fmt.Println("Shutdown cancelled.")
|
||||
fmt.Println("Shutdown canceled.")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -515,8 +509,8 @@ func runGracefulShutdown(t *tmux.Tmux, gtSessions []string, townRoot string) err
|
||||
|
||||
// Phase 4: Kill sessions in correct order
|
||||
fmt.Printf("\nPhase 4: Terminating sessions...\n")
|
||||
mayorSession, _ := getMayorSessionName()
|
||||
deaconSession, _ := getDeaconSessionName()
|
||||
mayorSession := getMayorSessionName()
|
||||
deaconSession := getDeaconSessionName()
|
||||
stopped := killSessionsInOrder(t, gtSessions, mayorSession, deaconSession)
|
||||
|
||||
// Phase 5: Cleanup polecat worktrees and branches
|
||||
@@ -533,8 +527,8 @@ func runGracefulShutdown(t *tmux.Tmux, gtSessions []string, townRoot string) err
|
||||
func runImmediateShutdown(t *tmux.Tmux, gtSessions []string, townRoot string) error {
|
||||
fmt.Println("Shutting down Gas Town...")
|
||||
|
||||
mayorSession, _ := getMayorSessionName()
|
||||
deaconSession, _ := getDeaconSessionName()
|
||||
mayorSession := getMayorSessionName()
|
||||
deaconSession := getDeaconSessionName()
|
||||
stopped := killSessionsInOrder(t, gtSessions, mayorSession, deaconSession)
|
||||
|
||||
// Cleanup polecat worktrees and branches
|
||||
|
||||
@@ -469,7 +469,7 @@ func outputStatusText(status TownStatus) error {
|
||||
}
|
||||
|
||||
// renderAgentDetails renders full agent bead details
|
||||
func renderAgentDetails(agent AgentRuntime, indent string, hooks []AgentHookInfo, townRoot string) {
|
||||
func renderAgentDetails(agent AgentRuntime, indent string, hooks []AgentHookInfo, townRoot string) { //nolint:unparam // indent kept for future customization
|
||||
// Line 1: Agent bead ID + status
|
||||
// Reconcile bead state with tmux session state to surface mismatches
|
||||
// States: "running" (active), "idle" (waiting), "stopped", "dead", etc.
|
||||
@@ -646,8 +646,8 @@ func discoverRigHooks(r *rig.Rig, crews []string) []AgentHookInfo {
|
||||
// allHookBeads is a preloaded map of hook beads for O(1) lookup.
|
||||
func discoverGlobalAgents(allSessions map[string]bool, allAgentBeads map[string]*beads.Issue, allHookBeads map[string]*beads.Issue, mailRouter *mail.Router, skipMail bool) []AgentRuntime {
|
||||
// Get session names dynamically
|
||||
mayorSession, _ := getMayorSessionName()
|
||||
deaconSession, _ := getDeaconSessionName()
|
||||
mayorSession := getMayorSessionName()
|
||||
deaconSession := getDeaconSessionName()
|
||||
|
||||
// Define agents to discover
|
||||
agentDefs := []struct {
|
||||
|
||||
@@ -53,8 +53,8 @@ func runStatusLine(cmd *cobra.Command, args []string) error {
|
||||
}
|
||||
|
||||
// Get session names for comparison
|
||||
mayorSession, _ := getMayorSessionName()
|
||||
deaconSession, _ := getDeaconSessionName()
|
||||
mayorSession := getMayorSessionName()
|
||||
deaconSession := getDeaconSessionName()
|
||||
|
||||
// Determine identity and output based on role
|
||||
if role == "mayor" || statusLineSession == mayorSession {
|
||||
@@ -164,7 +164,7 @@ func runMayorStatusLine(t *tmux.Tmux) error {
|
||||
|
||||
// Get town root from mayor pane's working directory
|
||||
var townRoot string
|
||||
mayorSession, _ := getMayorSessionName()
|
||||
mayorSession := getMayorSessionName()
|
||||
paneDir, err := t.GetPaneWorkDir(mayorSession)
|
||||
if err == nil && paneDir != "" {
|
||||
townRoot, _ = workspace.Find(paneDir)
|
||||
@@ -241,7 +241,7 @@ func runDeaconStatusLine(t *tmux.Tmux) error {
|
||||
|
||||
// Get town root from deacon pane's working directory
|
||||
var townRoot string
|
||||
deaconSession, _ := getDeaconSessionName()
|
||||
deaconSession := getDeaconSessionName()
|
||||
paneDir, err := t.GetPaneWorkDir(deaconSession)
|
||||
if err == nil && paneDir != "" {
|
||||
townRoot, _ = workspace.Find(paneDir)
|
||||
|
||||
@@ -146,7 +146,7 @@ func runStop(cmd *cobra.Command, args []string) error {
|
||||
// Log kill event
|
||||
agent := fmt.Sprintf("%s/%s", r.Name, info.Polecat)
|
||||
logger := townlog.NewLogger(townRoot)
|
||||
logger.Log(townlog.EventKill, agent, "gt stop")
|
||||
_ = logger.Log(townlog.EventKill, agent, "gt stop")
|
||||
|
||||
// Log kill event to activity feed
|
||||
_ = events.LogFeed(events.TypeKill, "gt", events.KillPayload(r.Name, info.Polecat, "gt stop"))
|
||||
|
||||
@@ -114,7 +114,7 @@ var swarmCancelCmd = &cobra.Command{
|
||||
Short: "Cancel a swarm",
|
||||
Long: `Cancel an active swarm.
|
||||
|
||||
Marks the swarm as cancelled and optionally cleans up branches.`,
|
||||
Marks the swarm as canceled and optionally cleans up branches.`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: runSwarmCancel,
|
||||
}
|
||||
@@ -158,7 +158,7 @@ func init() {
|
||||
swarmStatusCmd.Flags().BoolVar(&swarmStatusJSON, "json", false, "Output as JSON")
|
||||
|
||||
// List flags
|
||||
swarmListCmd.Flags().StringVar(&swarmListStatus, "status", "", "Filter by status (active, landed, cancelled, failed)")
|
||||
swarmListCmd.Flags().StringVar(&swarmListStatus, "status", "", "Filter by status (active, landed, canceled, failed)")
|
||||
swarmListCmd.Flags().BoolVar(&swarmListJSON, "json", false, "Output as JSON")
|
||||
|
||||
// Dispatch flags
|
||||
@@ -526,7 +526,7 @@ func runSwarmDispatch(cmd *cobra.Command, args []string) error {
|
||||
func spawnSwarmWorkersFromBeads(r *rig.Rig, townRoot string, swarmID string, workers []string, tasks []struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
}) error {
|
||||
}) error { //nolint:unparam // error return kept for future use
|
||||
t := tmux.NewTmux()
|
||||
sessMgr := session.NewManager(t, r)
|
||||
polecatGit := git.NewGit(r.Path)
|
||||
@@ -866,8 +866,8 @@ func runSwarmCancel(cmd *cobra.Command, args []string) error {
|
||||
}
|
||||
}
|
||||
|
||||
// Close the swarm epic in beads with cancelled reason
|
||||
closeArgs := []string{"close", swarmID, "--reason", "Swarm cancelled"}
|
||||
// Close the swarm epic in beads with canceled reason
|
||||
closeArgs := []string{"close", swarmID, "--reason", "Swarm canceled"}
|
||||
if sessionID := os.Getenv("CLAUDE_SESSION_ID"); sessionID != "" {
|
||||
closeArgs = append(closeArgs, "--session="+sessionID)
|
||||
}
|
||||
@@ -877,7 +877,7 @@ func runSwarmCancel(cmd *cobra.Command, args []string) error {
|
||||
return fmt.Errorf("closing swarm: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("%s Swarm %s cancelled\n", style.Bold.Render("✓"), swarmID)
|
||||
fmt.Printf("%s Swarm %s canceled\n", style.Bold.Render("✓"), swarmID)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -408,7 +408,7 @@ func getConvoyMeta(convoyID string) (*ConvoyMeta, error) {
|
||||
}
|
||||
|
||||
// collectLegOutputs gathers outputs from all convoy legs.
|
||||
func collectLegOutputs(meta *ConvoyMeta, f *formula.Formula) ([]LegOutput, bool, error) {
|
||||
func collectLegOutputs(meta *ConvoyMeta, f *formula.Formula) ([]LegOutput, bool, error) { //nolint:unparam // error return kept for future use
|
||||
var outputs []LegOutput
|
||||
allComplete := true
|
||||
|
||||
|
||||
@@ -15,13 +15,9 @@ import (
|
||||
var townCycleSession string
|
||||
|
||||
// getTownLevelSessions returns the town-level session names for the current workspace.
|
||||
// Returns empty slice if workspace cannot be determined.
|
||||
func getTownLevelSessions() []string {
|
||||
mayorSession, errMayor := getMayorSessionName()
|
||||
deaconSession, errDeacon := getDeaconSessionName()
|
||||
if errMayor != nil || errDeacon != nil {
|
||||
return nil
|
||||
}
|
||||
mayorSession := getMayorSessionName()
|
||||
deaconSession := getDeaconSessionName()
|
||||
return []string{mayorSession, deaconSession}
|
||||
}
|
||||
|
||||
@@ -35,8 +31,8 @@ func isTownLevelSession(sessionName string) bool {
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
mayorSession, _ := getMayorSessionName()
|
||||
deaconSession, _ := getDeaconSessionName()
|
||||
mayorSession := getMayorSessionName()
|
||||
deaconSession := getDeaconSessionName()
|
||||
_ = townName // used for session name generation
|
||||
return sessionName == mayorSession || sessionName == deaconSession
|
||||
}
|
||||
|
||||
+2
-2
@@ -80,8 +80,8 @@ func runUp(cmd *cobra.Command, args []string) error {
|
||||
}
|
||||
|
||||
// Get session names
|
||||
deaconSession, _ := getDeaconSessionName()
|
||||
mayorSession, _ := getMayorSessionName()
|
||||
deaconSession := getDeaconSessionName()
|
||||
mayorSession := getMayorSessionName()
|
||||
|
||||
// 2. Deacon (Claude agent)
|
||||
if err := ensureSession(t, deaconSession, townRoot, "deacon"); err != nil {
|
||||
|
||||
@@ -333,9 +333,9 @@ func ensureWitnessSession(rigName string, r *rig.Rig) (bool, error) {
|
||||
|
||||
// Set environment
|
||||
bdActor := fmt.Sprintf("%s/witness", rigName)
|
||||
t.SetEnvironment(sessionName, "GT_ROLE", "witness")
|
||||
t.SetEnvironment(sessionName, "GT_RIG", rigName)
|
||||
t.SetEnvironment(sessionName, "BD_ACTOR", bdActor)
|
||||
_ = t.SetEnvironment(sessionName, "GT_ROLE", "witness")
|
||||
_ = t.SetEnvironment(sessionName, "GT_RIG", rigName)
|
||||
_ = t.SetEnvironment(sessionName, "BD_ACTOR", bdActor)
|
||||
|
||||
// Apply Gas Town theming (non-fatal: theming failure doesn't affect operation)
|
||||
theme := tmux.AssignTheme(rigName)
|
||||
|
||||
+16
-16
@@ -27,7 +27,7 @@ var (
|
||||
|
||||
// LoadTownConfig loads and validates a town configuration file.
|
||||
func LoadTownConfig(path string) (*TownConfig, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
data, err := os.ReadFile(path) //nolint:gosec // G304: path is from trusted config location
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, fmt.Errorf("%w: %s", ErrNotFound, path)
|
||||
@@ -62,7 +62,7 @@ func SaveTownConfig(path string, config *TownConfig) error {
|
||||
return fmt.Errorf("encoding config: %w", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(path, data, 0644); err != nil {
|
||||
if err := os.WriteFile(path, data, 0600); err != nil {
|
||||
return fmt.Errorf("writing config: %w", err)
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ func SaveTownConfig(path string, config *TownConfig) error {
|
||||
|
||||
// LoadRigsConfig loads and validates a rigs registry file.
|
||||
func LoadRigsConfig(path string) (*RigsConfig, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
data, err := os.ReadFile(path) //nolint:gosec // G304: path is constructed internally, not from user input
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, fmt.Errorf("%w: %s", ErrNotFound, path)
|
||||
@@ -106,7 +106,7 @@ func SaveRigsConfig(path string, config *RigsConfig) error {
|
||||
return fmt.Errorf("encoding config: %w", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(path, data, 0644); err != nil {
|
||||
if err := os.WriteFile(path, data, 0600); err != nil {
|
||||
return fmt.Errorf("writing config: %w", err)
|
||||
}
|
||||
|
||||
@@ -115,7 +115,7 @@ func SaveRigsConfig(path string, config *RigsConfig) error {
|
||||
|
||||
// LoadAgentState loads an agent state file.
|
||||
func LoadAgentState(path string) (*AgentState, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
data, err := os.ReadFile(path) //nolint:gosec // G304: path is constructed internally, not from user input
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, fmt.Errorf("%w: %s", ErrNotFound, path)
|
||||
@@ -150,7 +150,7 @@ func SaveAgentState(path string, state *AgentState) error {
|
||||
return fmt.Errorf("encoding state: %w", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(path, data, 0644); err != nil {
|
||||
if err := os.WriteFile(path, data, 0644); err != nil { //nolint:gosec // G306: state files don't contain secrets
|
||||
return fmt.Errorf("writing state: %w", err)
|
||||
}
|
||||
|
||||
@@ -192,7 +192,7 @@ func validateAgentState(s *AgentState) error {
|
||||
|
||||
// LoadRigConfig loads and validates a rig configuration file.
|
||||
func LoadRigConfig(path string) (*RigConfig, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
data, err := os.ReadFile(path) //nolint:gosec // G304: path is constructed internally, not from user input
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, fmt.Errorf("%w: %s", ErrNotFound, path)
|
||||
@@ -227,7 +227,7 @@ func SaveRigConfig(path string, config *RigConfig) error {
|
||||
return fmt.Errorf("encoding config: %w", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(path, data, 0644); err != nil {
|
||||
if err := os.WriteFile(path, data, 0644); err != nil { //nolint:gosec // G306: config files don't contain secrets
|
||||
return fmt.Errorf("writing config: %w", err)
|
||||
}
|
||||
|
||||
@@ -315,7 +315,7 @@ func NewRigSettings() *RigSettings {
|
||||
|
||||
// LoadRigSettings loads and validates a rig settings file.
|
||||
func LoadRigSettings(path string) (*RigSettings, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
data, err := os.ReadFile(path) //nolint:gosec // G304: path is constructed internally, not from user input
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, fmt.Errorf("%w: %s", ErrNotFound, path)
|
||||
@@ -350,7 +350,7 @@ func SaveRigSettings(path string, settings *RigSettings) error {
|
||||
return fmt.Errorf("encoding settings: %w", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(path, data, 0644); err != nil {
|
||||
if err := os.WriteFile(path, data, 0644); err != nil { //nolint:gosec // G306: settings files don't contain secrets
|
||||
return fmt.Errorf("writing settings: %w", err)
|
||||
}
|
||||
|
||||
@@ -359,7 +359,7 @@ func SaveRigSettings(path string, settings *RigSettings) error {
|
||||
|
||||
// LoadMayorConfig loads and validates a mayor config file.
|
||||
func LoadMayorConfig(path string) (*MayorConfig, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
data, err := os.ReadFile(path) //nolint:gosec // G304: path is constructed internally, not from user input
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, fmt.Errorf("%w: %s", ErrNotFound, path)
|
||||
@@ -394,7 +394,7 @@ func SaveMayorConfig(path string, config *MayorConfig) error {
|
||||
return fmt.Errorf("encoding config: %w", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(path, data, 0644); err != nil {
|
||||
if err := os.WriteFile(path, data, 0644); err != nil { //nolint:gosec // G306: config files don't contain secrets
|
||||
return fmt.Errorf("writing config: %w", err)
|
||||
}
|
||||
|
||||
@@ -422,7 +422,7 @@ func NewMayorConfig() *MayorConfig {
|
||||
|
||||
// LoadAccountsConfig loads and validates an accounts configuration file.
|
||||
func LoadAccountsConfig(path string) (*AccountsConfig, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
data, err := os.ReadFile(path) //nolint:gosec // G304: path is constructed internally, not from user input
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, fmt.Errorf("%w: %s", ErrNotFound, path)
|
||||
@@ -457,7 +457,7 @@ func SaveAccountsConfig(path string, config *AccountsConfig) error {
|
||||
return fmt.Errorf("encoding accounts config: %w", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(path, data, 0644); err != nil {
|
||||
if err := os.WriteFile(path, data, 0644); err != nil { //nolint:gosec // G306: accounts config doesn't contain sensitive credentials
|
||||
return fmt.Errorf("writing accounts config: %w", err)
|
||||
}
|
||||
|
||||
@@ -569,7 +569,7 @@ func expandPath(path string) string {
|
||||
|
||||
// LoadMessagingConfig loads and validates a messaging configuration file.
|
||||
func LoadMessagingConfig(path string) (*MessagingConfig, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
data, err := os.ReadFile(path) //nolint:gosec // G304: path is constructed internally, not from user input
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, fmt.Errorf("%w: %s", ErrNotFound, path)
|
||||
@@ -604,7 +604,7 @@ func SaveMessagingConfig(path string, config *MessagingConfig) error {
|
||||
return fmt.Errorf("encoding messaging config: %w", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(path, data, 0644); err != nil {
|
||||
if err := os.WriteFile(path, data, 0644); err != nil { //nolint:gosec // G306: messaging config doesn't contain secrets
|
||||
return fmt.Errorf("writing messaging config: %w", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ func OverseerConfigPath(townRoot string) string {
|
||||
|
||||
// LoadOverseerConfig loads and validates an overseer configuration file.
|
||||
func LoadOverseerConfig(path string) (*OverseerConfig, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
data, err := os.ReadFile(path) //nolint:gosec // G304: path is constructed internally, not from user input
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, fmt.Errorf("%w: %s", ErrNotFound, path)
|
||||
@@ -65,7 +65,7 @@ func SaveOverseerConfig(path string, config *OverseerConfig) error {
|
||||
return fmt.Errorf("encoding overseer config: %w", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(path, data, 0644); err != nil {
|
||||
if err := os.WriteFile(path, data, 0644); err != nil { //nolint:gosec // G306: overseer config doesn't contain secrets
|
||||
return fmt.Errorf("writing overseer config: %w", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ func (c *LocalConnection) IsLocal() bool {
|
||||
|
||||
// ReadFile reads the named file.
|
||||
func (c *LocalConnection) ReadFile(path string) ([]byte, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
data, err := os.ReadFile(path) //nolint:gosec // G304: path is from Connection interface, validated by caller
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, &NotFoundError{Path: path}
|
||||
|
||||
@@ -46,7 +46,7 @@ func New(config *Config) (*Daemon, error) {
|
||||
}
|
||||
|
||||
// Open log file
|
||||
logFile, err := os.OpenFile(config.LogFile, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
|
||||
logFile, err := os.OpenFile(config.LogFile, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("opening log file: %w", err)
|
||||
}
|
||||
@@ -108,7 +108,7 @@ func (d *Daemon) Run() error {
|
||||
for {
|
||||
select {
|
||||
case <-d.ctx.Done():
|
||||
d.logger.Println("Daemon context cancelled, shutting down")
|
||||
d.logger.Println("Daemon context canceled, shutting down")
|
||||
return d.shutdown(state)
|
||||
|
||||
case sig := <-sigChan:
|
||||
@@ -660,7 +660,7 @@ func (d *Daemon) processLifecycleRequests() {
|
||||
}
|
||||
|
||||
// shutdown performs graceful shutdown.
|
||||
func (d *Daemon) shutdown(state *State) error {
|
||||
func (d *Daemon) shutdown(state *State) error { //nolint:unparam // error return kept for future use
|
||||
d.logger.Println("Daemon shutting down")
|
||||
|
||||
// Stop feed curator
|
||||
@@ -899,7 +899,7 @@ restart_error: %v
|
||||
Manual intervention may be required.`,
|
||||
polecatName, hookBead, restartErr)
|
||||
|
||||
cmd := exec.Command("gt", "mail", "send", witnessAddr, "-s", subject, "-m", body)
|
||||
cmd := exec.Command("gt", "mail", "send", witnessAddr, "-s", subject, "-m", body) //nolint:gosec // G204: args are constructed internally
|
||||
cmd.Dir = d.config.TownRoot
|
||||
if err := cmd.Run(); err != nil {
|
||||
d.logger.Printf("Warning: failed to notify witness of crashed polecat: %v", err)
|
||||
|
||||
@@ -55,7 +55,7 @@ func WriteHeartbeat(townRoot string, hb *Heartbeat) error {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.WriteFile(hbFile, data, 0644)
|
||||
return os.WriteFile(hbFile, data, 0600)
|
||||
}
|
||||
|
||||
// ReadHeartbeat reads the Deacon heartbeat from disk.
|
||||
@@ -63,7 +63,7 @@ func WriteHeartbeat(townRoot string, hb *Heartbeat) error {
|
||||
func ReadHeartbeat(townRoot string) *Heartbeat {
|
||||
hbFile := HeartbeatFile(townRoot)
|
||||
|
||||
data, err := os.ReadFile(hbFile)
|
||||
data, err := os.ReadFile(hbFile) //nolint:gosec // G304: path is constructed from trusted townRoot
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ func HealthCheckStateFile(townRoot string) string {
|
||||
func LoadHealthCheckState(townRoot string) (*HealthCheckState, error) {
|
||||
stateFile := HealthCheckStateFile(townRoot)
|
||||
|
||||
data, err := os.ReadFile(stateFile)
|
||||
data, err := os.ReadFile(stateFile) //nolint:gosec // G304: path is constructed from trusted townRoot
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
// Return empty state
|
||||
@@ -112,7 +112,7 @@ func SaveHealthCheckState(townRoot string, state *HealthCheckState) error {
|
||||
return fmt.Errorf("marshaling health check state: %w", err)
|
||||
}
|
||||
|
||||
return os.WriteFile(stateFile, data, 0644)
|
||||
return os.WriteFile(stateFile, data, 0600)
|
||||
}
|
||||
|
||||
// GetAgentState returns the health state for an agent, creating if needed.
|
||||
|
||||
@@ -44,7 +44,7 @@ func (c *BdDaemonCheck) Run(ctx *CheckContext) *CheckResult {
|
||||
healthCmd.Dir = ctx.TownRoot
|
||||
var healthOut bytes.Buffer
|
||||
healthCmd.Stdout = &healthOut
|
||||
healthCmd.Run() // Ignore error, health check is optional
|
||||
_ = healthCmd.Run() // Ignore error, health check is optional
|
||||
|
||||
healthOutput := healthOut.String()
|
||||
if strings.Contains(healthOutput, "HEALTHY") {
|
||||
|
||||
@@ -80,7 +80,7 @@ func (c *HookAttachmentValidCheck) Run(ctx *CheckContext) *CheckResult {
|
||||
}
|
||||
|
||||
// checkBeadsDir checks all pinned beads in a directory for invalid attachments.
|
||||
func (c *HookAttachmentValidCheck) checkBeadsDir(beadsDir, location string) []invalidAttachment {
|
||||
func (c *HookAttachmentValidCheck) checkBeadsDir(beadsDir, _ string) []invalidAttachment { // location unused but kept for future diagnostic output
|
||||
var invalid []invalidAttachment
|
||||
|
||||
b := beads.New(filepath.Dir(beadsDir))
|
||||
|
||||
@@ -227,8 +227,8 @@ func (c *LifecycleHygieneCheck) findStateFiles(townRoot string) []stateFileInfo
|
||||
}
|
||||
|
||||
// isSessionHealthy checks if the tmux session for this identity exists and is running.
|
||||
func (c *LifecycleHygieneCheck) isSessionHealthy(identity, townRoot string) bool {
|
||||
sessionName := identityToSessionName(identity, townRoot)
|
||||
func (c *LifecycleHygieneCheck) isSessionHealthy(identity, _ string) bool {
|
||||
sessionName := identityToSessionName(identity)
|
||||
if sessionName == "" {
|
||||
return false
|
||||
}
|
||||
@@ -239,7 +239,7 @@ func (c *LifecycleHygieneCheck) isSessionHealthy(identity, townRoot string) bool
|
||||
}
|
||||
|
||||
// identityToSessionName converts an identity to its tmux session name.
|
||||
func identityToSessionName(identity, townRoot string) string {
|
||||
func identityToSessionName(identity string) string {
|
||||
switch identity {
|
||||
case "mayor":
|
||||
return session.MayorSessionName()
|
||||
@@ -259,7 +259,7 @@ func (c *LifecycleHygieneCheck) Fix(ctx *CheckContext) error {
|
||||
|
||||
// Delete stale lifecycle messages
|
||||
for _, msg := range c.staleMessages {
|
||||
cmd := exec.Command("gt", "mail", "delete", msg.ID)
|
||||
cmd := exec.Command("gt", "mail", "delete", msg.ID) //nolint:gosec // G204: msg.ID is from internal state, not user input
|
||||
cmd.Dir = ctx.TownRoot
|
||||
if err := cmd.Run(); err != nil {
|
||||
errors = append(errors, fmt.Sprintf("failed to delete message %s: %v", msg.ID, err))
|
||||
|
||||
@@ -400,7 +400,7 @@ func (c *OrphanProcessCheck) hasCrewAncestor(pid int, crewPanePIDs map[int]bool)
|
||||
}
|
||||
|
||||
// Get parent PID
|
||||
out, err := exec.Command("ps", "-p", fmt.Sprintf("%d", currentPID), "-o", "ppid=").Output()
|
||||
out, err := exec.Command("ps", "-p", fmt.Sprintf("%d", currentPID), "-o", "ppid=").Output() //nolint:gosec // G204: PID is numeric from internal state
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
@@ -422,7 +422,7 @@ type processInfo struct {
|
||||
}
|
||||
|
||||
// getTmuxSessionPIDs returns PIDs of all tmux server processes and pane shell PIDs.
|
||||
func (c *OrphanProcessCheck) getTmuxSessionPIDs() (map[int]bool, error) {
|
||||
func (c *OrphanProcessCheck) getTmuxSessionPIDs() (map[int]bool, error) { //nolint:unparam // error return kept for future use
|
||||
// Get tmux server PID and all pane PIDs
|
||||
pids := make(map[int]bool)
|
||||
|
||||
@@ -534,7 +534,7 @@ func (c *OrphanProcessCheck) isOrphanProcess(proc processInfo, tmuxPIDs map[int]
|
||||
}
|
||||
|
||||
// Get parent's parent
|
||||
out, err := exec.Command("ps", "-p", fmt.Sprintf("%d", currentPPID), "-o", "ppid=").Output()
|
||||
out, err := exec.Command("ps", "-p", fmt.Sprintf("%d", currentPPID), "-o", "ppid=").Output() //nolint:gosec // G204: PID is numeric from internal state
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
@@ -115,7 +115,7 @@ func (c *PatrolMoleculesExistCheck) Fix(ctx *CheckContext) error {
|
||||
rigPath := filepath.Join(ctx.TownRoot, rigName)
|
||||
for _, mol := range missing {
|
||||
desc := getPatrolMoleculeDesc(mol)
|
||||
cmd := exec.Command("bd", "create",
|
||||
cmd := exec.Command("bd", "create", //nolint:gosec // G204: args are constructed internally
|
||||
"--type=molecule",
|
||||
"--title="+mol,
|
||||
"--description="+desc,
|
||||
|
||||
@@ -162,7 +162,7 @@ func (c *GitExcludeConfiguredCheck) Run(ctx *CheckContext) *CheckResult {
|
||||
existing[line] = true
|
||||
}
|
||||
}
|
||||
file.Close()
|
||||
_ = file.Close() //nolint:gosec // G104: best-effort close
|
||||
}
|
||||
|
||||
// Check for missing entries
|
||||
@@ -203,7 +203,7 @@ func (c *GitExcludeConfiguredCheck) Fix(ctx *CheckContext) error {
|
||||
}
|
||||
|
||||
// Append missing entries
|
||||
f, err := os.OpenFile(c.excludePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
f, err := os.OpenFile(c.excludePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open exclude file: %w", err)
|
||||
}
|
||||
|
||||
@@ -509,7 +509,7 @@ func (m *Manager) saveState(name string, state *DogState) error {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.WriteFile(m.stateFilePath(name), data, 0644)
|
||||
return os.WriteFile(m.stateFilePath(name), data, 0644) //nolint:gosec // G306: dog state is non-sensitive operational data
|
||||
}
|
||||
|
||||
// GetIdleDog returns an idle dog suitable for work assignment.
|
||||
|
||||
@@ -117,7 +117,7 @@ func write(event Event) error {
|
||||
mutex.Lock()
|
||||
defer mutex.Unlock()
|
||||
|
||||
f, err := os.OpenFile(eventsPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
f, err := os.OpenFile(eventsPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) //nolint:gosec // G302: events file is non-sensitive operational data
|
||||
if err != nil {
|
||||
return fmt.Errorf("opening events file: %w", err)
|
||||
}
|
||||
|
||||
@@ -88,14 +88,14 @@ func (c *Curator) Start() error {
|
||||
eventsPath := filepath.Join(c.townRoot, events.EventsFile)
|
||||
|
||||
// Open events file, creating if needed
|
||||
file, err := os.OpenFile(eventsPath, os.O_RDONLY|os.O_CREATE, 0644)
|
||||
file, err := os.OpenFile(eventsPath, os.O_RDONLY|os.O_CREATE, 0644) //nolint:gosec // G302: events file is non-sensitive operational data
|
||||
if err != nil {
|
||||
return fmt.Errorf("opening events file: %w", err)
|
||||
}
|
||||
|
||||
// Seek to end to only process new events
|
||||
if _, err := file.Seek(0, io.SeekEnd); err != nil {
|
||||
file.Close()
|
||||
_ = file.Close() //nolint:gosec // G104: best effort cleanup on error
|
||||
return fmt.Errorf("seeking to end: %w", err)
|
||||
}
|
||||
|
||||
@@ -285,13 +285,13 @@ func (c *Curator) writeFeedEvent(event *events.Event) {
|
||||
data = append(data, '\n')
|
||||
|
||||
feedPath := filepath.Join(c.townRoot, FeedFile)
|
||||
f, err := os.OpenFile(feedPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
f, err := os.OpenFile(feedPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) //nolint:gosec // G302: feed file is non-sensitive operational data
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
f.Write(data)
|
||||
_, _ = f.Write(data)
|
||||
}
|
||||
|
||||
// generateSummary creates a human-readable summary of an event.
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
|
||||
// ParseFile reads and parses a formula.toml file.
|
||||
func ParseFile(path string) (*Formula, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
data, err := os.ReadFile(path) //nolint:gosec // G304: path is from trusted formula directory
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading formula file: %w", err)
|
||||
}
|
||||
|
||||
@@ -186,7 +186,7 @@ func (l *Lock) write(sessionID string) error {
|
||||
return fmt.Errorf("marshaling lock info: %w", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(l.lockPath, data, 0644); err != nil {
|
||||
if err := os.WriteFile(l.lockPath, data, 0644); err != nil { //nolint:gosec // G306: lock files are non-sensitive operational data
|
||||
return fmt.Errorf("writing lock file: %w", err)
|
||||
}
|
||||
|
||||
@@ -340,7 +340,7 @@ type execCmdWrapper struct {
|
||||
}
|
||||
|
||||
func (c *execCmdWrapper) Output() ([]byte, error) {
|
||||
cmd := exec.Command(c.name, c.args...)
|
||||
cmd := exec.Command(c.name, c.args...) //nolint:gosec // G204: command args are controlled internally
|
||||
return cmd.Output()
|
||||
}
|
||||
|
||||
|
||||
@@ -112,7 +112,7 @@ func (m *Mailbox) listBeads() ([]*Message, error) {
|
||||
// listFromDir queries messages from a beads directory.
|
||||
// Returns messages where identity is the assignee OR a CC recipient.
|
||||
// Includes both open and hooked messages (hooked = auto-assigned handoff mail).
|
||||
func (m *Mailbox) listFromDir(beadsDir string) ([]*Message, error) {
|
||||
func (m *Mailbox) listFromDir(beadsDir string) ([]*Message, error) { //nolint:unparam // error return kept for future use
|
||||
seen := make(map[string]bool)
|
||||
var messages []*Message
|
||||
|
||||
@@ -346,7 +346,7 @@ func (m *Mailbox) closeInDir(id, beadsDir string) error {
|
||||
if sessionID := os.Getenv("CLAUDE_SESSION_ID"); sessionID != "" {
|
||||
args = append(args, "--session="+sessionID)
|
||||
}
|
||||
cmd := exec.Command("bd", args...)
|
||||
cmd := exec.Command("bd", args...) //nolint:gosec // G204: bd is a trusted internal tool
|
||||
cmd.Dir = m.workDir
|
||||
cmd.Env = append(cmd.Environ(), "BEADS_DIR="+beadsDir)
|
||||
|
||||
@@ -506,7 +506,7 @@ func (m *Mailbox) appendToArchive(msg *Message) error {
|
||||
}
|
||||
|
||||
// Open for append
|
||||
file, err := os.OpenFile(archivePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
file, err := os.OpenFile(archivePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) //nolint:gosec // G302: archive is non-sensitive operational data
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -740,7 +740,7 @@ func (m *Mailbox) appendLegacy(msg *Message) error {
|
||||
}
|
||||
|
||||
// Open for append
|
||||
file, err := os.OpenFile(m.path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
file, err := os.OpenFile(m.path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -184,7 +184,7 @@ func detectTownRoot(startDir string) string {
|
||||
// - Rig-level beads ({rig}/.beads) are for project issues only, not mail
|
||||
//
|
||||
// This ensures messages are visible to all agents in the town.
|
||||
func (r *Router) resolveBeadsDir(address string) string {
|
||||
func (r *Router) resolveBeadsDir(_ string) string { // address unused: all mail uses town-level beads
|
||||
// If no town root, fall back to workDir's .beads
|
||||
if r.townRoot == "" {
|
||||
return filepath.Join(r.workDir, ".beads")
|
||||
@@ -622,7 +622,7 @@ func (r *Router) sendToSingle(msg *Message) error {
|
||||
}
|
||||
|
||||
beadsDir := r.resolveBeadsDir(msg.To)
|
||||
cmd := exec.Command("bd", args...)
|
||||
cmd := exec.Command("bd", args...) //nolint:gosec // G204: bd is a trusted internal tool
|
||||
cmd.Env = append(cmd.Environ(),
|
||||
"BEADS_DIR="+beadsDir,
|
||||
)
|
||||
@@ -744,7 +744,7 @@ func (r *Router) sendToQueue(msg *Message) error {
|
||||
|
||||
// Queue messages go to town-level beads (shared location)
|
||||
beadsDir := r.resolveBeadsDir("")
|
||||
cmd := exec.Command("bd", args...)
|
||||
cmd := exec.Command("bd", args...) //nolint:gosec // G204: args are constructed internally, not from user input
|
||||
cmd.Env = append(cmd.Environ(),
|
||||
"BEADS_DIR="+beadsDir,
|
||||
)
|
||||
@@ -827,7 +827,7 @@ func (r *Router) sendToAnnounce(msg *Message) error {
|
||||
|
||||
// Announce messages go to town-level beads (shared location)
|
||||
beadsDir := r.resolveBeadsDir("")
|
||||
cmd := exec.Command("bd", args...)
|
||||
cmd := exec.Command("bd", args...) //nolint:gosec // G204: args are constructed internally, not from user input
|
||||
cmd.Env = append(cmd.Environ(),
|
||||
"BEADS_DIR="+beadsDir,
|
||||
)
|
||||
@@ -869,7 +869,7 @@ func (r *Router) pruneAnnounce(announceName string, retainCount int) error {
|
||||
"--asc", // Oldest first
|
||||
}
|
||||
|
||||
cmd := exec.Command("bd", args...)
|
||||
cmd := exec.Command("bd", args...) //nolint:gosec // G204: args are constructed internally
|
||||
cmd.Env = append(cmd.Environ(), "BEADS_DIR="+beadsDir)
|
||||
cmd.Dir = filepath.Dir(beadsDir)
|
||||
|
||||
@@ -904,7 +904,7 @@ func (r *Router) pruneAnnounce(announceName string, retainCount int) error {
|
||||
// Delete oldest messages
|
||||
for i := 0; i < toDelete && i < len(messages); i++ {
|
||||
deleteArgs := []string{"close", messages[i].ID, "--reason=retention pruning"}
|
||||
deleteCmd := exec.Command("bd", deleteArgs...)
|
||||
deleteCmd := exec.Command("bd", deleteArgs...) //nolint:gosec // G204: args are constructed internally
|
||||
deleteCmd.Env = append(deleteCmd.Environ(), "BEADS_DIR="+beadsDir)
|
||||
deleteCmd.Dir = filepath.Dir(beadsDir)
|
||||
|
||||
|
||||
@@ -78,7 +78,7 @@ func (l *EventLogger) LogEvent(event Event) error {
|
||||
}
|
||||
|
||||
// Append to log file
|
||||
f, err := os.OpenFile(l.logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
f, err := os.OpenFile(l.logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
|
||||
if err != nil {
|
||||
return fmt.Errorf("opening event log: %w", err)
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ func (q *Queue) EnsureDir() error {
|
||||
// generateID creates a unique MR ID.
|
||||
func generateID() string {
|
||||
b := make([]byte, 4)
|
||||
rand.Read(b)
|
||||
_, _ = rand.Read(b)
|
||||
return fmt.Sprintf("mr-%d-%s", time.Now().Unix(), hex.EncodeToString(b))
|
||||
}
|
||||
|
||||
@@ -280,7 +280,7 @@ func (q *Queue) Claim(id, workerID string) error {
|
||||
return fmt.Errorf("writing temp file: %w", err)
|
||||
}
|
||||
if err := os.Rename(tmpPath, path); err != nil {
|
||||
os.Remove(tmpPath) // cleanup
|
||||
_ = os.Remove(tmpPath) // cleanup
|
||||
return fmt.Errorf("renaming temp file: %w", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -51,10 +51,10 @@ func (h *DefaultRefineryHandler) SetOutput(w io.Writer) {
|
||||
// 2. Adds it to the merge queue
|
||||
// 3. Acknowledges receipt
|
||||
func (h *DefaultRefineryHandler) HandleMergeReady(payload *MergeReadyPayload) error {
|
||||
fmt.Fprintf(h.Output, "[Refinery] MERGE_READY received for polecat %s\n", payload.Polecat)
|
||||
fmt.Fprintf(h.Output, " Branch: %s\n", payload.Branch)
|
||||
fmt.Fprintf(h.Output, " Issue: %s\n", payload.Issue)
|
||||
fmt.Fprintf(h.Output, " Verified: %s\n", payload.Verified)
|
||||
_, _ = fmt.Fprintf(h.Output, "[Refinery] MERGE_READY received for polecat %s\n", payload.Polecat)
|
||||
_, _ = fmt.Fprintf(h.Output, " Branch: %s\n", payload.Branch)
|
||||
_, _ = fmt.Fprintf(h.Output, " Issue: %s\n", payload.Issue)
|
||||
_, _ = fmt.Fprintf(h.Output, " Verified: %s\n", payload.Verified)
|
||||
|
||||
// Validate required fields
|
||||
if payload.Branch == "" {
|
||||
@@ -77,12 +77,12 @@ func (h *DefaultRefineryHandler) HandleMergeReady(payload *MergeReadyPayload) er
|
||||
|
||||
// Add to queue
|
||||
if err := h.Queue.Submit(mr); err != nil {
|
||||
fmt.Fprintf(h.Output, "[Refinery] Error adding to queue: %v\n", err)
|
||||
_, _ = fmt.Fprintf(h.Output, "[Refinery] Error adding to queue: %v\n", err)
|
||||
return fmt.Errorf("failed to add merge request to queue: %w", err)
|
||||
}
|
||||
|
||||
fmt.Fprintf(h.Output, "[Refinery] ✓ Added to merge queue: %s\n", mr.ID)
|
||||
fmt.Fprintf(h.Output, " Queue length: %d\n", h.Queue.Count())
|
||||
_, _ = fmt.Fprintf(h.Output, "[Refinery] ✓ Added to merge queue: %s\n", mr.ID)
|
||||
_, _ = fmt.Fprintf(h.Output, " Queue length: %d\n", h.Queue.Count())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -46,12 +46,12 @@ func (h *DefaultWitnessHandler) SetOutput(w io.Writer) {
|
||||
// 2. Notifies the polecat of successful merge
|
||||
// 3. Initiates polecat cleanup (nuke worktree)
|
||||
func (h *DefaultWitnessHandler) HandleMerged(payload *MergedPayload) error {
|
||||
fmt.Fprintf(h.Output, "[Witness] MERGED received for polecat %s\n", payload.Polecat)
|
||||
fmt.Fprintf(h.Output, " Branch: %s\n", payload.Branch)
|
||||
fmt.Fprintf(h.Output, " Issue: %s\n", payload.Issue)
|
||||
fmt.Fprintf(h.Output, " Merged to: %s\n", payload.TargetBranch)
|
||||
_, _ = fmt.Fprintf(h.Output, "[Witness] MERGED received for polecat %s\n", payload.Polecat)
|
||||
_, _ = fmt.Fprintf(h.Output, " Branch: %s\n", payload.Branch)
|
||||
_, _ = fmt.Fprintf(h.Output, " Issue: %s\n", payload.Issue)
|
||||
_, _ = fmt.Fprintf(h.Output, " Merged to: %s\n", payload.TargetBranch)
|
||||
if payload.MergeCommit != "" {
|
||||
fmt.Fprintf(h.Output, " Commit: %s\n", payload.MergeCommit)
|
||||
_, _ = fmt.Fprintf(h.Output, " Commit: %s\n", payload.MergeCommit)
|
||||
}
|
||||
|
||||
// Notify the polecat about successful merge
|
||||
|
||||
@@ -215,10 +215,10 @@ func (e *Engineer) ProcessMR(ctx context.Context, mr *beads.Issue) ProcessResult
|
||||
}
|
||||
|
||||
// Log what we're processing
|
||||
fmt.Fprintln(e.output, "[Engineer] Processing MR:")
|
||||
fmt.Fprintf(e.output, " Branch: %s\n", mrFields.Branch)
|
||||
fmt.Fprintf(e.output, " Target: %s\n", mrFields.Target)
|
||||
fmt.Fprintf(e.output, " Worker: %s\n", mrFields.Worker)
|
||||
_, _ = fmt.Fprintln(e.output, "[Engineer] Processing MR:")
|
||||
_, _ = fmt.Fprintf(e.output, " Branch: %s\n", mrFields.Branch)
|
||||
_, _ = fmt.Fprintf(e.output, " Target: %s\n", mrFields.Target)
|
||||
_, _ = fmt.Fprintf(e.output, " Worker: %s\n", mrFields.Worker)
|
||||
|
||||
return e.doMerge(ctx, mrFields.Branch, mrFields.Target, mrFields.SourceIssue)
|
||||
}
|
||||
@@ -227,7 +227,7 @@ func (e *Engineer) ProcessMR(ctx context.Context, mr *beads.Issue) ProcessResult
|
||||
// This is the core merge logic shared by ProcessMR and ProcessMRFromQueue.
|
||||
func (e *Engineer) doMerge(ctx context.Context, branch, target, sourceIssue string) ProcessResult {
|
||||
// Step 1: Fetch the source branch from origin
|
||||
fmt.Fprintf(e.output, "[Engineer] Fetching branch %s from origin...\n", branch)
|
||||
_, _ = fmt.Fprintf(e.output, "[Engineer] Fetching branch %s from origin...\n", branch)
|
||||
if err := e.git.FetchBranch("origin", branch); err != nil {
|
||||
return ProcessResult{
|
||||
Success: false,
|
||||
@@ -236,7 +236,7 @@ func (e *Engineer) doMerge(ctx context.Context, branch, target, sourceIssue stri
|
||||
}
|
||||
|
||||
// Step 2: Checkout the target branch
|
||||
fmt.Fprintf(e.output, "[Engineer] Checking out target branch %s...\n", target)
|
||||
_, _ = fmt.Fprintf(e.output, "[Engineer] Checking out target branch %s...\n", target)
|
||||
if err := e.git.Checkout(target); err != nil {
|
||||
return ProcessResult{
|
||||
Success: false,
|
||||
@@ -247,11 +247,11 @@ func (e *Engineer) doMerge(ctx context.Context, branch, target, sourceIssue stri
|
||||
// Make sure target is up to date with origin
|
||||
if err := e.git.Pull("origin", target); err != nil {
|
||||
// Pull might fail if nothing to pull, that's ok
|
||||
fmt.Fprintf(e.output, "[Engineer] Warning: pull from origin/%s: %v (continuing)\n", target, err)
|
||||
_, _ = fmt.Fprintf(e.output, "[Engineer] Warning: pull from origin/%s: %v (continuing)\n", target, err)
|
||||
}
|
||||
|
||||
// Step 3: Check for merge conflicts
|
||||
fmt.Fprintf(e.output, "[Engineer] Checking for conflicts...\n")
|
||||
_, _ = fmt.Fprintf(e.output, "[Engineer] Checking for conflicts...\n")
|
||||
remoteBranch := "origin/" + branch
|
||||
conflicts, err := e.git.CheckConflicts(remoteBranch, target)
|
||||
if err != nil {
|
||||
@@ -271,7 +271,7 @@ func (e *Engineer) doMerge(ctx context.Context, branch, target, sourceIssue stri
|
||||
|
||||
// Step 4: Run tests if configured
|
||||
if e.config.RunTests && e.config.TestCommand != "" {
|
||||
fmt.Fprintf(e.output, "[Engineer] Running tests: %s\n", e.config.TestCommand)
|
||||
_, _ = fmt.Fprintf(e.output, "[Engineer] Running tests: %s\n", e.config.TestCommand)
|
||||
result := e.runTests(ctx)
|
||||
if !result.Success {
|
||||
return ProcessResult{
|
||||
@@ -280,7 +280,7 @@ func (e *Engineer) doMerge(ctx context.Context, branch, target, sourceIssue stri
|
||||
Error: result.Error,
|
||||
}
|
||||
}
|
||||
fmt.Fprintln(e.output, "[Engineer] Tests passed")
|
||||
_, _ = fmt.Fprintln(e.output, "[Engineer] Tests passed")
|
||||
}
|
||||
|
||||
// Step 5: Perform the actual merge
|
||||
@@ -288,7 +288,7 @@ func (e *Engineer) doMerge(ctx context.Context, branch, target, sourceIssue stri
|
||||
if sourceIssue != "" {
|
||||
mergeMsg = fmt.Sprintf("Merge %s into %s (%s)", branch, target, sourceIssue)
|
||||
}
|
||||
fmt.Fprintf(e.output, "[Engineer] Merging with message: %s\n", mergeMsg)
|
||||
_, _ = fmt.Fprintf(e.output, "[Engineer] Merging with message: %s\n", mergeMsg)
|
||||
if err := e.git.MergeNoFF(remoteBranch, mergeMsg); err != nil {
|
||||
if errors.Is(err, git.ErrMergeConflict) {
|
||||
_ = e.git.AbortMerge()
|
||||
@@ -314,7 +314,7 @@ func (e *Engineer) doMerge(ctx context.Context, branch, target, sourceIssue stri
|
||||
}
|
||||
|
||||
// Step 7: Push to origin
|
||||
fmt.Fprintf(e.output, "[Engineer] Pushing to origin/%s...\n", target)
|
||||
_, _ = fmt.Fprintf(e.output, "[Engineer] Pushing to origin/%s...\n", target)
|
||||
if err := e.git.Push("origin", target, false); err != nil {
|
||||
return ProcessResult{
|
||||
Success: false,
|
||||
@@ -322,7 +322,7 @@ func (e *Engineer) doMerge(ctx context.Context, branch, target, sourceIssue stri
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Fprintf(e.output, "[Engineer] Successfully merged: %s\n", mergeCommit[:8])
|
||||
_, _ = fmt.Fprintf(e.output, "[Engineer] Successfully merged: %s\n", mergeCommit[:8])
|
||||
return ProcessResult{
|
||||
Success: true,
|
||||
MergeCommit: mergeCommit,
|
||||
@@ -344,12 +344,12 @@ func (e *Engineer) runTests(ctx context.Context) ProcessResult {
|
||||
var lastErr error
|
||||
for attempt := 1; attempt <= maxRetries; attempt++ {
|
||||
if attempt > 1 {
|
||||
fmt.Fprintf(e.output, "[Engineer] Retrying tests (attempt %d/%d)...\n", attempt, maxRetries)
|
||||
_, _ = fmt.Fprintf(e.output, "[Engineer] Retrying tests (attempt %d/%d)...\n", attempt, maxRetries)
|
||||
}
|
||||
|
||||
// Note: TestCommand comes from rig's config.json (trusted infrastructure config),
|
||||
// not from PR branches. Shell execution is intentional for flexibility (pipes, etc).
|
||||
cmd := exec.CommandContext(ctx, "sh", "-c", e.config.TestCommand)
|
||||
cmd := exec.CommandContext(ctx, "sh", "-c", e.config.TestCommand) //nolint:gosec // G204: TestCommand is from trusted rig config
|
||||
cmd.Dir = e.workDir
|
||||
var stdout, stderr bytes.Buffer
|
||||
cmd.Stdout = &stdout
|
||||
@@ -361,11 +361,11 @@ func (e *Engineer) runTests(ctx context.Context) ProcessResult {
|
||||
}
|
||||
lastErr = err
|
||||
|
||||
// Check if context was cancelled
|
||||
// Check if context was canceled
|
||||
if ctx.Err() != nil {
|
||||
return ProcessResult{
|
||||
Success: false,
|
||||
Error: "test run cancelled",
|
||||
Error: "test run canceled",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -396,42 +396,42 @@ func (e *Engineer) handleSuccess(mr *beads.Issue, result ProcessResult) {
|
||||
mrFields.CloseReason = "merged"
|
||||
newDesc := beads.SetMRFields(mr, mrFields)
|
||||
if err := e.beads.Update(mr.ID, beads.UpdateOptions{Description: &newDesc}); err != nil {
|
||||
fmt.Fprintf(e.output, "[Engineer] Warning: failed to update MR %s with merge commit: %v\n", mr.ID, err)
|
||||
_, _ = fmt.Fprintf(e.output, "[Engineer] Warning: failed to update MR %s with merge commit: %v\n", mr.ID, err)
|
||||
}
|
||||
|
||||
// 2. Close MR with reason 'merged'
|
||||
if err := e.beads.CloseWithReason("merged", mr.ID); err != nil {
|
||||
fmt.Fprintf(e.output, "[Engineer] Warning: failed to close MR %s: %v\n", mr.ID, err)
|
||||
_, _ = fmt.Fprintf(e.output, "[Engineer] Warning: failed to close MR %s: %v\n", mr.ID, err)
|
||||
}
|
||||
|
||||
// 3. Close source issue with reference to MR
|
||||
if mrFields.SourceIssue != "" {
|
||||
closeReason := fmt.Sprintf("Merged in %s", mr.ID)
|
||||
if err := e.beads.CloseWithReason(closeReason, mrFields.SourceIssue); err != nil {
|
||||
fmt.Fprintf(e.output, "[Engineer] Warning: failed to close source issue %s: %v\n", mrFields.SourceIssue, err)
|
||||
_, _ = fmt.Fprintf(e.output, "[Engineer] Warning: failed to close source issue %s: %v\n", mrFields.SourceIssue, err)
|
||||
} else {
|
||||
fmt.Fprintf(e.output, "[Engineer] Closed source issue: %s\n", mrFields.SourceIssue)
|
||||
_, _ = fmt.Fprintf(e.output, "[Engineer] Closed source issue: %s\n", mrFields.SourceIssue)
|
||||
}
|
||||
}
|
||||
|
||||
// 3.5. Clear agent bead's active_mr reference (traceability cleanup)
|
||||
if mrFields.AgentBead != "" {
|
||||
if err := e.beads.UpdateAgentActiveMR(mrFields.AgentBead, ""); err != nil {
|
||||
fmt.Fprintf(e.output, "[Engineer] Warning: failed to clear agent bead %s active_mr: %v\n", mrFields.AgentBead, err)
|
||||
_, _ = fmt.Fprintf(e.output, "[Engineer] Warning: failed to clear agent bead %s active_mr: %v\n", mrFields.AgentBead, err)
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Delete source branch if configured (local only - branches never go to origin)
|
||||
if e.config.DeleteMergedBranches && mrFields.Branch != "" {
|
||||
if err := e.git.DeleteBranch(mrFields.Branch, true); err != nil {
|
||||
fmt.Fprintf(e.output, "[Engineer] Warning: failed to delete branch %s: %v\n", mrFields.Branch, err)
|
||||
_, _ = fmt.Fprintf(e.output, "[Engineer] Warning: failed to delete branch %s: %v\n", mrFields.Branch, err)
|
||||
} else {
|
||||
fmt.Fprintf(e.output, "[Engineer] Deleted local branch: %s\n", mrFields.Branch)
|
||||
_, _ = fmt.Fprintf(e.output, "[Engineer] Deleted local branch: %s\n", mrFields.Branch)
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Log success
|
||||
fmt.Fprintf(e.output, "[Engineer] ✓ Merged: %s (commit: %s)\n", mr.ID, result.MergeCommit)
|
||||
_, _ = fmt.Fprintf(e.output, "[Engineer] ✓ Merged: %s (commit: %s)\n", mr.ID, result.MergeCommit)
|
||||
}
|
||||
|
||||
// handleFailure handles a failed merge request.
|
||||
@@ -440,25 +440,25 @@ func (e *Engineer) handleFailure(mr *beads.Issue, result ProcessResult) {
|
||||
// Reopen the MR (back to open status for rework)
|
||||
open := "open"
|
||||
if err := e.beads.Update(mr.ID, beads.UpdateOptions{Status: &open}); err != nil {
|
||||
fmt.Fprintf(e.output, "[Engineer] Warning: failed to reopen MR %s: %v\n", mr.ID, err)
|
||||
_, _ = fmt.Fprintf(e.output, "[Engineer] Warning: failed to reopen MR %s: %v\n", mr.ID, err)
|
||||
}
|
||||
|
||||
// Log the failure
|
||||
fmt.Fprintf(e.output, "[Engineer] ✗ Failed: %s - %s\n", mr.ID, result.Error)
|
||||
_, _ = fmt.Fprintf(e.output, "[Engineer] ✗ Failed: %s - %s\n", mr.ID, result.Error)
|
||||
}
|
||||
|
||||
// ProcessMRFromQueue processes a merge request from wisp queue.
|
||||
func (e *Engineer) ProcessMRFromQueue(ctx context.Context, mr *mrqueue.MR) ProcessResult {
|
||||
// MR fields are directly on the struct (no parsing needed)
|
||||
fmt.Fprintln(e.output, "[Engineer] Processing MR from queue:")
|
||||
fmt.Fprintf(e.output, " Branch: %s\n", mr.Branch)
|
||||
fmt.Fprintf(e.output, " Target: %s\n", mr.Target)
|
||||
fmt.Fprintf(e.output, " Worker: %s\n", mr.Worker)
|
||||
fmt.Fprintf(e.output, " Source: %s\n", mr.SourceIssue)
|
||||
_, _ = fmt.Fprintln(e.output, "[Engineer] Processing MR from queue:")
|
||||
_, _ = fmt.Fprintf(e.output, " Branch: %s\n", mr.Branch)
|
||||
_, _ = fmt.Fprintf(e.output, " Target: %s\n", mr.Target)
|
||||
_, _ = fmt.Fprintf(e.output, " Worker: %s\n", mr.Worker)
|
||||
_, _ = fmt.Fprintf(e.output, " Source: %s\n", mr.SourceIssue)
|
||||
|
||||
// Emit merge_started event
|
||||
if err := e.eventLogger.LogMergeStarted(mr); err != nil {
|
||||
fmt.Fprintf(e.output, "[Engineer] Warning: failed to log merge_started event: %v\n", err)
|
||||
_, _ = fmt.Fprintf(e.output, "[Engineer] Warning: failed to log merge_started event: %v\n", err)
|
||||
}
|
||||
|
||||
// Use the shared merge logic
|
||||
@@ -469,7 +469,7 @@ func (e *Engineer) ProcessMRFromQueue(ctx context.Context, mr *mrqueue.MR) Proce
|
||||
func (e *Engineer) handleSuccessFromQueue(mr *mrqueue.MR, result ProcessResult) {
|
||||
// Emit merged event
|
||||
if err := e.eventLogger.LogMerged(mr, result.MergeCommit); err != nil {
|
||||
fmt.Fprintf(e.output, "[Engineer] Warning: failed to log merged event: %v\n", err)
|
||||
_, _ = fmt.Fprintf(e.output, "[Engineer] Warning: failed to log merged event: %v\n", err)
|
||||
}
|
||||
|
||||
// Release merge slot if this was a conflict resolution
|
||||
@@ -480,10 +480,10 @@ func (e *Engineer) handleSuccessFromQueue(mr *mrqueue.MR, result ProcessResult)
|
||||
// Only log if it seems like an actual issue
|
||||
errStr := err.Error()
|
||||
if !strings.Contains(errStr, "not held") && !strings.Contains(errStr, "not found") {
|
||||
fmt.Fprintf(e.output, "[Engineer] Warning: failed to release merge slot: %v\n", err)
|
||||
_, _ = fmt.Fprintf(e.output, "[Engineer] Warning: failed to release merge slot: %v\n", err)
|
||||
}
|
||||
} else {
|
||||
fmt.Fprintf(e.output, "[Engineer] Released merge slot\n")
|
||||
_, _ = fmt.Fprintf(e.output, "[Engineer] Released merge slot\n")
|
||||
}
|
||||
|
||||
// Update and close the MR bead (matches handleSuccess behavior)
|
||||
@@ -491,7 +491,7 @@ func (e *Engineer) handleSuccessFromQueue(mr *mrqueue.MR, result ProcessResult)
|
||||
// Fetch the MR bead to update its fields
|
||||
mrBead, err := e.beads.Show(mr.ID)
|
||||
if err != nil {
|
||||
fmt.Fprintf(e.output, "[Engineer] Warning: failed to fetch MR bead %s: %v\n", mr.ID, err)
|
||||
_, _ = fmt.Fprintf(e.output, "[Engineer] Warning: failed to fetch MR bead %s: %v\n", mr.ID, err)
|
||||
} else {
|
||||
// Update MR with merge_commit SHA and close_reason
|
||||
mrFields := beads.ParseMRFields(mrBead)
|
||||
@@ -502,15 +502,15 @@ func (e *Engineer) handleSuccessFromQueue(mr *mrqueue.MR, result ProcessResult)
|
||||
mrFields.CloseReason = "merged"
|
||||
newDesc := beads.SetMRFields(mrBead, mrFields)
|
||||
if err := e.beads.Update(mr.ID, beads.UpdateOptions{Description: &newDesc}); err != nil {
|
||||
fmt.Fprintf(e.output, "[Engineer] Warning: failed to update MR %s with merge commit: %v\n", mr.ID, err)
|
||||
_, _ = fmt.Fprintf(e.output, "[Engineer] Warning: failed to update MR %s with merge commit: %v\n", mr.ID, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Close MR bead with reason 'merged'
|
||||
if err := e.beads.CloseWithReason("merged", mr.ID); err != nil {
|
||||
fmt.Fprintf(e.output, "[Engineer] Warning: failed to close MR %s: %v\n", mr.ID, err)
|
||||
_, _ = fmt.Fprintf(e.output, "[Engineer] Warning: failed to close MR %s: %v\n", mr.ID, err)
|
||||
} else {
|
||||
fmt.Fprintf(e.output, "[Engineer] Closed MR bead: %s\n", mr.ID)
|
||||
_, _ = fmt.Fprintf(e.output, "[Engineer] Closed MR bead: %s\n", mr.ID)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -518,35 +518,35 @@ func (e *Engineer) handleSuccessFromQueue(mr *mrqueue.MR, result ProcessResult)
|
||||
if mr.SourceIssue != "" {
|
||||
closeReason := fmt.Sprintf("Merged in %s", mr.ID)
|
||||
if err := e.beads.CloseWithReason(closeReason, mr.SourceIssue); err != nil {
|
||||
fmt.Fprintf(e.output, "[Engineer] Warning: failed to close source issue %s: %v\n", mr.SourceIssue, err)
|
||||
_, _ = fmt.Fprintf(e.output, "[Engineer] Warning: failed to close source issue %s: %v\n", mr.SourceIssue, err)
|
||||
} else {
|
||||
fmt.Fprintf(e.output, "[Engineer] Closed source issue: %s\n", mr.SourceIssue)
|
||||
_, _ = fmt.Fprintf(e.output, "[Engineer] Closed source issue: %s\n", mr.SourceIssue)
|
||||
}
|
||||
}
|
||||
|
||||
// 1.5. Clear agent bead's active_mr reference (traceability cleanup)
|
||||
if mr.AgentBead != "" {
|
||||
if err := e.beads.UpdateAgentActiveMR(mr.AgentBead, ""); err != nil {
|
||||
fmt.Fprintf(e.output, "[Engineer] Warning: failed to clear agent bead %s active_mr: %v\n", mr.AgentBead, err)
|
||||
_, _ = fmt.Fprintf(e.output, "[Engineer] Warning: failed to clear agent bead %s active_mr: %v\n", mr.AgentBead, err)
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Delete source branch if configured (local only)
|
||||
if e.config.DeleteMergedBranches && mr.Branch != "" {
|
||||
if err := e.git.DeleteBranch(mr.Branch, true); err != nil {
|
||||
fmt.Fprintf(e.output, "[Engineer] Warning: failed to delete branch %s: %v\n", mr.Branch, err)
|
||||
_, _ = fmt.Fprintf(e.output, "[Engineer] Warning: failed to delete branch %s: %v\n", mr.Branch, err)
|
||||
} else {
|
||||
fmt.Fprintf(e.output, "[Engineer] Deleted local branch: %s\n", mr.Branch)
|
||||
_, _ = fmt.Fprintf(e.output, "[Engineer] Deleted local branch: %s\n", mr.Branch)
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Remove MR from queue (ephemeral - just delete the file)
|
||||
if err := e.mrQueue.Remove(mr.ID); err != nil {
|
||||
fmt.Fprintf(e.output, "[Engineer] Warning: failed to remove MR from queue: %v\n", err)
|
||||
_, _ = fmt.Fprintf(e.output, "[Engineer] Warning: failed to remove MR from queue: %v\n", err)
|
||||
}
|
||||
|
||||
// 4. Log success
|
||||
fmt.Fprintf(e.output, "[Engineer] ✓ Merged: %s (commit: %s)\n", mr.ID, result.MergeCommit)
|
||||
_, _ = fmt.Fprintf(e.output, "[Engineer] ✓ Merged: %s (commit: %s)\n", mr.ID, result.MergeCommit)
|
||||
}
|
||||
|
||||
// handleFailureFromQueue handles a failed merge from wisp queue.
|
||||
@@ -555,7 +555,7 @@ func (e *Engineer) handleSuccessFromQueue(mr *mrqueue.MR, result ProcessResult)
|
||||
func (e *Engineer) handleFailureFromQueue(mr *mrqueue.MR, result ProcessResult) {
|
||||
// Emit merge_failed event
|
||||
if err := e.eventLogger.LogMergeFailed(mr, result.Error); err != nil {
|
||||
fmt.Fprintf(e.output, "[Engineer] Warning: failed to log merge_failed event: %v\n", err)
|
||||
_, _ = fmt.Fprintf(e.output, "[Engineer] Warning: failed to log merge_failed event: %v\n", err)
|
||||
}
|
||||
|
||||
// If this was a conflict, create a conflict-resolution task for dispatch
|
||||
@@ -563,24 +563,24 @@ func (e *Engineer) handleFailureFromQueue(mr *mrqueue.MR, result ProcessResult)
|
||||
if result.Conflict {
|
||||
taskID, err := e.createConflictResolutionTask(mr, result)
|
||||
if err != nil {
|
||||
fmt.Fprintf(e.output, "[Engineer] Warning: failed to create conflict resolution task: %v\n", err)
|
||||
_, _ = fmt.Fprintf(e.output, "[Engineer] Warning: failed to create conflict resolution task: %v\n", err)
|
||||
} else {
|
||||
// Block the MR on the conflict resolution task
|
||||
// When the task closes, the MR unblocks and re-enters the ready queue
|
||||
if err := e.mrQueue.SetBlockedBy(mr.ID, taskID); err != nil {
|
||||
fmt.Fprintf(e.output, "[Engineer] Warning: failed to block MR on task: %v\n", err)
|
||||
_, _ = fmt.Fprintf(e.output, "[Engineer] Warning: failed to block MR on task: %v\n", err)
|
||||
} else {
|
||||
fmt.Fprintf(e.output, "[Engineer] MR %s blocked on conflict task %s (non-blocking delegation)\n", mr.ID, taskID)
|
||||
_, _ = fmt.Fprintf(e.output, "[Engineer] MR %s blocked on conflict task %s (non-blocking delegation)\n", mr.ID, taskID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Log the failure - MR stays in queue but may be blocked
|
||||
fmt.Fprintf(e.output, "[Engineer] ✗ Failed: %s - %s\n", mr.ID, result.Error)
|
||||
_, _ = fmt.Fprintf(e.output, "[Engineer] ✗ Failed: %s - %s\n", mr.ID, result.Error)
|
||||
if mr.BlockedBy != "" {
|
||||
fmt.Fprintln(e.output, "[Engineer] MR blocked pending conflict resolution - queue continues to next MR")
|
||||
_, _ = fmt.Fprintln(e.output, "[Engineer] MR blocked pending conflict resolution - queue continues to next MR")
|
||||
} else {
|
||||
fmt.Fprintln(e.output, "[Engineer] MR remains in queue for retry")
|
||||
_, _ = fmt.Fprintln(e.output, "[Engineer] MR remains in queue for retry")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -600,29 +600,29 @@ func (e *Engineer) handleFailureFromQueue(mr *mrqueue.MR, result ProcessResult)
|
||||
// This serializes conflict resolution - only one polecat can resolve conflicts at a time.
|
||||
// If the slot is already held, we skip creating the task and let the MR stay in queue.
|
||||
// When the current resolution completes and merges, the slot is released.
|
||||
func (e *Engineer) createConflictResolutionTask(mr *mrqueue.MR, result ProcessResult) (string, error) {
|
||||
func (e *Engineer) createConflictResolutionTask(mr *mrqueue.MR, _ ProcessResult) (string, error) { // result unused but kept for future merge diagnostics
|
||||
// === MERGE SLOT GATE: Serialize conflict resolution ===
|
||||
// Ensure merge slot exists (idempotent)
|
||||
slotID, err := e.beads.MergeSlotEnsureExists()
|
||||
if err != nil {
|
||||
fmt.Fprintf(e.output, "[Engineer] Warning: could not ensure merge slot: %v\n", err)
|
||||
_, _ = fmt.Fprintf(e.output, "[Engineer] Warning: could not ensure merge slot: %v\n", err)
|
||||
// Continue anyway - slot is optional for now
|
||||
} else {
|
||||
// Try to acquire the merge slot
|
||||
holder := e.rig.Name + "/refinery"
|
||||
status, err := e.beads.MergeSlotAcquire(holder, false)
|
||||
if err != nil {
|
||||
fmt.Fprintf(e.output, "[Engineer] Warning: could not acquire merge slot: %v\n", err)
|
||||
_, _ = fmt.Fprintf(e.output, "[Engineer] Warning: could not acquire merge slot: %v\n", err)
|
||||
// Continue anyway - slot is optional
|
||||
} else if !status.Available && status.Holder != "" && status.Holder != holder {
|
||||
// Slot is held by someone else - skip creating the task
|
||||
// The MR stays in queue and will retry when slot is released
|
||||
fmt.Fprintf(e.output, "[Engineer] Merge slot held by %s - deferring conflict resolution\n", status.Holder)
|
||||
fmt.Fprintf(e.output, "[Engineer] MR %s will retry after current resolution completes\n", mr.ID)
|
||||
_, _ = fmt.Fprintf(e.output, "[Engineer] Merge slot held by %s - deferring conflict resolution\n", status.Holder)
|
||||
_, _ = fmt.Fprintf(e.output, "[Engineer] MR %s will retry after current resolution completes\n", mr.ID)
|
||||
return "", nil // Not an error - just deferred
|
||||
}
|
||||
// Either we acquired the slot, or status indicates we already hold it
|
||||
fmt.Fprintf(e.output, "[Engineer] Acquired merge slot: %s\n", slotID)
|
||||
_, _ = fmt.Fprintf(e.output, "[Engineer] Acquired merge slot: %s\n", slotID)
|
||||
}
|
||||
|
||||
// Get the current main SHA for conflict tracking
|
||||
@@ -694,7 +694,7 @@ The Refinery will automatically retry the merge after you force-push.`,
|
||||
// The conflict task's ID is returned so the MR can be blocked on it.
|
||||
// When the task closes, the MR unblocks and re-enters the ready queue.
|
||||
|
||||
fmt.Fprintf(e.output, "[Engineer] Created conflict resolution task: %s (P%d)\n", task.ID, task.Priority)
|
||||
_, _ = fmt.Fprintf(e.output, "[Engineer] Created conflict resolution task: %s (P%d)\n", task.ID, task.Priority)
|
||||
|
||||
// Update the MR's retry count for priority scoring
|
||||
mr.RetryCount = retryCount
|
||||
|
||||
@@ -143,7 +143,7 @@ func (m *Manager) Start(foreground bool) error {
|
||||
return ErrAlreadyRunning
|
||||
}
|
||||
// Zombie - tmux alive but Claude dead. Kill and recreate.
|
||||
fmt.Fprintln(m.output, "⚠ Detected zombie session (tmux alive, Claude dead). Recreating...")
|
||||
_, _ = fmt.Fprintln(m.output, "⚠ Detected zombie session (tmux alive, Claude dead). Recreating...")
|
||||
if err := t.KillSession(sessionID); err != nil {
|
||||
return fmt.Errorf("killing zombie session: %w", err)
|
||||
}
|
||||
@@ -402,15 +402,15 @@ func parseTime(s string) time.Time {
|
||||
// run is deprecated - foreground mode now just prints a message.
|
||||
// The Refinery agent (Claude) handles all merge processing.
|
||||
// See: ZFC #5 - Move merge/conflict decisions from Go to Refinery agent
|
||||
func (m *Manager) run(ref *Refinery) error {
|
||||
fmt.Fprintln(m.output, "")
|
||||
fmt.Fprintln(m.output, "╔══════════════════════════════════════════════════════════════╗")
|
||||
fmt.Fprintln(m.output, "║ Foreground mode is deprecated. ║")
|
||||
fmt.Fprintln(m.output, "║ ║")
|
||||
fmt.Fprintln(m.output, "║ The Refinery agent (Claude) handles all merge decisions. ║")
|
||||
fmt.Fprintln(m.output, "║ Use 'gt refinery start' to run in background mode. ║")
|
||||
fmt.Fprintln(m.output, "╚══════════════════════════════════════════════════════════════╝")
|
||||
fmt.Fprintln(m.output, "")
|
||||
func (m *Manager) run(_ *Refinery) error { // ref unused: deprecated function
|
||||
_, _ = fmt.Fprintln(m.output, "")
|
||||
_, _ = fmt.Fprintln(m.output, "╔══════════════════════════════════════════════════════════════╗")
|
||||
_, _ = fmt.Fprintln(m.output, "║ Foreground mode is deprecated. ║")
|
||||
_, _ = fmt.Fprintln(m.output, "║ ║")
|
||||
_, _ = fmt.Fprintln(m.output, "║ The Refinery agent (Claude) handles all merge decisions. ║")
|
||||
_, _ = fmt.Fprintln(m.output, "║ Use 'gt refinery start' to run in background mode. ║")
|
||||
_, _ = fmt.Fprintln(m.output, "╚══════════════════════════════════════════════════════════════╝")
|
||||
_, _ = fmt.Fprintln(m.output, "")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -458,7 +458,7 @@ func (m *Manager) completeMR(mr *MergeRequest, closeReason CloseReason, errMsg s
|
||||
// Close the MR (in_progress → closed)
|
||||
if err := mr.Close(closeReason); err != nil {
|
||||
// Log error but continue - this shouldn't happen
|
||||
fmt.Fprintf(m.output, "Warning: failed to close MR: %v\n", err)
|
||||
_, _ = fmt.Fprintf(m.output, "Warning: failed to close MR: %v\n", err)
|
||||
}
|
||||
switch closeReason {
|
||||
case CloseReasonMerged:
|
||||
@@ -471,7 +471,7 @@ func (m *Manager) completeMR(mr *MergeRequest, closeReason CloseReason, errMsg s
|
||||
// Reopen the MR for rework (in_progress → open)
|
||||
if err := mr.Reopen(); err != nil {
|
||||
// Log error but continue
|
||||
fmt.Fprintf(m.output, "Warning: failed to reopen MR: %v\n", err)
|
||||
_, _ = fmt.Fprintf(m.output, "Warning: failed to reopen MR: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -486,7 +486,7 @@ func (m *Manager) runTests(testCmd string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
cmd := exec.Command(parts[0], parts[1:]...)
|
||||
cmd := exec.Command(parts[0], parts[1:]...) //nolint:gosec // G204: testCmd is from trusted rig config
|
||||
cmd.Dir = m.workDir
|
||||
|
||||
var stderr bytes.Buffer
|
||||
@@ -571,7 +571,7 @@ func (m *Manager) pushWithRetry(targetBranch string, config MergeConfig) error {
|
||||
|
||||
for attempt := 0; attempt <= config.PushRetryCount; attempt++ {
|
||||
if attempt > 0 {
|
||||
fmt.Fprintf(m.output, "Push retry %d/%d after %v\n", attempt, config.PushRetryCount, delay)
|
||||
_, _ = fmt.Fprintf(m.output, "Push retry %d/%d after %v\n", attempt, config.PushRetryCount, delay)
|
||||
time.Sleep(delay)
|
||||
delay *= 2 // Exponential backoff
|
||||
}
|
||||
@@ -731,7 +731,7 @@ func (m *Manager) Retry(id string, processNow bool) error {
|
||||
// The Refinery agent handles merge processing.
|
||||
// It will pick up this MR in its next patrol cycle.
|
||||
if processNow {
|
||||
fmt.Fprintln(m.output, "Note: --now is deprecated. The Refinery agent will process this MR in its next patrol cycle.")
|
||||
_, _ = fmt.Fprintln(m.output, "Note: --now is deprecated. The Refinery agent will process this MR in its next patrol cycle.")
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -334,7 +334,7 @@ func (m *Manager) AddRig(opts AddRigOptions) (*Rig, error) {
|
||||
// bd init --prefix will create the database and auto-import from issues.jsonl.
|
||||
sourceBeadsDB := filepath.Join(mayorRigPath, ".beads", "beads.db")
|
||||
if _, err := os.Stat(sourceBeadsDB); os.IsNotExist(err) {
|
||||
cmd := exec.Command("bd", "init", "--prefix", sourcePrefix)
|
||||
cmd := exec.Command("bd", "init", "--prefix", sourcePrefix) //nolint:gosec // G204: bd is a trusted internal tool
|
||||
cmd.Dir = mayorRigPath
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
fmt.Printf(" Warning: Could not init bd database: %v (%s)\n", err, strings.TrimSpace(string(output)))
|
||||
@@ -568,7 +568,7 @@ func (m *Manager) initBeads(rigPath, prefix string) error {
|
||||
// be initialized with 'gt' prefix for this to work.
|
||||
//
|
||||
// Agent beads track lifecycle state for ZFC compliance (gt-h3hak, gt-pinkq).
|
||||
func (m *Manager) initAgentBeads(rigPath, rigName, prefix string, isFirstRig bool) error {
|
||||
func (m *Manager) initAgentBeads(_, rigName, _ string, isFirstRig bool) error { // rigPath and prefix unused: agents use town beads not rig beads
|
||||
// Agent beads go in town beads (gt-* prefix), not rig beads.
|
||||
// This enables cross-rig agent coordination via canonical IDs.
|
||||
townBeadsDir := filepath.Join(m.townRoot, ".beads")
|
||||
@@ -662,7 +662,7 @@ func (m *Manager) ensureGitignoreEntry(gitignorePath, entry string) error {
|
||||
}
|
||||
|
||||
// Append entry
|
||||
f, err := os.OpenFile(gitignorePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
f, err := os.OpenFile(gitignorePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) //nolint:gosec // G302: .gitignore should be readable by git tools
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -923,7 +923,7 @@ func (m *Manager) seedPatrolMoleculesManually(rigPath string) error {
|
||||
}
|
||||
|
||||
// Create the molecule
|
||||
cmd := exec.Command("bd", "create",
|
||||
cmd := exec.Command("bd", "create", //nolint:gosec // G204: bd is a trusted internal tool
|
||||
"--type=molecule",
|
||||
"--title="+mol.title,
|
||||
"--description="+mol.desc,
|
||||
|
||||
@@ -440,7 +440,7 @@ func (m *Manager) StopAll(force bool) error {
|
||||
// This makes the work visible via 'gt hook' when the session starts.
|
||||
func (m *Manager) hookIssue(issueID, agentID, workDir string) error {
|
||||
// Use bd update to set status=hooked and assign to the polecat
|
||||
cmd := exec.Command("bd", "update", issueID, "--status=hooked", "--assignee="+agentID)
|
||||
cmd := exec.Command("bd", "update", issueID, "--status=hooked", "--assignee="+agentID) //nolint:gosec // G204: bd is a trusted internal tool
|
||||
cmd.Dir = workDir
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
|
||||
@@ -195,7 +195,7 @@ func (m *Manager) gitRunOutput(dir string, args ...string) (string, error) {
|
||||
}
|
||||
|
||||
// notifyMayorCodeAtRisk sends an alert to Mayor about code at risk.
|
||||
func (m *Manager) notifyMayorCodeAtRisk(townRoot, swarmID string, workers []string) {
|
||||
func (m *Manager) notifyMayorCodeAtRisk(_, swarmID string, workers []string) { // townRoot unused: router uses gitDir
|
||||
router := mail.NewRouter(m.gitDir)
|
||||
msg := &mail.Message{
|
||||
From: fmt.Sprintf("%s/refinery", m.rig.Name),
|
||||
@@ -214,7 +214,7 @@ Manual intervention required.`,
|
||||
}
|
||||
|
||||
// notifyMayorLanded sends a landing report to Mayor.
|
||||
func (m *Manager) notifyMayorLanded(townRoot string, swarm *Swarm, result *LandingResult) {
|
||||
func (m *Manager) notifyMayorLanded(_ string, swarm *Swarm, result *LandingResult) { // townRoot unused: router uses gitDir
|
||||
router := mail.NewRouter(m.gitDir)
|
||||
msg := &mail.Message{
|
||||
From: fmt.Sprintf("%s/refinery", m.rig.Name),
|
||||
|
||||
@@ -190,12 +190,12 @@ func (m *Manager) IsComplete(swarmID string) (bool, error) {
|
||||
// isValidTransition checks if a state transition is allowed.
|
||||
func isValidTransition(from, to SwarmState) bool {
|
||||
transitions := map[SwarmState][]SwarmState{
|
||||
SwarmCreated: {SwarmActive, SwarmCancelled},
|
||||
SwarmActive: {SwarmMerging, SwarmFailed, SwarmCancelled},
|
||||
SwarmMerging: {SwarmLanded, SwarmFailed, SwarmCancelled},
|
||||
SwarmLanded: {}, // Terminal
|
||||
SwarmFailed: {}, // Terminal
|
||||
SwarmCancelled: {}, // Terminal
|
||||
SwarmCreated: {SwarmActive, SwarmCanceled},
|
||||
SwarmActive: {SwarmMerging, SwarmFailed, SwarmCanceled},
|
||||
SwarmMerging: {SwarmLanded, SwarmFailed, SwarmCanceled},
|
||||
SwarmLanded: {}, // Terminal
|
||||
SwarmFailed: {}, // Terminal
|
||||
SwarmCanceled: {}, // Terminal
|
||||
}
|
||||
|
||||
allowed, ok := transitions[from]
|
||||
|
||||
@@ -22,13 +22,13 @@ const (
|
||||
// SwarmFailed means the swarm failed and cannot be recovered.
|
||||
SwarmFailed SwarmState = "failed"
|
||||
|
||||
// SwarmCancelled means the swarm was explicitly cancelled.
|
||||
SwarmCancelled SwarmState = "cancelled"
|
||||
// SwarmCanceled means the swarm was explicitly canceled.
|
||||
SwarmCanceled SwarmState = "canceled"
|
||||
)
|
||||
|
||||
// IsTerminal returns true if the swarm is in a terminal state.
|
||||
func (s SwarmState) IsTerminal() bool {
|
||||
return s == SwarmLanded || s == SwarmFailed || s == SwarmCancelled
|
||||
return s == SwarmLanded || s == SwarmFailed || s == SwarmCanceled
|
||||
}
|
||||
|
||||
// IsActive returns true if the swarm is actively running.
|
||||
|
||||
@@ -15,7 +15,7 @@ func TestSwarmStateIsTerminal(t *testing.T) {
|
||||
{SwarmMerging, false},
|
||||
{SwarmLanded, true},
|
||||
{SwarmFailed, true},
|
||||
{SwarmCancelled, true},
|
||||
{SwarmCanceled, true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
@@ -35,7 +35,7 @@ func TestSwarmStateIsActive(t *testing.T) {
|
||||
{SwarmMerging, true},
|
||||
{SwarmLanded, false},
|
||||
{SwarmFailed, false},
|
||||
{SwarmCancelled, false},
|
||||
{SwarmCanceled, false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
||||
@@ -190,7 +190,7 @@ func ProvisionCommands(workspacePath string) error {
|
||||
return fmt.Errorf("reading %s: %w", entry.Name(), err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(destPath, content, 0644); err != nil {
|
||||
if err := os.WriteFile(destPath, content, 0644); err != nil { //nolint:gosec // G306: template files are non-sensitive
|
||||
return fmt.Errorf("writing %s: %w", entry.Name(), err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ func AssignThemeFromPalette(rigName string, palette []Theme) Theme {
|
||||
return DefaultPalette[0]
|
||||
}
|
||||
h := fnv.New32a()
|
||||
h.Write([]byte(rigName))
|
||||
_, _ = h.Write([]byte(rigName))
|
||||
idx := int(h.Sum32()) % len(palette)
|
||||
return palette[idx]
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ func (l *Logger) LogEvent(event Event) error {
|
||||
}
|
||||
|
||||
// Open file for appending
|
||||
f, err := os.OpenFile(l.logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
f, err := os.OpenFile(l.logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
|
||||
if err != nil {
|
||||
return fmt.Errorf("opening log file: %w", err)
|
||||
}
|
||||
@@ -211,7 +211,7 @@ func truncate(s string, maxLen int) string {
|
||||
func ReadEvents(townRoot string) ([]Event, error) {
|
||||
path := logPath(townRoot)
|
||||
|
||||
content, err := os.ReadFile(path)
|
||||
content, err := os.ReadFile(path) //nolint:gosec // G304: path is constructed from trusted townRoot
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, nil // No log file yet
|
||||
|
||||
@@ -142,7 +142,7 @@ func loadTrackedIssues(townBeads, convoyID string) ([]IssueItem, int, int) {
|
||||
WHERE d.issue_id = '%s' AND d.type = 'tracks'
|
||||
`, convoyID)
|
||||
|
||||
cmd := exec.CommandContext(ctx, "sqlite3", "-json", dbPath, query)
|
||||
cmd := exec.CommandContext(ctx, "sqlite3", "-json", dbPath, query) //nolint:gosec // G204: sqlite3 with controlled query
|
||||
var stdout bytes.Buffer
|
||||
cmd.Stdout = &stdout
|
||||
|
||||
@@ -210,7 +210,7 @@ func getIssueDetailsBatch(townBeads string, issueIDs []string) map[string]IssueI
|
||||
args := append([]string{"show"}, issueIDs...)
|
||||
args = append(args, "--json")
|
||||
|
||||
cmd := exec.CommandContext(ctx, "bd", args...)
|
||||
cmd := exec.CommandContext(ctx, "bd", args...) //nolint:gosec // G204: bd is a trusted internal tool
|
||||
cmd.Dir = townBeads
|
||||
var stdout bytes.Buffer
|
||||
cmd.Stdout = &stdout
|
||||
|
||||
@@ -93,7 +93,7 @@ func listConvoys(beadsDir, status string) ([]convoyListItem, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), convoySubprocessTimeout)
|
||||
defer cancel()
|
||||
|
||||
cmd := exec.CommandContext(ctx, "bd", listArgs...)
|
||||
cmd := exec.CommandContext(ctx, "bd", listArgs...) //nolint:gosec // G204: args are constructed internally
|
||||
cmd.Dir = beadsDir
|
||||
var stdout bytes.Buffer
|
||||
cmd.Stdout = &stdout
|
||||
@@ -169,7 +169,7 @@ func getTrackedIssueStatus(beadsDir, convoyID string) []trackedStatus {
|
||||
|
||||
// Query tracked dependencies from SQLite
|
||||
// convoyID is validated above to match ^hq-[a-zA-Z0-9-]+$
|
||||
cmd := exec.CommandContext(ctx, "sqlite3", "-json", dbPath,
|
||||
cmd := exec.CommandContext(ctx, "sqlite3", "-json", dbPath, //nolint:gosec // G204: convoyID is validated against strict pattern
|
||||
fmt.Sprintf(`SELECT depends_on_id FROM dependencies WHERE issue_id = '%s' AND type = 'tracks'`, convoyID))
|
||||
|
||||
var stdout bytes.Buffer
|
||||
|
||||
@@ -255,7 +255,7 @@ func (s *GtEventsSource) tail(ctx context.Context) {
|
||||
defer close(s.events)
|
||||
|
||||
// Seek to end for live tailing
|
||||
s.file.Seek(0, 2)
|
||||
_, _ = s.file.Seek(0, 2)
|
||||
|
||||
scanner := bufio.NewScanner(s.file)
|
||||
ticker := time.NewTicker(100 * time.Millisecond)
|
||||
|
||||
@@ -35,7 +35,7 @@ func NewMQEventSource(beadsDir string) (*MQEventSource, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
f.Close()
|
||||
_ = f.Close() //nolint:gosec // G104: best-effort close on file creation
|
||||
}
|
||||
|
||||
file, err := os.Open(logPath)
|
||||
@@ -71,7 +71,7 @@ func (s *MQEventSource) tail(ctx context.Context) {
|
||||
defer close(s.events)
|
||||
|
||||
// Seek to end for live tailing
|
||||
s.file.Seek(0, 2)
|
||||
_, _ = s.file.Seek(0, 2)
|
||||
|
||||
scanner := bufio.NewScanner(s.file)
|
||||
ticker := time.NewTicker(100 * time.Millisecond)
|
||||
|
||||
+2
-2
@@ -30,12 +30,12 @@ func writeJSON(path string, v interface{}) error {
|
||||
|
||||
// Write to temp file then rename for atomicity
|
||||
tmp := path + ".tmp"
|
||||
if err := os.WriteFile(tmp, data, 0644); err != nil {
|
||||
if err := os.WriteFile(tmp, data, 0644); err != nil { //nolint:gosec // G306: wisp messages are non-sensitive operational data
|
||||
return fmt.Errorf("write temp: %w", err)
|
||||
}
|
||||
|
||||
if err := os.Rename(tmp, path); err != nil {
|
||||
os.Remove(tmp) // cleanup on failure
|
||||
_ = os.Remove(tmp) // cleanup on failure
|
||||
return fmt.Errorf("rename: %w", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -335,7 +335,7 @@ func createCleanupWisp(workDir, polecatName, issueID, branch string) (string, er
|
||||
|
||||
labels := strings.Join(CleanupWispLabels(polecatName, "pending"), ",")
|
||||
|
||||
cmd := exec.Command("bd", "create",
|
||||
cmd := exec.Command("bd", "create", //nolint:gosec // G204: args are constructed internally
|
||||
"--wisp",
|
||||
"--title", title,
|
||||
"--description", description,
|
||||
@@ -380,7 +380,7 @@ func createSwarmWisp(workDir string, payload *SwarmStartPayload) (string, error)
|
||||
|
||||
labels := strings.Join(SwarmWispLabels(payload.SwarmID, payload.Total, 0, payload.StartedAt), ",")
|
||||
|
||||
cmd := exec.Command("bd", "create",
|
||||
cmd := exec.Command("bd", "create", //nolint:gosec // G204: args are constructed internally
|
||||
"--wisp",
|
||||
"--title", title,
|
||||
"--description", description,
|
||||
@@ -410,7 +410,7 @@ func createSwarmWisp(workDir string, payload *SwarmStartPayload) (string, error)
|
||||
|
||||
// findCleanupWisp finds an existing cleanup wisp for a polecat.
|
||||
func findCleanupWisp(workDir, polecatName string) (string, error) {
|
||||
cmd := exec.Command("bd", "list",
|
||||
cmd := exec.Command("bd", "list", //nolint:gosec // G204: bd is a trusted internal tool
|
||||
"--wisp",
|
||||
"--labels", fmt.Sprintf("polecat:%s,state:merge-requested", polecatName),
|
||||
"--status", "open",
|
||||
@@ -475,7 +475,7 @@ func getCleanupStatus(workDir, rigName, polecatName string) string {
|
||||
prefix := beads.GetPrefixForRig(townRoot, rigName)
|
||||
agentBeadID := beads.PolecatBeadIDWithPrefix(prefix, rigName, polecatName)
|
||||
|
||||
cmd := exec.Command("bd", "show", agentBeadID, "--json")
|
||||
cmd := exec.Command("bd", "show", agentBeadID, "--json") //nolint:gosec // G204: agentBeadID is validated internally
|
||||
cmd.Dir = workDir
|
||||
|
||||
var stdout, stderr bytes.Buffer
|
||||
@@ -624,7 +624,7 @@ func UpdateCleanupWispState(workDir, wispID, newState string) error {
|
||||
// Update with new state
|
||||
newLabels := strings.Join(CleanupWispLabels(polecatName, newState), ",")
|
||||
|
||||
updateCmd := exec.Command("bd", "update", wispID, "--labels", newLabels)
|
||||
updateCmd := exec.Command("bd", "update", wispID, "--labels", newLabels) //nolint:gosec // G204: args are constructed internally
|
||||
updateCmd.Dir = workDir
|
||||
|
||||
var stderr bytes.Buffer
|
||||
@@ -647,7 +647,7 @@ func UpdateCleanupWispState(workDir, wispID, newState string) error {
|
||||
func NukePolecat(workDir, rigName, polecatName string) error {
|
||||
address := fmt.Sprintf("%s/%s", rigName, polecatName)
|
||||
|
||||
cmd := exec.Command("gt", "polecat", "nuke", address)
|
||||
cmd := exec.Command("gt", "polecat", "nuke", address) //nolint:gosec // G204: address is constructed from validated internal data
|
||||
cmd.Dir = workDir
|
||||
|
||||
var stderr bytes.Buffer
|
||||
|
||||
@@ -221,7 +221,7 @@ func ParseSwarmStart(body string) (*SwarmStartPayload, error) {
|
||||
if strings.HasPrefix(line, "SwarmID:") || strings.HasPrefix(line, "swarm_id:") {
|
||||
payload.SwarmID = strings.TrimSpace(strings.TrimPrefix(strings.TrimPrefix(line, "SwarmID:"), "swarm_id:"))
|
||||
} else if strings.HasPrefix(line, "Total:") {
|
||||
fmt.Sscanf(line, "Total: %d", &payload.Total)
|
||||
_, _ = fmt.Sscanf(line, "Total: %d", &payload.Total)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user