Compare commits
12
Commits
ee7ed15ce7
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8169978311 | ||
|
|
f02cbefa74 | ||
|
|
e6fdd6884d | ||
|
|
a35c0bb744 | ||
|
|
ab3b67af92 | ||
|
|
8bcc11c108 | ||
|
|
4c6e85c97c | ||
|
|
b7a956ebc0 | ||
|
|
9522a36040 | ||
|
|
4a0bff0331 | ||
|
|
6eee2509a4 | ||
|
|
bcabdec9c7 |
+10
@@ -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
|
||||
@@ -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.
|
||||
@@ -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
@@ -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": {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
'';
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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" }]
|
||||
}
|
||||
@@ -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"
|
||||
@@ -0,0 +1,3 @@
|
||||
require('vim_config')
|
||||
require('plugin')
|
||||
require('keys')
|
||||
@@ -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" }
|
||||
}
|
||||
@@ -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' ]])
|
||||
|
||||
@@ -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/
|
||||
|
||||
@@ -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' },
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
return {
|
||||
{
|
||||
'folke/which-key.nvim',
|
||||
lazy = false,
|
||||
config = true, -- popup that shows what my leader keys do
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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.
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
Executable
+71
@@ -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
@@ -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" "$@"
|
||||
Executable
+124
@@ -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
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user