All checks were successful
CI / check (push) Successful in 3m25s
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>
514 lines
17 KiB
Nix
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
|
|
};
|
|
}
|