Files
nixos-configs/home/roles/development/default.nix
mayor 21a8b5c5d9
All checks were successful
CI / check (push) Successful in 3m25s
Fix bd SearchIssues inefficient WHERE IN query pattern for Dolt
The Dolt backend's SearchIssues was using a two-phase query:
1. SELECT id FROM issues WHERE ... -> collect all IDs
2. SELECT * FROM issues WHERE id IN (id1, id2, ... id8000+)

With 8000+ issues, this second query with 8000+ placeholders hammers
Dolt CPU at 100%+. The fix changes SearchIssues to select all columns
directly in the first query and scan results inline.

See: hq-ihwsj

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-29 18:29:46 -08:00

514 lines
17 KiB
Nix

{ config, lib, pkgs, globalInputs, system, ... }:
with lib;
let
cfg = config.home.roles.development;
# FIXME: Temporary override for upstream beads vendorHash mismatch
# Remove after upstream fix: https://github.com/steveyegge/beads/issues/XXX
beadsPackage = globalInputs.beads.packages.${system}.default.overrideAttrs (old: {
vendorHash = "sha256-YU+bRLVlWtHzJ1QPzcKJ70f+ynp8lMoIeFlm+29BNPE=";
# Performance fix: avoid WHERE IN (8000+ IDs) query pattern that hammers Dolt CPU
# See: hq-ihwsj - bd list uses inefficient WHERE IN (all_ids) query pattern
# The fix changes SearchIssues to SELECT all columns directly instead of:
# 1. SELECT id FROM issues WHERE ... -> collect IDs
# 2. SELECT * FROM issues WHERE id IN (all_ids) -> 8000+ placeholder IN clause
patches = (old.patches or []) ++ [
./beads-search-query-optimization.patch
];
});
# Gastown - multi-agent workspace manager (no upstream flake.nix yet)
# Source is tracked via flake input for renovate updates
gastownRev = builtins.substring 0 8 (globalInputs.gastown.rev or "unknown");
gastownPackage = pkgs.buildGoModule {
pname = "gastown";
version = "unstable-${gastownRev}";
src = globalInputs.gastown;
vendorHash = "sha256-ripY9vrYgVW8bngAyMLh0LkU/Xx1UUaLgmAA7/EmWQU=";
subPackages = [ "cmd/gt" ];
doCheck = false;
# Must match ldflags from gastown Makefile - BuiltProperly=1 is required
# or gt will error with "This binary was built with 'go build' directly"
ldflags = [
"-X github.com/steveyegge/gastown/internal/cmd.Version=${gastownRev}"
"-X github.com/steveyegge/gastown/internal/cmd.Commit=${gastownRev}"
"-X github.com/steveyegge/gastown/internal/cmd.BuildTime=nix-build"
"-X github.com/steveyegge/gastown/internal/cmd.BuiltProperly=1"
];
# Bug fixes not yet merged upstream
postPatch = ''
# 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) {'
# 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)'
# 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: <prefix>-<rig>-<role>-<name>
// 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
}'
# 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)'
# 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" {'
# 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() {'
# 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'
'';
meta = with lib; {
description = "Gas Town - multi-agent workspace manager by Steve Yegge";
homepage = "https://github.com/steveyegge/gastown";
license = licenses.mit;
mainProgram = "gt";
};
};
# Perles - TUI for beads issue tracking (no upstream flake.nix yet)
# Source is tracked via flake input for renovate updates
perlesRev = builtins.substring 0 8 (globalInputs.perles.rev or "unknown");
perlesPackage = pkgs.buildGoModule {
pname = "perles";
version = "unstable-${perlesRev}";
src = globalInputs.perles;
vendorHash = "sha256-JHERJDzbiqgjWXwRhXVjgDEiDQ3AUXRIONotfPF21B0=";
doCheck = false;
ldflags = [
"-X main.version=${perlesRev}"
];
meta = with lib; {
description = "Perles - Terminal UI for beads issue tracking";
homepage = "https://github.com/zjrosen/perles";
license = licenses.mit;
mainProgram = "perles";
};
};
# Fetch the claude-plugins repository (for humanlayer commands/agents)
# Update the rev to get newer versions of the commands
claudePluginsRepo = builtins.fetchGit {
url = "https://github.com/jeffh/claude-plugins.git";
# To update: change this to the latest commit hash
# You can find the latest commit at: https://github.com/jeffh/claude-plugins/commits/main
rev = "5e3e4d937162185b6d78c62022cbfd1c8ad42c4c";
ref = "main";
};
in
{
options.home.roles.development = {
enable = mkEnableOption "Enable development tools and utilities";
allowArbitraryClaudeCodeModelSelection = mkOption {
type = types.bool;
default = false;
description = ''
Whether to preserve model specifications in Claude Code humanlayer commands and agents.
When false (default), the model: line is stripped from frontmatter, allowing Claude Code
to use its default model selection.
When true, the model: specifications from the source files are preserved, allowing
commands to specify opus/sonnet/haiku explicitly.
'';
};
};
config = mkIf cfg.enable {
home.packages = [
beadsPackage
gastownPackage
perlesPackage
pkgs.unstable.claude-code
pkgs.unstable.claude-code-router
pkgs.unstable.codex
pkgs.dolt
pkgs.sqlite
# Custom packages
pkgs.custom.tea-rbw
];
# Install Claude Code humanlayer command and agent plugins
home.activation.claudeCodeCommands = lib.hm.dag.entryAfter ["writeBoundary"] ''
# Clean up old plugin-installed commands and agents to avoid duplicates
rm -f ~/.claude/commands/humanlayer:* 2>/dev/null || true
rm -f ~/.claude/agents/humanlayer:* 2>/dev/null || true
# Create directories if they don't exist
mkdir -p ~/.claude/commands
mkdir -p ~/.claude/agents
# Copy all humanlayer command files and remove model specifications
for file in ${claudePluginsRepo}/humanlayer/commands/*.md; do
if [ -f "$file" ]; then
filename=$(basename "$file" .md)
dest="$HOME/.claude/commands/humanlayer:''${filename}.md"
rm -f "$dest" 2>/dev/null || true
# Copy file and conditionally remove the "model:" line from frontmatter
${if cfg.allowArbitraryClaudeCodeModelSelection
then "cp \"$file\" \"$dest\""
else "${pkgs.gnused}/bin/sed '/^model:/d' \"$file\" > \"$dest\""
}
chmod u+w "$dest" 2>/dev/null || true
fi
done
# Copy all humanlayer agent files and remove model specifications
for file in ${claudePluginsRepo}/humanlayer/agents/*.md; do
if [ -f "$file" ]; then
filename=$(basename "$file" .md)
dest="$HOME/.claude/agents/humanlayer:''${filename}.md"
rm -f "$dest" 2>/dev/null || true
# Copy file and conditionally remove the "model:" line from frontmatter
${if cfg.allowArbitraryClaudeCodeModelSelection
then "cp \"$file\" \"$dest\""
else "${pkgs.gnused}/bin/sed '/^model:/d' \"$file\" > \"$dest\""
}
chmod u+w "$dest" 2>/dev/null || true
fi
done
# Copy local commands from this repo (with retry for race conditions with running Claude)
for file in ${./commands}/*.md; do
if [ -f "$file" ]; then
filename=$(basename "$file" .md)
dest="$HOME/.claude/commands/''${filename}.md"
# Remove existing file first, then copy with retry on failure
rm -f "$dest" 2>/dev/null || true
if ! cp "$file" "$dest" 2>/dev/null; then
sleep 0.5
cp "$file" "$dest" || echo "Warning: Failed to copy $filename.md to commands"
fi
chmod u+w "$dest" 2>/dev/null || true
fi
done
# Copy local skills (reference materials) to skills subdirectory
mkdir -p ~/.claude/commands/skills
for file in ${./skills}/*.md; do
if [ -f "$file" ]; then
filename=$(basename "$file" .md)
dest="$HOME/.claude/commands/skills/''${filename}.md"
rm -f "$dest" 2>/dev/null || true
if ! cp "$file" "$dest" 2>/dev/null; then
sleep 0.5
cp "$file" "$dest" || echo "Warning: Failed to copy $filename.md to skills"
fi
chmod u+w "$dest" 2>/dev/null || true
fi
done
# Copy micro-skills (compact reusable knowledge referenced by formulas)
for file in ${./skills/micro}/*.md; do
if [ -f "$file" ]; then
dest="$HOME/.claude/commands/skills/$(basename "$file")"
rm -f "$dest" 2>/dev/null || true
cp "$file" "$dest"
chmod u+w "$dest" 2>/dev/null || true
fi
done
# Install beads formulas to user-level formula directory
mkdir -p ~/.beads/formulas
for file in ${./formulas}/*.formula.toml; do
if [ -f "$file" ]; then
dest="$HOME/.beads/formulas/$(basename "$file")"
rm -f "$dest" 2>/dev/null || true
cp "$file" "$dest"
chmod u+w "$dest" 2>/dev/null || true
fi
done
$DRY_RUN_CMD echo "Claude Code plugins installed: humanlayer commands/agents + local commands + local skills + formulas"
'';
# Set up beads Claude Code integration (hooks for SessionStart/PreCompact)
# This uses the CLI + hooks approach which is recommended over MCP for Claude Code
home.activation.claudeCodeBeadsSetup = lib.hm.dag.entryAfter ["writeBoundary" "claudeCodeCommands"] ''
# Run bd setup claude to install hooks into ~/.claude/settings.json
# This is idempotent - safe to run multiple times
${beadsPackage}/bin/bd setup claude 2>/dev/null || true
$DRY_RUN_CMD echo "Claude Code beads integration configured (hooks installed)"
'';
# Beads timer gate checker (Linux only - uses systemd)
# Runs every 5 minutes to auto-resolve expired timer gates across all beads projects
# This enables self-scheduling molecules (watchers, patrols, etc.)
systemd.user.services.beads-gate-check = lib.mkIf pkgs.stdenv.isLinux {
Unit = {
Description = "Check and resolve expired beads timer gates";
};
Service = {
Type = "oneshot";
# Check gates in all workspaces that have running daemons
ExecStart = pkgs.writeShellScript "beads-gate-check-all" ''
# Get list of workspaces from daemon registry
workspaces=$(${beadsPackage}/bin/bd daemon list --json 2>/dev/null | ${pkgs.jq}/bin/jq -r '.[].workspace // empty' 2>/dev/null)
if [ -z "$workspaces" ]; then
exit 0 # No beads workspaces, nothing to do
fi
for ws in $workspaces; do
if [ -d "$ws" ]; then
cd "$ws" && ${beadsPackage}/bin/bd gate check --type=timer --quiet 2>/dev/null || true
fi
done
'';
};
};
systemd.user.timers.beads-gate-check = lib.mkIf pkgs.stdenv.isLinux {
Unit = {
Description = "Periodic beads timer gate check";
};
Timer = {
OnBootSec = "5min";
OnUnitActiveSec = "5min";
};
Install = {
WantedBy = [ "timers.target" ];
};
};
# Note: modules must be imported at top-level home config
};
}