From daf539397e74bb283d2a54bfe55ed4a26b93750b Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Fri, 26 Dec 2025 19:28:39 -0800 Subject: [PATCH] feat: add town-level activity signal for Gas Town daemon (bd-v8ku) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Writes activity.json to ~/gt/daemon/ when bd runs inside a Gas Town workspace. This enables the daemon to detect bd usage and adjust its polling frequency with exponential backoff. Best-effort: silently skips if not in Gas Town or on any error. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- cmd/bd/main.go | 78 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/cmd/bd/main.go b/cmd/bd/main.go index 49f220c8..03fbee8b 100644 --- a/cmd/bd/main.go +++ b/cmd/bd/main.go @@ -2,6 +2,7 @@ package main import ( "context" + "encoding/json" "fmt" "os" "os/signal" @@ -312,6 +313,9 @@ var rootCmd = &cobra.Command{ // Set up signal-aware context for graceful cancellation rootCtx, rootCancel = signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) + // Signal Gas Town daemon about bd activity (best-effort, for exponential backoff) + defer signalGasTownActivity() + // Apply verbosity flags early (before any output) debug.SetVerbose(verboseFlag) debug.SetQuiet(quietFlag) @@ -944,6 +948,80 @@ var rootCmd = &cobra.Command{ // Configurable via config file or BEADS_FLUSH_DEBOUNCE env var (e.g., "500ms", "10s") // Defaults to 5 seconds if not set or invalid +// signalGasTownActivity writes an activity signal for Gas Town daemon. +// This enables exponential backoff based on bd usage detection (gt-ws8ol). +// Best-effort: silent on any failure, never affects bd operation. +func signalGasTownActivity() { + // Determine town root + // Priority: GT_ROOT env > detect from cwd path > skip + townRoot := os.Getenv("GT_ROOT") + if townRoot == "" { + // Try to detect from cwd - if under ~/gt/, use that as town root + home, err := os.UserHomeDir() + if err != nil { + return + } + gtRoot := filepath.Join(home, "gt") + cwd, err := os.Getwd() + if err != nil { + return + } + if strings.HasPrefix(cwd, gtRoot+string(os.PathSeparator)) { + townRoot = gtRoot + } + } + + if townRoot == "" { + return // Not in Gas Town, skip + } + + // Ensure daemon directory exists + daemonDir := filepath.Join(townRoot, "daemon") + if err := os.MkdirAll(daemonDir, 0755); err != nil { + return + } + + // Build command line from os.Args + cmdLine := strings.Join(os.Args, " ") + + // Determine actor (use package-level var if set, else fall back to env) + actorName := actor + if actorName == "" { + if bdActor := os.Getenv("BD_ACTOR"); bdActor != "" { + actorName = bdActor + } else if user := os.Getenv("USER"); user != "" { + actorName = user + } else { + actorName = "unknown" + } + } + + // Build activity signal + activity := struct { + LastCommand string `json:"last_command"` + Actor string `json:"actor"` + Timestamp string `json:"timestamp"` + }{ + LastCommand: cmdLine, + Actor: actorName, + Timestamp: time.Now().UTC().Format(time.RFC3339), + } + + data, err := json.Marshal(activity) + if err != nil { + return + } + + // Write atomically (write to temp, rename) + activityPath := filepath.Join(daemonDir, "activity.json") + tmpPath := activityPath + ".tmp" + // nolint:gosec // G306: 0644 is appropriate for a status file + if err := os.WriteFile(tmpPath, data, 0644); err != nil { + return + } + _ = os.Rename(tmpPath, activityPath) +} + func main() { if err := rootCmd.Execute(); err != nil { os.Exit(1)