Files
nixos-configs/home/roles/development/default.nix
obsidian 8f8582b0f3
All checks were successful
CI / check (push) Successful in 3m24s
feat(gastown): add statusline cache writes for CPU optimization
Complete the statusline optimization by adding cache writes to all
output functions. The existing patch added cache functions and cache
reads, but never wrote to the cache.

Changes:
- Add early-return for detached sessions (return static "○ |")
- Add cache read check for attached sessions
- Add setStatusLineCache() calls in all 5 output functions:
  - runWorkerStatusLine
  - runMayorStatusLine
  - runDeaconStatusLine
  - runWitnessStatusLine
  - runRefineryStatusLine

This should reduce Dolt CPU from ~70% to ~20% when agents are idle,
as tmux status lines will use cached results instead of spawning
beads queries every 5 seconds.

Testing: Run `nix switch` then monitor Dolt CPU with `top`

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

464 lines
16 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=";
});
# 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
# Cache functions already exist in upstream, we just add the early-return + cache writes
# See: https://github.com/steveyegge/gastown/issues/TBD
substituteInPlace internal/cmd/statusline.go \
--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
}
// runMayorStatusLine' \
'// Output
if len(parts) > 0 {
output := strings.Join(parts, " | ") + " |"
if statusLineSession != "" {
setStatusLineCache(statusLineSession, output)
}
fmt.Print(output)
}
return nil
}
// runMayorStatusLine' \
--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
};
}