Files
dotfiles/home.nix
T
Lap Tran e6fdd6884d chore: move agent routing to OpenCode Go; add firstmate tooling
- crew-dispatch.json: route crew tasks to OpenCode Go models
- opencode/opencode2: pin opencode-go/gpt-6-luna
- grok: default to grok-4.5 with high reasoning, permission auto
- home.nix: axiTools activation for the *-axi CLIs; crewmate harness
  opencode; no-mistakes agent list opencode-only
2026-09-27 21:30:11 -04:00

332 lines
14 KiB
Nix
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{ config, pkgs, lib, ... }:
let
home = config.home.homeDirectory;
dotfiles = "${home}/.dotfiles";
firstmateHome = "${home}/Documents/firstmate";
in
{
home.username = "laptran";
home.homeDirectory = "/Users/laptran";
home.stateVersion = "24.11";
# 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;
home.packages = with pkgs; [
ripgrep
fd
fzf
jq
lazygit
neovim
nerd-fonts.hack
ngrok # reverse TCP tunnel so the phone can SSH in over cellular (unfree)
beets # music tagger / library organizer + navidrome sync plugin
];
fonts.fontconfig.enable = true;
home.sessionVariables.EDITOR = "nvim";
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";
# High-agency agent launchers (same idea across tools)
cc = "claude --dangerously-skip-permissions";
co = "codex --full-auto";
gb = "grok --yolo";
# firstmate primary session via OpenCode 2 (isolated config; crewmates = OpenCode)
fm = "cd ${firstmateHome} && exec ${home}/.local/bin/oc2";
oc2 = "${home}/.local/bin/oc2";
# 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";
};
};
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) ";
};
};
# 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;
};
# Claude Code settings (also read by Grok for permissions/compat)
home.file.".claude/settings.json" = {
source = config.lib.file.mkOutOfStoreSymlink "${dotfiles}/home/.claude/settings.json";
force = true;
};
# Shared agent policy - one file, many harnesses
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;
};
home.file.".config/opencode/opencode.json" = {
source = config.lib.file.mkOutOfStoreSymlink "${dotfiles}/home/.config/opencode/opencode.json";
force = true;
};
home.file.".config/opencode/skills" = {
source = config.lib.file.mkOutOfStoreSymlink "${dotfiles}/home/.config/opencode/skills";
force = true;
};
# 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;
};
home.file.".grok/AGENTS.md" = {
source = config.lib.file.mkOutOfStoreSymlink "${dotfiles}/home/AGENTS.md";
force = true;
};
# Grok Build native config
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 OpenCode+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). The firstmate companion CLIs (*-axi)
# are installed by home.activation.axiTools below via Homebrew's node.
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 'opencode\n' > "$fm/config/crew-harness"
# Crew dispatch profile - source of truth in dotfiles (reproducible across
# machines). Symlinked into firstmate config so a rebuild restores routing.
# Crew harness and model routing are task-specific in 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"
'';
# Firstmate companion CLIs (*-axi). None is a Homebrew or Nix formula, so
# install them through Homebrew's npm. tasks-axi is version-gated by firstmate
# (>= 0.2.6 is required before a spawn/teardown automatic backlog transition
# will run), so pin it and reinstall on drift; the rest install when absent.
home.activation.axiTools = lib.hm.dag.entryAfter [ "writeBoundary" ] ''
set -euo pipefail
# Activation PATH lacks /opt/homebrew/bin, so npm's `#!/usr/bin/env node`
# shebang and the installed shims would fail. Prepend only - replacing PATH
# breaks the rest of the HM activation (nix-env, gettext).
export PATH="/opt/homebrew/bin:$PATH"
npm=/opt/homebrew/bin/npm
if [ ! -x "$npm" ]; then
echo "axiTools: /opt/homebrew/bin/npm missing (declare node in homebrew.brews)" >&2
exit 1
fi
# Pin tasks-axi; reinstall only when the installed version differs.
want_tasks_axi="0.2.6"
have_tasks_axi="$(tasks-axi --version 2>/dev/null | head -1 | tr -d '[:space:]')"
if [ "$have_tasks_axi" != "$want_tasks_axi" ]; then
"$npm" install -g "tasks-axi@$want_tasks_axi"
fi
# Companion CLIs without a firstmate-enforced version floor: install once.
for tool in quota-axi gh-axi lavish-axi chrome-devtools-axi; do
[ -x "/opt/homebrew/bin/$tool" ] || "$npm" install -g "$tool"
done
'';
# 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"
'';
# 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
'';
# backpass (kunchenguid/backpass) - gradient descent for agent memory: reads
# the transcript stores of the harnesses this config already declares
# (claude/codex/pi/opencode/grok), proposes evidence-backed edits to AGENTS.md
# and skills, gated by `backpass apply`. Every model call goes through `acpx`
# to a harness you already authenticated, so acpx is a hard runtime dep and
# must be on PATH. Homebrew node v26 satisfies backpass (>= 22.5) and acpx
# (>= 22.13). Neither is a Homebrew formula: install via Homebrew's npm,
# never Nix's npm (EACCES on the store). Skip-if-present like opencode2 so a
# rebuild never pulls a new version mid-session; refresh manually with
# /opt/homebrew/bin/npm update -g backpass acpx
home.activation.backpass = lib.hm.dag.entryAfter [ "writeBoundary" ] ''
set -euo pipefail
# Activation PATH lacks /opt/homebrew/bin, so npm's `#!/usr/bin/env node`
# shebang fails with "env: node: No such file or directory". Prepend only -
# replacing PATH breaks the rest of the HM activation (nix-env, gettext).
export PATH="/opt/homebrew/bin:$PATH"
npm=/opt/homebrew/bin/npm
if [ ! -x "$npm" ]; then
echo "backpass: /opt/homebrew/bin/npm missing (declare node in homebrew.brews)" >&2
exit 1
fi
if [ ! -x /opt/homebrew/bin/acpx ]; then
"$npm" install -g acpx@latest
fi
if [ ! -x /opt/homebrew/bin/backpass ]; then
"$npm" install -g backpass
fi
'';
# kunchenguid/no-mistakes gate (git push no-mistakes) - declarative install + daemon + global config
# Uses OpenCode only for pipeline steps (the opencode-go subscription), by the
# captain's decision. 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 selects OpenCode (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: [opencode]";
next
}
{ print }
' "$cfg" > "$tmp" && mv "$tmp" "$cfg"
fi
if [ ! -f "$cfg" ]; then
mkdir -p "$(dirname "$cfg")"
printf 'agent: [opencode]\n' > "$cfg"
fi
# Ensure daemon running (launchd on macOS)
"$bin" daemon restart >/dev/null 2>&1 || "$bin" daemon start >/dev/null 2>&1 || true
'';
}