- 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
332 lines
14 KiB
Nix
332 lines
14 KiB
Nix
{ 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
|
||
'';
|
||
}
|