From 56097aefa4d0d42b73896011d1d8a3804f319b88 Mon Sep 17 00:00:00 2001 From: nixos_configs/crew/harry Date: Sat, 31 Jan 2026 09:06:05 -0800 Subject: [PATCH] refactor(development): move gastown patches to separate files Replace inline postPatch substituteInPlace calls with proper unified diff patch files, following the pattern established by beads. This improves maintainability: - Each patch is in its own file with clear naming - Patches use proper unified diff format - Easier to review, update, and track individual fixes - Default.nix is cleaner (237 lines of substituteInPlace -> 15 lines) Patches included: - gastown-fix-validate-recipient.patch - gastown-fix-agent-bead-address-title.patch - gastown-fix-agent-bead-rig-prefix.patch - gastown-fix-role-home-paths.patch - gastown-fix-town-root-detection.patch - gastown-fix-copydir-symlinks.patch - gastown-statusline-optimization.patch Co-Authored-By: Claude Opus 4.5 --- home/roles/development/default.nix | 247 +----------------- ...gastown-fix-agent-bead-address-title.patch | 16 ++ .../gastown-fix-agent-bead-rig-prefix.patch | 36 +++ .../gastown-fix-copydir-symlinks.patch | 25 ++ .../gastown-fix-role-home-paths.patch | 19 ++ .../gastown-fix-town-root-detection.patch | 19 ++ .../gastown-fix-validate-recipient.patch | 13 + .../gastown-statusline-optimization.patch | 136 ++++++++++ 8 files changed, 274 insertions(+), 237 deletions(-) create mode 100644 home/roles/development/gastown-fix-agent-bead-address-title.patch create mode 100644 home/roles/development/gastown-fix-agent-bead-rig-prefix.patch create mode 100644 home/roles/development/gastown-fix-copydir-symlinks.patch create mode 100644 home/roles/development/gastown-fix-role-home-paths.patch create mode 100644 home/roles/development/gastown-fix-town-root-detection.patch create mode 100644 home/roles/development/gastown-fix-validate-recipient.patch create mode 100644 home/roles/development/gastown-statusline-optimization.patch diff --git a/home/roles/development/default.nix b/home/roles/development/default.nix index 5a2d7df..bf6107d 100644 --- a/home/roles/development/default.nix +++ b/home/roles/development/default.nix @@ -41,251 +41,24 @@ let ]; # Bug fixes not yet merged upstream - postPatch = '' + # Each patch is stored in a separate file for clarity and maintainability + patches = [ # Fix validateRecipient bug: normalize addresses before comparison - # See: https://github.com/steveyegge/gastown/issues/TBD - substituteInPlace internal/mail/router.go \ - --replace-fail \ - 'if agentBeadToAddress(agent) == identity {' \ - 'if AddressToIdentity(agentBeadToAddress(agent)) == AddressToIdentity(identity) {' - + ./gastown-fix-validate-recipient.patch # Fix agentBeadToAddress to use title field for hq- prefixed beads - # Title should contain the address (e.g., "java/crew/americano") - substituteInPlace internal/mail/router.go \ - --replace-fail \ - 'return parseAgentAddressFromDescription(bead.Description)' \ - 'if bead.Title != "" && strings.Contains(bead.Title, "/") { return bead.Title }; return parseAgentAddressFromDescription(bead.Description)' - + ./gastown-fix-agent-bead-address-title.patch # Fix agentBeadToAddress to handle rig-specific prefixes (j-, sc-, etc.) - # Bead IDs like j-java-crew-americano should map to java/crew/americano - substituteInPlace internal/mail/router.go \ - --replace-fail \ - '// Handle gt- prefixed IDs (legacy format) - if !strings.HasPrefix(id, "gt-") { - return "" // Not a valid agent bead ID - }' \ - '// Handle rig-specific prefixes: --- - // Examples: j-java-crew-americano -> java/crew/americano - idParts := strings.Split(id, "-") - if len(idParts) >= 3 { - for i, part := range idParts { - if part == "crew" || part == "polecat" || part == "polecats" { - if i >= 1 && i < len(idParts)-1 { - rig := idParts[i-1] - name := strings.Join(idParts[i+1:], "-") - return rig + "/" + part + "/" + name - } - } - if part == "witness" || part == "refinery" { - if i >= 1 { - return idParts[i-1] + "/" + part - } - } - } - } - - // Handle gt- prefixed IDs (legacy format) - if !strings.HasPrefix(id, "gt-") { - return "" // Not a valid agent bead ID - }' - + ./gastown-fix-agent-bead-rig-prefix.patch # Fix crew/polecat home paths: remove incorrect /rig suffix - substituteInPlace internal/cmd/role.go \ - --replace-fail \ - 'return filepath.Join(townRoot, rig, "polecats", polecat, "rig")' \ - 'return filepath.Join(townRoot, rig, "polecats", polecat)' \ - --replace-fail \ - 'return filepath.Join(townRoot, rig, "crew", polecat, "rig")' \ - 'return filepath.Join(townRoot, rig, "crew", polecat)' - + ./gastown-fix-role-home-paths.patch # Fix town root detection: don't map to Mayor (causes spurious mismatch warnings) - substituteInPlace internal/cmd/prime.go \ - --replace-fail \ - 'if relPath == "." || relPath == "" { - ctx.Role = RoleMayor - return ctx - } - if len(parts) >= 1 && parts[0] == "mayor" {' \ - 'if relPath == "." || relPath == "" { - return ctx // RoleUnknown - town root is shared space - } - - // Check for mayor role: mayor/ or mayor/rig/ - if len(parts) >= 1 && parts[0] == "mayor" {' - + ./gastown-fix-town-root-detection.patch # Fix copyDir to handle symlinks (broken symlinks cause "no such file" errors) - # See: https://github.com/steveyegge/gastown/issues/TBD - substituteInPlace internal/git/git.go \ - --replace-fail \ - 'if entry.IsDir() {' \ - '// Handle symlinks (recreate them, do not follow) - if entry.Type()&os.ModeSymlink != 0 { - linkTarget, err := os.Readlink(srcPath) - if err != nil { - return err - } - if err := os.Symlink(linkTarget, destPath); err != nil { - return err - } - continue - } - - if entry.IsDir() {' - + ./gastown-fix-copydir-symlinks.patch # Statusline optimization: skip detached sessions and cache results # Reduces Dolt CPU from ~70% to ~20% by avoiding beads queries for sessions nobody is watching - # See: https://github.com/steveyegge/gastown/issues/TBD - substituteInPlace internal/cmd/statusline.go \ - --replace-fail \ - '"strings"' \ - '"strings" - "time"' \ - --replace-fail \ - 'var ( - statusLineSession string -)' \ - '// statusLineCacheTTL is how long cached status output remains valid. -const statusLineCacheTTL = 10 * time.Second - -// statusLineCachePath returns the cache file path for a session. -func statusLineCachePath(session string) string { - return filepath.Join(os.TempDir(), fmt.Sprintf("gt-status-%s", session)) -} - -// getStatusLineCache returns cached status if fresh, empty string otherwise. -func getStatusLineCache(session string) string { - path := statusLineCachePath(session) - info, err := os.Stat(path) - if err != nil { - return "" - } - if time.Since(info.ModTime()) > statusLineCacheTTL { - return "" - } - data, err := os.ReadFile(path) - if err != nil { - return "" - } - return string(data) -} - -// setStatusLineCache writes status to cache file. -func setStatusLineCache(session, status string) { - path := statusLineCachePath(session) - _ = os.WriteFile(path, []byte(status), 0644) -} - -var ( - statusLineSession string -)' \ - --replace-fail \ - 'func runStatusLine(cmd *cobra.Command, args []string) error { - t := tmux.NewTmux() - - // Get session environment' \ - 'func runStatusLine(cmd *cobra.Command, args []string) error { - t := tmux.NewTmux() - - // Optimization: skip expensive beads queries for detached sessions - if statusLineSession != "" { - if !t.IsSessionAttached(statusLineSession) { - fmt.Print("○ |") - return nil - } - // Check cache for attached sessions too - if cached := getStatusLineCache(statusLineSession); cached != "" { - fmt.Print(cached) - return nil - } - } - - // Get session environment' \ - --replace-fail \ - ' // Output - if len(parts) > 0 { - fmt.Print(strings.Join(parts, " | ") + " |") - } - - return nil -} - -func runMayorStatusLine(t *tmux.Tmux) error {' \ - ' // Output - if len(parts) > 0 { - output := strings.Join(parts, " | ") + " |" - if statusLineSession != "" { - setStatusLineCache(statusLineSession, output) - } - fmt.Print(output) - } - - return nil -} - -func runMayorStatusLine(t *tmux.Tmux) error {' \ - --replace-fail \ - 'fmt.Print(strings.Join(parts, " | ") + " |") - return nil -} - -// runDeaconStatusLine outputs status for the deacon session.' \ - 'output := strings.Join(parts, " | ") + " |" - if statusLineSession != "" { - setStatusLineCache(statusLineSession, output) - } - fmt.Print(output) - return nil -} - -// runDeaconStatusLine outputs status for the deacon session.' \ - --replace-fail \ - 'fmt.Print(strings.Join(parts, " | ") + " |") - return nil -} - -// runWitnessStatusLine outputs status for a witness session. -// Shows: crew count, hook or mail preview' \ - 'output := strings.Join(parts, " | ") + " |" - if statusLineSession != "" { - setStatusLineCache(statusLineSession, output) - } - fmt.Print(output) - return nil -} - -// runWitnessStatusLine outputs status for a witness session. -// Shows: crew count, hook or mail preview' \ - --replace-fail \ - 'fmt.Print(strings.Join(parts, " | ") + " |") - return nil -} - -// runRefineryStatusLine outputs status for a refinery session.' \ - 'output := strings.Join(parts, " | ") + " |" - if statusLineSession != "" { - setStatusLineCache(statusLineSession, output) - } - fmt.Print(output) - return nil -} - -// runRefineryStatusLine outputs status for a refinery session.' \ - --replace-fail \ - 'fmt.Print(strings.Join(parts, " | ") + " |") - return nil -} - -// isSessionWorking detects' \ - 'output := strings.Join(parts, " | ") + " |" - if statusLineSession != "" { - setStatusLineCache(statusLineSession, output) - } - fmt.Print(output) - return nil -} - -// isSessionWorking detects' - ''; + ./gastown-statusline-optimization.patch + ]; meta = with lib; { description = "Gas Town - multi-agent workspace manager by Steve Yegge"; diff --git a/home/roles/development/gastown-fix-agent-bead-address-title.patch b/home/roles/development/gastown-fix-agent-bead-address-title.patch new file mode 100644 index 0000000..b2d89b7 --- /dev/null +++ b/home/roles/development/gastown-fix-agent-bead-address-title.patch @@ -0,0 +1,16 @@ +diff --git a/internal/mail/router.go b/internal/mail/router.go +index 0000000..1111111 100644 +--- a/internal/mail/router.go ++++ b/internal/mail/router.go +@@ -326,7 +326,11 @@ func agentBeadToAddress(bead *agentBead) string { + } + + // Fall back to parsing description for role_type and rig +- return parseAgentAddressFromDescription(bead.Description) ++ if bead.Title != "" && strings.Contains(bead.Title, "/") { ++ return bead.Title ++ } ++ return parseAgentAddressFromDescription(bead.Description) + } + + // Handle gt- prefixed IDs (legacy format) diff --git a/home/roles/development/gastown-fix-agent-bead-rig-prefix.patch b/home/roles/development/gastown-fix-agent-bead-rig-prefix.patch new file mode 100644 index 0000000..f462bd1 --- /dev/null +++ b/home/roles/development/gastown-fix-agent-bead-rig-prefix.patch @@ -0,0 +1,36 @@ +diff --git a/internal/mail/router.go b/internal/mail/router.go +index 0000000..1111111 100644 +--- a/internal/mail/router.go ++++ b/internal/mail/router.go +@@ -330,8 +330,28 @@ func agentBeadToAddress(bead *agentBead) string { + } + + // Handle gt- prefixed IDs (legacy format) +- if !strings.HasPrefix(id, "gt-") { +- return "" // Not a valid agent bead ID ++ // Handle rig-specific prefixes: --- ++ // Examples: j-java-crew-americano -> java/crew/americano ++ idParts := strings.Split(id, "-") ++ if len(idParts) >= 3 { ++ for i, part := range idParts { ++ if part == "crew" || part == "polecat" || part == "polecats" { ++ if i >= 1 && i < len(idParts)-1 { ++ rig := idParts[i-1] ++ name := strings.Join(idParts[i+1:], "-") ++ return rig + "/" + part + "/" + name ++ } ++ } ++ if part == "witness" || part == "refinery" { ++ if i >= 1 { ++ return idParts[i-1] + "/" + part ++ } ++ } ++ } ++ } ++ ++ // Handle gt- prefixed IDs (legacy format) ++ if !strings.HasPrefix(id, "gt-") { ++ return "" // Not a valid agent bead ID + } + + // Strip prefix diff --git a/home/roles/development/gastown-fix-copydir-symlinks.patch b/home/roles/development/gastown-fix-copydir-symlinks.patch new file mode 100644 index 0000000..98cc19f --- /dev/null +++ b/home/roles/development/gastown-fix-copydir-symlinks.patch @@ -0,0 +1,25 @@ +diff --git a/internal/git/git.go b/internal/git/git.go +index 0000000..1111111 100644 +--- a/internal/git/git.go ++++ b/internal/git/git.go +@@ -73,7 +73,18 @@ func copyDir(src, dest string) error { + srcPath := filepath.Join(src, entry.Name()) + destPath := filepath.Join(dest, entry.Name()) + +- if entry.IsDir() { ++ // Handle symlinks (recreate them, do not follow) ++ if entry.Type()&os.ModeSymlink != 0 { ++ linkTarget, err := os.Readlink(srcPath) ++ if err != nil { ++ return err ++ } ++ if err := os.Symlink(linkTarget, destPath); err != nil { ++ return err ++ } ++ continue ++ } ++ ++ if entry.IsDir() { + if err := copyDir(srcPath, destPath); err != nil { + return err + } diff --git a/home/roles/development/gastown-fix-role-home-paths.patch b/home/roles/development/gastown-fix-role-home-paths.patch new file mode 100644 index 0000000..eaf049f --- /dev/null +++ b/home/roles/development/gastown-fix-role-home-paths.patch @@ -0,0 +1,19 @@ +diff --git a/internal/cmd/role.go b/internal/cmd/role.go +index 0000000..1111111 100644 +--- a/internal/cmd/role.go ++++ b/internal/cmd/role.go +@@ -326,11 +326,11 @@ func getRoleHome(role Role, rig, polecat, townRoot string) string { + if rig == "" || polecat == "" { + return "" + } +- return filepath.Join(townRoot, rig, "polecats", polecat, "rig") ++ return filepath.Join(townRoot, rig, "polecats", polecat) + case RoleCrew: + if rig == "" || polecat == "" { + return "" + } +- return filepath.Join(townRoot, rig, "crew", polecat, "rig") ++ return filepath.Join(townRoot, rig, "crew", polecat) + default: + return "" + } diff --git a/home/roles/development/gastown-fix-town-root-detection.patch b/home/roles/development/gastown-fix-town-root-detection.patch new file mode 100644 index 0000000..de5170a --- /dev/null +++ b/home/roles/development/gastown-fix-town-root-detection.patch @@ -0,0 +1,19 @@ +diff --git a/internal/cmd/prime.go b/internal/cmd/prime.go +index 0000000..1111111 100644 +--- a/internal/cmd/prime.go ++++ b/internal/cmd/prime.go +@@ -276,12 +276,12 @@ func detectRole(cwd, townRoot string) RoleInfo { + + // Check for mayor role + // At town root, or in mayor/ or mayor/rig/ + if relPath == "." || relPath == "" { +- ctx.Role = RoleMayor +- return ctx ++ return ctx // RoleUnknown - town root is shared space + } ++ ++ // Check for mayor role: mayor/ or mayor/rig/ + if len(parts) >= 1 && parts[0] == "mayor" { + ctx.Role = RoleMayor + return ctx + } diff --git a/home/roles/development/gastown-fix-validate-recipient.patch b/home/roles/development/gastown-fix-validate-recipient.patch new file mode 100644 index 0000000..96e51c1 --- /dev/null +++ b/home/roles/development/gastown-fix-validate-recipient.patch @@ -0,0 +1,13 @@ +diff --git a/internal/mail/router.go b/internal/mail/router.go +index b864c069..4b6a045b 100644 +--- a/internal/mail/router.go ++++ b/internal/mail/router.go +@@ -646,7 +646,7 @@ func (r *Router) validateRecipient(identity string) error { + } + + for _, agent := range agents { +- if agentBeadToAddress(agent) == identity { ++ if AddressToIdentity(agentBeadToAddress(agent)) == AddressToIdentity(identity) { + return nil // Found matching agent + } + } diff --git a/home/roles/development/gastown-statusline-optimization.patch b/home/roles/development/gastown-statusline-optimization.patch new file mode 100644 index 0000000..395c391 --- /dev/null +++ b/home/roles/development/gastown-statusline-optimization.patch @@ -0,0 +1,136 @@ +diff --git a/internal/cmd/statusline.go b/internal/cmd/statusline.go +index 0000000..1111111 100644 +--- a/internal/cmd/statusline.go ++++ b/internal/cmd/statusline.go +@@ -6,6 +6,7 @@ import ( + "os" + "path/filepath" + "sort" + "strings" ++ "time" + + "github.com/spf13/cobra" + "github.com/steveyegge/gastown/internal/beads" +@@ -15,6 +16,43 @@ import ( + "github.com/steveyegge/gastown/internal/workspace" + ) + ++// statusLineCacheTTL is how long cached status output remains valid. ++const statusLineCacheTTL = 10 * time.Second ++ ++// statusLineCachePath returns the cache file path for a session. ++func statusLineCachePath(session string) string { ++ return filepath.Join(os.TempDir(), fmt.Sprintf("gt-status-%s", session)) ++} ++ ++// getStatusLineCache returns cached status if fresh, empty string otherwise. ++func getStatusLineCache(session string) string { ++ path := statusLineCachePath(session) ++ info, err := os.Stat(path) ++ if err != nil { ++ return "" ++ } ++ if time.Since(info.ModTime()) > statusLineCacheTTL { ++ return "" ++ } ++ data, err := os.ReadFile(path) ++ if err != nil { ++ return "" ++ } ++ return string(data) ++} ++ ++// setStatusLineCache writes status to cache file. ++func setStatusLineCache(session, status string) { ++ path := statusLineCachePath(session) ++ _ = os.WriteFile(path, []byte(status), 0644) ++} ++ + var ( + statusLineSession string + ) +@@ -32,6 +70,20 @@ func init() { + func runStatusLine(cmd *cobra.Command, args []string) error { + t := tmux.NewTmux() + ++ // Optimization: skip expensive beads queries for detached sessions ++ if statusLineSession != "" { ++ if !t.IsSessionAttached(statusLineSession) { ++ fmt.Print("○ |") ++ return nil ++ } ++ // Check cache for attached sessions too ++ if cached := getStatusLineCache(statusLineSession); cached != "" { ++ fmt.Print(cached) ++ return nil ++ } ++ } ++ + // Get session environment + var rigName, polecat, crew, issue, role string + +@@ -149,7 +201,12 @@ func runWorkerStatusLine(t *tmux.Tmux, session, rigName, polecat, crew, issue st + + // Output + if len(parts) > 0 { +- fmt.Print(strings.Join(parts, " | ") + " |") ++ output := strings.Join(parts, " | ") + " |" ++ if statusLineSession != "" { ++ setStatusLineCache(statusLineSession, output) ++ } ++ fmt.Print(output) + } + + return nil +@@ -389,7 +446,12 @@ func runMayorStatusLine(t *tmux.Tmux) error { + } + } + +- fmt.Print(strings.Join(parts, " | ") + " |") ++ output := strings.Join(parts, " | ") + " |" ++ if statusLineSession != "" { ++ setStatusLineCache(statusLineSession, output) ++ } ++ fmt.Print(output) + return nil + } + +@@ -458,7 +520,12 @@ func runDeaconStatusLine(t *tmux.Tmux) error { + } + } + +- fmt.Print(strings.Join(parts, " | ") + " |") ++ output := strings.Join(parts, " | ") + " |" ++ if statusLineSession != "" { ++ setStatusLineCache(statusLineSession, output) ++ } ++ fmt.Print(output) + return nil + } + +@@ -526,7 +593,12 @@ func runWitnessStatusLine(t *tmux.Tmux, rigName string) error { + } + } + +- fmt.Print(strings.Join(parts, " | ") + " |") ++ output := strings.Join(parts, " | ") + " |" ++ if statusLineSession != "" { ++ setStatusLineCache(statusLineSession, output) ++ } ++ fmt.Print(output) + return nil + } + +@@ -617,7 +689,12 @@ func runRefineryStatusLine(t *tmux.Tmux, rigName string) error { + } + } + +- fmt.Print(strings.Join(parts, " | ") + " |") ++ output := strings.Join(parts, " | ") + " |" ++ if statusLineSession != "" { ++ setStatusLineCache(statusLineSession, output) ++ } ++ fmt.Print(output) + return nil + } +