Compare commits

...
12 Commits
Author SHA1 Message Date
Lap Tran 8169978311 chore(opencode): cap Luna and DeepSeek V4.1 Flash context under Luna's 272K pricing cliff 2026-09-28 15:49:15 -04:00
Lap Tran f02cbefa74 chore: ignore runtime state and build artifacts 2026-09-28 15:26:59 -04:00
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
Lap Tran a35c0bb744 Manage opencode 1.x config via home-manager and isolate opencode2
- Symlink ~/.config/opencode/opencode.json and skills/ from dotfiles
  so opencode 1.x config survives rebuild with zap cleanup
- Track cloud-deploy, cloudflared, github-cli, no-mistakes,
  playwright-cli, and read-tweet skills in repo
- Update opencode2 model to opencode/muse-spark-1.2-contributor-free
- Rotate opencode.json/skills in rebuild.sh alongside existing
  managed paths
2026-09-21 21:55:00 -04:00
Lap Tran ab3b67af92 gate: add kunchenguid/no-mistakes for all Nix harnesses
- home.nix: add home.activation.noMistakes (declarative install to
  ~/.no-mistakes/bin + daemon via launchd, global config fallback
  agent: [pi, opencode, grok, claude, codex] covering every harness
  declared in Nix; fix shadowing of jonathanong npm no-mistakes)
- ~/.no-mistakes/config.yaml: switched from auto to ordered fallback,
  pi first (firstmate crewmates), then opencode/grok
