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:
max
2026-01-03 16:11:40 -08:00
committed by Steve Yegge
parent 62848065e3
commit 1b69576573
82 changed files with 325 additions and 355 deletions
+20 -2
View File
@@ -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'
+1 -1
View File
@@ -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)
}
+2 -2
View File
@@ -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)
+1 -1
View File
@@ -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
}
+2 -2
View File
@@ -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)
+1 -1
View File
@@ -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.
+2 -2
View File
@@ -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)
}
+1 -1
View File
@@ -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)
}
+1 -1
View File
@@ -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
+1 -4
View File
@@ -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)
+3 -3
View File
@@ -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 {
+1 -1
View File
@@ -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])
}
+2 -2
View File
@@ -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 {
+3 -3
View File
@@ -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)
+8 -2
View File
@@ -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
View File
@@ -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
+2 -2
View File
@@ -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 {
+2 -2
View File
@@ -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 {
+4 -4
View File
@@ -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()
}
+1 -1
View File
@@ -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
View File
@@ -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:
+1 -1
View File
@@ -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
+1 -4
View File
@@ -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 ""
+1 -1
View File
@@ -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
View File
@@ -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 {
+1 -1
View File
@@ -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")
+1 -1
View File
@@ -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)
+4 -4
View File
@@ -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
}
+4 -4
View File
@@ -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 {
+3 -3
View File
@@ -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.
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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
View File
@@ -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
+3 -3
View File
@@ -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 {
+4 -4
View File
@@ -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)
+1 -1
View File
@@ -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"))
+6 -6
View File
@@ -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
}
+1 -1
View File
@@ -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
+4 -8
View File
@@ -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
View File
@@ -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 {
+3 -3
View File
@@ -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
View File
@@ -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)
}
+2 -2
View File
@@ -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)
}
+1 -1
View File
@@ -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}
+4 -4
View File
@@ -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)
+2 -2
View File
@@ -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
}
+2 -2
View File
@@ -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.
+1 -1
View File
@@ -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") {
+1 -1
View File
@@ -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))
+4 -4
View File
@@ -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))
+3 -3
View File
@@ -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
}
+1 -1
View File
@@ -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,
+2 -2
View File
@@ -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)
}
+1 -1
View File
@@ -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.
+1 -1
View File
@@ -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)
}
+4 -4
View File
@@ -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.
+1 -1
View File
@@ -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)
}
+2 -2
View File
@@ -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()
}
+4 -4
View File
@@ -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
}
+6 -6
View File
@@ -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)
+1 -1
View File
@@ -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)
}
+2 -2
View File
@@ -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)
}
+7 -7
View File
@@ -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
}
+5 -5
View File
@@ -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
+61 -61
View File
@@ -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
+15 -15
View File
@@ -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
+4 -4
View File
@@ -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,
+1 -1
View File
@@ -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 {
+2 -2
View File
@@ -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),
+6 -6
View File
@@ -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]
+3 -3
View File
@@ -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.
+2 -2
View File
@@ -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 {
+1 -1
View File
@@ -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)
}
}
+1 -1
View File
@@ -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]
}
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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
+1 -1
View File
@@ -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)
+2 -2
View File
@@ -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
View File
@@ -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)
}
+6 -6
View File
@@ -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
+1 -1
View File
@@ -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)
}
}