Files
nixos-configs/home/roles/development/default.nix
rust 3acf9d2796 fix(gastown): regenerate statusline optimization patch with correct line numbers
The patch file had malformed hunk headers with incorrect line numbers
and counts, causing it to fail to apply against the locked gastown rev
(177094a2). This was NOT a flake.lock issue - gastown source was properly
locked.

Changes:
- Regenerated patch from scratch against locked gastown revision
- Re-enabled the patch in default.nix (was commented out with TODO)
- Updated comment to accurately describe the optimization

The optimization skips expensive beads queries for detached tmux sessions
and caches status line output with a 10-second TTL, reducing Dolt CPU
usage from ~70% to ~20%.

Closes: hq-0h1p9m

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-31 13:59:11 -08:00

287 lines
11 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
# Each patch is stored in a separate file for clarity and maintainability
patches = [
# Fix validateRecipient bug: normalize addresses before comparison
./gastown-fix-validate-recipient.patch
# Fix agentBeadToAddress to use title field for hq- prefixed beads
./gastown-fix-agent-bead-address-title.patch
# Fix agentBeadToAddress to handle rig-specific prefixes (j-, sc-, etc.)
./gastown-fix-agent-bead-rig-prefix.patch
# Fix crew/polecat home paths: remove incorrect /rig suffix
./gastown-fix-role-home-paths.patch
# Fix town root detection: don't map to Mayor (causes spurious mismatch warnings)
./gastown-fix-town-root-detection.patch
# Fix copyDir to handle symlinks (broken symlinks cause "no such file" errors)
./gastown-fix-copydir-symlinks.patch
# Statusline optimization: skip expensive beads queries for detached sessions
# Reduces Dolt CPU from ~70% to ~20% by caching and early-exit
./gastown-statusline-optimization.patch
];
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
};
}