- home/.pi/agent/*, home/.grok/config.toml, home/.config/opencode2:
  align to oMLX/qwen3.6-35b local model (was mixed Talos/genesis)
- rebuild.sh: ensure grok-build upgrade and OpenCode 1.x install on
  rebuild (stable CLI for free-tier, opencode2 beta is 426 there)
- dotfiles repo: no-mistakes init (gate at
  ~/.no-mistakes/repos/916cb6ded81b.git, remote no-mistakes)
2026-09-21 17:38:35 -04:00
Lap Tran 8bcc11c108 Silence Determinate Nix options.json warning on rebuild
Disable Home Manager's home-configuration.nix manpage so rebuilds no longer generate options.json with a nixpkgs store path that lacks string context. Package man pages are unchanged.
2026-09-18 15:14:43 -04:00
Lap Tran 4c6e85c97c Switch firstmate to OpenCode 2 and refresh nix-darwin pins
Point fm at the isolated oc2 wrapper, upgrade Homebrew packages on rebuild, and declare ffmpeg, docker-desktop, puremac, utm, node, and tea. Update nixpkgs, home-manager, and nix-homebrew locks. Track Pi agent config, crew-dispatch, and the Gitea PR helper that home.nix already expected.
2026-09-18 13:58:49 -04:00
Lap Tran b7a956ebc0 sync-music: scan ~/Music recursively so nested folders sync too 2026-08-09 16:17:58 -04:00
Lap Tran 9522a36040 fix sync-music: import as singletons to stop deleting album tracks
The script piped 'R' (remove old) into beets' duplicate-album prompt while
never actually clearing the library DB (it cleared a 0-byte dummy at the
wrong path, not beets/beets_library.db). Any two tracks sharing artist+album
caused the second import to delete the first's files from the NAS.

Import with --singletons so beets never matches against existing albums,
and answer 'S' (skip) if a prompt does appear. Drop the useless DB clear.
2026-08-09 14:28:14 -04:00
Lap Tran 4a0bff0331 dotfiles: add beets, sync-music script, firstmate, ngrok, openssh, gh/tmux/treehouse/opencode/pi brews 2026-08-08 21:20:27 -04:00
Lap Tran 6eee2509a4 Add Grok Build agent config adapted from Claude Code setup
Wire shared AGENTS.md and Claude settings through home-manager, plus a
native Grok config (always-approve, rose-pine) and a `gb` yolo alias.
2026-07-18 23:03:12 -04:00
Lap Tran bcabdec9c7 Fix ls aliases and add herdr config 2026-07-18 22:54:23 -04:00
46 changed files with 2902 additions and 18 deletions
Vendored
BIN
View File
Binary file not shown.
+10
View File
@@ -0,0 +1,10 @@
# Nix build result symlink
/result
# Runtime state and caches (not user config)
home/.config/beets/state.pickle
home/.config/herdr/.plugins.lock
home/.config/herdr/*.log
home/.config/herdr/*.sock
home/.config/herdr/session.json
home/.config/herdr/release-notes.json
+18
View File
@@ -0,0 +1,18 @@
# Project notes for agents
Deliberate decisions in this repo - do NOT silently revert them:
- `homebrew.onActivation.cleanup = "zap"` in `configuration.nix` is intentional. It forces the good habit of declaring every Homebrew package in the Nix config instead of installing things ad-hoc, which keeps the machine reproducible. Do not soften it to `uninstall` or `none`. Users are warned about its effect; this note is for anyone tempted to change the setting itself.
- Agent configs under `home/` are the source of truth. `home.nix` installs them with `mkOutOfStoreSymlink` + `force = true`, so edits to files in `home/` apply live after the first rebuild that creates the symlink. Prefer editing those paths, not the live `~/.claude` / `~/.grok` / `~/.pi` copies. `rebuild.sh` rotates stale `*.backup` files before switch so home-manager never fails with "would be clobbered".
- Grok Build is first-class here: `home/.grok/config.toml` and `home/AGENTS.md` (linked as `~/.grok/AGENTS.md`). Claude, Codex, opencode, and Pi share the same `home/AGENTS.md` for multi-agent work.
- Pi is managed the same way: `home/.pi/agent/{settings,models}.json`, `themes/`, and custom extensions (`terminal-status-title`, `calm`). Do not symlink the whole `~/.pi/agent` tree - sessions, auth, npm cache, and herdr's `extensions/herdr-agent-state.ts` stay live.
- firstmate lives at `~/Documents/firstmate` as a mutable git clone (not in the Nix store). `home.activation.firstmate` clones it if missing and seeds `config/backend=herdr` + `config/crew-harness=pi` only when those files are absent. Launch with shell alias `fm` (`cd` + OpenCode 2 via `~/.local/bin/oc2`). Self-update via `/updatefirstmate`, not rebuild.
- OpenCode 2 is the firstmate primary. It is not a Homebrew formula: install `@opencode-ai/cli@beta` with Homebrew's npm (`home.activation.opencode2`). Launch through `home/bin/oc2` so `OPENCODE_CONFIG` stays at `home/.config/opencode2/opencode.json` and cannot rewrite OpenCode 1.x's `~/.config/opencode`. Do not add a brew `opencode` / `opencode2` formula; `zap` would fight the npm install. firstmate harness detection matches `*opencode*`, so the `opencode2` process name still classifies as OpenCode.
- Shell aliases: `cc` (Claude skip-permissions), `co` (Codex full-auto), `gb` (Grok --yolo), `fm` (firstmate via OpenCode 2), `oc2` (OpenCode 2, isolated). They are intentional high-agency shortcuts.
## Maintaining this file
Keep this file for knowledge useful to almost every future agent session in this project.
Do not repeat what the codebase already shows; point to the authoritative file or command instead.
Prefer rewriting or pruning existing entries over appending new ones.
When updating this file, preserve this bar for all agents and keep entries concise.
+16
View File
@@ -3,6 +3,10 @@
{
# Determinate already manages the Nix daemon, so nix-darwin shouldn't.
nix.enable = false;
programs.zsh.enable = true;
# SSH server (macOS 14.4+ gates `systemsetup -setremotelogin` behind Full Disk Access; nix-darwin loads ssh.plist via launchd directly instead).
services.openssh.enable = true;
nixpkgs.config.allowUnfree = true;
nixpkgs.hostPlatform = "aarch64-darwin"; # use x86_64-darwin for Intel CPU
@@ -37,11 +41,23 @@
enable = true;
onActivation.cleanup = "zap"; # remove anything not listed here
onActivation.autoUpdate = true;
onActivation.upgrade = true; # upgrade outdated formulae/casks on rebuild
onActivation.extraFlags = [ "--force" ];
brews = [
"ffmpeg"
"gh"
"herdr"
"node" # required: opencode2 is an npm global, not a Homebrew formula
"pi-coding-agent"
"tea" # Gitea CLI - PRs for firstmate crewmates on unraid.local:3003
"tmux"
"treehouse"
];
casks = [
"docker-desktop"
"grok-build"
"puremac"
"utm"
"wezterm"
];
};
Generated
+13 -13
View File
@@ -3,16 +3,16 @@
"brew-src": {
"flake": false,
"locked": {
"lastModified": 1784068757,
"narHash": "sha256-7KnV7rTlMpys+2J+TFVxUDDyKPLXFs4wIMtckMC72VM=",
"lastModified": 1788591592,
"narHash": "sha256-NbwVKwKLFl0oXub7oPjvmDaOygCtV2oboeKTu4xXFTk=",
"owner": "Homebrew",
"repo": "brew",
"rev": "6bd951d96e7ebc54787799dba77bfb26ec956c4c",
"rev": "08e85c4e42f5d8f1ea17c36cb59cf61c2ccb26c3",
"type": "github"
},
"original": {
"owner": "Homebrew",
"ref": "6.0.11",
"ref": "6.0.22",
"repo": "brew",
"type": "github"
}
@@ -24,11 +24,11 @@
]
},
"locked": {
"lastModified": 1784350909,
"narHash": "sha256-ZWyzLbS1yKUTeFJLmdVuWNnHttL333/ldJbEE+KzCrM=",
"lastModified": 1789267039,
"narHash": "sha256-LWiBv9yAYFi2LPbUhDGHPGKYskJQjj2fw12OlyO1uQo=",
"owner": "nix-community",
"repo": "home-manager",
"rev": "4ce190229c73d44536caa7072f6308fb2d8feeb3",
"rev": "ec172013fa62135f58fb58dd17ae9651e8f39727",
"type": "github"
},
"original": {
@@ -64,11 +64,11 @@
"brew-src": "brew-src"
},
"locked": {
"lastModified": 1784159664,
"narHash": "sha256-I/B6YoRLImHEqNWh8bs+tPjEDeteACeFhcLyoSMo1GE=",
"lastModified": 1788981439,
"narHash": "sha256-fEaFq0XgpgWFPLfpq1UK4/8ylHd2T0+fo6O3hKz1LUM=",
"owner": "zhaofengli",
"repo": "nix-homebrew",
"rev": "842eeb863ecca0eeb463f7a814cdc51e1d925776",
"rev": "09a921d0181146cf6163ec2cc1db7b6fd539a885",
"type": "github"
},
"original": {
@@ -79,11 +79,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1784255083,
"narHash": "sha256-0Jz8G3+3g6i/l4aISUri0EidLnREelCff0m6+pE6aZg=",
"lastModified": 1789485310,
"narHash": "sha256-VsDMTLSkAAJmbNha6sGNzlF33X7RESWLl3SnjDQu+4U=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "fc51889f81924f15fba77a3c0b79cfb3f78fe0d4",
"rev": "0c32f40fe3e2a9adfc427fd5abc061a31043ea44",
"type": "github"
},
"original": {
+2 -1
View File
@@ -22,7 +22,8 @@
nix-homebrew.darwinModules.nix-homebrew
home-manager.darwinModules.home-manager
{
home-manager.useGlobalPkgs = true;
home-manager.backupFileExtension = "backup";
home-manager.useGlobalPkgs = true;
home-manager.useUserPackages = true;
home-manager.users.laptran = import ./home.nix;
}
+310 -4
View File
@@ -1,13 +1,19 @@
{ config, pkgs, ... }:
{ config, pkgs, lib, ... }:
let
dotfiles = "${config.home.homeDirectory}/.dotfiles";
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
@@ -16,10 +22,310 @@ in
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";
home.file.".config/wezterm".source =
config.lib.file.mkOutOfStoreSymlink "${dotfiles}/home/.config/wezterm";
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
'';
}
+7
View File
@@ -0,0 +1,7 @@
{
"theme": "dark-ansi",
"statusLine": {
"type": "command",
"command": "input=$(cat); model=$(echo \"$input\" | jq -r '.model.display_name'); used=$(echo \"$input\" | jq -r '.context_window.used_percentage // empty'); if [ -n \"$used\" ]; then printf \"%s | ctx: %.0f%% used\" \"$model\" \"$used\"; else printf \"%s\" \"$model\"; fi"
}
}
+15
View File
@@ -0,0 +1,15 @@
directory: /Volumes/data/media/music
library: /Volumes/data/media/music/beets/beets_library.db
import:
move: yes
copy: no
resume: ask
timid: no
quiet: no
paths:
default: $artist/$album/$track $title
singleton: Non-Album/$artist/$title
comp: Compilations/$album/$track $title
albumtype_soundtrack: Soundtracks/$album/$track $title
+20
View File
@@ -0,0 +1,20 @@
{
"rules": [
{
"when": "Well-specified implementation with clear acceptance criteria: pure functions, tests, repositories, adapters, or external-fetch integration.",
"use": [{ "harness": "opencode", "model": "opencode-go/deepseek-v4.1-flash", "provider": "opencode-go" }],
"why": "DeepSeek V4.1 Flash is the default OpenCode Go implementation worker; it is fast, economical, and strong on agentic coding tasks."
},
{
"when": "UI work where supplied screenshots or other image context materially affect the implementation or verification.",
"use": [{ "harness": "opencode", "model": "opencode-go/glm-5.3-flash", "provider": "opencode-go" }],
"why": "GLM-5.3-Flash is the Go worker with image input; use it when visual context matters."
},
{
"when": "Ambiguous investigation, difficult debugging, architectural tradeoffs, or broad changes with high uncertainty or blast radius.",
"use": [{ "harness": "opencode", "model": "opencode-go/gpt-6-luna", "provider": "opencode-go" }],
"why": "Reserve GPT-6 Luna for work that benefits from stronger reasoning than the fast implementation tier."
}
],
"default": [{ "harness": "opencode", "model": "opencode-go/deepseek-v4.1-flash", "provider": "opencode-go" }]
}
+18
View File
@@ -0,0 +1,18 @@
onboarding = false
[keys]
prefix = "ctrl+b"
focus_pane_left = "prefix+h"
focus_pane_down = "prefix+j"
focus_pane_up = "prefix+k"
focus_pane_right = "prefix+l"
split_horizontal = "prefix+double_quote"
split_vertical = "prefix+percent"
new_tab = "prefix+c"
close_tab = "prefix+ampersand"
workspace_picker = "prefix+w"
goto = "prefix+g"
copy_mode = "prefix+y" # herdr's copy-mode entry key; copy-mode's own internal keys (v/space select, y/Enter copy, q/Esc cancel) aren't configurable
[ui]
# Agents panel order: explicit choice of "spaces" (grouped by space, the default) over "priority".
agent_panel_sort = "spaces"
+3
View File
@@ -0,0 +1,3 @@
require('vim_config')
require('plugin')
require('keys')
+10
View File
@@ -0,0 +1,10 @@
{
"diffview.nvim": { "branch": "main", "commit": "4516612fe98ff56ae0415a259ff6361a89419b0a" },
"gitsigns.nvim": { "branch": "main", "commit": "31d6fb2d618bca1482b9f274751ead5f03461408" },
"lazy.nvim": { "branch": "main", "commit": "306a05526ada86a7b30af95c5cc81ffba93fef97" },
"neogit": { "branch": "master", "commit": "f8674ec894315c02449b61c9de9a116c5aafeb90" },
"oil.nvim": { "branch": "master", "commit": "b73018b75affd13fa38e2fc94ef753b465f770d7" },
"plenary.nvim": { "branch": "master", "commit": "74b06c6c75e4eeb3108ec01852001636d85a932b" },
"snacks.nvim": { "branch": "main", "commit": "882c996cf28183f4d63640de0b4c02ec886d01f2" },
"which-key.nvim": { "branch": "main", "commit": "3aab2147e74890957785941f0c1ad87d0a44c15a" }
}
+7
View File
@@ -0,0 +1,7 @@
-- save by pressing Escape
vim.keymap.set('n', '<Esc>', ':w<CR>', { desc = 'Save' })
-- select all
vim.keymap.set('n', '<C-a>', 'ggVG', { desc = 'Select All' })
-- pasting over a selection no longer clobbers your clipboard
vim.cmd([[ xnoremap <expr> p 'pgv"'.v:register.'y' ]])
+8
View File
@@ -0,0 +1,8 @@
local lazypath = vim.fn.stdpath('data') .. '/lazy/lazy.nvim'
if not vim.uv.fs_stat(lazypath) then
vim.fn.system({ 'git', 'clone', '--filter=blob:none',
'https://github.com/folke/lazy.nvim.git', '--branch=stable', lazypath })
end
vim.opt.rtp:prepend(lazypath)
require('lazy').setup('plugins') -- load every file in lua/plugins/
+13
View File
@@ -0,0 +1,13 @@
return {
{
'NeogitOrg/neogit',
dependencies = { 'nvim-lua/plenary.nvim', 'sindrets/diffview.nvim' },
keys = { { '<leader>g', function() require('neogit').open() end, desc = 'Neogit' } },
},
{
'lewis6991/gitsigns.nvim',
event = 'BufWinEnter',
opts = { current_line_blame = true }, -- who last touched this line
},
}
@@ -0,0 +1,24 @@
return {
{
'stevearc/oil.nvim',
opts = { view_options = { show_hidden = true } },
keys = { { '<leader>e', '<cmd>Oil<cr>', desc = 'File Browser' } },
},
{
'folke/snacks.nvim',
priority = 1000,
lazy = false,
opts = {
picker = { enabled = true },
notifier = { enabled = true },
input = { enabled = true },
},
keys = {
{ '<leader>f', function() Snacks.picker.files() end, desc = 'Find Files' },
{ '<leader>s', function() Snacks.picker.grep() end, desc = 'Search Text' },
{ '<leader>b', function() Snacks.picker.buffers() end, desc = 'Buffers' },
{ 'gd', function() Snacks.picker.lsp_definitions() end, desc = 'Goto Definition' },
},
},
}
+8
View File
@@ -0,0 +1,8 @@
return {
{
'folke/which-key.nvim',
lazy = false,
config = true, -- popup that shows what my leader keys do
},
}
+13
View File
@@ -0,0 +1,13 @@
local o = vim.opt
vim.g.mapleader = ' ' -- space is the leader key
o.expandtab = true -- spaces, not tabs
o.shiftwidth = 2 -- 2 spaces per indent level
o.number = true -- absolute number on the cursor line, relative elsewhere
o.relativenumber = true -- relative line numbers for fast jumps
o.ignorecase = true -- search is case-insensitive by default
o.smartcase = true -- case-sensitive only if i type a capital
o.clipboard = 'unnamedplus' -- share the system clipboard
o.scrolloff = 16 -- keep cursor away from the screen edge
o.undofile = true -- persistent undo across sessions
o.mouse = '' -- no mouse in nvim; also lets Herdr keep host mouse capture off so Escape isn't swallowed
+51
View File
@@ -0,0 +1,51 @@
{
"$schema": "https://opencode.ai/config.json",
"model": "opencode-go/gpt-6-luna",
"instructions": ["AGENTS.md"],
"provider": {
"omlx": {
"npm": "@ai-sdk/openai-compatible",
"name": "oMLX (Local)",
"options": {
"baseURL": "http://localhost:8000/v1",
"apiKey": "none"
},
"models": {
"qwen3.6-35b": {
"id": "Jundot/Qwen3.6-35B-A3B-oQ4-mtp",
"name": "Qwen 3.6 35B (o4-mtp)",
"tool_call": true,
"limit": {
"context": 65536,
"output": 65536
}
}
}
},
"opencode-go": {
"models": {
"gpt-6-luna": {
"limit": {
"context": 256000,
"input": 256000,
"output": 128000
}
},
"deepseek-v4.1-flash": {
"limit": {
"context": 256000,
"input": 256000,
"output": 128000
}
}
}
}
},
"plugin": ["/Users/laptran/.automaton/plugins/automaton-guard"],
"command": {
"no-mistakes": {
"description": "Validate code changes through the no-mistakes pipeline",
"template": "Read $HOME/.config/opencode/skills/no-mistakes/SKILL.md and follow it. User request: {{args}}"
}
}
}
@@ -0,0 +1,150 @@
---
name: cloud-deploy-cli
description: Use when the user asks to deploy a project, create/manage a cloud deployment, roll back a release, tail production logs, or run a cloud command for Railway, Vercel, AWS, Azure, or Google Cloud. Triggers on keywords like "deploy", "railway up", "vercel deploy", "aws cli", "az ", "gcloud", "ship it", "push to prod", "cloud CLI", "rollback". Installs and drives the cloud provider's CLI so the agent can deploy on its own.
---
# Cloud deployment CLIs — let the agent deploy on its own
The agent should run the deploy command itself, not hand the user a link to a web console. Pick the CLI for the user's cloud and run it.
## Pick the CLI by what the user named
| Cloud | CLI binary | Install (macOS) | Install (other) |
|---|---|---|---|
| Railway | `railway` | `brew install railway` / `npm i -g @railway/cli` | `sh -c "$(curl -fsSL cli.railway.app/install.sh)"` |
| Vercel | `vercel` | `npm i -g vercel` | same |
| AWS | `aws` | `brew install awscli` | `pip install awscli` / `winget install Amazon.AWSCLI` |
| Azure | `az` | `brew install azure-cli` | `winget install Microsoft.AzureCLI` / `curl -sL https://aka.ms/InstallAzureCLIDeb \| sudo bash` |
| Google Cloud | `gcloud` | `brew install --cask google-cloud-sdk` | `winget install Google.CloudSDK` / install from cloud.google.com/sdk |
If the user said "deploy" without naming a cloud, ask which one (use the `question` tool) — or infer from repo files: `vercel.json` → Vercel, `railway.toml`/`Railway.json` → Railway, `appspec.yml`/`.ebextensions` → AWS, `azure-pipelines.yml`/`appservice` → Azure, `app.yaml`/`Dockerfile`+GCP files → GCP.
Verify install: `<cli> --version`. If missing, install it (table above) then continue.
## Authenticate (one-time per CLI)
```bash
railway login # browser login
vercel login # browser login (vercel link on first deploy in a repo)
aws configure # prompts for Access Key ID, Secret, region, output
az login # browser login
gcloud auth login # browser login; then gcloud config set project <PROJECT_ID>
```
For headless/agent runs use env vars instead of interactive login:
| CLI | Env vars |
|---|---|
| Railway | `RAILWAY_TOKEN` (from Railway dashboard → Settings → API Tokens) |
| Vercel | `VERCEL_TOKEN` (or `VERCEL_ORG_ID` + `VERCEL_PROJECT_ID` after `vercel link`) |
| AWS | `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_REGION` (or `AWS_PROFILE`) |
| Azure | `az login --service-principal -u <app-id> -p <cert/password> --tenant <tenant>` |
| Google | `gcloud auth activate-service-account --key-file=<sa.json>` then `GOOGLE_APPLICATION_CREDENTIALS=<sa.json>` |
## Railway
```bash
railway link # bind cwd to a Railway project/service
railway up # deploy current directory (detected buildpack or Dockerfile)
railway up --service api # deploy to a specific service
railway status # current deployment, URL
railway logs # tail live logs
railway variables # view env vars (railway variables set KEY=val to add)
railway rollback # revert to the previous deployment
```
## Vercel
```bash
vercel link # bind repo (writes .vercel/ with org + project IDs)
vercel # preview deploy; prints a *.vercel.app URL
vercel --prod # production deploy (uses production domain)
vercel logs <url> # tail function/build logs
vercel env ls # vercel env add KEY (then vercel --prod to redeploy)
vercel rm <url> # remove a deployment
vercel inspect <url> # build/runtime details
```
For a fully scripted agent deploy:
```bash
vercel --prod --yes --token "$VERCEL_TOKEN"
# --yes skips all prompts; requires .vercel/ linked (vercel link --yes --token $TOKEN)
```
## AWS
AWS is broad — only use it when the user names a specific AWS service. Common agent tasks:
```bash
# S3
aws s3 sync ./dist s3://<bucket>/ --delete
aws s3 presign s3://<bucket>/file.zip --expires-in 3600
# Lambda
aws lambda update-function-code --function-name <fn> --zip-file fileb://fn.zip
aws lambda invoke --function-name <fn> --payload fileb://event.json out.json
# ECS
aws ecs update-service --cluster <c> --service <s> --force-new-deployment
aws ecs describe-services --cluster <c> --services <s>
# Logs (CloudWatch Logs)
aws logs tail /aws/lambda/<fn> --follow
aws logs get-log-events --log-group-name <g> --log-stream-name <s>
# Elastic Beanstalk
eb deploy # needs `eb` CLI (brew install aws-elasticbeanstalk)
```
Always pass `--region <region>` or rely on `aws configure`'s default. For `--query`/`--output text|json` to get parseable results.
## Azure
```bash
az group list -o table
az webapp up --runtime "NODE:20-lts" --sku F1 -n <app> -g <group> # one-shot deploy from cwd
az webapp deployment source config --name <app> -g <group> --repo-url <git> --branch main
az webapp log tail -n <app> -g <group>
az webapp config appsettings set -n <app> -g <group> --settings KEY=val
az functionapp deployment source config-zip -g <group> -n <fn> --src ./deploy.zip
```
`-o table|json|tsv` controls output; `--query` for JMESPath filtering.
## Google Cloud
```bash
gcloud config set project <PROJECT_ID>
gcloud app deploy # App Engine
gcloud run deploy <svc> --source . --region <r> --allow-unauthenticated # Cloud Run
gcloud run services list
gcloud run services describe <svc> --region <r>
gcloud functions deploy <fn> --runtime nodejs20 --trigger-http --allow-unauthenticated
gcloud app logs tail -s default
gcloud builds submit --tag gcr.io/<proj>/<img>
```
## When to use
- "Deploy this" / "ship to prod" / "push to staging" with a cloud named or inferable from repo files
- "Roll back the last deploy" / "tail the logs" / "restart the service"
- "Set env var KEY=val on the deployed app" then redeploy
- Any cloud CLI operation where the agent would otherwise say "go to the dashboard"
## When NOT to use
- Just running the app locally → use the project's dev server, not a cloud deploy
- Building a Docker image for local use → `docker build`; only deploy to cloud if the user asks
- Provisioning infra from scratch (VPCs, databases, IAM) → confirm scope first; these CLIs can do it but the user should explicitly ask before the agent creates billable resources
## Gotchas
- Always confirm the **environment** (production vs preview/staging) before `--prod`/production deploys. Prefer a preview deploy first; ask before promoting.
- `vercel` without `--prod` is a safe preview; `vercel --prod` hits the real domain.
- Railway/Vercel deploys read the repo's build config — check `package.json` scripts / `Dockerfile` / `vercel.json` / `railway.toml` before deploying so the agent knows what will run.
- AWS/Azure/GCP commands can create billable resources; only run provisioning when the user explicitly asked for it.
## Source
Referenced in https://x.com/heyshruti7/status/2069083108092350823 — "Your cloud's CLI — every cloud ships one so your agent can deploy on its own: Railway, Vercel, AWS, Azure, Google Cloud."
@@ -0,0 +1,102 @@
---
name: cloudflared
description: Use when the user asks to expose a local server to the internet, share a localhost URL publicly, test a webhook from a third-party, demo a local dev server to someone else, or get a public HTTPS URL for a port on this machine. Triggers on keywords like "cloudflared", "tunnel", "expose localhost", "public URL", "share local server", "webhook test", "ngrok alternative", "quick tunnel". Installs and runs `cloudflared` to put a localhost port on the public internet in one command.
---
# cloudflared — expose localhost to the world
Use this when the user needs a public HTTPS URL pointing at a port on this machine — for demos, webhook callbacks, or letting someone else hit a local server. One command, no account, no firewall changes.
## Install (first use)
If `cloudflared` is not on PATH:
```bash
brew install cloudflared # macOS
# or: sudo apt install cloudflared # Debian/Ubuntu (or download the .deb from github.com/cloudflare/cloudflared)
# or: winget install Cloudflare.cloudflared # Windows
```
Verify: `cloudflared --version`.
## Quick tunnel (no account, no config)
The fastest path — gives you a random `https://<random>.trycloudflare.com` URL instantly:
```bash
cloudflared tunnel --url http://localhost:3000
```
Output contains a line like:
```
+--------------------------------------------------------------------------------------------+
| Your quick Tunnel has been created! Visit it at (it may take some time to be reachable): |
| https://example-words-tomorrow.trycloudflare.com |
+--------------------------------------------------------------------------------------------+
```
Share that URL. The tunnel stays up as long as the process runs; kill it with Ctrl-C. No Cloudflare account needed. Random URL each run.
## Named tunnel (stable URL, requires account)
For a persistent hostname you can point clients/webhooks at repeatedly:
```bash
cloudflared tunnel login # browser auth, one-time
cloudflared tunnel create my-tunnel # creates tunnel UUID
cloudflared tunnel route dns my-tunnel dev.example.com # bind a hostname to it
```
Then run it with a config file `~/.cloudflared/config.yml`:
```yaml
tunnel: <tunnel-UUID>
credentials-file: /Users/<you>/.cloudflared/<tunnel-UUID>.json
ingress:
- hostname: dev.example.com
service: http://localhost:3000
- service: http_status:404
```
```bash
cloudflared tunnel run my-tunnel
# or as a service:
sudo cloudflared service install # macOS/Linux daemon
```
## Common patterns
| Goal | Command |
|---|---|
| Expose port 3000 with random URL | `cloudflared tunnel --url http://localhost:3000` |
| Expose a different local port | `cloudflared tunnel --url http://localhost:8080` |
| Expose HTTPS local server | `cloudflared tunnel --url https://localhost:3000` |
| Point a hostname at local server | named tunnel + `route dns` (above) |
| Run named tunnel in background | `cloudflared tunnel run my-tunnel &` or `cloudflared service install` |
| TCP port (e.g. SSH) | `cloudflared tunnel --url tcp://localhost:22` (needs named tunnel + `cloudflared access` client) |
## When to use
- "Give me a public URL for this local server" / "share this with my teammate"
- "I need to test a Stripe/GitHub/Slack webhook hitting my local machine"
- "Demo the dev server without deploying"
- "ngrok alternative" / "expose localhost"
## When NOT to use
- Production ingress → set up a real CDN/reverse proxy; quick tunnels are for dev/demo
- Internal-only access on the same machine → just use `localhost:PORT`
- Long-running stable hostnames without a Cloudflare account → quick tunnels are random and not persistent; use a named tunnel or a different tool
## Gotchas
- Quick tunnel URLs are random per run and can take 10-30s to become reachable after the banner prints.
- Cloudflare's free tier is fine for dev; rate limits and TOS apply to high-volume production traffic.
- If the local server binds to `127.0.0.1` only, that's fine — cloudflared connects from the same machine.
- Webhooks that verify SSL: the `*.trycloudflare.com` URL is real HTTPS with a valid cert, so verifiers pass.
## Source
Referenced in https://x.com/heyshruti7/status/2069083108092350823 — "cloudflared — expose localhost to the world with one prompt. Demo links in seconds."
@@ -0,0 +1,135 @@
---
name: github-cli
description: Use when the user asks to open a PR, review or merge a PR, list/check/review issues, react to or comment on PRs, create/manage releases, browse a repo's metadata, or do any GitHub operation without opening the web UI. Triggers on keywords like "gh", "GitHub CLI", "open a PR", "merge the PR", "review request", "list my PRs", "create release". Installs and drives the `gh` CLI so the agent never needs the browser for GitHub workflows.
---
# GitHub CLI (`gh`) — GitHub without the web UI
The agent should open PRs, review, merge, and manage issues/releases from the terminal instead of telling the user to "go to GitHub and click merge."
## Install (first use)
If `gh` is not on PATH:
```bash
brew install gh # macOS
# or: sudo apt install gh # Debian/Ubuntu
# or: winget install GitHub.cli # Windows
```
Authenticate (one-time):
```bash
gh auth login
# choose GitHub.com → HTTPS → "Login with a web browser"
# or use a token: echo "$GH_TOKEN" | gh auth login --with-token
```
Verify: `gh auth status`.
If automation is needed and interactive login isn't possible, set `GH_TOKEN` (or `GITHUB_TOKEN`) env var from a fine-grained PAT with the repo scopes needed (contents, pull-requests, workflows, issues).
## Core workflows
### Open a PR from the current branch
```bash
gh pr create --title "<title>" --body "<body>" --base main
# body supports markdown; can also read from a file: --body-file pr.md
# add reviewers/assignees/labels:
gh pr create --title "..." --body "..." --reviewer alice,bob --label "needs review" --assignee @me
```
The opencode convention: only open PRs when the user explicitly asks. Inspect `git status`, `git diff`, and `git log --oneline -10` first, and stage only intended files.
### Review and merge
```bash
gh pr view <N> # PR details, checks, mergeable state
gh pr view <N> --comments # read the discussion
gh pr diff <N> # the diff in the terminal
gh pr checks <N> # CI status
gh pr review <N> --approve --body "lgtm"
gh pr review <N> --request-changes --body "please fix X"
gh pr merge <N> --squash --delete-branch
# --merge / --squash / --rebase match repo allowed methods
# --auto waits for required checks before merging
```
### List and filter
```bash
gh pr list --author @me --state open
gh pr list --search "review-requested:@me"
gh issue list --assignee @me --state open
gh pr list --state closed --limit 5
```
### Issues
```bash
gh issue create --title "..." --body "..." --label bug --assignee @me
gh issue view <N>
gh issue close <N>
gh issue comment <N> --body "..."
```
### Releases
```bash
gh release create v1.2.3 --notes "..." --title "v1.2.3"
gh release create v1.2.3 ./dist/* # attach build artifacts
gh release list
gh release view v1.2.3
gh release download v1.2.3
```
### Repo metadata & browsing
```bash
gh repo view # current repo info
gh repo view owner/repo # another repo
gh repo clone owner/repo
gh repo create <name> --private --source=. --push # create + push cwd
gh repo fork owner/repo --clone
```
### Cross-repo / search
```bash
gh search prs --author @me --state open
gh search issues "is:open label:bug"
gh search repos "topic:local-llm"
```
## Aliases (optional quality-of-life)
```bash
gh alias set co 'pr checkout'
gh alias set rv 'pr review --view'
# then: gh co 123 → gh pr checkout 123
```
## When to use
- "Open a PR for this branch" / "merge it" / "what's the status of PR #123"
- "Review the latest PR" / "request changes on #45"
- "List my open issues" / "create an issue for this bug"
- "Cut a release" / "attach these binaries to v2.0"
- Any GitHub action where the agent would otherwise say "go to the web UI"
## When NOT to use
- Reading the repo's source code → use Read/Glob/Grep on the working copy, not `gh`
- Git operations (commit, push, branch) → use `git` directly; `gh` wraps GitHub, not git
- Long-form PR body authoring → write to a file first, pass `--body-file`
## Auth troubleshooting
- `gh auth status` fails → run `gh auth login` again, or confirm `GH_TOKEN` is exported in the shell.
- "could not find any releases" on a fork → releases live on the upstream repo; use `gh release list --repo owner/repo`.
- 403 on merge → the token lacks `pull-requests: write` (or repo `contents: write`); regenerate with the scope and re-login.
## Source
Referenced in https://x.com/heyshruti7/status/2069083108092350823 — "GitHub CLI — open PRs, review, merge. Your agent never touches the web UI."
@@ -0,0 +1,392 @@
---
name: no-mistakes
description: Validate your code changes through the no-mistakes pipeline - automated code review, tests, lint, docs, push, PR, and CI - before they reach the configured push target. Use when the user asks to run no-mistakes, gate or ship or validate their changes, push safely, asks you to do a task and then validate it, or invokes /no-mistakes.
user-invocable: true
---
# no-mistakes
`no-mistakes` is a local gate that validates your code changes through a pipeline
(intent, rebase, review, test, document, lint, push, PR, CI) before they reach
the configured push target. You drive it through the `no-mistakes axi` command family, which prints
machine-readable [TOON](https://toonformat.dev) to stdout and progress to stderr.
## Active validation-step boundary
A no-mistakes validation-step agent is already inside an active outer run. It
must inspect, fix, and return only its assigned phase. It must never initialize,
start, reattach, rerun, respond to, synchronize, abort, eject, or directly push
a no-mistakes pipeline. Delivery requirements in user intent remain
acceptance context, but the outer executor alone performs the other validation,
push, PR, and CI phases.
`NO_MISTAKES_GATE` is fast diagnostic evidence, not authorization by
itself. The runtime combines managed Git identity with authenticated process
ancestry. If a pipeline-control command returns
`error.code: nested_gate_context`, stop immediately and
return control to the outer executor. Safe inspection remains available through
`no-mistakes axi status`, `no-mistakes axi logs`, help, and
`no-mistakes doctor`.
When the user invokes `/no-mistakes`, report the outcome at the end. If the user
asks for something specific, translate that request into the matching `axi run`
flags yourself - for example, "skip the lint step" becomes `--skip=lint`. Run
`no-mistakes axi run --help` to see the available flags.
## Two ways to invoke
`/no-mistakes` works in two modes, depending on whether the user hands you a
task along with the command:
- **Validate-only** - bare `/no-mistakes` (optionally with flag-style requests
like "skip the lint step"). The user's code changes are already committed;
validate them and report the outcome.
- **Task-first** - `/no-mistakes <task>`, e.g.
`/no-mistakes add a --json flag to the status command`. First carry out the
task yourself, then validate the result through the pipeline:
1. **Check scope.** Inspect `git status` before you change or commit anything.
Preserve unrelated pre-existing uncommitted changes, and when you commit,
commit only the changes that belong to the user's task.
2. **Do the work.** Make the changes the task describes, then **commit them on
a feature branch**. If the user is on the repository's default branch,
create a feature branch first - the gate validates committed history on a
non-default branch, so the work must land there before you run.
3. **Then validate**, passing the user's task as your `--intent`. The task
text is exactly what the user set out to accomplish, in their own words, so
it *is* the intent - preserve requirements stated directly by the user,
including constraints, exclusions, acceptance criteria, and later decisions;
do not condense them into a diff summary or drop them while adding
implementation context. Enrich it with the decisions and tradeoffs you
made while doing the work (see
[Intent is required](#intent-is-required)).
## Test-quality rule
Never add a test whose only evidence is that it opens, reads, greps, parses, or
snapshots implementation source code and finds or omits particular strings,
tokens, lines, commands, function names, prompt phrases, regex matches, AST
shapes, or incidental snapshots. That does not prove behavior: matching text
can be dead or commented out, and a behavior-preserving refactor can change it.
Instead execute a public or executable interface and assert observable behavior,
state, output, side effects, and failure modes. For machine-consumed declarative
artifacts such as workflow YAML, JSON, policy, .gitignore, or generated
configuration, invoke the real consumer when feasible or parse into a typed or
normalized semantic model and assert meaning. A raw substring or regex over the
file is still the anti-pattern.
Reading a file is legitimate when the file is itself generated public output, a
serialized protocol, persisted state, an intentional snapshot, or another
explicitly owned text or byte contract. Name that contract, and do not use its
contents as a proxy that unrelated code works. A natural-language prompt or
instruction is not proven effective because its source contains a sentence.
Deterministic CI may test the final emitted prompt delivered to an agent as an
intentional generated interface; model interpretation belongs in
development-only evaluation, not live-LLM CI.
For a regression, reproduce the reported failure when feasible: the test should
fail before the fix and pass after it.
Everything below - preconditions, intent, the validate-and-decide loop - applies
the same way once the work is committed on a feature branch.
## Before you start
- The work you want validated must be **committed** on a branch. The gate
validates committed history, not your uncommitted working tree.
- You must be on a **feature branch**, not the repository's default branch.
- The repository must already be initialized with `no-mistakes init`.
- The daemon must have a runnable configured pipeline agent: a supported native
agent binary, the `agent: cursor` ACP alias, or an explicit `acp:<target>` through
`acpx`. You are the AXI driver, not
an implicit pipeline-agent backend. If none is available, the run fails
before its first step; `no-mistakes doctor` reports the configuration problem.
If any of these is not met, `axi run` returns an `error:` with the exact command
to fix it - read it and act on it (commit your work, or create a branch). If the
repository is not initialized, run `no-mistakes init` first; if the `no-mistakes`
command itself is missing or misbehaving, `no-mistakes doctor` reports what is
wrong.
Before starting, run `no-mistakes axi` (home view).
If it shows an active run on your current branch, inspect it with `no-mistakes axi status`.
If it is parked at a gate, drive it with `no-mistakes axi respond`.
Reattach an in-flight run by re-running `no-mistakes axi run` when it still matches your current `HEAD` - either as the submitted head or as the current pipeline head.
Only `no-mistakes axi abort` it when you mean to discard that run before starting over; aborting is a between-runs action, never a way to take over or bypass a gate while a run is still going (see [Validate and decide](#validate-and-decide)).
If it shows an active run on another branch, leave that run alone and start validation for your current branch with `no-mistakes axi run --intent "..."`.
## Intent is required
When you start a run you must pass `--intent`: **what the user set out to
accomplish** - the goal or request behind this work, in their terms. This is not
a description of the diff or the files you changed; it is the objective the
change is meant to achieve. You know it from the conversation, so pass it
directly - no-mistakes uses it verbatim instead of inferring it from local agent
transcripts (slower and flakier).
Err on the side of completeness, not brevity. The review step uses `--intent`
to tell a deliberate decision apart from a mistake, so a thin one-line summary
makes it flag things the user already chose. Capture the nuance: the user's
goal, the specific decisions and tradeoffs they made along the way, any
constraints or approaches they ruled in or out, and anything they explicitly
asked for that might otherwise look surprising in the diff. A few sentences to a
short paragraph is normal - write down what you learned from the conversation
that a reviewer reading only the diff would not know.
## Validate and decide
Run the pipeline and decide on its findings as they come up:
1. Start the run. It blocks until the first decision point or the end:
```sh
no-mistakes axi run --intent "<what the user set out to accomplish>"
```
`axi run` and every `axi respond` block synchronously - the review, test,
and CI steps can each take **several minutes**, so a single call may not
return for a while. That is normal; do not cancel or re-issue the command
because it seems slow. Both commands default to `--wait 8m` so a harness
with a 10-minute tool cap gets a structured return instead of an unbounded
hang. If the command returns because that wait elapsed, it is not a failed
run and does not mean the daemon is dead: inspect with `no-mistakes axi status`
and re-run `axi run` or `axi respond` to reattach. A slow live daemon is
retried after a health probe rather than treated as I/O failure. To check
progress without disturbing the run, use `no-mistakes axi status` from a
separate call.
A long-running call is working, not stalled - background it if your harness
needs to, but the run **never advances past a gate on its own**. Read every
return; on a `gate:`, respond; loop until an `outcome:`. Never idle-wait
for the run to move forward by itself.
When that status output includes `awaiting_agent: parked <duration>` under the run,
the run is parked at an approval or fix-review gate and waiting for you to
send `axi respond`. The field is observability only: it does not change
gate resolution, auto-resume the run, or make `--yes` the default.
While a step is actively `running` or `fixing`, `axi status` may include
`active_steps` with step-scoped `active_for`, current-round `round_active_for`,
`last_activity`, a native `agent_pid` when a subprocess agent is running, and the current round such as `round 1`,
`auto-fix 1/3`, or `fix 2`. If `last_activity` is prefixed with
`quiet`, no step log or native-agent lifecycle activity has arrived for
longer than `step_quiet_warning`. Treat that as a liveness clue, not as
permission to cancel, rerun, or edit the worktree yourself.
2. If the output contains a `gate:` object, the pipeline is waiting on you.
Read its `findings` table. Each finding has an `id`, `severity`,
`file`, `description`, and an `action` that tells you how the
pipeline classified it:
- `auto-fix` - mechanical and low-risk; you can authorize the fix on
your own judgment by responding with `--action fix`.
- `no-op` - informational only; nothing to do.
- `ask-user` - the finding challenges the user's deliberate intent or
touches product behavior. This is a call only the user can make - see
[Escalate `ask-user` findings](#escalate-ask-user-findings) below.
**Review auto-fix is disabled by default** (`auto_fix.review: 0`; a repo
or global `auto_fix.review > 0` override re-enables it), so blocking and
ask-user review findings park for your decision rather than being silently
self-fixed. (Other steps such as test and lint may auto-fix within the
pipeline and re-run before they ever gate.)
Choose one response:
```sh
# accept the step as-is and continue
no-mistakes axi respond --action approve
# have the pipeline fix specific findings, then continue
no-mistakes axi respond --action fix --findings <id1,id2> --instructions "<optional guidance>"
# skip this step
no-mistakes axi respond --action skip
```
While a run is active, never fix findings by editing the code yourself -
the pipeline owns both the findings and the fixes. Your job at a gate is to
decide and respond; `--action fix` has the pipeline apply the fix and
re-review the result. For the same reason, while a run is active do **not**
`abort` or `rerun` to go fix a finding yourself - even a real bug in
your own code - because that discards the pipeline's in-flight work and
forces a full re-validation. `abort` and `rerun` are for *between*
runs (after a `failed` or `cancelled` outcome), never to circumvent a
gate.
Each `respond` blocks until the next `gate:`, `checks-passed` decision point, or final outcome, subject to the same default `--wait 8m` hold.
Extra flags on `respond`:
- `--wait` bounds the hold (default 8m).
- `--reason "the operator's explanation"` records an explicitly authorized Test exception with `--step test --action approve`.
This does not grant approval authority; escalate ask-user findings as before.
Without a reason, Test approval remains effective; an approval past a failing command, `no-go`, or `inconclusive` verdict is reported as an exception with no operator reason supplied.
- `--add-finding '<json>'` (with `--action fix`) folds a finding you
spotted yourself - one the pipeline did not surface - into the fix round,
as a JSON finding object. Use it for a problem you noticed that is not in
the gate's own `findings` table.
- `--step <name>` responds to a specific step instead of the one currently
awaiting approval. You rarely need this; omit it to answer the active gate.
3. Repeat step 2 until the output has an `outcome:` instead of a `gate:`. The
outcomes are:
- `checks-passed` - the change is validated and CI is green (or the
trusted default-branch config declares `no_ci: true` and no checks are
registered - the help line names that declaration when it applies), but
the PR is not merged yet. **You are done driving the pipeline.** Do not
wait for the merge: tell the user the PR is ready and ask them to review
and merge it (the PR link is in the `help` line). A generic empty forge
check list without that declaration is not ready. no-mistakes keeps
monitoring the PR in the background until it is merged, closed, or its
configured idle timeout elapses, so a human can watch it in the TUI.
- `passed` - the pipeline completed under the requested steps, including any
explicit per-run skips. This alone is not evidence that a PR was merged.
- `passed-with-override` - the pipeline completed with an explicitly approved Test exception or CI failure.
Report the exception, not a clean pass.
Test evidence is in `run.test_override_reason`, including when CI readiness returns `checks-passed`; do not omit it from the summary.
- `passed-with-skips` - publication or CI verification automatically skipped.
Report the missing evidence and its cause from `run.automatic_skips`,
bound to the full `run.head_sha`. This is neither CI readiness nor a
failing code verdict. Explicit per-run skips retain their existing behavior.
- `failed` or `cancelled` - they did not; read the output and address it.
Follow the custody guidance below before fixing whatever the output
points at (a failing test, a lint error, a finding you skipped). Commit the
fix on the same feature branch, then submit it with
`no-mistakes axi run --intent "..."`. A fresh run or `rerun` is a
*between-runs* action, correct only after a terminal outcome like this -
never mid-run to circumvent a gate. Do not leave the user at a `failed`
outcome without either retrying or explaining what blocks it.
`no-mistakes rerun` keeps its existing head selection: the gate head, or the
latest terminal run's verified unpublished preserved head while custody remains
outstanding. If a known clean caller `HEAD` differs from that selected head,
it refuses before starting or superseding any run and reports both full SHAs.
It never substitutes the caller head or moves either branch to make them match.
On refusal, inspect `no-mistakes axi status` and follow the custody guidance
below. Dirty callers and callers without clean-head evidence retain existing
selection behavior.
Before any post-pipeline local commit or fresh run, read the structured `branch_sync` object returned by AXI home, status, or a drive result.
Only when its `next_action.code` is `sync`, run `no-mistakes axi sync` first.
That guarded sync may be a strict fast-forward or a content-equivalent diverged advance that anchors the pre-sync head before moving the branch with reset semantics; genuine divergence stays blocked.
If it reports `next_action.code` is `continue_active_run`, the pipeline still owns the branch: run the reported command, keep driving the active run, and do not make local follow-up commits.
When `next_action.code` is `recover_custody`, run its exact `next_action.command` rather than reconstructing one. That is `no-mistakes axi sync --recover` to take a still-available preserved pipeline head, or `no-mistakes axi sync --recover --keep-local` in two keep-local cases: when an accessible gate confirms the verified preserved head is missing and you are explicitly discarding those unpublished commits, or when a bound archive proves divergent later work remains preserved while recovery keeps the branch at the exact reported required head and never selects, merges, or replays the archive. Do not substitute plain `--recover` or `rerun` for a reported keep-local action. `no-mistakes rerun` can resume validating a still-available ordinary preserved head instead, subject to the clean-head check above.
Ordinary recovery takes that head by fast-forward, or by adopting a diverged preserved head proven to carry every local change - the ordinary result of the pipeline rebasing your commits onto a newer base - after anchoring your pre-recovery head under `refs/no-mistakes/recover-local/<run>`.
The ordinary containment proof is deliberately narrow, so a rebase whose fix rounds also rewrote your own lines refuses instead of being adopted: when nothing can tell a deliberate pipeline fix from a dropped change, the decision is yours.
A `branch_sync.state` of `user_owned` means the run went terminal before changing the submitted head and cancellation released the branch: the exact branch and head are yours and immediately usable for whichever delivery path is authorized - no sync action is needed, and a repeated `--recover` there is a harmless no-op.
A dirty worktree, or divergence that cannot be proven contained, makes the recovery refuse with explicit choices; `--keep-local` keeps your current head while the preserved commits stay anchored under `refs/no-mistakes/recover/<run>`. The same flag is the recovery when an accessible gate confirms that the verified preserved head is missing and recovery refs are compatible: it returns custody at the current local head without requiring that object.
If synchronization is blocked, process that structured state instead of improvising reset, stash, merge, rebase, force, or branch replacement.
After synchronization, commit the follow-up on top and re-run `no-mistakes axi run --intent "..."` with the original user intent.
This preserves every prior gate-fix commit regardless of its configured subject.
The CI step deliberately keeps watching the PR after checks pass, so
`axi run` returns `checks-passed` the moment checks are green (or a trusted
`no_ci: true` declaration covers a zero-check repository) rather than
blocking on the human merge. Never poll or re-run waiting for the merge yourself.
Never treat "no CI checks reported" alone as green.
Because that monitor stays live, a PR that falls behind the default branch or
hits a merge conflict after checks pass - commonly because another PR merged
first - needs **no command from you**: never hand-rebase. When the CI monitor
sees an actual conflict it **rebases onto the base, resolves it, revalidates from Review
because rebasing cannot prove continuity with the reviewed head, and re-pushes
the branch through Push**; a PR that is merely behind but still clean needs nothing
either, since the platform merges it. The one exception is when that monitor is
no longer running - the PR was closed, the run was aborted or superseded, it
idle-timed-out, or its auto-fix attempts were exhausted - in which case recover
with `no-mistakes rerun`, subject to the clean-head check above. An accepted
rerun cancels the stale monitor and re-runs the full pipeline including a
deterministic rebase step. Do **not** reach for
`no-mistakes axi run` to refresh a still-active PR: after `checks-passed` it
reattaches to the running monitor (HEAD unchanged) and returns its output
without rebasing.
On a successful outcome (`checks-passed` or `passed`), close the loop with the
user: summarize what happened during the pipeline in a concise, easily readable
format - what was validated and what was found. If the output includes a
`fixes` table, the pipeline fixed findings your original change missed:
acknowledge those misses and explicitly list each fix so the user can easily
review them.
## Escalate `ask-user` findings
A gate whose findings are all `auto-fix` or `no-op` is safe to drive on your
own judgment: respond with `--action fix` or `--action approve` as
appropriate. But a finding marked
`ask-user` is a decision that belongs to the user, not you - the pipeline
flagged it because it challenges their deliberate intent or changes product
behavior. Do not approve, fix, or skip it on your own. Instead, stop and bring
it to the user before you respond:
- Relay each `ask-user` finding to them as the pipeline wrote it - its
`id`, `file`, and full `description` verbatim. Do not paraphrase,
summarize away the detail, or pre-judge the answer.
- Ask how they want to proceed, then translate their decision into the matching
`respond` call: `--action fix` (pass their guidance through
`--instructions`), `--action approve`, or `--action skip`.
The exception is `--yes` (below): it is the user's standing consent to
drive eligible gates unattended, so under `--yes` you resolve ordinary
`ask-user` findings automatically instead of stopping to ask.
If you have clear consent to drive the run automatically, pass `--yes` to `axi run`
or `axi respond`. For eligible gates, it treats actionable findings - `auto-fix` and
`ask-user` alike - as consent to fix it, selects every current finding for one
fix round, accepts the resulting fix review, and approves gates with only
`no-op` findings. Only use it when the user has asked you to drive the whole
run without checking back.
A `protected-path-refusal` gate still requires an explicit operator response
under `--yes`. Relay its path and rule; do not automatically fix, approve,
or skip it. Approval is rejected. Have the operator inspect and resolve the
reported edit, then send `--action fix` to retry the unfinished step.
The [protected-path reference](https://kunchenguid.github.io/no-mistakes/reference/repo-config/#protected_paths)
owns the staging guard's scope and limitations.
A `test-agent-unvalidated-work` finding means a timed-out Test agent left
commits or changes no Test turn validated. Approval is rejected, so `--yes`
stops at that gate without responding. Relay what the finding names and do
not skip Test, which would publish that work. Ask the operator to choose:
`--action fix` spends another agent budget to validate the work, and
`no-mistakes axi abort` stops the run.
## Inspecting state
```sh
no-mistakes axi # home view: current branch, active runs, next steps
no-mistakes axi status # full detail plus cached branch_sync when relevant
no-mistakes axi sync --check # freshly verify an offered synchronization plan
no-mistakes axi sync # apply only an offered guarded synchronization
no-mistakes axi sync --recover # return custody after a terminal run left unpublished pipeline commits
no-mistakes axi logs --step <name> --full # full log output of one step
no-mistakes axi abort # cancel the current-branch active run
no-mistakes axi abort --run <id> # cancel a specific run by id (works outside its worktree)
```
## Reading the output
- Output is TOON: `key: value` pairs, `name[N]{cols}:` tables, and `help[N]:` hints.
- `axi status` is scoped to your current branch when `--run` is omitted: with a known current branch, an implicitly resolved `run:` is this branch's. A run under `other_branch_run:` is one you named with `--run <id>` that belongs to another branch - never read its status or outcome as your own work. An explicit `--run <id>` rendered under `run:` while the current branch is unknown (detached `HEAD` or a branch-lookup failure) encodes no branch relationship. In a successful status response, no run object at all means this branch has no run yet, whatever the recent-runs table lists; an `error:` response proves nothing about run ownership, so act on the error instead of concluding the branch is idle.
- A non-terminal run object may include `awaiting_agent: parked <duration>` immediately after `status`; that means the run is parked at a gate. Only an implicitly resolved current-branch gate offers `axi respond`; an explicit `--run <id>` status is inspection-only even when its branch matches, because the branch may have a newer active run. Follow the response's `help`.
- A run object with a `running` or `fixing` step may include an `active_steps` table. `active_for` is the enclosing step duration; `round_active_for` is the displayed execution or fix round duration and resets for a fix round. Older runs without round timing leave `round_active_for` empty.
- The `help` list at the bottom of most responses tells you the next commands to run.
- Errors are printed as `error: ...` on stdout with a `help` list; act on the suggestion.
- Exit codes: `0` success, no-op, or normal decision gates, `1` failed or cancelled final outcomes, `2` bad usage.
A `gate:` waiting on you looks roughly like this - a `gate:` line naming the step, optional step-specific fields such as `note`, a `findings[N]{...}:` table with one row per finding, and a `help[N]:` list of next commands:
```
gate: review
note: Review auto-fix is disabled by default (auto_fix.review: 0; a repo or global auto_fix.review > 0 override re-enables it), so blocking and ask-user review findings park for your decision rather than being silently self-fixed.
findings[2]{id,severity,file,line,action,description}:
r1,warning,internal/pipeline/executor.go,,auto-fix,Error from os.Remove is ignored
r2,error,cmd/no-mistakes/main.go,,ask-user,New --force flag bypasses the confirm prompt
help[6]:
Run `no-mistakes axi respond --action approve` to accept this step and continue
Run `no-mistakes axi respond --action fix --findings <ids>` to have the pipeline fix the selected findings (do not edit files yourself)
Run `no-mistakes axi respond --action skip` to skip this step
Run `no-mistakes axi logs --step review --full` to read the full step log
A long-running call is working, not stalled - background it if your harness needs to, but the run never advances past a gate on its own. Read every return; on a `gate:`, respond; loop until an `outcome:`.
Commit post-pipeline follow-up work on top of the existing branch so every pipeline fix commit remains present. Never abort-and-restart, reset, or replace the branch in a way that drops prior gate-fix commits.
```
Read the `action` column per row: decide `r1` (auto-fix) on your own
judgment - `respond --action fix --findings r1` hands it to the pipeline to
fix - but stop and escalate `r2` (ask-user) to the user before responding. A
final state
instead shows `outcome: <checks-passed|passed|passed-with-override|passed-with-skips|failed|cancelled>` with no
`findings` table. Field names and exact columns can vary by step and version,
so read the actual `findings` header rather than assuming this layout.
@@ -0,0 +1,87 @@
---
name: playwright-cli
description: Use when the user asks to test a web app in a real browser, automate browser clicks/form submissions, take screenshots of flows, verify a UI works end-to-end, or do end-to-end (E2E) testing. Triggers on keywords like "browser test", "E2E test", "click through", "playwright", "verify the flow", "screenshot the page". Installs and drives the Playwright CLI to let the agent act as a real browser user.
---
# Playwright CLI — agent-driven browser testing
The agent should not say "looks good to me" about a UI it never opened. Use Playwright to actually click through the flow, fill forms, assert outcomes, and screenshot the result.
## Install (first use)
If `playwright` is not on PATH, install it:
```bash
npm install -g @playwright/mcp@latest
npx playwright install --with-deps
```
`npx playwright install` downloads the browser binaries (chromium, firefox, webkit). `--with-deps` installs OS libraries they need (Linux only; on macOS it's a no-op for the deps portion).
Verify: `npx playwright --version`.
## Core CLI commands
| Task | Command |
|---|---|
| Scaffold a new test project | `npm init playwright@latest` (creates `tests/`, `playwright.config.ts`) |
| Run all tests | `npx playwright test` |
| Run one file | `npx playwright test tests/login.spec.ts` |
| Run by title grep | `npx playwright test -g "logs in"` |
| Run headed (see the browser) | `npx playwright test --headed` |
| Run with browser visible + slow | `npx playwright test --headed --workers=1` |
| UI mode (interactive watcher) | `npx playwright test --ui` |
| Trace viewer (post-mortem) | `npx playwright show-trace trace.zip` |
| Codegen a flow by clicking | `npx playwright codegen <url>` |
| Codegen to a file | `npx playwright codegen <url> -o tests/flow.spec.ts` |
| Screenshot a page | `npx playwright screenshot --browser chromium <url> out.png` |
| PDF a page | `npx playwright pdf <url> out.pdf` |
| Open a page in a real browser | `npx playwright open <url>` |
## When to use
- User says "test the login flow", "verify the checkout works", "click through and make sure nothing breaks"
- User asks to record a new E2E test from a manual flow → `playwright codegen`
- User wants a screenshot/PDF of a rendered page for verification
- After touching auth, forms, navigation, or anything with state, run the relevant spec instead of asserting "should work"
## When NOT to use
- Unit testing component logic → use the project's existing unit test runner (vitest, jest, etc.)
- API/endpoint testing → use `curl`/httpie or the API test framework already in the repo
- Load testing → Playwright is functional, not perf; suggest k6 or similar
## Writing tests (codegen first, edit second)
The fastest path to a working test is `codegen`, not hand-writing:
```bash
npx playwright codegen http://localhost:3000 -o tests/auth.spec.ts
```
Click through the flow in the browser that pops up; Playwright writes the spec live. Then edit the generated file to add assertions (`expect(locator).toHaveText(...)`, `expect(page).toHaveURL(...)`) and clean up selectors (prefer `getByRole`, `getByLabel` over CSS).
## Assertions quick reference
```ts
import { test, expect } from '@playwright/test';
test('user can log in', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('Email').fill('user@example.com');
await page.getByLabel('Password').fill('secret');
await page.getByRole('button', { name: 'Sign in' }).click();
await expect(page).toHaveURL(/dashboard/);
await expect(page.getByRole('heading', { name: 'Welcome' })).toBeVisible();
});
```
## Debugging a failing test
1. `npx playwright test tests/x.spec.ts --headed --workers=1` — watch it run.
2. If still unclear, add `--trace on` then `npx playwright show-trace trace.zip`.
3. `page.pause()` in the spec drops into the Playwright Inspector (step through, try selectors live).
## Source
Referenced in https://x.com/heyshruti7/status/2069083108092350823 — "Playwright CLI — your agent tests the browser itself. No more 'looks good to me.' It actually clicks through the flow."
@@ -0,0 +1,35 @@
---
name: read-tweet
description: Use when the user asks to read, fetch, or show the content of an X (Twitter) tweet or thread by URL or ID. Triggers on x.com/twitter.com URLs or tweet IDs. Uses the local `bird` CLI to pull content via the browser cookie session.
---
# Read X/Twitter Tweet Content with bird
When the user asks to read, fetch, view, or summarize a tweet (or thread), use the locally installed `bird` CLI instead of `webfetch` — it authenticates via the active browser session and returns full tweet content + metadata.
## Commands
Use `/opt/homebrew/bin/bird` (absolute path since PATH can be sparse in non-interactive shells):
- Single tweet: `bird read <tweet-url-or-id> --json`
- Full thread/conversation: `bird thread <tweet-url-or-id> --json`
- Replies to a tweet: `bird replies <tweet-url-or-id> --json`
Prefer `--json` for parseable output (author, text, media, created_at, metrics). Drop `--json` only if the user wants pretty-printed terminal output.
## When to use
- User pastes an `x.com`/`twitter.com` URL and asks anything about it
- User references a tweet by numeric ID
- User asks for "the thread", "the replies", or "what does this tweet say"
## When NOT to use
- User asks to post, reply, like, bookmark, follow, or search tweets — use other `bird` subcommands directly (see `bird --help`)
- User asks about article content linked FROM a tweet — `bird` only returns tweet text/metadata, not outbound article bodies; fall back to `webfetch` on the linked URL
## Notes
- `bird` reads `auth_token` and `ct0` cookies from Safari/Chrome/Firefox; no paid dev account needed
- If a fetch fails with auth errors, tell the user to sign in to x.com in their browser and retry
- Quote tweets and retweets include the referenced tweet in the JSON payload
+40
View File
@@ -0,0 +1,40 @@
{
"$schema": "https://opencode.ai/config.json",
"model": "opencode-go/gpt-6-luna",
"autoupdate": false,
"providers": {
"omlx": {
"name": "oMLX (Local)",
"package": "@opencode-ai/ai/providers/openai-compatible",
"settings": {
"baseURL": "http://localhost:8000/v1"
},
"models": {
"qwen3.6-35b": {
"modelID": "Jundot/Qwen3.6-35B-A3B-oQ4-mtp",
"name": "Qwen 3.6 35B (o4-mtp)",
"capabilities": {
"tools": true,
"input": ["text"],
"output": ["text"]
},
"limit": {
"context": 65536,
"output": 65536
}
}
}
},
"opencode-go": {
"models": {
"gpt-6-luna": {
"limit": {
"context": 256000,
"input": 256000,
"output": 128000
}
}
}
}
}
}
+13
View File
@@ -0,0 +1,13 @@
local wezterm = require("wezterm")
local config = wezterm.config_builder()
config.color_scheme = "rose-pine-moon"
config.font = wezterm.font("Hack Nerd Font")
config.font_size = 15.0
config.window_background_opacity = 0.8
config.macos_window_background_blur = 50
config.hide_tab_bar_if_only_one_tab = true
config.window_decorations = "RESIZE"
return config
+36
View File
@@ -0,0 +1,36 @@
[marketplace]
official_marketplace_auto_installed = true
default_skills_installs_purged = true
[[marketplace.sources]]
name = "xAI Official"
git = "https://github.com/xai-org/plugin-marketplace.git"
[models]
default = "grok-4.5"
default_reasoning_effort = "high"
[model.qwen]
model = "Jundot/Qwen3.6-35B-A3B-oQ4-mtp"
base_url = "http://localhost:8000/v1"
name = "Qwen 3.6 35B (o4-mtp)"
description = "Qwen3.6-35B-A3B with o4-mtp on oMLX"
api_backend = "chat_completions"
temperature = 0.7
top_p = 0.95
context_window = 65536
max_completion_tokens = 65536
[ui]
theme = "rosepine"
max_thoughts_width = 120
fork_secondary_model = "grok-build"
permission_mode = "auto"
yolo = false
compact_mode = false
[cli]
installer = "internal"
[privacy]
privacy_banner_acked = "2026-09-24T02:33:29Z"
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Kun Chen
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+157
View File
@@ -0,0 +1,157 @@
// Pi Calm - a standalone conversation-presentation toggle for Pi.
//
// Adapted from the Firstmate project's Calm implementation.
// Copyright (c) 2026 Kun Chen. MIT License - see the LICENSE file in this directory.
//
// Verified against Pi 0.82.0, which exports its shared tool-row component,
// session_start replacement reasons, agent_start
// and agent_settled, ExtensionUIContext.setToolsExpanded(), setWorkingVisible(),
// setWidget() with a disposable component factory, and setHiddenThinkingLabel().
// ./lib/working-ship.ts owns the animated working presentation this file
// installs. ./lib/preference.ts owns the local state file. The collapsed-thinking
// presentation adapter probes the exact public API seam it patches and degrades
// independently with one clear diagnostic (see installCalmPresentationAdapter
// below) if a future Pi removes it. The shared tool-row adapter is limited to
// Pi's seven known built-in names, so generic custom tools and unsupported
// transcript classes deliberately stay visible.
//
// Calm changes presentation only. It never intercepts, transforms, reroutes,
// removes, or reorders semantic input, tool execution, model context, session
// storage, or export data; /export and /share render the complete stock
// transcript.
import { type ExtensionAPI, type ExtensionUIContext } from "@earendil-works/pi-coding-agent";
import { getKeybindings } from "@earendil-works/pi-tui";
import { installCalmBuiltInToolShellLayout } from "./lib/built-in-tool-shells.ts";
import { installCalmCollapsedThinkingLayout } from "./lib/collapsed-thinking.ts";
import { loadCalmPreference, persistCalmPreference } from "./lib/preference.ts";
import {
calmPresentationIsActive,
setCalmPresentation,
setCalmStockExportRendering,
} from "./lib/visibility.ts";
import {
CALM_WORKING_SHIP_WIDGET_KEY,
createCalmWorkingShipAnimation,
createCalmWorkingShipWidget,
} from "./lib/working-ship.ts";
// Each presentation adapter probes the exact Pi API it patches. If a future Pi
// removes that API, only the affected adapter degrades; the rest of Calm keeps
// working.
function installCalmPresentationAdapter(name: string, install: () => void): void {
try {
install();
} catch (error) {
const reason = error instanceof Error ? error.message : String(error);
console.error(`Pi Calm: ${name} presentation adapter unavailable, skipping. ${reason}`);
}
}
export default function (pi: ExtensionAPI) {
installCalmPresentationAdapter("collapsed-thinking", installCalmCollapsedThinkingLayout);
installCalmPresentationAdapter("built-in-tool-shells", installCalmBuiltInToolShellLayout);
let removeTerminalInputHandler: (() => void) | undefined;
// One logical agent run, tracked from agent_start through agent_settled rather
// than from turns or tool calls, so the boat never flickers between tool calls,
// automatic continuations, retries, or compaction that stay inside the same run.
let agentRunActive = false;
let workingShipShown = false;
// One animation instance per extension lifetime. Hiding the working widget
// freezes this state; the next working period resumes it. session_start resets
// it so a fresh Pi session starts at the normal initial position. Never
// module-global.
const workingShipAnimation = createCalmWorkingShipAnimation();
// Single owner of Calm's working-row presentation choice. The widget is only
// created or removed on a real transition, so repeated starts cannot duplicate
// its timer.
const applyWorkingPresentation = (
ui: ExtensionUIContext,
forceStockVisibility = false,
): void => {
const showShip = agentRunActive && calmPresentationIsActive();
if (showShip !== workingShipShown) {
workingShipShown = showShip;
ui.setWidget(
CALM_WORKING_SHIP_WIDGET_KEY,
showShip
? (tui) => createCalmWorkingShipWidget(tui, workingShipAnimation)
: undefined,
);
ui.setWorkingVisible(!showShip);
} else if (forceStockVisibility && !showShip) {
ui.setWorkingVisible(true);
}
};
pi.on("session_start", (_event, ctx) => {
setCalmPresentation(loadCalmPreference());
setCalmStockExportRendering(false);
agentRunActive = false;
workingShipShown = false;
// A genuine new session lifetime starts the boat at the normal initial position.
workingShipAnimation.reset();
applyWorkingPresentation(ctx.ui, true);
ctx.ui.setHiddenThinkingLabel(calmPresentationIsActive() ? "" : undefined);
removeTerminalInputHandler?.();
removeTerminalInputHandler = ctx.ui.onTerminalInput((data) => {
if (!getKeybindings().matches(data, "tui.input.submit")) return;
const input = ctx.ui.getEditorText().trim();
if (
input !== "/share" &&
input !== "/export" &&
!input.startsWith("/export ")
) {
return;
}
// /export and /share render through the same tool renderers the transcript
// uses, so force stock output for the duration of the command. Session and
// export data are never filtered; this only concerns the visual components.
setCalmStockExportRendering(true);
setTimeout(() => {
setCalmStockExportRendering(false);
const expanded = ctx.ui.getToolsExpanded();
ctx.ui.setToolsExpanded(!expanded);
ctx.ui.setToolsExpanded(expanded);
}, 0);
});
});
pi.on("agent_start", (_event, ctx) => {
agentRunActive = true;
applyWorkingPresentation(ctx.ui);
});
// agent_settled is emitted from a finally block, so it also covers abort and failure.
pi.on("agent_settled", (_event, ctx) => {
agentRunActive = false;
applyWorkingPresentation(ctx.ui);
});
pi.on("session_shutdown", (_event, ctx) => {
agentRunActive = false;
applyWorkingPresentation(ctx.ui);
});
pi.registerCommand("calm", {
description: "Toggle Calm: hide collapsed thinking and built-in tool shells from the transcript (presentation only).",
handler: async (_args, ctx) => {
const active = !calmPresentationIsActive();
// Persist first: if the state file cannot be written, the toggle fails
// with a clear error instead of silently reverting on the next restart.
persistCalmPreference(active);
setCalmPresentation(active);
applyWorkingPresentation(ctx.ui, true);
ctx.ui.setHiddenThinkingLabel(active ? "" : undefined);
// Flip expansion twice to force a transcript redraw while preserving the
// user's exact Ctrl+O tools-expanded state.
const expanded = ctx.ui.getToolsExpanded();
ctx.ui.setToolsExpanded(!expanded);
ctx.ui.setToolsExpanded(expanded);
},
});
}
@@ -0,0 +1,117 @@
// Pi Calm - gapless built-in tool-shell presentation adapter.
//
// Adapted from the Firstmate project's Calm implementation.
// Copyright (c) 2026 Kun Chen. MIT License - see the LICENSE file in this directory.
//
// Verified against Pi 0.82.0, which exports AgentSession and
// ToolExecutionComponent. The source-aware lookup returns Pi's active definition
// unchanged and the adapter changes only its final TUI row layout. Execution,
// settings, SDK overrides, extension collisions, and stored results remain
// owned by Pi. Image results remain visible without their call/result shell,
// and custom tools or tools outside Pi's seven built-ins render unchanged.
import {
AgentSession,
ToolExecutionComponent,
type ToolDefinition,
} from "@earendil-works/pi-coding-agent";
import type { Component } from "@earendil-works/pi-tui";
import { calmHidesTranscriptChrome } from "./visibility.ts";
const CALM_BUILT_IN_TOOL_NAMES = new Set([
"read",
"bash",
"edit",
"write",
"grep",
"find",
"ls",
]);
type ToolRowPresentationState = {
toolName: string;
toolDefinition?: ToolDefinition;
imageComponents: Component[];
imageSpacers: Component[];
};
type AgentSessionPresentationState = {
_baseToolsOverride?: Record<string, unknown>;
};
type CalmBuiltInToolShellPatch = {
hidesShell: () => boolean;
builtInDefinitions: WeakSet<ToolDefinition>;
};
const CALM_BUILT_IN_TOOL_SHELL_PATCH = Symbol.for(
"pi-calm:built-in-tool-shell-layout:pi-0.82.0",
);
export function installCalmBuiltInToolShellLayout(): void {
const registry = globalThis as typeof globalThis & {
[key: symbol]: CalmBuiltInToolShellPatch | undefined;
};
const hidesShell = (): boolean => calmHidesTranscriptChrome();
const installed = registry[CALM_BUILT_IN_TOOL_SHELL_PATCH];
if (installed?.builtInDefinitions) {
installed.hidesShell = hidesShell;
return;
}
const originalGetToolDefinition = AgentSession.prototype.getToolDefinition;
if (typeof originalGetToolDefinition !== "function") {
throw new Error("Pi Calm requires Pi AgentSession.getToolDefinition");
}
if (typeof ToolExecutionComponent !== "function") {
throw new Error("Pi Calm requires Pi ToolExecutionComponent");
}
const originalRender = ToolExecutionComponent.prototype.render;
if (typeof originalRender !== "function") {
throw new Error("Pi Calm requires Pi ToolExecutionComponent.render");
}
if (installed) installed.hidesShell = () => false;
const patch: CalmBuiltInToolShellPatch = {
hidesShell,
builtInDefinitions: new WeakSet(),
};
AgentSession.prototype.getToolDefinition = function (
name: string,
): ToolDefinition | undefined {
const definition = originalGetToolDefinition.call(this, name);
const source = this.getAllTools().find((tool) => tool.name === name)?.sourceInfo.source;
if (definition) {
const session = this as unknown as AgentSessionPresentationState;
const isSdkBaseOverride = Object.hasOwn(session._baseToolsOverride ?? {}, name);
if (source === "builtin" && !isSdkBaseOverride) {
patch.builtInDefinitions.add(definition);
} else {
patch.builtInDefinitions.delete(definition);
}
}
return definition;
};
ToolExecutionComponent.prototype.render = function (width: number): string[] {
const state = this as unknown as ToolRowPresentationState;
const isKnownBuiltIn =
CALM_BUILT_IN_TOOL_NAMES.has(state.toolName) &&
state.toolDefinition !== undefined &&
patch.builtInDefinitions.has(state.toolDefinition);
if (!isKnownBuiltIn || !patch.hidesShell()) {
return originalRender.call(this, width);
}
const lines: string[] = [];
for (let index = 0; index < state.imageComponents.length; index += 1) {
const spacer = state.imageSpacers[index];
if (spacer) lines.push(...spacer.render(width));
const image = state.imageComponents[index];
if (image) lines.push(...image.render(width));
}
return lines;
};
registry[CALM_BUILT_IN_TOOL_SHELL_PATCH] = patch;
}
@@ -0,0 +1,82 @@
// Pi Calm - gapless collapsed-thinking presentation adapter.
//
// Adapted from the Firstmate project's Calm implementation.
// Copyright (c) 2026 Kun Chen. MIT License - see the LICENSE file in this directory.
//
// Verified against Pi 0.82.0, which exports AssistantMessageComponent with an
// updateContent method. installCalmCollapsedThinkingLayout() probes that exact
// public seam and throws if it is missing; index.ts catches that and skips only
// this adapter with one clear diagnostic instead of blocking Calm or Pi.
//
// How it works: Pi renders a hidden thinking block as one static label row.
// Calm sets that label to the empty string and this adapter filters thinking
// blocks out of the message handed to the stock renderer, so a collapsed
// thinking block occupies zero rows instead of one blank one. The unfiltered
// message is kept on lastMessage so expanding thinking (Ctrl+T) and turning
// Calm off both restore the original reasoning content byte-for-byte. Only
// collapsed thinking is affected: expanded reasoning, assistant text, and tool
// calls render exactly as Pi renders them.
import type { AssistantMessageComponent as PiAssistantMessageComponent } from "@earendil-works/pi-coding-agent";
import * as PiCodingAgent from "@earendil-works/pi-coding-agent";
import { calmHidesTranscriptChrome } from "./visibility.ts";
type AssistantMessage = Parameters<PiAssistantMessageComponent["updateContent"]>[0];
type AssistantMessagePresentationState = {
hiddenThinkingLabel: string;
hideThinkingBlock: boolean;
lastMessage?: AssistantMessage;
};
type CalmCollapsedThinkingPatch = {
hidesThinking: () => boolean;
};
// Keep the introduction-version symbol stable so a compatible upgrade cannot
// double-patch a live process.
const CALM_COLLAPSED_THINKING_PATCH = Symbol.for(
"pi-calm:collapsed-thinking-layout:pi-0.82.0",
);
export function installCalmCollapsedThinkingLayout(): void {
const registry = globalThis as typeof globalThis & {
[key: symbol]: CalmCollapsedThinkingPatch | undefined;
};
const hidesThinking = (): boolean => calmHidesTranscriptChrome();
const installed = registry[CALM_COLLAPSED_THINKING_PATCH];
if (installed) {
installed.hidesThinking = hidesThinking;
return;
}
const patch: CalmCollapsedThinkingPatch = { hidesThinking };
const AssistantMessageComponent = PiCodingAgent.AssistantMessageComponent;
if (typeof AssistantMessageComponent !== "function") {
throw new Error("Pi Calm requires Pi AssistantMessageComponent");
}
const originalUpdateContent = AssistantMessageComponent.prototype.updateContent;
if (typeof originalUpdateContent !== "function") {
throw new Error("Pi Calm requires Pi AssistantMessageComponent.updateContent");
}
AssistantMessageComponent.prototype.updateContent = function (
message: AssistantMessage,
): void {
const state = this as unknown as AssistantMessagePresentationState;
const hideThinking =
state.hiddenThinkingLabel === "" &&
state.hideThinkingBlock &&
patch.hidesThinking();
const presentationMessage = hideThinking
? {
...message,
content: message.content.filter((block) => block.type !== "thinking"),
}
: message;
originalUpdateContent.call(this, presentationMessage);
if (presentationMessage !== message) state.lastMessage = message;
};
registry[CALM_COLLAPSED_THINKING_PATCH] = patch;
}
@@ -0,0 +1,77 @@
// Pi Calm - persisted on/off preference.
//
// Copyright (c) 2026 Kun Chen. MIT License - see the LICENSE file in this directory.
//
// The preference lives in a plain local state file named "calm" directly under
// Pi's agent directory (~/.pi/agent by default, PI_CODING_AGENT_DIR when set).
// That directory is Pi runtime territory: this repository never tracks the
// state file and Home Manager never manages it. The file contains exactly
// "on\n" or "off\n"; anything else, including a missing or unreadable file,
// means off.
import { randomUUID } from "node:crypto";
import {
mkdirSync,
readFileSync,
renameSync,
rmSync,
writeFileSync,
} from "node:fs";
import { homedir } from "node:os";
import { dirname, join } from "node:path";
import * as PiCodingAgent from "@earendil-works/pi-coding-agent";
export const CALM_PREFERENCE_FILE_NAME = "calm";
/**
* Resolve Pi's agent directory through Pi's exported getAgentDir(), which
* honors PI_CODING_AGENT_DIR and tilde expansion. If a future Pi stops
* exporting it, fall back to the documented environment variable and default
* path instead of failing.
*/
export function calmAgentDir(): string {
if (typeof PiCodingAgent.getAgentDir === "function") return PiCodingAgent.getAgentDir();
const envDir = process.env.PI_CODING_AGENT_DIR?.trim();
if (envDir) return envDir;
return join(homedir(), ".pi", "agent");
}
export function calmPreferencePath(): string {
return join(calmAgentDir(), CALM_PREFERENCE_FILE_NAME);
}
/** Load the persisted preference. Calm is off by default and on any read error. */
export function loadCalmPreference(): boolean {
try {
return readFileSync(calmPreferencePath(), "utf8").trim() === "on";
} catch {
return false;
}
}
/**
* Persist the preference atomically (unique temp file plus rename) so a
* crashed write never leaves a truncated state file. A failure throws a clear
* error naming the path so /calm can surface it instead of silently applying
* a toggle that would not survive a restart.
*/
export function persistCalmPreference(active: boolean): void {
const path = calmPreferencePath();
try {
mkdirSync(dirname(path), { recursive: true });
const temporaryPath = `${path}.${process.pid}.${randomUUID()}.tmp`;
try {
writeFileSync(temporaryPath, active ? "on\n" : "off\n", {
encoding: "utf8",
flag: "wx",
mode: 0o600,
});
renameSync(temporaryPath, path);
} finally {
rmSync(temporaryPath, { force: true });
}
} catch (error) {
const reason = error instanceof Error ? error.message : String(error);
throw new Error(`Pi Calm could not persist its preference to ${path}: ${reason}`);
}
}
@@ -0,0 +1,39 @@
// Pi Calm - shared presentation state for the standalone Calm extension.
//
// Adapted from the Firstmate project's Calm implementation.
// Copyright (c) 2026 Kun Chen. MIT License - see the LICENSE file in this directory.
//
// This module owns only the in-memory presentation flags. Presentation filtering
// must never delete or alter semantic, session, or export data, so the export
// path forces stock rendering for the duration of an /export or /share command.
let active = false;
let stockExportRendering = false;
/** True while Calm presentation filtering is enabled. */
export function calmPresentationIsActive(): boolean {
return active;
}
export function setCalmPresentation(next: boolean): void {
active = next;
}
/** True while an /export or /share render is in flight and stock output is required. */
export function calmStockExportRenderingIsActive(): boolean {
return stockExportRendering;
}
export function setCalmStockExportRendering(next: boolean): void {
stockExportRendering = next;
}
/**
* True while Calm should hide the supported transcript chrome: collapsed
* thinking labels and the known Pi built-in tool call/result shells. Genuine
* user prompts, assistant text, custom tools, and every other transcript row
* class are never filtered by this flag.
*/
export function calmHidesTranscriptChrome(): boolean {
return active && !stockExportRendering;
}
@@ -0,0 +1,247 @@
// Pi Calm - animated working presentation.
//
// Adapted from the Firstmate project's Calm implementation.
// Copyright (c) 2026 Kun Chen. MIT License - see the LICENSE file in this directory.
//
// Calm replaces Pi's stock working row with a tiny two-row ASCII boat while one
// logical agent run is active. This module owns only the sprite geometry, the
// bounce track, the two animation cadences, the session-scoped freeze/resume
// state, and the temporary TUI widget; ../index.ts owns when the presentation
// is installed and removed, and stays the sole caller of setWorkingVisible().
//
// Cadence: one scheduler drives two logically independent clocks. Every tick
// advances the water phase, and only every CALM_WORKING_SHIP_TICKS_PER_MOVE-th
// tick moves the boat, so the water visibly ripples several times between boat
// steps and the boat itself reads as calm. Both clocks stop together when the
// widget is disposed. Ticks, not wall-clock timestamps, drive every state
// change, so tests can seek time exactly.
//
// Continuity: one extension-owned animation instance survives hide/show within
// the same Pi process and Calm extension lifetime. Disposing the widget freezes
// column, direction, water phase, and tick cadence without advancing them for
// hidden wall time. The next working period resumes from that exact logical
// state. A fresh session or new extension lifetime calls reset() and starts at
// the normal initial position. State is never a module-level or process-global
// singleton.
//
// Verified against Pi 0.82.0, which exposes ExtensionUIContext.setWidget() with
// a component factory, per-widget dispose(), and TUI.requestRender(). Pi renders
// a widget through Component.render(width), so this module recomputes its track
// from that width on every frame instead of caching a terminal size that a
// resize would invalidate. A resize while the boat is hidden is applied on the
// first resumed frame through the same clamp path.
import type { Component, TUI } from "@earendil-works/pi-tui";
// The hull is symmetric and replaces waves on its row rather than adding a third row.
const HULL = "\\__/";
// A mainsail extends aft of the mast, so it trails behind the bow relative to travel.
const SAIL_RIGHT = "<|";
const SAIL_LEFT = "|>";
// Centers the two-cell sail over the four-cell hull.
const SAIL_OFFSET = 1;
const HULL_WIDTH = HULL.length;
const SAIL_WIDTH = SAIL_RIGHT.length;
// Bounded deterministic fixed-cell water phases. Every entry is exactly one column, so
// advancing the phase ripples the surface without changing visible width or row count.
const WAVE_CYCLE = ["~", "~", "-", "~"] as const;
// Standard ANSI foreground codes only: no theme lookup, bright variant, or 256/RGB.
const BLUE = "\u001b[34m";
const YELLOW = "\u001b[33m";
// Restores the default foreground so color never bleeds into padding or later frames.
const RESET = "\u001b[39m";
export const CALM_WORKING_SHIP_WIDGET_KEY = "calm-working-ship";
/** Scheduler period. One tick advances the water by one phase. */
export const CALM_WORKING_SHIP_TICK_MS = 220;
/** Boat moves one column every Nth tick, so it travels at 220 * 4 = 880ms per column. */
export const CALM_WORKING_SHIP_TICKS_PER_MOVE = 4;
export type CalmWorkingShipAnimation = {
/** Render one frame that exactly fits `width`, clamping the track to it first. */
render(width: number): string[];
/** Advance one scheduler tick: water every tick, boat on its slower cadence. */
tick(): void;
restoreLastRendered(): void;
/** Restore the normal initial column, direction, water phase, and cadence. */
reset(): void;
/**
* Clamp the frozen column and direction to `width` without advancing time.
* Used when a terminal resize lands while the working presentation is hidden.
*/
clampToWidth(width: number): void;
/** Current hull column, exposed for deterministic motion assertions. */
position(): number;
/** Current travel direction: 1 travelling right, -1 travelling left. */
direction(): number;
/** Current water phase, exposed for deterministic ripple assertions. */
waterPhase(): number;
};
/** Longest hull start column that still fits the sprite in `width` usable cells. */
function trackSpan(width: number): number {
if (width >= HULL_WIDTH) return width - HULL_WIDTH;
if (width >= SAIL_WIDTH) return width - SAIL_WIDTH;
return 0;
}
export function createCalmWorkingShipAnimation(): CalmWorkingShipAnimation {
let position = 0;
let direction = 1;
let span = 0;
let phase = 0;
let ticks = 0;
let renderedPosition = position;
let renderedDirection = direction;
let renderedSpan = span;
let renderedPhase = phase;
let renderedTicks = ticks;
// Reversing the moment the boat lands on an endpoint means the endpoint frame itself
// already shows the new heading, so no frame at or after a bounce shows the old sail.
const settleDirectionAtEdges = (): void => {
if (span <= 0) return;
if (position >= span) direction = -1;
else if (position <= 0) direction = 1;
};
const applyWidth = (width: number): void => {
if (width <= 0) {
span = 0;
position = 0;
return;
}
span = trackSpan(width);
position = Math.min(position, span);
settleDirectionAtEdges();
};
const commitRenderedState = (): void => {
renderedPosition = position;
renderedDirection = direction;
renderedSpan = span;
renderedPhase = phase;
renderedTicks = ticks;
};
const restoreLastRenderedState = (): void => {
position = renderedPosition;
direction = renderedDirection;
span = renderedSpan;
phase = renderedPhase;
ticks = renderedTicks;
};
/** One colored run of water covering absolute columns [from, from + count). */
const water = (from: number, count: number): string => {
if (count <= 0) return "";
let cells = "";
for (let column = from; column < from + count; column += 1) {
cells += WAVE_CYCLE[(column + phase) % WAVE_CYCLE.length];
}
return `${BLUE}${cells}${RESET}`;
};
const boat = (text: string): string => `${YELLOW}${text}${RESET}`;
return {
position: () => position,
direction: () => direction,
waterPhase: () => phase,
restoreLastRendered: restoreLastRenderedState,
reset(): void {
position = 0;
direction = 1;
span = 0;
phase = 0;
ticks = 0;
commitRenderedState();
},
clampToWidth(width: number): void {
applyWidth(width);
},
tick(): void {
ticks += 1;
phase = (phase + 1) % WAVE_CYCLE.length;
if (ticks % CALM_WORKING_SHIP_TICKS_PER_MOVE !== 0) return;
if (span <= 0) {
position = 0;
return;
}
position = Math.min(span, Math.max(0, position + direction));
settleDirectionAtEdges();
},
render(width: number): string[] {
if (width <= 0) return [];
// A resize lands here before the next frame, so recompute and clamp the track
// immediately rather than trusting a position measured against the old width.
applyWidth(width);
const sail = direction >= 0 ? SAIL_RIGHT : SAIL_LEFT;
let frame: string[];
if (width < SAIL_WIDTH) {
// Too narrow for even the sail: a deterministic single row of water.
frame = [water(0, width)];
} else if (width < HULL_WIDTH) {
// Too narrow for the hull: the sail alone rides the water row.
frame = [
water(0, position) +
boat(sail) +
water(position + SAIL_WIDTH, width - position - SAIL_WIDTH),
];
} else {
frame = [
" ".repeat(position + SAIL_OFFSET) + boat(sail),
water(0, position) +
boat(HULL) +
water(position + HULL_WIDTH, width - position - HULL_WIDTH),
];
}
commitRenderedState();
return frame;
},
};
}
/**
* Build the temporary Calm working widget bound to one caller-owned animation.
* Pi disposes the previous component before installing a replacement under the same
* key and when it clears extension widgets, so the single scheduler driving both
* cadences cannot outlive the widget or duplicate. Disposing freezes the shared
* animation in place; the next widget bound to the same animation resumes without
* applying hidden wall time.
*/
export function createCalmWorkingShipWidget(
tui: TUI,
animation: CalmWorkingShipAnimation = createCalmWorkingShipAnimation(),
): Component & { dispose(): void } {
let disposed = false;
const timer = setInterval(() => {
if (disposed) return;
animation.tick();
tui.requestRender();
}, CALM_WORKING_SHIP_TICK_MS);
// The animation must never keep Pi's process alive on its own.
timer.unref?.();
return {
render: (width) => (disposed ? [] : animation.render(width)),
// Every frame is rebuilt from fixed standard ANSI codes, so there is no cache.
invalidate: () => {},
dispose: () => {
if (disposed) return;
disposed = true;
clearInterval(timer);
animation.restoreLastRendered();
},
};
}
@@ -0,0 +1,137 @@
const DEFAULT_TITLE = "π";
const PREFIX = "π";
const MAX_TITLE_LENGTH = 40;
const SPINNER_INTERVAL_MS = 120;
const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
function truncateTitle(title) {
if (title.length <= MAX_TITLE_LENGTH) return title;
return title.slice(0, MAX_TITLE_LENGTH - 3) + "...";
}
function basename(path) {
if (!path) return DEFAULT_TITLE;
const trimmed = path.replace(/[\\/]+$/, "");
if (!trimmed) return DEFAULT_TITLE;
return trimmed.split(/[\\/]/).pop() || DEFAULT_TITLE;
}
function getSessionName(pi) {
const name = pi.getSessionName?.();
return typeof name === "string" ? name.trim() : "";
}
function getRawTitle(pi, ctx) {
return getSessionName(pi) || basename(ctx.cwd);
}
function isSpinningStatus(status) {
return status === "working";
}
function statusIndicator(status, spinnerFrame) {
if (isSpinningStatus(status)) {
if (SPINNER_FRAMES.length === 0) return "◉";
return SPINNER_FRAMES[spinnerFrame % SPINNER_FRAMES.length];
}
if (status === "done") return "✓";
if (status === "error") return "✗";
return "○";
}
function formatTitle(pi, ctx, status, spinnerFrame) {
const rawTitle = getRawTitle(pi, ctx);
const suffix = rawTitle === DEFAULT_TITLE ? DEFAULT_TITLE : `${PREFIX} | ${truncateTitle(rawTitle)}`;
return `${statusIndicator(status, spinnerFrame)} | ${suffix}`;
}
export default function terminalStatusTitle(pi) {
let status = "idle";
let spinnerFrame = 0;
let spinnerInterval;
let deferredWrite;
let lastCtx;
function clearDeferredWrite() {
if (!deferredWrite) return;
clearTimeout(deferredWrite);
deferredWrite = undefined;
}
function writeTitle(ctx = lastCtx) {
if (!ctx?.hasUI) return;
lastCtx = ctx;
ctx.ui.setTitle(formatTitle(pi, ctx, status, spinnerFrame));
}
function stopSpinner() {
if (!spinnerInterval) return;
clearInterval(spinnerInterval);
spinnerInterval = undefined;
spinnerFrame = 0;
}
function startSpinner(ctx) {
if (!ctx?.hasUI || spinnerInterval) return;
spinnerFrame = 0;
spinnerInterval = setInterval(() => {
if (!isSpinningStatus(status)) {
stopSpinner();
return;
}
spinnerFrame = (spinnerFrame + 1) % SPINNER_FRAMES.length;
writeTitle();
}, SPINNER_INTERVAL_MS);
spinnerInterval.unref?.();
}
function setStatus(nextStatus, ctx) {
clearDeferredWrite();
status = nextStatus;
lastCtx = ctx;
if (isSpinningStatus(status)) {
startSpinner(ctx);
} else {
stopSpinner();
}
writeTitle(ctx);
}
function scheduleWrite(ctx) {
clearDeferredWrite();
deferredWrite = setTimeout(() => {
deferredWrite = undefined;
writeTitle(ctx);
}, 0);
deferredWrite.unref?.();
}
pi.on("session_start", async (_event, ctx) => {
setStatus("idle", ctx);
scheduleWrite(ctx);
});
pi.on("agent_start", async (_event, ctx) => {
setStatus("working", ctx);
});
pi.on("agent_settled", async (_event, ctx) => {
setStatus("done", ctx);
});
pi.on("session_shutdown", async () => {
clearDeferredWrite();
stopSpinner();
});
}
+43
View File
@@ -0,0 +1,43 @@
{
"providers": {
"omlx": {
"baseUrl": "http://localhost:8000/v1",
"api": "openai-completions",
"apiKey": "none",
"compat": {
"maxTokensField": "max_tokens"
},
"models": [
{
"id": "Jundot/Qwen3.6-35B-A3B-oQ4-mtp",
"name": "Qwen 3.6 35B (o4-mtp)",
"reasoning": false,
"input": [
"text"
],
"contextWindow": 65536,
"maxTokens": 65536,
"cost": {
"input": 0,
"output": 0,
"cacheRead": 0,
"cacheWrite": 0
}
}
]
},
"openai-codex": {
"modelOverrides": {
"gpt-5.6-luna": {
"contextWindow": 272000
},
"gpt-5.6-sol": {
"contextWindow": 272000
},
"gpt-5.6-terra": {
"contextWindow": 272000
}
}
}
}
}
+37
View File
@@ -0,0 +1,37 @@
{
"lastChangelogVersion": "0.84.1",
"theme": "rose-pine-moon",
"defaultProvider": "omlx",
"defaultModel": "qwen3.6-35b",
"models": [
{
"name": "Qwen 3.6 35B (o4-mtp)",
"provider": "omlx",
"model": "Jundot/Qwen3.6-35B-A3B-oQ4-mtp"
}
],
"compaction": {
"enabled": true,
"reserveTokens": 16384,
"keepRecentTokens": 8000
},
"packages": [
"npm:pi-context",
"npm:pi-web-access@0.14.0",
"npm:@ryan_nookpi/pi-extension-codex-fast-mode@0.2.6",
"git:github.com/algal/pi-openai-server-compaction@c6d593087709e9481223dc6c6c2269b371b5e055",
"../../.automaton/plugins/pi-read-tweet"
],
"defaultThinkingLevel": "high",
"images": {
"blockImages": false
},
"terminal": {
"showImages": false
},
"hideThinkingBlock": true,
"quietStartup": true,
"steeringMode": "all",
"followUpMode": "all",
"collapseChangelog": true
}
+75
View File
@@ -0,0 +1,75 @@
{
"$schema": "https://raw.githubusercontent.com/earendil-works/pi/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json",
"name": "rose-pine-moon",
"vars": {
"base": "#232136",
"surface": "#2a273f",
"overlay": "#393552",
"muted": "#6e6a86",
"subtle": "#908caa",
"text": "#e0def4",
"love": "#eb6f92",
"gold": "#f6c177",
"rose": "#ea9a97",
"pine": "#3e8fb0",
"foam": "#9ccfd8",
"iris": "#c4a7e7",
"highlightLow": "#2a283e",
"highlightMed": "#44415a",
"highlightHigh": "#56526e"
},
"colors": {
"accent": "iris",
"border": "overlay",
"borderAccent": "iris",
"borderMuted": "muted",
"success": "foam",
"error": "love",
"warning": "gold",
"muted": "subtle",
"dim": "muted",
"text": "text",
"thinkingText": "subtle",
"selectedBg": "highlightMed",
"userMessageBg": "surface",
"userMessageText": "text",
"customMessageBg": "surface",
"customMessageText": "text",
"customMessageLabel": "iris",
"toolPendingBg": "highlightLow",
"toolSuccessBg": "surface",
"toolErrorBg": "surface",
"toolTitle": "foam",
"toolOutput": "text",
"mdHeading": "iris",
"mdLink": "foam",
"mdLinkUrl": "subtle",
"mdCode": "rose",
"mdCodeBlock": "text",
"mdCodeBlockBorder": "overlay",
"mdQuote": "subtle",
"mdQuoteBorder": "foam",
"mdHr": "overlay",
"mdListBullet": "iris",
"toolDiffAdded": "foam",
"toolDiffRemoved": "love",
"toolDiffContext": "subtle",
"syntaxComment": "muted",
"syntaxKeyword": "iris",
"syntaxFunction": "foam",
"syntaxVariable": "text",
"syntaxString": "gold",
"syntaxNumber": "rose",
"syntaxType": "pine",
"syntaxOperator": "iris",
"syntaxPunctuation": "subtle",
"thinkingOff": "muted",
"thinkingMinimal": "pine",
"thinkingLow": "foam",
"thinkingMedium": "iris",
"thinkingHigh": "rose",
"thinkingXhigh": "love",
"thinkingMax": "gold",
"bashMode": "gold"
}
}
+14
View File
@@ -0,0 +1,14 @@
# global agent instructions
- Never use the em dash "—". Use plain dash "-" instead
- When writing commit messages, NEVER auto-add your agent name as co-author
- Never manually modify CHANGELOG.md files or any files that are marked as auto-generated
- When making technical decisions, do not give much weight to development cost.
Instead, prefer quality, simplicity, robustness, scalability, and long term maintainability.
- When doing bug fixes, always start with reproducing the bug in an E2E setting as closely aligned with how an end user would experience it as possible.
This makes sure you find the real problem so your fix will actually solve it.
- When end-to-end testing a product, be picky about the UI you see and be obsessed with pixel perfection.
If something clearly looks off, even if it is not directly related to what you are doing, try to get it fixed along the way.
- Apply that same high standard to engineering excellence: lint, test failures, and test flakiness.
If you see one, even if it is not caused by what you are working on right now, still get it fixed.
- Before using "dynamic workflows", "ultra code" or any harness feature that immediately spawns a large swarm of subagents, always explain the tradeoffs and ask the user for explicit approval.
+71
View File
@@ -0,0 +1,71 @@
#!/usr/bin/env bash
# fm-gitea-pr.sh - Push a reviewed crewmate branch to Gitea and open a PR via tea.
#
# Used after the first mate (opencode) reviews a crewmate's local branch in
# local-only mode: this helper pushes the branch to the Gitea remote and opens
# a pull request for the record. The review decision already happened in
# opencode; this is the publish step.
#
# Usage:
# fm-gitea-pr.sh <branch> [--title "<title>"] [--body "<body>"] [--base <base>]
#
# Defaults: --title = branch name, --body = "(reviewed by first mate)", --base = main
#
# Prerequisites (one-time per machine, not automatable in nix - secret):
# tea login add --name unraid --host http://unraid.local:3003 --token <GITEA_TOKEN>
#
# The Gitea remote is read from the current git repo's `origin` URL.
set -euo pipefail
BRANCH=""
TITLE=""
BODY="(reviewed by first mate - tests green, Primary-Rule + ADR-0009 lint passed)"
BASE="main"
while [ $# -gt 0 ]; do
case "$1" in
--title) TITLE="$2"; shift 2 ;;
--body) BODY="$2"; shift 2 ;;
--base) BASE="$2"; shift 2 ;;
-*) echo "unknown flag: $1" >&2; exit 1 ;;
*) [ -z "$BRANCH" ] && BRANCH="$1" || { echo "unexpected extra arg: $1" >&2; exit 1; }; shift ;;
esac
done
[ -n "$BRANCH" ] || { echo "usage: fm-gitea-pr.sh <branch> [--title <t>] [--body <b>] [--base <b>]" >&2; exit 1; }
[ -z "$TITLE" ] && TITLE="$BRANCH"
command -v tea >/dev/null 2>&1 || { echo "error: tea not installed. brew install tea" >&2; exit 1; }
# Parse the Gitea owner/repo from the origin remote URL.
# Handles http://user:token@host:port/owner/repo(.git) and https://host/owner/repo.git
REMOTE=$(git remote get-url origin 2>/dev/null) || { echo "error: no git origin remote" >&2; exit 1; }
# Strip credentials and .git suffix, then take the path after the host.
PATH_PART=${REMOTE#*://}
PATH_PART=${PATH_PART#*@} # drop user:pass@
PATH_PART=${PATH_PART#*/} # drop host:port/ (first slash)
PATH_PART=${PATH_PART%.git}
OWNER_REPO=${PATH_PART}
case "$OWNER_REPO" in
*/*) : ;; # owner/repo form
*) echo "error: could not parse owner/repo from remote: $REMOTE" >&2; exit 1 ;;
esac
# Push the branch to origin (Gitea). --push-branch uses the configured remote.
git push -u origin "$BRANCH" 2>&1 || { echo "error: git push failed for branch $BRANCH" >&2; exit 1; }
# Open the PR via tea. tea resolves the login by host automatically.
PR_URL=$(tea pulls create \
--repo "$OWNER_REPO" \
--head "$BRANCH" \
--base "$BASE" \
--title "$TITLE" \
--description "$BODY" 2>&1) || {
echo "error: tea pulls create failed" >&2
echo "$PR_URL" >&2
exit 1
}
# tea prints the PR URL on success; echo it for the caller.
echo "$PR_URL"
Executable
+20
View File
@@ -0,0 +1,20 @@
#!/usr/bin/env bash
# Launch OpenCode 2 with an isolated config so it cannot rewrite
# ~/.config/opencode/opencode.json (OpenCode 1 / firstmate).
set -euo pipefail
bin="${OPENCODE2_BIN:-/opt/homebrew/bin/opencode2}"
cfg="${OPENCODE_CONFIG:-$HOME/.config/opencode2/opencode.json}"
if [ ! -x "$bin" ]; then
echo "oc2: opencode2 not installed. Rebuild, or run:" >&2
echo " /opt/homebrew/bin/npm install -g --allow-scripts=@opencode-ai/cli @opencode-ai/cli@beta" >&2
exit 1
fi
if [ ! -e "$cfg" ]; then
echo "oc2: missing config $cfg (managed by home.nix)" >&2
exit 1
fi
export OPENCODE_CONFIG="$cfg"
exec "$bin" "$@"
+124
View File
@@ -0,0 +1,124 @@
#!/bin/bash
# sync-music — manually sync ~/Music into the beets library on the Unraid NAS.
#
# sync-music # shell alias -> ~/.local/bin/sync-music
#
# How it works:
# 1. Ensures the NAS SMB share is mounted at /Volumes/data (uses your keychain
# credentials; falls back to a clear error if it can't mount).
# 2. Imports every audio file anywhere under ~/Music (recursively) into beets
# via `beet import --noautotag --move --singletons`, which moves each file
# into the library and removes it from ~/Music. Files already in the
# library are skipped (left in place). Safe to re-run any time.
#
# Why a manual script (not the old daemon)? The daemon kept dying because
# launchd/WezTerm spawn a minimal PATH that lacks /opt/homebrew/bin (where
# `beet` lives). Running this script yourself in your own shell avoids that.
#
# Configure your server below.
NAS_HOST="unraid.local"
NAS_SHARE="data"
NAS_USER="backup"
MOUNT_POINT="/Volumes/data"
LIBRARY_DIR="$MOUNT_POINT/media/music"
WATCH_DIR="$HOME/Music"
LOG_FILE="$HOME/Library/Logs/beet-watch.log"
AUDIO_EXT="flac|mp3|m4a|aac|wav|alac|ogg|wma|opus|aiff|aif"
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"; }
# --- locate beet — works whether it comes from nix profile or Homebrew ---
BEET="$(command -v beet 2>/dev/null || true)"
if [ ! -x "$BEET" ]; then
echo "ERROR: beet not found." >&2
exit 1
fi
# --- 1. ensure the NAS is mounted ---
if [ ! -d "$MOUNT_POINT" ]; then
log "NAS not mounted; mounting smb://$NAS_USER@$NAS_HOST/$NAS_SHARE ..."
open "smb://$NAS_USER@$NAS_HOST/$NAS_SHARE" 2>/dev/null || true
for _ in $(seq 1 30); do
[ -d "$MOUNT_POINT" ] && break
sleep 1
done
fi
if [ ! -d "$MOUNT_POINT" ]; then
echo "ERROR: $MOUNT_POINT is not mounted." >&2
echo " Mount it, then re-run:" >&2
echo " open smb://$NAS_USER@$NAS_HOST/$NAS_SHARE" >&2
exit 1
fi
# --- 2. import every (finished) audio file in ~/Music ---
# NOTE: We do NOT clear the beets DB. Files are imported as singletons
# (--singletons) so beets never matches them against existing albums in the
# library, which is what previously made it delete the prior tracks of an
# album via the 'R' (remove old) prompt. The DB lives at
# $LIBRARY_DIR/beets/beets_library.db (per ~/.config/beets/config.yaml).
log "sync-music: scanning $WATCH_DIR -> $LIBRARY_DIR"
# Recursively collect every audio file under $WATCH_DIR.
files=()
while IFS= read -r -d '' f; do
[[ "${f##*.}" =~ ^($AUDIO_EXT)$ ]] && files+=("$f")
done < <(find "$WATCH_DIR" -type f -print0 2>/dev/null)
total=${#files[@]}
log "Found $total audio file(s)."
ok=0; skipped=0; failed=0
for f in "${files[@]}"; do
# skip files still being written (size must be stable for 1s)
s1=$(stat -f%z "$f" 2>/dev/null || echo 0)
sleep 1
s2=$(stat -f%z "$f" 2>/dev/null || echo 0)
if [ "$s1" != "$s2" ]; then
log " still writing, skipping: $(basename "$f")"
skipped=$((skipped + 1))
continue
fi
bn=$(basename "$f")
# Pre-check: has beets already imported this file? Match by exact path, or
# by a title guess derived from the filename ("03 - Artist - Title").
existing="$(beet ls --path "$f" 2>/dev/null || true)"
stripped="$(echo "$bn" | sed 's/\.[A-Za-z0-9]*$//' | sed -E 's/^[0-9]+ - [^ -]+ - //' )"
if [ -n "$existing" ] || [ -n "$(beet ls "$stripped" 2>/dev/null || true)" ]; then
log " already in library (skipped): $bn"
ok=$((ok + 1))
continue
fi
log " importing: $bn"
# Import as a singleton so beets never offers to "remove old" and delete
# other tracks from the same album. Reads confirm from stdin; with no
# matches there is no prompt, and if one does appear 'S' (=skip new) is the
# safe default that never deletes anything.
out=$(printf 'S\n' | "$BEET" import --noautotag --move --singletons "$f" 2>&1)
rc=$?
if [ $rc -eq 0 ] && [ ! -f "$f" ]; then
log " moved into library: $bn"
ok=$((ok + 1))
elif [ $rc -eq 0 ]; then
# Import succeeded but source still exists — beets kept new file (didn't remove old)
if echo "$out" | grep -qi 'keep'; then
log " kept existing, source remains: $bn"
ok=$((ok + 1))
else
log " IMPORTED but source still present (rc=0): $bn :: $(printf '%s' "$out" | head -c 200)"
failed=$((failed + 1))
fi
else
log " FAILED (rc=$rc): $bn :: $(printf '%s' "$out" | head -c 200)"
failed=$((failed + 1))
fi
done
log "sync-music complete: $total considered, ok=$ok, still-writing-skipped=$skipped, failed=$failed"
[ "$failed" -gt 0 ] && log " some imports failed; fix and re-run."
exit 0
+15
View File
@@ -1 +1,16 @@
The reason you are still seeing command not found is that darwin-rebuild does not exist on your computer yet.When doing a brand-new installation of nix-darwin, the darwin-rebuild tool is not part of the standard Nix installer. You have to bootstrap it using the nix run command first, which will dynamically pull the tool from GitHub and execute the initial build.Run the following command from your terminal inside your ~/.dotfiles directory:zshsudo nix run nix-darwin#darwin-rebuild -- switch --flake ~/.dotfiles#lt-mbp
## firstmate + Gitea (one-time per machine, secrets not in nix)
The firstmate crew dispatch + Gitea PR helper are wired by `darwin-rebuild`, but two secrets must be set manually per machine (nix cannot manage tokens without exposing them):
1. tea login (Gitea CLI auth against unraid):
zsh
tea login add --name unraid --url http://unraid.local:3003 --token <GITEA_TOKEN>
2. The Gitea token is the one in investor-flow's origin remote URL. After `tea login`, optionally clean the embedded creds from the remote:
zsh
cd ~/Documents/investor-flow
git remote set-url origin http://unraid.local:3003/transnet/investor-flow
Verify: `fm` launches OpenCode 2 as the first mate (isolated `~/.config/opencode2`); `oc2 --version` prints an OpenCode 2 version; `pi --list-models mimo` shows `opencode-go/mimo-v2.5-pro`; `tea pulls list --repo transnet/investor-flow` works.
+70
View File
@@ -1,5 +1,75 @@
#!/usr/bin/env bash
# Rebuild nix-darwin + home-manager. Rotates stale home-manager *.backup files
# so a second switch never fails with "would be clobbered".
# Also upgrades grok-build and installs OpenCode 1.x (stable CLI for free-tier).
set -euo pipefail
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
ln -sfn "$DIR" ~/.dotfiles
# Upgrade grok-build to latest (Homebrew cask).
# Skip if Homebrew API is unreachable (DNS/network issues).
echo "==> Upgrading grok-build..."
if brew upgrade grok-build 2>&1; then
echo "==> grok-build upgrade complete"
else
echo "==> WARNING: grok-build upgrade failed (network issue). Skipping."
fi
# Install OpenCode 1.x (stable CLI, binary "opencode") if absent.
# Required for free-tier models; opencode2 beta returns HTTP 426 on those endpoints.
# Uses Homebrew's npm (not Nix's npm) to avoid EACCES on the store.
# Skip if npm is unavailable (e.g., network issues preventing fetch).
OPENCODE_BIN=/opt/homebrew/bin/opencode
if [ ! -x "$OPENCODE_BIN" ]; then
echo "==> Installing OpenCode 1.x (stable) via Homebrew npm..."
if /opt/homebrew/bin/npm install -g --allow-scripts=opencode-ai opencode-ai@1.18 2>&1; then
echo "==> OpenCode 1.x installed successfully"
else
echo "==> WARNING: OpenCode 1.x installation failed (network issue). Skipping."
fi
else
echo "==> OpenCode 1.x already installed: $(${OPENCODE_BIN} --version)"
fi
stamp="$(date +%Y%m%d-%H%M%S)"
# Paths home-manager manages via home.file (regular files become out-of-store symlinks).
managed=(
"$HOME/.grok/config.toml"
"$HOME/.grok/AGENTS.md"
"$HOME/.claude/settings.json"
"$HOME/.claude/CLAUDE.md"
"$HOME/.codex/AGENTS.md"
"$HOME/.config/opencode/AGENTS.md"
"$HOME/.config/opencode/opencode.json"
"$HOME/.config/opencode2/opencode.json"
"$HOME/.config/opencode2/AGENTS.md"
"$HOME/.local/bin/oc2"
"$HOME/.pi/agent/settings.json"
"$HOME/.pi/agent/models.json"
"$HOME/.pi/agent/AGENTS.md"
"$HOME/.pi/agent/extensions/terminal-status-title.js"
)
for path in "${managed[@]}"; do
bak="${path}.backup"
if [ -e "$bak" ] || [ -L "$bak" ]; then
mv "$bak" "${bak}.${stamp}"
echo "rotated stale backup: ${bak} -> ${bak}.${stamp}"
fi
done
# Directory targets use the same backup suffix on collision.
for path in \
"$HOME/.config/wezterm" \
"$HOME/.config/nvim" \
"$HOME/.config/herdr" \
"$HOME/.config/opencode/skills" \
"$HOME/.pi/agent/themes" \
"$HOME/.pi/agent/extensions/calm"
do
bak="${path}.backup"
if [ -e "$bak" ] || [ -L "$bak" ]; then
mv "$bak" "${bak}.${stamp}"
echo "rotated stale backup: ${bak} -> ${bak}.${stamp}"
fi
done
exec sudo darwin-rebuild switch --flake ~/.dotfiles#lt-mbp