{ config, lib, pkgs, globalInputs, system, ... }: with lib; let cfg = config.home.roles.development; # Build beads and gastown from flake inputs using shared package definitions beadsRev = builtins.substring 0 8 (globalInputs.beads.rev or "unknown"); beadsPackage = pkgs.callPackage ../../../packages/beads { src = globalInputs.beads; version = "0.52.0-${beadsRev}"; }; gastownRev = builtins.substring 0 8 (globalInputs.gastown.rev or "unknown"); gastownPackage = pkgs.callPackage ../../../packages/gastown { src = globalInputs.gastown; version = "unstable-${gastownRev}"; }; # 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.unstable.buildGoModule { pname = "perles"; version = "unstable-${perlesRev}"; src = globalInputs.perles; vendorHash = "sha256-A5LE9Cor/DRcJtVpiScSoqDYhJIKyaq0cbK+OGmr4XU="; 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"; }; # Claude Code statusline: shows model, cwd, git branch, and context usage % claudeCodeStatusLineConfig = pkgs.writeText "claude-statusline.json" (builtins.toJSON { type = "command"; command = ''input=$(cat); model=$(echo "$input" | jq -r '.model.display_name'); cwd=$(echo "$input" | jq -r '.workspace.current_dir'); if git -C "$cwd" rev-parse --git-dir > /dev/null 2>&1; then branch=$(git -C "$cwd" --no-optional-locks rev-parse --abbrev-ref HEAD 2>/dev/null || echo ""); if [ -n "$branch" ]; then git_info=" on $branch"; else git_info=""; fi; else git_info=""; fi; usage=$(echo "$input" | jq '.context_window.current_usage'); if [ "$usage" != "null" ]; then current=$(echo "$usage" | jq '.input_tokens + .cache_creation_input_tokens + .cache_read_input_tokens'); size=$(echo "$input" | jq '.context_window.context_window_size'); pct=$((current * 100 / size)); context_info=" | ''${pct}% context"; else context_info=""; fi; printf "%s in %s%s%s" "$model" "$cwd" "$git_info" "$context_info"''; }); 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.unstable.dolt pkgs.sqlite # Custom packages pkgs.custom.tea-rbw pkgs.custom.pi-coding-agent ]; # 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)" ''; # Configure Claude Code statusline (merge into existing settings.json) home.activation.claudeCodeStatusLine = lib.hm.dag.entryAfter ["writeBoundary" "claudeCodeBeadsSetup"] '' SETTINGS="$HOME/.claude/settings.json" mkdir -p "$HOME/.claude" if [ -f "$SETTINGS" ]; then ${pkgs.jq}/bin/jq --slurpfile sl ${claudeCodeStatusLineConfig} '.statusLine = $sl[0]' "$SETTINGS" > "''${SETTINGS}.tmp" && mv "''${SETTINGS}.tmp" "$SETTINGS" else ${pkgs.jq}/bin/jq -n --slurpfile sl ${claudeCodeStatusLineConfig} '{statusLine: $sl[0]}' > "$SETTINGS" fi $DRY_RUN_CMD echo "Claude Code statusline configured" ''; # 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 }; }