2026-08-08 21:20:27 -04:00
|
|
|
|
{ config, pkgs, lib, ... }:
|
2026-07-18 09:21:51 -04:00
|
|
|
|
|
|
|
|
|
|
let
|
2026-08-08 21:20:27 -04:00
|
|
|
|
home = config.home.homeDirectory;
|
|
|
|
|
|
dotfiles = "${home}/.dotfiles";
|
|
|
|
|
|
firstmateHome = "${home}/Documents/firstmate";
|
2026-07-18 09:21:51 -04:00
|
|
|
|
in
|
|
|
|
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
home.username = "laptran";
|
|
|
|
|
|
home.homeDirectory = "/Users/laptran";
|
|
|
|
|
|
home.stateVersion = "24.11";
|
2026-09-18 15:14:43 -04:00
|
|
|
|
# home-configuration.nix(5) is generated via options.json that embeds a
|
|
|
|
|
|
# nixpkgs store path without string context. Determinate Nix warns on every
|
|
|
|
|
|
# rebuild. Package man pages are unaffected.
|
|
|
|
|
|
manual.manpages.enable = false;
|
2026-07-18 09:21:51 -04:00
|
|
|
|
home.packages = with pkgs; [
|
|
|
|
|
|
ripgrep
|
|
|
|
|
|
fd
|
|
|
|
|
|
fzf
|
|
|
|
|
|
jq
|
|
|
|
|
|
lazygit
|
|
|
|
|
|
neovim
|
|
|
|
|
|
nerd-fonts.hack
|
2026-08-08 21:20:27 -04:00
|
|
|
|
ngrok # reverse TCP tunnel so the phone can SSH in over cellular (unfree)
|
|
|
|
|
|
beets # music tagger / library organizer + navidrome sync plugin
|
2026-07-18 09:21:51 -04:00
|
|
|
|
];
|
|
|
|
|
|
fonts.fontconfig.enable = true;
|
|
|
|
|
|
home.sessionVariables.EDITOR = "nvim";
|
2026-08-08 21:20:27 -04:00
|
|
|
|
|
2026-07-18 22:54:23 -04:00
|
|
|
|
programs.zsh = {
|
|
|
|
|
|
enable = true;
|
|
|
|
|
|
autosuggestion.enable = true; # ghost text from history
|
|
|
|
|
|
syntaxHighlighting.enable = true; # commands turn green when valid
|
|
|
|
|
|
initContent = ''
|
|
|
|
|
|
bindkey '^f' autosuggest-accept
|
|
|
|
|
|
'';
|
|
|
|
|
|
shellAliases = {
|
|
|
|
|
|
".." = "cd ..";
|
|
|
|
|
|
ll = "ls -plart";
|
|
|
|
|
|
add = "git add .";
|
|
|
|
|
|
push = "git push";
|
|
|
|
|
|
pull = "git pull";
|
|
|
|
|
|
m = "git switch main";
|
2026-07-18 23:03:12 -04:00
|
|
|
|
# High-agency agent launchers (same idea across tools)
|
2026-07-18 22:54:23 -04:00
|
|
|
|
cc = "claude --dangerously-skip-permissions";
|
|
|
|
|
|
co = "codex --full-auto";
|
2026-07-18 23:03:12 -04:00
|
|
|
|
gb = "grok --yolo";
|
2026-09-18 13:58:49 -04:00
|
|
|
|
# firstmate primary session via OpenCode 2 (isolated config; crewmates = Pi)
|
|
|
|
|
|
fm = "cd ${firstmateHome} && exec ${home}/.local/bin/oc2";
|
|
|
|
|
|
oc2 = "${home}/.local/bin/oc2";
|
2026-08-08 21:20:27 -04:00
|
|
|
|
# Sync ~/Music into the beets library on Unraid (requires NAS mounted).
|
|
|
|
|
|
sync-music = "~/.local/bin/sync-music";
|
|
|
|
|
|
# Start the reverse tunnel so you can SSH into this Mac from your phone.
|
|
|
|
|
|
# Run once when you go remote: `ngrok-tunnel`. Reads authtoken from
|
|
|
|
|
|
# ~/.config/ngrok (set once per machine with `ngrok config add-authtoken <TOK>`)
|
|
|
|
|
|
ngrok-tunnel = "ngrok tcp 22";
|
2026-07-18 22:54:23 -04:00
|
|
|
|
};
|
|
|
|
|
|
};
|
2026-07-18 09:21:51 -04:00
|
|
|
|
|
2026-07-18 22:54:23 -04:00
|
|
|
|
programs.starship = {
|
|
|
|
|
|
enable = true;
|
|
|
|
|
|
settings = {
|
|
|
|
|
|
add_newline = false;
|
|
|
|
|
|
format = "$directory$git_branch$git_status$cmd_duration$line_break$character";
|
|
|
|
|
|
character = {
|
|
|
|
|
|
success_symbol = "[❯](purple)";
|
|
|
|
|
|
error_symbol = "[❯](red)";
|
|
|
|
|
|
};
|
|
|
|
|
|
cmd_duration.format = "[$duration]($style) ";
|
|
|
|
|
|
};
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-08-08 21:20:27 -04:00
|
|
|
|
# Edit-in-place: real file stays in the repo; live path is an out-of-store symlink.
|
|
|
|
|
|
# force = true: replace a pre-existing regular file once; source of truth is home/.
|
|
|
|
|
|
home.file.".config/wezterm" = {
|
|
|
|
|
|
source = config.lib.file.mkOutOfStoreSymlink "${dotfiles}/home/.config/wezterm";
|
|
|
|
|
|
force = true;
|
|
|
|
|
|
};
|
|
|
|
|
|
home.file.".config/nvim" = {
|
|
|
|
|
|
source = config.lib.file.mkOutOfStoreSymlink "${dotfiles}/home/.config/nvim";
|
|
|
|
|
|
force = true;
|
|
|
|
|
|
};
|
|
|
|
|
|
home.file.".config/herdr" = {
|
|
|
|
|
|
source = config.lib.file.mkOutOfStoreSymlink "${dotfiles}/home/.config/herdr";
|
|
|
|
|
|
force = true;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
# Beets music library manager - managed by nixpkgs package, config symlinked below.
|
|
|
|
|
|
home.file.".config/beets" = {
|
|
|
|
|
|
source = config.lib.file.mkOutOfStoreSymlink "${dotfiles}/home/.config/beets";
|
|
|
|
|
|
force = true;
|
|
|
|
|
|
};
|
2026-07-18 23:03:12 -04:00
|
|
|
|
|
|
|
|
|
|
# Claude Code settings (also read by Grok for permissions/compat)
|
2026-08-08 21:20:27 -04:00
|
|
|
|
home.file.".claude/settings.json" = {
|
|
|
|
|
|
source = config.lib.file.mkOutOfStoreSymlink "${dotfiles}/home/.claude/settings.json";
|
|
|
|
|
|
force = true;
|
|
|
|
|
|
};
|
2026-07-18 23:03:12 -04:00
|
|
|
|
|
|
|
|
|
|
# Shared agent policy - one file, many harnesses
|
2026-08-08 21:20:27 -04:00
|
|
|
|
home.file.".claude/CLAUDE.md" = {
|
|
|
|
|
|
source = config.lib.file.mkOutOfStoreSymlink "${dotfiles}/home/AGENTS.md";
|
|
|
|
|
|
force = true;
|
|
|
|
|
|
};
|
|
|
|
|
|
home.file.".codex/AGENTS.md" = {
|
|
|
|
|
|
source = config.lib.file.mkOutOfStoreSymlink "${dotfiles}/home/AGENTS.md";
|
|
|
|
|
|
force = true;
|
|
|
|
|
|
};
|
|
|
|
|
|
home.file.".config/opencode/AGENTS.md" = {
|
|
|
|
|
|
source = config.lib.file.mkOutOfStoreSymlink "${dotfiles}/home/AGENTS.md";
|
|
|
|
|
|
force = true;
|
|
|
|
|
|
};
|
2026-09-18 13:58:49 -04:00
|
|
|
|
# OpenCode 2 isolated config - never share ~/.config/opencode with 1.x
|
|
|
|
|
|
home.file.".config/opencode2/opencode.json" = {
|
|
|
|
|
|
source = config.lib.file.mkOutOfStoreSymlink "${dotfiles}/home/.config/opencode2/opencode.json";
|
|
|
|
|
|
force = true;
|
|
|
|
|
|
};
|
|
|
|
|
|
home.file.".config/opencode2/AGENTS.md" = {
|
|
|
|
|
|
source = config.lib.file.mkOutOfStoreSymlink "${dotfiles}/home/AGENTS.md";
|
|
|
|
|
|
force = true;
|
|
|
|
|
|
};
|
|
|
|
|
|
home.file.".local/bin/oc2" = {
|
|
|
|
|
|
source = config.lib.file.mkOutOfStoreSymlink "${dotfiles}/home/bin/oc2";
|
|
|
|
|
|
force = true;
|
|
|
|
|
|
};
|
2026-08-08 21:20:27 -04:00
|
|
|
|
home.file.".grok/AGENTS.md" = {
|
|
|
|
|
|
source = config.lib.file.mkOutOfStoreSymlink "${dotfiles}/home/AGENTS.md";
|
|
|
|
|
|
force = true;
|
|
|
|
|
|
};
|
2026-07-18 23:03:12 -04:00
|
|
|
|
|
|
|
|
|
|
# Grok Build native config
|
2026-08-08 21:20:27 -04:00
|
|
|
|
home.file.".grok/config.toml" = {
|
|
|
|
|
|
source = config.lib.file.mkOutOfStoreSymlink "${dotfiles}/home/.grok/config.toml";
|
|
|
|
|
|
force = true;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
# Pi agent - source of truth under home/.pi (sessions/auth/npm stay live under ~/.pi)
|
|
|
|
|
|
home.file.".pi/agent/settings.json" = {
|
|
|
|
|
|
source = config.lib.file.mkOutOfStoreSymlink "${dotfiles}/home/.pi/agent/settings.json";
|
|
|
|
|
|
force = true;
|
|
|
|
|
|
};
|
|
|
|
|
|
home.file.".pi/agent/models.json" = {
|
|
|
|
|
|
source = config.lib.file.mkOutOfStoreSymlink "${dotfiles}/home/.pi/agent/models.json";
|
|
|
|
|
|
force = true;
|
|
|
|
|
|
};
|
|
|
|
|
|
home.file.".pi/agent/themes" = {
|
|
|
|
|
|
source = config.lib.file.mkOutOfStoreSymlink "${dotfiles}/home/.pi/agent/themes";
|
|
|
|
|
|
force = true;
|
|
|
|
|
|
};
|
|
|
|
|
|
home.file.".pi/agent/extensions/terminal-status-title.js" = {
|
|
|
|
|
|
source = config.lib.file.mkOutOfStoreSymlink "${dotfiles}/home/.pi/agent/extensions/terminal-status-title.js";
|
|
|
|
|
|
force = true;
|
|
|
|
|
|
};
|
|
|
|
|
|
home.file.".pi/agent/extensions/calm" = {
|
|
|
|
|
|
source = config.lib.file.mkOutOfStoreSymlink "${dotfiles}/home/.pi/agent/extensions/calm";
|
|
|
|
|
|
force = true;
|
|
|
|
|
|
};
|
|
|
|
|
|
home.file.".pi/agent/AGENTS.md" = {
|
|
|
|
|
|
source = config.lib.file.mkOutOfStoreSymlink "${dotfiles}/home/AGENTS.md";
|
|
|
|
|
|
force = true;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
# firstmate is a mutable agent distro (self-update, state/, projects/). Clone once;
|
|
|
|
|
|
# never put it in the Nix store. Seed Pi+herdr defaults only when absent.
|
|
|
|
|
|
# Do NOT npm install -g here: activation PATH often resolves Nix's npm, which
|
|
|
|
|
|
# cannot write into the store (EACCES). Use Homebrew's node for globals:
|
|
|
|
|
|
# /opt/homebrew/bin/npm install -g tasks-axi quota-axi no-mistakes gh-axi lavish-axi chrome-devtools-axi
|
|
|
|
|
|
home.activation.firstmate = lib.hm.dag.entryAfter [ "writeBoundary" ] ''
|
|
|
|
|
|
set -euo pipefail
|
|
|
|
|
|
fm="${firstmateHome}"
|
|
|
|
|
|
git="${pkgs.git}/bin/git"
|
|
|
|
|
|
|
|
|
|
|
|
if [ ! -d "$fm/.git" ]; then
|
|
|
|
|
|
mkdir -p "$(dirname "$fm")"
|
|
|
|
|
|
$git clone https://github.com/kunchenguid/firstmate.git "$fm"
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
mkdir -p "$fm/config" "$fm/data" "$fm/state" "$fm/projects"
|
|
|
|
|
|
|
|
|
|
|
|
# Local gitignored operating choices (do not overwrite captain edits)
|
|
|
|
|
|
[ -f "$fm/config/backend" ] || printf 'herdr\n' > "$fm/config/backend"
|
|
|
|
|
|
[ -f "$fm/config/crew-harness" ] || printf 'pi\n' > "$fm/config/crew-harness"
|
2026-09-18 13:58:49 -04:00
|
|
|
|
|
|
|
|
|
|
# Crew dispatch profile - source of truth in dotfiles (reproducible across
|
|
|
|
|
|
# machines). Symlinked into firstmate config so a rebuild restores routing.
|
|
|
|
|
|
# Crewmates run on Pi + opencode-go/mimo-v2.5-pro (see crew-dispatch.json).
|
|
|
|
|
|
ln -sfn "${dotfiles}/home/.config/firstmate/crew-dispatch.json" "$fm/config/crew-dispatch.json"
|
|
|
|
|
|
|
|
|
|
|
|
# Gitea PR helper for local-only mode: after the first mate (opencode)
|
|
|
|
|
|
# reviews a crewmate branch, this pushes it + opens a Gitea PR via tea.
|
|
|
|
|
|
ln -sfn "${dotfiles}/home/bin/fm-gitea-pr.sh" "$HOME/.local/bin/fm-gitea-pr.sh"
|
2026-08-08 21:20:27 -04:00
|
|
|
|
'';
|
|
|
|
|
|
|
|
|
|
|
|
# Authorize the SSH key so the phone can log in through the ngrok tunnel.
|
|
|
|
|
|
# (home-manager 26.05 removed programs.ssh.authorizedKeys; manage the file
|
|
|
|
|
|
# here so ~/.ssh is 0700 and authorized_keys is 0600. Public key is not a secret.)
|
|
|
|
|
|
home.activation.authorizeSSHKey = lib.hm.dag.entryAfter [ "writeBoundary" ] ''
|
|
|
|
|
|
set -euo pipefail
|
|
|
|
|
|
mkdir -p "$HOME/.ssh"
|
|
|
|
|
|
chmod 700 "$HOME/.ssh"
|
|
|
|
|
|
printf '%s\n' "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGCZEGVYMDztSryFwoZ6cfpBH3ksP3h0yxZSanlcbrZ0 unraid-omada" > "$HOME/.ssh/authorized_keys"
|
|
|
|
|
|
chmod 600 "$HOME/.ssh/authorized_keys"
|
|
|
|
|
|
'';
|
|
|
|
|
|
|
|
|
|
|
|
# Wire the sync-music script into ~/.local/bin without touching anything else
|
|
|
|
|
|
# that lives there (node, python3.11, hermes, etc.). Source of truth is the
|
|
|
|
|
|
# dotfiles repo so a rebuild restores it if it ever disappears.
|
|
|
|
|
|
home.activation.syncMusic = lib.hm.dag.entryAfter [ "writeBoundary" ] ''
|
|
|
|
|
|
set -euo pipefail
|
|
|
|
|
|
ln -sfn "${dotfiles}/home/bin/sync-music" "$HOME/.local/bin/sync-music"
|
|
|
|
|
|
'';
|
2026-09-18 13:58:49 -04:00
|
|
|
|
|
|
|
|
|
|
# OpenCode 2 is not on Homebrew (beta). Install via Homebrew's npm, never
|
|
|
|
|
|
# Nix's npm (EACCES on the store). Skip when already present so a rebuild
|
|
|
|
|
|
# does not pull a new beta under a live TUI.
|
|
|
|
|
|
home.activation.opencode2 = lib.hm.dag.entryAfter [ "writeBoundary" ] ''
|
|
|
|
|
|
set -euo pipefail
|
|
|
|
|
|
npm=/opt/homebrew/bin/npm
|
|
|
|
|
|
bin=/opt/homebrew/bin/opencode2
|
|
|
|
|
|
if [ -x "$bin" ]; then
|
|
|
|
|
|
exit 0
|
|
|
|
|
|
fi
|
|
|
|
|
|
if [ ! -x "$npm" ]; then
|
|
|
|
|
|
echo "opencode2: /opt/homebrew/bin/npm missing (declare node in homebrew.brews)" >&2
|
|
|
|
|
|
exit 1
|
|
|
|
|
|
fi
|
|
|
|
|
|
"$npm" install -g --allow-scripts=@opencode-ai/cli @opencode-ai/cli@beta
|
|
|
|
|
|
'';
|
2026-09-21 17:38:35 -04:00
|
|
|
|
|
|
|
|
|
|
# kunchenguid/no-mistakes gate (git push no-mistakes) - declarative install + daemon + global config
|
|
|
|
|
|
# Covers every harness declared in Nix: pi (pi-coding-agent), opencode (oc2/fm), grok (grok-build),
|
|
|
|
|
|
# plus claude/codex aspirational (aliases cc/co). Binary lives in ~/.no-mistakes/bin
|
|
|
|
|
|
# with symlink ~/.local/bin/no-mistakes (installer default). Do NOT npm install -g no-mistakes
|
|
|
|
|
|
# - that npm name is jonathanong's static-analysis tool (shadowing bug fixed 2026-09-21).
|
|
|
|
|
|
home.activation.noMistakes = lib.hm.dag.entryAfter [ "writeBoundary" ] ''
|
|
|
|
|
|
set -euo pipefail
|
|
|
|
|
|
bin="$HOME/.no-mistakes/bin/no-mistakes"
|
|
|
|
|
|
link="$HOME/.local/bin/no-mistakes"
|
|
|
|
|
|
# Install/refresh via upstream installer if missing or not kunchenguid build
|
|
|
|
|
|
if [ ! -x "$bin" ] || ! "$bin" --version 2>/dev/null | grep -q "kunchenguid\|v1\."; then
|
|
|
|
|
|
if [ -x "$bin" ]; then
|
|
|
|
|
|
echo "no-mistakes: replacing unexpected binary at $bin" >&2
|
|
|
|
|
|
fi
|
|
|
|
|
|
curl -fsSL https://raw.githubusercontent.com/kunchenguid/no-mistakes/main/docs/install.sh | sh
|
|
|
|
|
|
fi
|
|
|
|
|
|
# Ensure global config covers all Nix harnesses (idempotent - only writes if missing or still `agent: auto`)
|
|
|
|
|
|
cfg="$HOME/.no-mistakes/config.yaml"
|
|
|
|
|
|
if [ -f "$cfg" ] && grep -qE '^\s*agent:\s*auto\s*$' "$cfg"; then
|
|
|
|
|
|
tmp="$(mktemp)"
|
|
|
|
|
|
awk '
|
|
|
|
|
|
/^\s*agent:\s*auto\s*$/ {
|
|
|
|
|
|
print "# Managed by dotfiles/home.nix home.activation.noMistakes";
|
|
|
|
|
|
print "agent: [pi, opencode, grok, claude, codex]";
|
|
|
|
|
|
next
|
|
|
|
|
|
}
|
|
|
|
|
|
{ print }
|
|
|
|
|
|
' "$cfg" > "$tmp" && mv "$tmp" "$cfg"
|
|
|
|
|
|
fi
|
|
|
|
|
|
if [ ! -f "$cfg" ]; then
|
|
|
|
|
|
mkdir -p "$(dirname "$cfg")"
|
|
|
|
|
|
printf 'agent: [pi, opencode, grok, claude, codex]\n' > "$cfg"
|
|
|
|
|
|
fi
|
|
|
|
|
|
# Ensure daemon running (launchd on macOS)
|
|
|
|
|
|
"$bin" daemon restart >/dev/null 2>&1 || "$bin" daemon start >/dev/null 2>&1 || true
|
|
|
|
|
|
'';
|
2026-07-18 09:21:51 -04:00
|
|
|
|
}
|
2026-08-08 21:20:27 -04:00
|
|
|
|
|