From accffbae81460ad8643ee76ec5b452dc12b0b88e Mon Sep 17 00:00:00 2001 From: Juan Barbat Date: Tue, 14 Jul 2026 21:02:26 -0300 Subject: [PATCH 01/29] feat(zsh): load optional Cortex shell integration --- .github/workflows/ci.yml | 3 +++ README.md | 1 + local/env.zsh.example | 1 + scripts/check-cortex-shell-integration.sh | 28 +++++++++++++++++++++++ zsh/zshrc | 6 +++++ 5 files changed, 39 insertions(+) create mode 100755 scripts/check-cortex-shell-integration.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3056c5d..d807bee 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -58,6 +58,9 @@ jobs: jq empty "$file" done < <(git ls-files '*.json') + - name: Check Cortex shell integration + run: ./scripts/check-cortex-shell-integration.sh + - name: Validate TOML configs shell: bash run: | diff --git a/README.md b/README.md index 5f3b887..e51416d 100644 --- a/README.md +++ b/README.md @@ -147,6 +147,7 @@ Editá `local/env.zsh` (gitignored) para configurar: - `CORTEX_HOME` — raíz canónica de cortex, por defecto `~/.cortex` - `CORTEX_ROOT` — repo principal cortex, por defecto `~/.cortex/cortex` - `CORTEX_DOTFILES_DIR` — repo dotfiles, por defecto `~/.cortex/cortex-dotfiles` +- `CORTEX_SHELL_INTEGRATION` — fragmento zsh opcional administrado por Cortex, por defecto `$CORTEX_HOME/shell/cortex.zsh` - `OPENCODE_DEFAULT_FLAGS` — flags por defecto para `oc` - `INNIT_DIR` y overrides `INNIT_*_DIR` — navegación rápida de subdirectorios - Aliases y paths personales diff --git a/local/env.zsh.example b/local/env.zsh.example index 8ff6406..5f0fd81 100644 --- a/local/env.zsh.example +++ b/local/env.zsh.example @@ -11,6 +11,7 @@ # export CORTEX_HOME="$HOME/.cortex" # export CORTEX_ROOT="$BARBATDEV_DIR/cortex/cortex" # export CORTEX_DOTFILES_DIR="$BARBATDEV_DIR/cortex/cortex-dotfiles" +# export CORTEX_SHELL_INTEGRATION="$CORTEX_HOME/shell/cortex.zsh" # Proyectos personales # export PERSONAL_PROJECTS_DIR="$BARBATDEV_DIR/products" diff --git a/scripts/check-cortex-shell-integration.sh b/scripts/check-cortex-shell-integration.sh new file mode 100755 index 0000000..175a7f3 --- /dev/null +++ b/scripts/check-cortex-shell-integration.sh @@ -0,0 +1,28 @@ +#!/bin/bash +set -euo pipefail + +repo_root="$(cd "$(dirname "$0")/.." && pwd)" +tmp_dir="$(mktemp -d)" +trap 'rm -rf "$tmp_dir"' EXIT + +integration="$tmp_dir/cortex.zsh" +printf '%s\n' '_CORTEX_INTEGRATION_PROBE=loaded' > "$integration" + +env CORTEX_SHELL_INTEGRATION="$integration" zsh -f -c ' + source "$1" >/dev/null + [[ "$_CORTEX_INTEGRATION_PROBE" == loaded ]] + (( ! ${+_CORTEX_SHELL_INTEGRATION_PATH} )) +' _ "$repo_root/zsh/zshrc" + +env CORTEX_SHELL_INTEGRATION="$tmp_dir/missing.zsh" zsh -f -c ' + source "$1" >/dev/null + (( ! ${+_CORTEX_INTEGRATION_PROBE} )) +' _ "$repo_root/zsh/zshrc" + +mkdir -p "$tmp_dir/cortex-home/shell" +printf '%s\n' '_CORTEX_DEFAULT_INTEGRATION_PROBE=loaded' > "$tmp_dir/cortex-home/shell/cortex.zsh" + +env -u CORTEX_SHELL_INTEGRATION CORTEX_HOME="$tmp_dir/cortex-home" zsh -f -c ' + source "$1" >/dev/null + [[ "$_CORTEX_DEFAULT_INTEGRATION_PROBE" == loaded ]] +' _ "$repo_root/zsh/zshrc" diff --git a/zsh/zshrc b/zsh/zshrc index 2afc4fa..be6ab0d 100644 --- a/zsh/zshrc +++ b/zsh/zshrc @@ -255,6 +255,12 @@ _LOCAL_ENV="$_DOTFILES_DIR/local/env.zsh" [[ -f "$_LOCAL_ENV" ]] && source "$_LOCAL_ENV" #endregion +#region Cortex Shell Integration +_CORTEX_SHELL_INTEGRATION_PATH="${CORTEX_SHELL_INTEGRATION:-$CORTEX_HOME/shell/cortex.zsh}" +[[ -r "$_CORTEX_SHELL_INTEGRATION_PATH" ]] && source "$_CORTEX_SHELL_INTEGRATION_PATH" +unset _CORTEX_SHELL_INTEGRATION_PATH +#endregion + #region Atuin # Historial de shell con búsqueda avanzada (reemplaza Ctrl+R) if command -v atuin &>/dev/null; then From 754473c23c1219f737375639c0a25a0ee9245600 Mon Sep 17 00:00:00 2001 From: Juan Barbat Date: Fri, 7 Aug 2026 09:11:06 -0300 Subject: [PATCH 02/29] feat(zsh): add mise and pnpm to PATH with guards --- zsh/zshrc | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/zsh/zshrc b/zsh/zshrc index be6ab0d..f30e8c8 100644 --- a/zsh/zshrc +++ b/zsh/zshrc @@ -364,3 +364,12 @@ export PATH="$BUN_INSTALL/bin:$PATH" # bun completions [ -s "$BUN_INSTALL/_bun" ] && source "$BUN_INSTALL/_bun" export PATH="/opt/homebrew/opt/bc/bin:$PATH" + +# >>> grok installer >>> +export PATH="$HOME/.grok/bin:$PATH" +fpath=(~/.grok/completions/zsh $fpath) +autoload -Uz compinit && compinit -C +# <<< grok installer <<< + +# Added by Cap +export PATH="/Users/jbarbat/.cap/bin:$PATH" From fe649c16bf297a1cc80df3f96e09f62177ef3d22 Mon Sep 17 00:00:00 2001 From: Juan Barbat Date: Fri, 21 Aug 2026 12:48:09 -0300 Subject: [PATCH 03/29] feat(nix): add inert Home Manager boundary --- .github/workflows/ci.yml | 3 + README.md | 36 ++++++++++ flake.nix | 30 ++++++++ nix/home.nix | 16 +++++ scripts/nix-preflight.sh | 97 ++++++++++++++++++++++++++ scripts/test-nix-preflight.sh | 127 ++++++++++++++++++++++++++++++++++ 6 files changed, 309 insertions(+) create mode 100644 flake.nix create mode 100644 nix/home.nix create mode 100644 scripts/nix-preflight.sh create mode 100644 scripts/test-nix-preflight.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d807bee..a45b5ad 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -61,6 +61,9 @@ jobs: - name: Check Cortex shell integration run: ./scripts/check-cortex-shell-integration.sh + - name: Check pure Nix preflight contract + run: bash scripts/test-nix-preflight.sh + - name: Validate TOML configs shell: bash run: | diff --git a/README.md b/README.md index e51416d..9d4cb34 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,42 @@ El instalador macOS: 5. Intenta arrancar/recargar `sketchybar`, `yabai` y `skhd` sin cortar la instalación si macOS requiere permisos 6. Crea `local/env.zsh` desde el template +## Límite Nix + Home Manager (W1) + +W1 agrega solamente una base de Flake y Home Manager al repositorio. No instala Nix, no genera `flake.lock`, no activa Home Manager y no cambia ningún archivo del host. + +### Verificación segura ahora + +```bash +bash scripts/nix-preflight.sh +bash scripts/test-nix-preflight.sh +bash -n scripts/nix-preflight.sh scripts/test-nix-preflight.sh +``` + +El preflight es de solo lectura y sin red: usa `--offline` y `--no-write-lock-file`. En una máquina sin Nix termina con estado no cero de forma esperada, pero no cambia el host. Su contrato de readiness exige macOS `arm64` o `x86_64`, un usuario y `HOME` válidos, Nix disponible y los inputs bloqueados ya disponibles localmente. Una configuración Fish existente es una advertencia, no un bloqueo: W1 no la administra. + +### Ownership y próximo bootstrap + +| Área | Owner en W1 | +| --- | --- | +| `/opt/homebrew/bin/fish` | Homebrew actual | +| Symlinks, servicios y fuentes actuales | `install.sh` | +| Zsh y Ghostty | Configuración actual, sin cambios | +| `~/.config/fish/conf.d/99-local.fish` | Host; reservado para secretos/estado privado futuro, no creado ni gestionado por Nix | +| `flake.nix` y `nix/home.nix` | Base inactiva de Home Manager | + +El bootstrap queda explícitamente diferido a un work unit revisado. Después de instalar Nix por fuera de este repositorio, ese trabajo podrá generar el lock con: + +```bash +nix --extra-experimental-features 'nix-command flakes' flake lock +``` + +Ese comando resuelve inputs y modifica `flake.lock`; por eso no es parte de W1. También quedan diferidos cualquier `home-manager switch`, `nix run ... switch`, instalación de paquetes, cambio de shell, `chsh`, servicios o cambios a Fish. + +La configuración pura de W1 es `homeConfigurations.jbarbat`: declara `username = "jbarbat"`, `homeDirectory = "/Users/jbarbat"` y `system = "aarch64-darwin"` de forma explícita y revisable. Una futura activación debe seleccionar ese target sin derivar valores de la máquina en tiempo de evaluación. + +Para volver atrás de W1 basta quitar `flake.nix`, `nix/`, `scripts/nix-preflight.sh`, `scripts/test-nix-preflight.sh` y esta sección. No hay estado de host que revertir porque W1 no activó nada. + ## Estructura ``` diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..bfe437a --- /dev/null +++ b/flake.nix @@ -0,0 +1,30 @@ +{ + description = "Inert Home Manager boundary for cortex-dotfiles"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-24.11"; + home-manager = { + url = "github:nix-community/home-manager/release-24.11"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + }; + + outputs = { nixpkgs, home-manager, ... }: + let + mkHomeConfiguration = { username, homeDirectory, system }: + home-manager.lib.homeManagerConfiguration { + pkgs = nixpkgs.legacyPackages.${system}; + extraSpecialArgs = { + inherit username homeDirectory system; + }; + modules = [ ./nix/home.nix ]; + }; + in + { + homeConfigurations.jbarbat = mkHomeConfiguration { + username = "jbarbat"; + homeDirectory = "/Users/jbarbat"; + system = "aarch64-darwin"; + }; + }; +} diff --git a/nix/home.nix b/nix/home.nix new file mode 100644 index 0000000..38d0912 --- /dev/null +++ b/nix/home.nix @@ -0,0 +1,16 @@ +{ username, homeDirectory, system, ... }: + +{ + assertions = [ + { + assertion = username != "" && homeDirectory != ""; + message = "homeConfigurations.jbarbat must provide explicit non-empty username and homeDirectory values."; + } + ]; + + home = { + username = username; + homeDirectory = homeDirectory; + stateVersion = "24.11"; + }; +} diff --git a/scripts/nix-preflight.sh b/scripts/nix-preflight.sh new file mode 100644 index 0000000..5adc9b4 --- /dev/null +++ b/scripts/nix-preflight.sh @@ -0,0 +1,97 @@ +#!/bin/bash +set -euo pipefail + +repo_root="$(cd "$(dirname "$0")/.." && pwd)" +failures=0 + +pass() { + printf 'PASS %s\n' "$1" +} + +warn() { + printf 'WARN %s\n' "$1" +} + +fail() { + printf 'FAIL %s\n' "$1" + failures=$((failures + 1)) +} + +username="$(id -un)" +home_directory="$HOME" +host_os="$(uname -s)" +host_architecture="$(uname -m)" + +if [[ -n "$username" ]]; then + pass "Current username: $username" +else + fail 'Current username is unavailable.' +fi + +if [[ "$home_directory" == /* && -d "$home_directory" ]]; then + pass "Current home directory: $home_directory" +else + fail "HOME must be an existing absolute directory; received: $home_directory" +fi + +if [[ "$host_os" == "Darwin" ]]; then + pass 'Host platform: Darwin.' +else + fail "W1 currently targets macOS; detected platform: $host_os" +fi + +case "$host_architecture" in +arm64 | x86_64) + pass "Supported macOS architecture: $host_architecture" + ;; +*) + fail "Unsupported macOS architecture for W1: $host_architecture" + ;; +esac + +if grep -Fq 'home-manager' "$repo_root/flake.nix"; then + pass 'Home Manager is declared by this repository flake; no standalone home-manager command is required.' +else + fail 'Home Manager is not declared by flake.nix.' +fi + +if ! command -v nix >/dev/null 2>&1; then + fail 'Nix is unavailable; install Nix outside this repository before the future bootstrap.' +else + pass 'Nix is available.' + + if nix --extra-experimental-features 'nix-command flakes' flake metadata --no-write-lock-file --offline "$repo_root" >/dev/null 2>&1; then + pass 'Flake features and the local lock/input metadata are ready for read-only evaluation.' + + if nix --extra-experimental-features 'nix-command flakes' eval --no-write-lock-file --offline --raw "$repo_root#homeConfigurations.jbarbat.activationPackage.drvPath" >/dev/null 2>&1; then + pass 'Pure Home Manager configuration jbarbat is available through the flake input.' + else + fail 'Pure Home Manager configuration jbarbat cannot be evaluated locally; generate flake.lock during the future bootstrap, then retry.' + fi + else + fail 'Flake metadata is not available offline; generate flake.lock during the future bootstrap, then retry.' + fi +fi + +fish_config="$home_directory/.config/fish" +local_fish_config="$fish_config/conf.d/99-local.fish" + +if [[ -e "$fish_config" || -L "$fish_config" ]]; then + warn "Future Fish config path exists and will remain unmanaged: $fish_config" +else + pass "Future Fish config path is absent: $fish_config" +fi + +if [[ -e "$local_fish_config" || -L "$local_fish_config" ]]; then + warn "Future host-owned path exists and will remain unmanaged: $local_fish_config" +else + pass "Reserved host-owned future path is absent: $local_fish_config" +fi + +if [[ "$failures" -eq 0 ]]; then + pass 'W1 readiness complete: this check made no host changes.' + exit 0 +fi + +printf 'FAIL W1 readiness is incomplete: this check made no host changes.\n' +exit 1 diff --git a/scripts/test-nix-preflight.sh b/scripts/test-nix-preflight.sh new file mode 100644 index 0000000..ca5f5b1 --- /dev/null +++ b/scripts/test-nix-preflight.sh @@ -0,0 +1,127 @@ +#!/bin/bash +set -euo pipefail + +repo_root="$(cd "$(dirname "$0")/.." && pwd)" +preflight="$repo_root/scripts/nix-preflight.sh" +tmp_dir="$(mktemp -d)" +trap 'rm -rf "$tmp_dir"' EXIT + +fail() { + printf 'FAIL %s\n' "$1" >&2 + exit 1 +} + +assert_contains() { + local expected="$1" + local file="$2" + + grep -Fqx "$expected" "$file" || fail "expected output: $expected" +} + +assert_not_contains() { + local forbidden="$1" + local file="$2" + + if grep -Fq -- "$forbidden" "$file"; then + fail "forbidden W1 construct found in $file: $forbidden" + fi +} + +assert_pure_w1_configuration() { + assert_contains ' mkHomeConfiguration = { username, homeDirectory, system }:' "$repo_root/flake.nix" + assert_contains ' homeConfigurations.jbarbat = mkHomeConfiguration {' "$repo_root/flake.nix" + assert_contains ' username = "jbarbat";' "$repo_root/flake.nix" + assert_contains ' homeDirectory = "/Users/jbarbat";' "$repo_root/flake.nix" + assert_contains ' system = "aarch64-darwin";' "$repo_root/flake.nix" + assert_contains '{ username, homeDirectory, system, ... }:' "$repo_root/nix/home.nix" + assert_contains ' - name: Check pure Nix preflight contract' "$repo_root/.github/workflows/ci.yml" + grep -Fq 'La configuración pura de W1 es `homeConfigurations.jbarbat`:' "$repo_root/README.md" || + fail 'README does not document the pure W1 configuration' + + for forbidden in 'home.packages' 'programs.fish' 'services.' 'home.activation'; do + assert_not_contains "$forbidden" "$repo_root/nix/home.nix" + done + + assert_not_contains 'builtins.currentSystem' "$repo_root/flake.nix" + assert_not_contains 'builtins.getEnv' "$repo_root/flake.nix" + + for file in "$repo_root/scripts/nix-preflight.sh" "$repo_root/.github/workflows/ci.yml" "$repo_root/README.md"; do + assert_not_contains '--impure' "$file" + done +} + +make_fake_commands() { + local bin_dir="$1" + + mkdir -p "$bin_dir" + + cat >"$bin_dir/id" <<'EOF' +#!/bin/sh +if [ "$1" = "-un" ]; then + printf '%s\n' 'test-user' +fi +EOF + + cat >"$bin_dir/uname" <<'EOF' +#!/bin/sh +case "$1" in + -s) printf '%s\n' 'Darwin' ;; + -m) printf '%s\n' 'arm64' ;; +esac +EOF + + chmod +x "$bin_dir/id" "$bin_dir/uname" +} + +run_preflight() { + local home_dir="$1" + local bin_dir="$2" + local output="$3" + + HOME="$home_dir" PATH="$bin_dir:/usr/bin:/bin" bash "$preflight" >"$output" 2>&1 +} + +assert_pure_w1_configuration + +missing_nix_home="$tmp_dir/missing-nix-home" +missing_nix_bin="$tmp_dir/missing-nix-bin" +missing_nix_output="$tmp_dir/missing-nix.out" +mkdir -p "$missing_nix_home" "$missing_nix_bin" +make_fake_commands "$missing_nix_bin" + +if run_preflight "$missing_nix_home" "$missing_nix_bin" "$missing_nix_output"; then + fail 'preflight unexpectedly passed without nix' +fi +assert_contains 'FAIL Nix is unavailable; install Nix outside this repository before the future bootstrap.' "$missing_nix_output" +assert_contains 'PASS Home Manager is declared by this repository flake; no standalone home-manager command is required.' "$missing_nix_output" + +ready_home="$tmp_dir/ready-home" +ready_bin="$tmp_dir/ready-bin" +ready_output="$tmp_dir/ready.out" +mkdir -p "$ready_home" "$ready_bin" +make_fake_commands "$ready_bin" +cat >"$ready_bin/nix" <<'EOF' +#!/bin/sh +printf '%s\n' "$*" >>"$NIX_FAKE_LOG" +printf '%s\n' '/nix/store/fake-home-manager-activation.drv' +EOF +chmod +x "$ready_bin/nix" + +NIX_FAKE_LOG="$tmp_dir/nix.log" run_preflight "$ready_home" "$ready_bin" "$ready_output" +assert_contains 'PASS Nix is available.' "$ready_output" +assert_contains 'PASS Flake features and the local lock/input metadata are ready for read-only evaluation.' "$ready_output" +assert_contains 'PASS Pure Home Manager configuration jbarbat is available through the flake input.' "$ready_output" +grep -F -- '--no-write-lock-file' "$tmp_dir/nix.log" >/dev/null || fail 'preflight did not prevent lock writes' +grep -F -- '--offline' "$tmp_dir/nix.log" >/dev/null || fail 'preflight did not force offline evaluation' +grep -F -- '#homeConfigurations.jbarbat.activationPackage.drvPath' "$tmp_dir/nix.log" >/dev/null || fail 'preflight did not evaluate the explicit pure configuration' +if grep -F -- '--impure' "$tmp_dir/nix.log" >/dev/null; then + fail 'preflight evaluated the configuration impurely' +fi + +mkdir -p "$ready_home/.config/fish/conf.d" +printf '%s\n' '# host-owned local configuration' >"$ready_home/.config/fish/conf.d/99-local.fish" +collision_output="$tmp_dir/collision.out" +NIX_FAKE_LOG="$tmp_dir/nix.log" run_preflight "$ready_home" "$ready_bin" "$collision_output" +assert_contains "WARN Future host-owned path exists and will remain unmanaged: $ready_home/.config/fish/conf.d/99-local.fish" "$collision_output" + +printf '%s\n' 'nix preflight contract ok' From c6cd7828a9c7e50322a5f59f2257a191d7ffe512 Mon Sep 17 00:00:00 2001 From: Juan Barbat Date: Fri, 21 Aug 2026 12:49:44 -0300 Subject: [PATCH 04/29] feat(fish): port core profile workflows --- .github/workflows/ci.yml | 10 ++- README.md | 50 +++++++++--- fish/conf.d/10-core.fish | 73 +++++++++++++++++ fish/conf.d/99-local.fish.example | 5 ++ fish/functions/_go_dev_dir.fish | 8 ++ fish/functions/_go_first_existing_dir.fish | 10 +++ fish/functions/barbat.fish | 4 + fish/functions/cowork.fish | 4 + fish/functions/dev.fish | 4 + fish/functions/dotfiles.fish | 8 ++ fish/functions/innit-apis.fish | 4 + fish/functions/innit-mobile.fish | 4 + fish/functions/innit-pcsoft.fish | 4 + fish/functions/innit-webs.fish | 4 + fish/functions/innit.fish | 4 + fish/functions/personal.fish | 4 + fish/functions/tools.fish | 4 + fish/functions/work.fish | 4 + fish/functions/worktrees.fish | 4 + fish/tests/w2-core.fish | 91 ++++++++++++++++++++++ nix/home.nix | 19 +++++ 21 files changed, 311 insertions(+), 11 deletions(-) create mode 100644 fish/conf.d/10-core.fish create mode 100644 fish/conf.d/99-local.fish.example create mode 100644 fish/functions/_go_dev_dir.fish create mode 100644 fish/functions/_go_first_existing_dir.fish create mode 100644 fish/functions/barbat.fish create mode 100644 fish/functions/cowork.fish create mode 100644 fish/functions/dev.fish create mode 100644 fish/functions/dotfiles.fish create mode 100644 fish/functions/innit-apis.fish create mode 100644 fish/functions/innit-mobile.fish create mode 100644 fish/functions/innit-pcsoft.fish create mode 100644 fish/functions/innit-webs.fish create mode 100644 fish/functions/innit.fish create mode 100644 fish/functions/personal.fish create mode 100644 fish/functions/tools.fish create mode 100644 fish/functions/work.fish create mode 100644 fish/functions/worktrees.fish create mode 100644 fish/tests/w2-core.fish diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a45b5ad..0518b7b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,7 +20,7 @@ jobs: - name: Install minimal tools run: | sudo apt-get update - sudo apt-get install -y --no-install-recommends zsh jq shellcheck + sudo apt-get install -y --no-install-recommends fish zsh jq shellcheck - name: Validate shell syntax shell: bash @@ -34,6 +34,10 @@ jobs: zsh -n "$file" done + while IFS= read -r -d '' file; do + fish --no-execute "$file" + done < <(find fish -type f -name '*.fish' -print0) + bash_files=( install.sh claude/statusline.sh @@ -50,6 +54,10 @@ jobs: shellcheck --severity=error --shell=bash "${bash_files[@]}" fi + - name: Run Fish W2 harness + shell: bash + run: fish fish/tests/w2-core.fish + - name: Validate JSON configs shell: bash run: | diff --git a/README.md b/README.md index 9d4cb34..8ddb922 100644 --- a/README.md +++ b/README.md @@ -4,12 +4,12 @@ Configuración local extraida de `cortex`: terminal, shell, prompt, helpers de A ## CI -GitHub Actions ejecuta un smoke check mínimo en pull requests y pushes a `main`: sintaxis Zsh/Bash, JSON con `jq`, TOML con `python3`/`tomllib`, y `./install.sh --check` cuando el instalador lo soporte. +GitHub Actions ejecuta un smoke check mínimo en pull requests y pushes a `main`: sintaxis Zsh/Fish/Bash, el harness aislado de Fish, JSON con `jq`, TOML con `python3`/`tomllib`, y `./install.sh --check` cuando el instalador lo soporte. ## Stack - **Terminal**: [Ghostty](https://ghostty.org/) y Alacritty -- **Shell**: Zsh nativo de macOS +- **Shell**: Zsh nativo de macOS + profile Fish core opt-in (W2) - **Prompt**: [Starship](https://starship.rs/) — tema Gruvbox Dark - **Multiplexor**: tmux + helpers de sesión - **Barra macOS**: [SketchyBar](https://github.com/FelixKratz/SketchyBar) con tema Gruvbox @@ -46,6 +46,7 @@ bash install.sh --dry-run ``` El instalador macOS: + 1. Instala dependencias via Homebrew (cmux, starship, tmux, lazygit, neovim, eza, sketchybar, yabai, skhd, Karabiner-Elements, FiraCode Nerd Font) 2. Hace backup de configs existentes con timestamp 3. Crea symlinks de los dotfiles y guardrails globales (`.npmrc`, `pnpm/rc`, `.bunfig.toml`, `uv.toml`) @@ -83,12 +84,39 @@ El bootstrap queda explícitamente diferido a un work unit revisado. Después de nix --extra-experimental-features 'nix-command flakes' flake lock ``` -Ese comando resuelve inputs y modifica `flake.lock`; por eso no es parte de W1. También quedan diferidos cualquier `home-manager switch`, `nix run ... switch`, instalación de paquetes, cambio de shell, `chsh`, servicios o cambios a Fish. +Ese comando resuelve inputs y modifica `flake.lock`; por eso no es parte de W1. También quedan diferidos cualquier `home-manager switch`, `nix run ... switch`, instalación de paquetes, cambio de shell, `chsh`, servicios o extensiones Fish fuera del core W2. La configuración pura de W1 es `homeConfigurations.jbarbat`: declara `username = "jbarbat"`, `homeDirectory = "/Users/jbarbat"` y `system = "aarch64-darwin"` de forma explícita y revisable. Una futura activación debe seleccionar ese target sin derivar valores de la máquina en tiempo de evaluación. Para volver atrás de W1 basta quitar `flake.nix`, `nix/`, `scripts/nix-preflight.sh`, `scripts/test-nix-preflight.sh` y esta sección. No hay estado de host que revertir porque W1 no activó nada. +## Fish core profile (W2) + +W2 agrega un profile Fish inerte y opt-in: no cambia el login shell, no activa Home Manager, no toca `starship.toml` ni crea `99-local.fish`. + +### Uso seguro + +1. Copiá `fish/conf.d/99-local.fish.example` a `~/.config/fish/conf.d/99-local.fish` si necesitás paths o editor locales. +2. Mantené ese archivo fuera de Git: carga después de los defaults rastreados. +3. Probá el profile en una sesión Fish; Zsh permanece sin cambios. + +| Área | Owner en W2 | +| --- | --- | +| `fish/conf.d/10-core.fish` y `fish/functions/*.fish` | Fuentes rastreadas de Fish | +| `~/.config/fish/conf.d/99-local.fish` | Host; override privado no gestionado | +| Home Manager | Mapea cada fuente Fish explícitamente; no gestiona el directorio completo, historial ni variables Fish | +| Starship | Inicialización solo interactiva y cuando el comando existe; su TOML sigue fuera de este profile | + +El core incluye defaults de entorno, selección de editor, aliases Git/navegación y `dev`, `barbat`, `innit` con sus destinos relacionados (`innit-apis`, `innit-mobile`, `innit-webs`, `innit-pcsoft`), además de la navegación simple con `cowork`, `personal`, `tools`, `worktrees` y `work`. La creación, gestión y protecciones de Git worktrees siguen diferidas para W3+, junto con Git/SSH identities, PCSoft, tmux, screenshots, Herdr y helpers de agentes. + +Para validar sin tocar configuración real: + +```bash +/opt/homebrew/bin/fish fish/tests/w2-core.fish +``` + +La evaluación Nix permanece diferida: esta unidad no genera `flake.lock` ni ejecuta activación. + ## Estructura ``` @@ -155,7 +183,7 @@ Seguimiento: [cortex-dotfiles #31](https://github.com/barbatdev/cortex-dotfiles/ ## Comandos principales | Comando | Descripción | -|---------|-------------| +| --------- | ------------- | | `gs`, `ga`, `gc`, `gp`, `gl` | Git shortcuts | | `dev`, `barbat`, `cowork`, `personal`, `tools`, `worktrees` | Navegación rápida en `~/dev` | | `work`, `work-apis`, `work-mobile`, `work-webs`, `work-pcsoft` | Navegación rápida de trabajo | @@ -178,6 +206,7 @@ Seguimiento: [cortex-dotfiles #31](https://github.com/barbatdev/cortex-dotfiles/ ## Personalización Editá `local/env.zsh` (gitignored) para configurar: + - `SCREENSHOTS_DIR` — directorio de screenshots - `WORKSPACE_DIR` — directorio raíz de tus proyectos - `CORTEX_HOME` — raíz canónica de cortex, por defecto `~/.cortex` @@ -206,7 +235,7 @@ La config macOS enlaza `sketchybar/` en `~/.config/sketchybar`. El diseño es so Layout activo: | Pantalla | Uso | Layout | -|------|-----|--------| +| ------ | ----- | -------- | | Mac Retina (`display=1`) | apps generales: Discord, WhatsApp, Mail, Postman, Zen Browser | app activa + network, volumen, calendario, hora, batería | | ViewSonic vertical (`display=2`) | auxiliar/random, Ghostty/Herdr y Claude de formato vertical | brand + panel/spaces + app activa; derecha: RAM + CPU + hora | | LG Ultrawide (`display=3`) | mixto: Ghostty/Herdr, Claude, ChatGPT, Obsidian | brand + panel/spaces + app activa; derecha: RAM + CPU + hora | @@ -217,7 +246,7 @@ El centro queda libre para evitar el notch y reducir ruido visual. Interacciones: | Item | Acción | -|------|--------| +| ------ | -------- | | Glyph RefactorIA | abre `~/dev` | | Spaces | enfocan el space si `yabai` está corriendo | | Volumen | mute/unmute | @@ -261,7 +290,7 @@ El instalador crea symlinks en ambas rutas de configuración: `~/.config/yabai/y Decisiones: | Tema | Decisión | -|------|----------| +| ------ | ---------- | | Leader | `Option + Command` | | Scripting addition | No se usa | | Raycast | Evitar shortcuts con `Option + Command` para reducir colisiones | @@ -298,7 +327,7 @@ Binarios a permitir: Atajos principales (`option + command`): | Atajo | Acción | -|-------|--------| +| ------- | -------- | | `Option + Command + Left/Down/Up/Right` | Focus izquierda/abajo/arriba/derecha | | `Option + Command + Shift + Left/Down/Up/Right` | Mover ventana en el layout | | `Option + Command + 1..9` | Ir al space | @@ -324,7 +353,7 @@ La config enlaza `karabiner/karabiner.json` en `~/.config/karabiner/karabiner.js Remap incluido: | Tecla | Acción | -|-------|--------| +| ------- | -------- | | `Caps Lock` tap | `Escape` | | `Caps Lock` hold | `Left Control` | | `Right Command` | `Delete backward` | @@ -345,7 +374,7 @@ Después de instalar o cambiar permisos, puede hacer falta abrir o reiniciar Kar El prompt usa una variante local de FiraCode Nerd Font Mono con el glyph de la barba de RefactorIA en el Private Use Area. | Dato | Valor | -|------|-------| +| ------ | ------- | | Family | `FiraCode Nerd Font Mono Beard` | | Archivo instalado | `~/Library/Fonts/FiraCodeNerdFontMonoBeard-Reg.ttf` | | Codepoint | `U+F0F00` | @@ -388,6 +417,7 @@ refactoria ``` Uso recomendado: + - screenshots o demos donde conviene una marca visual sin depender de imágenes - intros manuales antes de grabar o compartir una terminal - banners puntuales en scripts propios, siempre que no tapen output útil diff --git a/fish/conf.d/10-core.fish b/fish/conf.d/10-core.fish new file mode 100644 index 0000000..2ea1e16 --- /dev/null +++ b/fish/conf.d/10-core.fish @@ -0,0 +1,73 @@ +# Core Fish defaults. Host overrides load later from 99-local.fish. + +if not set -q WORKSPACE_DIR; or test -z "$WORKSPACE_DIR" + set -gx WORKSPACE_DIR "$HOME/dev" +end +if not set -q BARBATDEV_DIR; or test -z "$BARBATDEV_DIR" + set -gx BARBATDEV_DIR "$WORKSPACE_DIR/barbatdev" +end +if not set -q CORTEX_HOME; or test -z "$CORTEX_HOME" + set -gx CORTEX_HOME "$HOME/.cortex" +end +if not set -q CORTEX_ROOT; or test -z "$CORTEX_ROOT" + set -gx CORTEX_ROOT "$BARBATDEV_DIR/cortex/cortex" +end +if not set -q CORTEX_DOTFILES_DIR; or test -z "$CORTEX_DOTFILES_DIR" + set -gx CORTEX_DOTFILES_DIR "$BARBATDEV_DIR/cortex/cortex-dotfiles" +end +if not set -q CORTEX_MULTIPLEXER; or test -z "$CORTEX_MULTIPLEXER" + set -gx CORTEX_MULTIPLEXER cmux +end +set -gx CLAUDE_CODE_EFFORT_LEVEL high +if not set -q WORK_PROJECTS_DIR; or test -z "$WORK_PROJECTS_DIR" + set -gx WORK_PROJECTS_DIR "$BARBATDEV_DIR/innit" +end +if not set -q PERSONAL_PROJECTS_DIR; or test -z "$PERSONAL_PROJECTS_DIR" + set -gx PERSONAL_PROJECTS_DIR "$BARBATDEV_DIR/products" +end +if not set -q INNIT_DIR; or test -z "$INNIT_DIR" + set -gx INNIT_DIR "$BARBATDEV_DIR/innit" +end +if not set -q SCREENSHOTS_DIR; or test -z "$SCREENSHOTS_DIR" + set -gx SCREENSHOTS_DIR "$HOME/Screenshots" +end + +if command -q nvim + set -gx EDITOR (command -s nvim) +else if command -q vim + set -gx EDITOR (command -s vim) +else + set -gx EDITOR nano +end +set -gx VISUAL "$EDITOR" + +alias g git +alias gs 'git status' +alias ga 'git add' +alias gc 'git commit -m' +alias gp 'git push' +alias gl 'git log --oneline --graph --decorate --all' +alias gd 'git diff' +alias gco 'git checkout' +alias gb 'git branch' +alias gpl 'git pull' +alias gf 'git fetch' +alias gst 'git stash' +alias .. 'cd ..' +alias ... 'cd ../..' +alias .... 'cd ../../..' +alias c clear +alias o open + +if command -q eza + alias ll 'eza -la --icons --git' + alias la 'eza -la --icons --git --all' + alias lt 'eza --tree --icons --level=2' +else + alias ll 'ls -lah' + alias la 'ls -lah -A' +end + +if status is-interactive; and command -q starship + starship init fish | source +end diff --git a/fish/conf.d/99-local.fish.example b/fish/conf.d/99-local.fish.example new file mode 100644 index 0000000..668ce7a --- /dev/null +++ b/fish/conf.d/99-local.fish.example @@ -0,0 +1,5 @@ +# Copy to ~/.config/fish/conf.d/99-local.fish and keep that file untracked. +# This loads after 10-core.fish and is for machine-specific overrides. +# set -gx WORKSPACE_DIR "$HOME/dev" +# set -gx INNIT_APIS_DIR "$HOME/dev/innit/apis" +# set -gx EDITOR nvim diff --git a/fish/functions/_go_dev_dir.fish b/fish/functions/_go_dev_dir.fish new file mode 100644 index 0000000..b12d0e3 --- /dev/null +++ b/fish/functions/_go_dev_dir.fish @@ -0,0 +1,8 @@ +function _go_dev_dir --argument-names target + if test -d "$target" + cd "$target" + return 0 + end + printf 'Directory not found: %s\n' "$target" >&2 + return 1 +end diff --git a/fish/functions/_go_first_existing_dir.fish b/fish/functions/_go_first_existing_dir.fish new file mode 100644 index 0000000..47f0e94 --- /dev/null +++ b/fish/functions/_go_first_existing_dir.fish @@ -0,0 +1,10 @@ +function _go_first_existing_dir + for target in $argv + if test -d "$target" + cd "$target" + return 0 + end + end + printf 'Directory not found: %s\n' "$argv[1]" >&2 + return 1 +end diff --git a/fish/functions/barbat.fish b/fish/functions/barbat.fish new file mode 100644 index 0000000..5ffde99 --- /dev/null +++ b/fish/functions/barbat.fish @@ -0,0 +1,4 @@ +# Usage: barbat — change to the Barbatdev workspace. +function barbat + _go_dev_dir "$BARBATDEV_DIR" +end diff --git a/fish/functions/cowork.fish b/fish/functions/cowork.fish new file mode 100644 index 0000000..42fda9d --- /dev/null +++ b/fish/functions/cowork.fish @@ -0,0 +1,4 @@ +# Usage: cowork — change to the work projects directory. +function cowork + _go_first_existing_dir "$WORK_PROJECTS_DIR" "$BARBATDEV_DIR/innit" +end diff --git a/fish/functions/dev.fish b/fish/functions/dev.fish new file mode 100644 index 0000000..fe30b96 --- /dev/null +++ b/fish/functions/dev.fish @@ -0,0 +1,4 @@ +# Usage: dev — change to the development workspace. +function dev + _go_dev_dir "$HOME/dev" +end diff --git a/fish/functions/dotfiles.fish b/fish/functions/dotfiles.fish new file mode 100644 index 0000000..69f0dae --- /dev/null +++ b/fish/functions/dotfiles.fish @@ -0,0 +1,8 @@ +# Usage: dotfiles — change to the Cortex dotfiles repository. +function dotfiles + if set -q _DOTFILES_DIR; and test -n "$_DOTFILES_DIR" + _go_dev_dir "$_DOTFILES_DIR" + else + _go_dev_dir "$CORTEX_DOTFILES_DIR" + end +end diff --git a/fish/functions/innit-apis.fish b/fish/functions/innit-apis.fish new file mode 100644 index 0000000..d173e2a --- /dev/null +++ b/fish/functions/innit-apis.fish @@ -0,0 +1,4 @@ +# Usage: innit-apis — change to the InnIT APIs directory or fallback. +function innit-apis + _go_first_existing_dir "$INNIT_APIS_DIR" "$INNIT_DIR" +end diff --git a/fish/functions/innit-mobile.fish b/fish/functions/innit-mobile.fish new file mode 100644 index 0000000..b7d4952 --- /dev/null +++ b/fish/functions/innit-mobile.fish @@ -0,0 +1,4 @@ +# Usage: innit-mobile — change to the InnIT mobile directory or fallback. +function innit-mobile + _go_first_existing_dir "$INNIT_MOBILE_DIR" "$INNIT_DIR" +end diff --git a/fish/functions/innit-pcsoft.fish b/fish/functions/innit-pcsoft.fish new file mode 100644 index 0000000..19bcf66 --- /dev/null +++ b/fish/functions/innit-pcsoft.fish @@ -0,0 +1,4 @@ +# Usage: innit-pcsoft — change to the InnIT PCSoft directory or fallback. +function innit-pcsoft + _go_first_existing_dir "$INNIT_PCSOFT_DIR" "$INNIT_DIR" +end diff --git a/fish/functions/innit-webs.fish b/fish/functions/innit-webs.fish new file mode 100644 index 0000000..177066d --- /dev/null +++ b/fish/functions/innit-webs.fish @@ -0,0 +1,4 @@ +# Usage: innit-webs — change to the InnIT web directory or fallback. +function innit-webs + _go_first_existing_dir "$INNIT_WEBS_DIR" "$INNIT_DIR" +end diff --git a/fish/functions/innit.fish b/fish/functions/innit.fish new file mode 100644 index 0000000..96815c9 --- /dev/null +++ b/fish/functions/innit.fish @@ -0,0 +1,4 @@ +# Usage: innit — change to the first available InnIT directory. +function innit + _go_first_existing_dir "$INNIT_DIR" "$BARBATDEV_DIR/innit" +end diff --git a/fish/functions/personal.fish b/fish/functions/personal.fish new file mode 100644 index 0000000..38212bc --- /dev/null +++ b/fish/functions/personal.fish @@ -0,0 +1,4 @@ +# Usage: personal — change to the personal projects directory. +function personal + _go_first_existing_dir "$PERSONAL_PROJECTS_DIR" "$BARBATDEV_DIR/products" +end diff --git a/fish/functions/tools.fish b/fish/functions/tools.fish new file mode 100644 index 0000000..8847863 --- /dev/null +++ b/fish/functions/tools.fish @@ -0,0 +1,4 @@ +# Usage: tools — change to the shared tools directory. +function tools + _go_dev_dir "$BARBATDEV_DIR/tools" +end diff --git a/fish/functions/work.fish b/fish/functions/work.fish new file mode 100644 index 0000000..3b11466 --- /dev/null +++ b/fish/functions/work.fish @@ -0,0 +1,4 @@ +# Usage: work — change to the first available work projects directory. +function work + _go_first_existing_dir "$WORK_PROJECTS_DIR" "$BARBATDEV_DIR/innit" +end diff --git a/fish/functions/worktrees.fish b/fish/functions/worktrees.fish new file mode 100644 index 0000000..c00a382 --- /dev/null +++ b/fish/functions/worktrees.fish @@ -0,0 +1,4 @@ +# Usage: worktrees — change to the development worktrees directory. +function worktrees + _go_dev_dir "$HOME/dev/worktrees" +end diff --git a/fish/tests/w2-core.fish b/fish/tests/w2-core.fish new file mode 100644 index 0000000..ce287d3 --- /dev/null +++ b/fish/tests/w2-core.fish @@ -0,0 +1,91 @@ +#!/opt/homebrew/bin/fish + +set -l repo_root (cd (dirname (dirname (status filename))); and pwd) +set -l core "$repo_root/conf.d/10-core.fish" +set -g functions_dir "$repo_root/functions" +set -g workspace (mktemp -d) +if test -z "$workspace" + printf 'FAIL: could not create temporary workspace\n' >&2 + exit 1 +end + +function _cleanup_workspace --on-event fish_exit + test -d "$workspace"; and rm -rf -- "$workspace" +end + +set -g home "$workspace/home" +set -g xdg "$home/.config" +set -g bin "$workspace/bin" +mkdir -p "$xdg/fish/conf.d" "$bin" +printf '#!/bin/sh\n' >"$bin/nvim" +chmod +x "$bin/nvim" + +function fail + printf 'FAIL: %s\n' "$argv" >&2 + exit 1 +end + +function assert_equal + test "$argv[1]" = "$argv[2]"; or fail "$argv[3] (expected '$argv[2]', got '$argv[1]')" +end + +function run_fish + env HOME="$home" XDG_CONFIG_HOME="$xdg" PATH="$bin:/usr/bin:/bin" /opt/homebrew/bin/fish $argv +end + +function assert_navigation_error + set -l command "$argv[1]" + set -l expected "$argv[2]" + set -l description "$argv[3]" + + run_fish -c "set -gx fish_function_path '$functions_dir' \$fish_function_path; $command" >/dev/null 2>"$workspace/error"; and fail "$description must fail" + grep -Fq "Directory not found: $expected" "$workspace/error"; or fail "$description error message" +end + +cp "$core" "$xdg/fish/conf.d/10-core.fish" +set -l defaults (run_fish -c 'string join "|" $WORKSPACE_DIR $BARBATDEV_DIR $CORTEX_HOME $CORTEX_ROOT $CORTEX_DOTFILES_DIR $CORTEX_MULTIPLEXER $CLAUDE_CODE_EFFORT_LEVEL $WORK_PROJECTS_DIR $PERSONAL_PROJECTS_DIR $INNIT_DIR $SCREENSHOTS_DIR $EDITOR $VISUAL') +assert_equal "$defaults" "$home/dev|$home/dev/barbatdev|$home/.cortex|$home/dev/barbatdev/cortex/cortex|$home/dev/barbatdev/cortex/cortex-dotfiles|cmux|high|$home/dev/barbatdev/innit|$home/dev/barbatdev/products|$home/dev/barbatdev/innit|$home/Screenshots|$bin/nvim|$bin/nvim" 'defaults and editor selection' + +set -l empty_defaults (env HOME="$home" XDG_CONFIG_HOME="$xdg" PATH="$bin:/usr/bin:/bin" WORKSPACE_DIR= BARBATDEV_DIR= CORTEX_HOME= CORTEX_ROOT= CORTEX_DOTFILES_DIR= CORTEX_MULTIPLEXER= CLAUDE_CODE_EFFORT_LEVEL= WORK_PROJECTS_DIR= PERSONAL_PROJECTS_DIR= INNIT_DIR= SCREENSHOTS_DIR= /opt/homebrew/bin/fish -c 'string join "|" $WORKSPACE_DIR $BARBATDEV_DIR $CORTEX_HOME $CORTEX_ROOT $CORTEX_DOTFILES_DIR $CORTEX_MULTIPLEXER $CLAUDE_CODE_EFFORT_LEVEL $WORK_PROJECTS_DIR $PERSONAL_PROJECTS_DIR $INNIT_DIR $SCREENSHOTS_DIR') +assert_equal "$empty_defaults" "$home/dev|$home/dev/barbatdev|$home/.cortex|$home/dev/barbatdev/cortex/cortex|$home/dev/barbatdev/cortex/cortex-dotfiles|cmux|high|$home/dev/barbatdev/innit|$home/dev/barbatdev/products|$home/dev/barbatdev/innit|$home/Screenshots" 'empty environment values use defaults' + +printf 'set -gx WORKSPACE_DIR "$HOME/private-dev"\nset -gx EDITOR private-editor\nset -gx VISUAL private-editor\n' >"$xdg/fish/conf.d/99-local.fish" +set -l overrides (run_fish -c 'string join "|" $WORKSPACE_DIR $EDITOR $VISUAL') +assert_equal "$overrides" "$home/private-dev|private-editor|private-editor" '99-local override precedence' + +mkdir -p "$home/dev" "$home/dev/barbatdev/innit" "$workspace/apis" "$workspace/mobile" "$workspace/webs" "$workspace/pcsoft" "$workspace/dotfiles" +set -l navigation (run_fish -c "set -gx fish_function_path '$functions_dir' \$fish_function_path; set -gx INNIT_APIS_DIR '$workspace/apis'; set -gx INNIT_MOBILE_DIR '$workspace/mobile'; set -gx INNIT_WEBS_DIR '$workspace/webs'; set -gx INNIT_PCSOFT_DIR '$workspace/pcsoft'; set -gx _DOTFILES_DIR '$workspace/dotfiles'; dev; pwd; barbat; pwd; innit; pwd; innit-apis; pwd; innit-mobile; pwd; innit-webs; pwd; innit-pcsoft; pwd; dotfiles; pwd" | string collect) +set -l expected_navigation (printf '%s\n' "$home/dev" "$home/dev/barbatdev" "$home/dev/barbatdev/innit" "$workspace/apis" "$workspace/mobile" "$workspace/webs" "$workspace/pcsoft" "$workspace/dotfiles" | string collect) +assert_equal "$navigation" "$expected_navigation" 'navigation destinations' + +set -l fallback (run_fish -c "set -gx fish_function_path '$functions_dir' \$fish_function_path; set -gx INNIT_APIS_DIR '$workspace/missing'; innit-apis; pwd") +assert_equal "$fallback" "$home/dev/barbatdev/innit" 'navigation fallback' +assert_navigation_error "set -gx BARBATDEV_DIR '$workspace/missing'; barbat" "$workspace/missing" 'barbat missing directory' + +mkdir -p "$workspace/cowork" "$workspace/personal" "$workspace/barbatdev/tools" "$workspace/barbatdev/innit" "$workspace/barbatdev/products" "$home/dev/worktrees" +set -l extended_navigation (run_fish -c "set -gx fish_function_path '$functions_dir' \$fish_function_path; set -gx WORK_PROJECTS_DIR '$workspace/cowork'; set -gx PERSONAL_PROJECTS_DIR '$workspace/personal'; set -gx BARBATDEV_DIR '$workspace/barbatdev'; cowork; pwd; personal; pwd; tools; pwd; worktrees; pwd; work; pwd" | string collect) +set -l expected_extended_navigation (printf '%s\n' "$workspace/cowork" "$workspace/personal" "$workspace/barbatdev/tools" "$home/dev/worktrees" "$workspace/cowork" | string collect) +assert_equal "$extended_navigation" "$expected_extended_navigation" 'extended navigation destinations' + +set -l extended_fallback (run_fish -c "set -gx fish_function_path '$functions_dir' \$fish_function_path; set -gx WORK_PROJECTS_DIR '$workspace/missing-work'; set -gx PERSONAL_PROJECTS_DIR '$workspace/missing-personal'; set -gx BARBATDEV_DIR '$workspace/barbatdev'; cowork; pwd; work; pwd; personal; pwd" | string collect) +set -l expected_extended_fallback (printf '%s\n' "$workspace/barbatdev/innit" "$workspace/barbatdev/innit" "$workspace/barbatdev/products" | string collect) +assert_equal "$extended_fallback" "$expected_extended_fallback" 'extended navigation fallbacks' + +assert_navigation_error "set -gx WORK_PROJECTS_DIR '$workspace/missing-work'; set -gx BARBATDEV_DIR '$workspace/missing-barbat'; cowork" "$workspace/missing-work" 'cowork missing directories' +assert_navigation_error "set -gx PERSONAL_PROJECTS_DIR '$workspace/missing-personal'; set -gx BARBATDEV_DIR '$workspace/missing-barbat'; personal" "$workspace/missing-personal" 'personal missing directories' +assert_navigation_error "set -gx BARBATDEV_DIR '$workspace/missing-barbat'; tools" "$workspace/missing-barbat/tools" 'tools missing directory' +assert_navigation_error "set -gx HOME '$workspace/missing-home'; worktrees" "$workspace/missing-home/dev/worktrees" 'worktrees missing directory' +assert_navigation_error "set -gx WORK_PROJECTS_DIR '$workspace/missing-work'; set -gx BARBATDEV_DIR '$workspace/missing-barbat'; work" "$workspace/missing-work" 'work missing directories' + +set -l aliases (run_fish -c 'functions gs; functions gpl' | string collect) +string match -q '*git status*' "$aliases"; or fail 'gs alias' +string match -q '*git pull*' "$aliases"; or fail 'gpl alias' + +printf '#!/bin/sh\n[ "$1" = init ] && printf "set -gx W2_STARSHIP_READY 1\\n"\n' >"$bin/starship" +chmod +x "$bin/starship" +set -l noninteractive (run_fish -c 'set -q W2_STARSHIP_READY; and echo ready; or echo skipped') +assert_equal "$noninteractive" skipped 'noninteractive Starship guard' +set -l interactive (run_fish -i -c 'set -q W2_STARSHIP_READY; and echo ready; or echo skipped' 2>/dev/null) +assert_equal "$interactive" ready 'interactive Starship initialization' + +printf 'PASS: Fish W2 core profile\n' diff --git a/nix/home.nix b/nix/home.nix index 38d0912..cfac77d 100644 --- a/nix/home.nix +++ b/nix/home.nix @@ -13,4 +13,23 @@ homeDirectory = homeDirectory; stateVersion = "24.11"; }; + + xdg.configFile = { + "fish/conf.d/10-core.fish".source = ../fish/conf.d/10-core.fish; + "fish/functions/_go_dev_dir.fish".source = ../fish/functions/_go_dev_dir.fish; + "fish/functions/_go_first_existing_dir.fish".source = ../fish/functions/_go_first_existing_dir.fish; + "fish/functions/dev.fish".source = ../fish/functions/dev.fish; + "fish/functions/barbat.fish".source = ../fish/functions/barbat.fish; + "fish/functions/cowork.fish".source = ../fish/functions/cowork.fish; + "fish/functions/personal.fish".source = ../fish/functions/personal.fish; + "fish/functions/tools.fish".source = ../fish/functions/tools.fish; + "fish/functions/worktrees.fish".source = ../fish/functions/worktrees.fish; + "fish/functions/work.fish".source = ../fish/functions/work.fish; + "fish/functions/innit.fish".source = ../fish/functions/innit.fish; + "fish/functions/innit-apis.fish".source = ../fish/functions/innit-apis.fish; + "fish/functions/innit-mobile.fish".source = ../fish/functions/innit-mobile.fish; + "fish/functions/innit-webs.fish".source = ../fish/functions/innit-webs.fish; + "fish/functions/innit-pcsoft.fish".source = ../fish/functions/innit-pcsoft.fish; + "fish/functions/dotfiles.fish".source = ../fish/functions/dotfiles.fish; + }; } From e72fe91fe7bc28fe90b9954a6c8758fc05f74539 Mon Sep 17 00:00:00 2001 From: Juan Barbat Date: Fri, 21 Aug 2026 13:20:20 -0300 Subject: [PATCH 05/29] feat(fish): port identity and worktree safeguards --- .github/workflows/ci.yml | 6 +- README.md | 24 +++++ fish/conf.d/99-local.fish.example | 5 + fish/functions/_git_config_identity.fish | 23 +++++ fish/functions/_parse_github_repo.fish | 5 + fish/functions/_wt_git_root.fish | 3 + fish/functions/_wt_is_pcsoft_repo.fish | 4 + fish/functions/_wt_path_for.fish | 3 + fish/functions/clone-personaldev.fish | 8 ++ fish/functions/clone-workdev.fish | 8 ++ fish/functions/edit.fish | 19 ++++ fish/functions/git-personaldev.fish | 3 + fish/functions/git-whoami.fish | 6 ++ fish/functions/git-workdev.fish | 3 + fish/functions/is-pcsoft-editable.fish | 4 + fish/functions/is-pcsoft-forbidden.fish | 4 + fish/functions/wtadd.fish | 37 +++++++ fish/functions/wtlist.fish | 9 ++ fish/functions/wtremove.fish | 18 ++++ fish/tests/w3-helpers.fish | 117 +++++++++++++++++++++++ nix/home.nix | 16 ++++ 21 files changed, 323 insertions(+), 2 deletions(-) create mode 100644 fish/functions/_git_config_identity.fish create mode 100644 fish/functions/_parse_github_repo.fish create mode 100644 fish/functions/_wt_git_root.fish create mode 100644 fish/functions/_wt_is_pcsoft_repo.fish create mode 100644 fish/functions/_wt_path_for.fish create mode 100644 fish/functions/clone-personaldev.fish create mode 100644 fish/functions/clone-workdev.fish create mode 100644 fish/functions/edit.fish create mode 100644 fish/functions/git-personaldev.fish create mode 100644 fish/functions/git-whoami.fish create mode 100644 fish/functions/git-workdev.fish create mode 100644 fish/functions/is-pcsoft-editable.fish create mode 100644 fish/functions/is-pcsoft-forbidden.fish create mode 100644 fish/functions/wtadd.fish create mode 100644 fish/functions/wtlist.fish create mode 100644 fish/functions/wtremove.fish create mode 100644 fish/tests/w3-helpers.fish diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0518b7b..c56a5dd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -54,9 +54,11 @@ jobs: shellcheck --severity=error --shell=bash "${bash_files[@]}" fi - - name: Run Fish W2 harness + - name: Run Fish W2 and W3 harnesses shell: bash - run: fish fish/tests/w2-core.fish + run: | + fish fish/tests/w2-core.fish + fish fish/tests/w3-helpers.fish - name: Validate JSON configs shell: bash diff --git a/README.md b/README.md index 8ddb922..69c4f73 100644 --- a/README.md +++ b/README.md @@ -117,6 +117,30 @@ Para validar sin tocar configuración real: La evaluación Nix permanece diferida: esta unidad no genera `flake.lock` ni ejecuta activación. +## Helpers Fish: Git, PCSoft y worktrees (W3) + +W3 suma helpers Fish opt-in para identidades Git, protección de archivos PCSoft y worktrees; no crea `~/.ssh/config`, no clona durante la configuración y no modifica la identidad global de Git. + +### Uso seguro + +1. Copiá `fish/conf.d/99-local.fish.example` a tu `99-local.fish` privado y definí las cuatro variables `GIT_*_NAME` y `GIT_*_EMAIL`. +2. En un repositorio, usá `git-workdev` o `git-personaldev`; si falta un valor privado, el helper falla antes de cambiar la configuración local o el remote. +3. Usá `clone-workdev` o `clone-personaldev` solo cuando quieras clonar: enrutan la URL mediante `github-workdev` o `github-personaldev`. + +| Área | Interfaz rastreada | Estado privado del host | +| --- | --- | --- | +| Identidades Git | `git-workdev`, `git-personaldev`, `git-whoami`, `clone-*` y aliases SSH `github-workdev` / `github-personaldev` | nombre, email, claves y `~/.ssh/config` | +| PCSoft | `is-pcsoft-forbidden`, `is-pcsoft-editable`, `edit` | IDE Windows y estado del proyecto | +| Worktrees | `wtadd`, `wtlist`, `wtremove`; `wtadd` bloquea repos PCSoft antes de mutar | directorios de worktree y procesos locales | + +`edit` rechaza extensiones PCSoft prohibidas y pide confirmación para las editables. `wtremove` elimina solamente el worktree nombrado: verificá `wtlist` antes de usarlo. Home Manager mapea cada función explícitamente; no administra el directorio completo, claves, remotes, historial ni variables privadas. + +Para validar sin tocar identidades reales ni la red: + +```bash +/opt/homebrew/bin/fish fish/tests/w3-helpers.fish +``` + ## Estructura ``` diff --git a/fish/conf.d/99-local.fish.example b/fish/conf.d/99-local.fish.example index 668ce7a..1e46392 100644 --- a/fish/conf.d/99-local.fish.example +++ b/fish/conf.d/99-local.fish.example @@ -3,3 +3,8 @@ # set -gx WORKSPACE_DIR "$HOME/dev" # set -gx INNIT_APIS_DIR "$HOME/dev/innit/apis" # set -gx EDITOR nvim +# Private Git identities: both values are required before git-workdev or git-personaldev can change a repository. +# set -gx GIT_WORKDEV_NAME "Your work name" +# set -gx GIT_WORKDEV_EMAIL "you@company.invalid" +# set -gx GIT_PERSONALDEV_NAME "Your personal name" +# set -gx GIT_PERSONALDEV_EMAIL "you@personal.invalid" diff --git a/fish/functions/_git_config_identity.fish b/fish/functions/_git_config_identity.fish new file mode 100644 index 0000000..63109e4 --- /dev/null +++ b/fish/functions/_git_config_identity.fish @@ -0,0 +1,23 @@ +function _git_config_identity --argument-names name email alias label route_marker + if test -z "$name"; or test -z "$email" + printf 'Missing private %s identity: set name and email in 99-local.fish.\n' "$label" >&2 + return 1 + end + + git rev-parse --is-inside-work-tree >/dev/null 2>&1; or begin + printf 'Not inside a Git repository.\n' >&2 + return 1 + end + + git config --local user.name "$name"; and git config --local user.email "$email"; or return 1 + set -l url (git remote get-url origin 2>/dev/null) + if string match -q '*github.com*' -- "$url"; or string match -q "*$route_marker*" -- "$url" + set -l new_url (string replace -r '^git@github[^:]*:' "git@$alias:" -- "$url") + if test "$new_url" != "$url" + git remote set-url origin "$new_url"; or return 1 + printf 'Remote updated: %s\n' "$new_url" + end + end + + printf 'Identity: %s (%s)\n' "$label" "$email" +end diff --git a/fish/functions/_parse_github_repo.fish b/fish/functions/_parse_github_repo.fish new file mode 100644 index 0000000..4e54519 --- /dev/null +++ b/fish/functions/_parse_github_repo.fish @@ -0,0 +1,5 @@ +function _parse_github_repo --argument-names input + set input (string replace -r '\.git$' '' -- "$input") + set input (string replace -r '^https://github\.com/' '' -- "$input") + string replace -r '^git@github[^:]*:' '' -- "$input" +end diff --git a/fish/functions/_wt_git_root.fish b/fish/functions/_wt_git_root.fish new file mode 100644 index 0000000..bd001ac --- /dev/null +++ b/fish/functions/_wt_git_root.fish @@ -0,0 +1,3 @@ +function _wt_git_root + git rev-parse --show-toplevel 2>/dev/null +end diff --git a/fish/functions/_wt_is_pcsoft_repo.fish b/fish/functions/_wt_is_pcsoft_repo.fish new file mode 100644 index 0000000..427f491 --- /dev/null +++ b/fish/functions/_wt_is_pcsoft_repo.fish @@ -0,0 +1,4 @@ +function _wt_is_pcsoft_repo + set -l root (_wt_git_root); or return 1 + git -C "$root" ls-files 2>/dev/null | string match -rqi '\.(wdp|wwp|wpp|wpj|wwh|wpw|prw|wdd|fic|mmo|ndx|wdr|wdq|wdi|wdt|wdv|wdk|rep|sty|cpl|bkp|wdg|wdc|wde|wdw)$' +end diff --git a/fish/functions/_wt_path_for.fish b/fish/functions/_wt_path_for.fish new file mode 100644 index 0000000..d0ca87e --- /dev/null +++ b/fish/functions/_wt_path_for.fish @@ -0,0 +1,3 @@ +function _wt_path_for --argument-names git_root name + printf '%s/dev/worktrees/%s/%s\n' "$HOME" (path basename "$git_root") "$name" +end diff --git a/fish/functions/clone-personaldev.fish b/fish/functions/clone-personaldev.fish new file mode 100644 index 0000000..9a9fe9f --- /dev/null +++ b/fish/functions/clone-personaldev.fish @@ -0,0 +1,8 @@ +function clone-personaldev + if test (count $argv) -lt 1 + printf 'Usage: clone-personaldev \n' >&2 + return 1 + end + set -l repo (_parse_github_repo "$argv[1]") + git clone "git@github-personaldev:$repo.git" +end diff --git a/fish/functions/clone-workdev.fish b/fish/functions/clone-workdev.fish new file mode 100644 index 0000000..818255e --- /dev/null +++ b/fish/functions/clone-workdev.fish @@ -0,0 +1,8 @@ +function clone-workdev + if test (count $argv) -lt 1 + printf 'Usage: clone-workdev \n' >&2 + return 1 + end + set -l repo (_parse_github_repo "$argv[1]") + git clone "git@github-workdev:$repo.git" +end diff --git a/fish/functions/edit.fish b/fish/functions/edit.fish new file mode 100644 index 0000000..c3a8ac2 --- /dev/null +++ b/fish/functions/edit.fish @@ -0,0 +1,19 @@ +function edit + if test (count $argv) -lt 1 + printf 'Usage: edit \n' >&2 + return 1 + end + + set -l target "$argv[1]" + if is-pcsoft-forbidden "$target" + printf 'PCSoft forbidden file: %s\nUse the WinDev/WebDev IDE.\n' "$target" >&2 + return 1 + end + if is-pcsoft-editable "$target" + printf "PCSoft editable file: %s\nOnly edit 'code : |1+' blocks with the IDE closed.\nContinue? [s/N] " "$target" + read -l confirm + string match -qr '^[sS]$' -- "$confirm"; or return 0 + end + + "$EDITOR" "$target" +end diff --git a/fish/functions/git-personaldev.fish b/fish/functions/git-personaldev.fish new file mode 100644 index 0000000..324580b --- /dev/null +++ b/fish/functions/git-personaldev.fish @@ -0,0 +1,3 @@ +function git-personaldev + _git_config_identity "$GIT_PERSONALDEV_NAME" "$GIT_PERSONALDEV_EMAIL" github-personaldev personaldev github-personal +end diff --git a/fish/functions/git-whoami.fish b/fish/functions/git-whoami.fish new file mode 100644 index 0000000..3dd23c5 --- /dev/null +++ b/fish/functions/git-whoami.fish @@ -0,0 +1,6 @@ +function git-whoami + set -l name (git config user.name 2>/dev/null; or echo 'no configurado') + set -l email (git config user.email 2>/dev/null; or echo 'no configurado') + set -l remote (git remote get-url origin 2>/dev/null; or echo 'sin remote') + printf ' nombre : %s\n email : %s\n remote : %s\n' "$name" "$email" "$remote" +end diff --git a/fish/functions/git-workdev.fish b/fish/functions/git-workdev.fish new file mode 100644 index 0000000..2e71198 --- /dev/null +++ b/fish/functions/git-workdev.fish @@ -0,0 +1,3 @@ +function git-workdev + _git_config_identity "$GIT_WORKDEV_NAME" "$GIT_WORKDEV_EMAIL" github-workdev workdev github-work +end diff --git a/fish/functions/is-pcsoft-editable.fish b/fish/functions/is-pcsoft-editable.fish new file mode 100644 index 0000000..fe89a63 --- /dev/null +++ b/fish/functions/is-pcsoft-editable.fish @@ -0,0 +1,4 @@ +function is-pcsoft-editable + set -l extension (string lower -- (path extension "$argv[1]")) + contains -- "$extension" .wdg .wdc .wde .wdw +end diff --git a/fish/functions/is-pcsoft-forbidden.fish b/fish/functions/is-pcsoft-forbidden.fish new file mode 100644 index 0000000..788f743 --- /dev/null +++ b/fish/functions/is-pcsoft-forbidden.fish @@ -0,0 +1,4 @@ +function is-pcsoft-forbidden + set -l extension (string lower -- (path extension "$argv[1]")) + contains -- "$extension" .wdp .wwp .wpp .wpj .wwh .wpw .prw .wdd .fic .mmo .ndx .wdr .wdq .wdi .wdt .wdv .wdk .rep .sty .cpl .bkp .tk .cfg +end diff --git a/fish/functions/wtadd.fish b/fish/functions/wtadd.fish new file mode 100644 index 0000000..7816ffb --- /dev/null +++ b/fish/functions/wtadd.fish @@ -0,0 +1,37 @@ +function wtadd + set -l name "$argv[1]" + if test -z "$name" + printf 'Usage: wtadd [branch]\n' >&2 + return 1 + end + set -l branch "$name" + if test (count $argv) -ge 2 + set branch "$argv[2]" + end + + set -l root (_wt_git_root); or begin + printf 'Not inside a Git repository.\n' >&2 + return 1 + end + if _wt_is_pcsoft_repo + printf 'PCSoft repository detected — worktrees are not allowed.\n' >&2 + return 1 + end + + set -l worktree (_wt_path_for "$root" "$name") + if test -d "$worktree" + printf 'Worktree already exists: %s\n' "$worktree" >&2 + return 1 + end + mkdir -p (path dirname "$worktree"); or return 1 + if git -C "$root" show-ref --verify --quiet "refs/heads/$branch" + git worktree add "$worktree" "$branch" + else + git worktree add -b "$branch" "$worktree" + end; or return 1 + + printf 'Worktree created: %s\n' "$worktree" + if test "$CORTEX_MULTIPLEXER" = cmux + cc "$worktree" + end +end diff --git a/fish/functions/wtlist.fish b/fish/functions/wtlist.fish new file mode 100644 index 0000000..f13381f --- /dev/null +++ b/fish/functions/wtlist.fish @@ -0,0 +1,9 @@ +function wtlist + _wt_git_root >/dev/null; or begin + printf 'Not inside a Git repository.\n' >&2 + return 1 + end + printf '\n' + git worktree list + printf '\n' +end diff --git a/fish/functions/wtremove.fish b/fish/functions/wtremove.fish new file mode 100644 index 0000000..0b0a732 --- /dev/null +++ b/fish/functions/wtremove.fish @@ -0,0 +1,18 @@ +function wtremove + set -l name "$argv[1]" + if test -z "$name" + printf 'Usage: wtremove \n' >&2 + return 1 + end + set -l root (_wt_git_root); or begin + printf 'Not inside a Git repository.\n' >&2 + return 1 + end + set -l worktree (_wt_path_for "$root" "$name") + if not test -d "$worktree" + printf 'Worktree not found: %s\nUse wtlist to see active worktrees.\n' "$worktree" >&2 + return 1 + end + git worktree remove "$worktree"; and git worktree prune; or return 1 + printf 'Worktree removed: %s\n' "$worktree" +end diff --git a/fish/tests/w3-helpers.fish b/fish/tests/w3-helpers.fish new file mode 100644 index 0000000..d996694 --- /dev/null +++ b/fish/tests/w3-helpers.fish @@ -0,0 +1,117 @@ +#!/usr/bin/env fish + +set -l repo_root (cd (dirname (dirname (status filename))); and pwd) +set -g functions_dir "$repo_root/functions" +set -g workspace (mktemp -d) +test -n "$workspace"; or exit 1 + +function _cleanup_workspace --on-event fish_exit + test -d "$workspace"; and rm -rf -- "$workspace" +end + +set -g home "$workspace/home" +set -g bin "$workspace/bin" +set -g repo "$workspace/repo" +set -gx W3_GIT_LOG "$workspace/git.log" +mkdir -p "$home" "$bin" "$repo" + +function fail + printf 'FAIL: %s\n' "$argv" >&2 + exit 1 +end + +function assert_equal + test "$argv[1]" = "$argv[2]"; or fail "$argv[3] (expected '$argv[2]', got '$argv[1]')" +end + +function run_fish + env HOME="$home" PATH="$bin:/usr/bin:/bin" GIT_CONFIG_NOSYSTEM=1 GIT_CONFIG_GLOBAL="$workspace/global-gitconfig" /opt/homebrew/bin/fish $argv +end + +printf '#!/bin/sh\nprintf "%%s\\n" "$*" >> "$W3_GIT_LOG"\n[ "$1" = clone ] && exit 0\nexec /usr/bin/git "$@"\n' >"$bin/git" +printf '#!/bin/sh\nprintf editor >> "$W3_EDITOR_LOG"\n' >"$bin/editor" +printf '#!/bin/sh\nprintf ssh >> "$W3_SSH_LOG"\nexit 97\n' >"$bin/ssh" +chmod +x "$bin/git" "$bin/editor" "$bin/ssh" + +/usr/bin/git -C "$repo" init -q +/usr/bin/git -C "$repo" config user.name Baseline +/usr/bin/git -C "$repo" config user.email baseline@example.invalid +/usr/bin/git -C "$repo" remote add origin git@github.com:org/repo.git +printf 'baseline\n' >"$repo/README.md" +/usr/bin/git -C "$repo" add README.md +/usr/bin/git -C "$repo" commit -q -m baseline + +set -l configured (run_fish -c "set -gx fish_function_path '$functions_dir' \$fish_function_path; cd '$repo'; set -gx GIT_WORKDEV_NAME Work; set -gx GIT_WORKDEV_EMAIL work@example.invalid; git-workdev >/dev/null; string join '|' (git config --local user.name) (git config --local user.email) (git remote get-url origin)") +assert_equal "$configured" "Work|work@example.invalid|git@github-workdev:org/repo.git" 'work identity and alias routing' + +/usr/bin/git -C "$repo" remote set-url origin git@github-work-legacy:org/repo.git +set -l legacy_alias (run_fish -c "set -gx fish_function_path '$functions_dir' \$fish_function_path; cd '$repo'; set -gx GIT_WORKDEV_NAME Work; set -gx GIT_WORKDEV_EMAIL work@example.invalid; git-workdev >/dev/null; git remote get-url origin") +assert_equal "$legacy_alias" git@github-workdev:org/repo.git 'legacy work alias routing' + +/usr/bin/git -C "$repo" remote set-url origin git@github.com:org/repo.git +set -l personal (run_fish -c "set -gx fish_function_path '$functions_dir' \$fish_function_path; cd '$repo'; set -gx GIT_PERSONALDEV_NAME Personal; set -gx GIT_PERSONALDEV_EMAIL personal@example.invalid; git-personaldev >/dev/null; string join '|' (git config --local user.name) (git config --local user.email) (git remote get-url origin)") +assert_equal "$personal" "Personal|personal@example.invalid|git@github-personaldev:org/repo.git" 'personal identity and alias routing' + +/usr/bin/git -C "$repo" config --unset-all user.name +/usr/bin/git -C "$repo" config --unset-all user.email +/usr/bin/git -C "$repo" remote set-url origin git@github.com:org/repo.git +run_fish -c "set -gx fish_function_path '$functions_dir' \$fish_function_path; cd '$repo'; git-workdev" >/dev/null 2>&1; and fail 'missing private identity must fail' +set -l missing_name (/usr/bin/git -C "$repo" config --local --get user.name) +test -z "$missing_name"; or fail 'missing identity changed local name' +set -l unchanged_remote (/usr/bin/git -C "$repo" remote get-url origin) +assert_equal "$unchanged_remote" git@github.com:org/repo.git 'missing identity changed remote' + +set -l whoami (run_fish -c "set -gx fish_function_path '$functions_dir' \$fish_function_path; cd '$repo'; git-whoami") +string match -q '*nombre : no configurado*' "$whoami"; or fail 'git-whoami name fallback' +string match -q '*remote : git@github.com:org/repo.git*' "$whoami"; or fail 'git-whoami remote' + +run_fish -c "set -gx fish_function_path '$functions_dir' \$fish_function_path; clone-workdev https://github.com/org/work.git; clone-personaldev git@github.com:org/personal.git" +set -l git_log (cat "$workspace/git.log") +string match -q '*clone git@github-workdev:org/work.git*' "$git_log"; or fail "work clone alias: $git_log" +string match -q '*clone git@github-personaldev:org/personal.git*' "$git_log"; or fail 'personal clone alias' + +set -gx W3_EDITOR_LOG "$workspace/editor.log" +run_fish -c "set -gx fish_function_path '$functions_dir' \$fish_function_path; is-pcsoft-forbidden FILE.WDP"; or fail 'forbidden extension decision' +run_fish -c "set -gx fish_function_path '$functions_dir' \$fish_function_path; is-pcsoft-editable window.WDW"; or fail 'editable extension decision' +run_fish -c "set -gx fish_function_path '$functions_dir' \$fish_function_path; is-pcsoft-forbidden window.WDW"; and fail 'editable must not be forbidden' +run_fish -c "set -gx fish_function_path '$functions_dir' \$fish_function_path; set -gx EDITOR editor; edit '$workspace/file.wdp'" >/dev/null 2>&1; and fail 'forbidden edit must fail' +test ! -e "$workspace/editor.log"; or fail 'forbidden edit invoked editor' +printf 'n\n' | run_fish -c "set -gx fish_function_path '$functions_dir' \$fish_function_path; set -gx EDITOR editor; edit '$workspace/file.wdw'" >/dev/null +test ! -e "$workspace/editor.log"; or fail 'declined editable edit invoked editor' +printf 's\n' | run_fish -c "set -gx fish_function_path '$functions_dir' \$fish_function_path; set -gx EDITOR editor; edit '$workspace/file.wdw'" >/dev/null +cat "$workspace/editor.log" | grep -qx editor; or fail 'confirmed editable edit' + +printf '' >"$workspace/git.log" +set -l worktree_path "$home/dev/worktrees/repo/w3" +set -l wtadd_output (run_fish -c "set -gx fish_function_path '$functions_dir' \$fish_function_path; set -gx CORTEX_MULTIPLEXER none; cd '$repo'; wtadd w3") +string match -q "*Worktree created: $worktree_path*" "$wtadd_output"; or fail "worktree add output: $wtadd_output" +test -e "$worktree_path/.git"; or fail 'worktree add did not create a linked worktree' +set -l canonical_worktree_path (cd "$worktree_path"; and pwd -P) +/usr/bin/git -C "$repo" show-ref --verify --quiet refs/heads/w3; or fail 'worktree branch was not created' +set -l worktree_list (/usr/bin/git -C "$repo" worktree list --porcelain | string collect) +string match -q "*worktree $canonical_worktree_path*" "$worktree_list"; or fail 'worktree add missing from porcelain list' +string match -q '*branch refs/heads/w3*' "$worktree_list"; or fail 'worktree branch missing from porcelain list' + +set -l wtlist_output (run_fish -c "set -gx fish_function_path '$functions_dir' \$fish_function_path; cd '$repo'; wtlist" | string collect) +string match -q "*$canonical_worktree_path*" "$wtlist_output"; or fail 'wtlist missing linked worktree' +set -l worktree_log (string collect <"$workspace/git.log") +string match -q "*worktree add -b w3 $worktree_path*" "$worktree_log"; or fail 'git wrapper missing worktree add command' +string match -q '*worktree list*' "$worktree_log"; or fail 'git wrapper missing worktree list command' + +set -l wtremove_output (run_fish -c "set -gx fish_function_path '$functions_dir' \$fish_function_path; cd '$repo'; wtremove w3") +assert_equal "$wtremove_output" "Worktree removed: $worktree_path" 'worktree remove output' +test ! -e "$worktree_path"; or fail 'worktree remove left linked path' +/usr/bin/git -C "$repo" show-ref --verify --quiet refs/heads/w3; or fail 'worktree remove deleted branch' +set -l worktree_list_after (/usr/bin/git -C "$repo" worktree list --porcelain | string collect) +string match -q "*worktree $canonical_worktree_path*" "$worktree_list_after"; and fail 'worktree remove left porcelain entry' +set worktree_log (string collect <"$workspace/git.log") +string match -q "*worktree remove $worktree_path*" "$worktree_log"; or fail 'git wrapper missing worktree remove command' +string match -q '*worktree prune*' "$worktree_log"; or fail 'git wrapper missing worktree prune command' + +printf 'tracked\n' >"$repo/project.wdp" +/usr/bin/git -C "$repo" add project.wdp +run_fish -c "set -gx fish_function_path '$functions_dir' \$fish_function_path; cd '$repo'; wtadd blocked" >/dev/null 2>&1; and fail 'PCSoft worktree guard must fail' +test ! -e "$home/dev/worktrees/repo/blocked"; or fail 'PCSoft guard created worktree path' +test ! -e "$workspace/ssh.log"; or fail 'test invoked SSH' + +printf 'PASS: Fish W3 helpers\n' diff --git a/nix/home.nix b/nix/home.nix index cfac77d..0df499a 100644 --- a/nix/home.nix +++ b/nix/home.nix @@ -31,5 +31,21 @@ "fish/functions/innit-webs.fish".source = ../fish/functions/innit-webs.fish; "fish/functions/innit-pcsoft.fish".source = ../fish/functions/innit-pcsoft.fish; "fish/functions/dotfiles.fish".source = ../fish/functions/dotfiles.fish; + "fish/functions/_parse_github_repo.fish".source = ../fish/functions/_parse_github_repo.fish; + "fish/functions/_git_config_identity.fish".source = ../fish/functions/_git_config_identity.fish; + "fish/functions/clone-workdev.fish".source = ../fish/functions/clone-workdev.fish; + "fish/functions/clone-personaldev.fish".source = ../fish/functions/clone-personaldev.fish; + "fish/functions/git-workdev.fish".source = ../fish/functions/git-workdev.fish; + "fish/functions/git-personaldev.fish".source = ../fish/functions/git-personaldev.fish; + "fish/functions/git-whoami.fish".source = ../fish/functions/git-whoami.fish; + "fish/functions/is-pcsoft-forbidden.fish".source = ../fish/functions/is-pcsoft-forbidden.fish; + "fish/functions/is-pcsoft-editable.fish".source = ../fish/functions/is-pcsoft-editable.fish; + "fish/functions/edit.fish".source = ../fish/functions/edit.fish; + "fish/functions/_wt_git_root.fish".source = ../fish/functions/_wt_git_root.fish; + "fish/functions/_wt_is_pcsoft_repo.fish".source = ../fish/functions/_wt_is_pcsoft_repo.fish; + "fish/functions/_wt_path_for.fish".source = ../fish/functions/_wt_path_for.fish; + "fish/functions/wtadd.fish".source = ../fish/functions/wtadd.fish; + "fish/functions/wtlist.fish".source = ../fish/functions/wtlist.fish; + "fish/functions/wtremove.fish".source = ../fish/functions/wtremove.fish; }; } From c9c313211501e261ae8018817e4aba82ea453b8e Mon Sep 17 00:00:00 2001 From: Juan Barbat Date: Fri, 21 Aug 2026 13:20:57 -0300 Subject: [PATCH 06/29] feat(fish): restore hremote parity --- .github/workflows/ci.yml | 3 ++- fish/functions/hremote.fish | 20 ++++++++++++++ fish/tests/w5a-hremote.fish | 52 +++++++++++++++++++++++++++++++++++++ nix/home.nix | 1 + 4 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 fish/functions/hremote.fish create mode 100644 fish/tests/w5a-hremote.fish diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c56a5dd..bbce4ad 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -54,11 +54,12 @@ jobs: shellcheck --severity=error --shell=bash "${bash_files[@]}" fi - - name: Run Fish W2 and W3 harnesses + - name: Run Fish migration harnesses shell: bash run: | fish fish/tests/w2-core.fish fish fish/tests/w3-helpers.fish + fish fish/tests/w5a-hremote.fish - name: Validate JSON configs shell: bash diff --git a/fish/functions/hremote.fish b/fish/functions/hremote.fish new file mode 100644 index 0000000..60ee328 --- /dev/null +++ b/fish/functions/hremote.fish @@ -0,0 +1,20 @@ +# Attach to a remote Herdr session. Usage: hremote [session] +function hremote --description 'Attach to a remote Herdr session' + if test (count $argv) -lt 1 + printf 'Usage: hremote [session]\n' >&2 + return 2 + end + + if not command -q herdr + printf 'hremote: herdr is not available\n' >&2 + return 127 + end + + set -l target $argv[1] + set -l session main + if test (count $argv) -ge 2 + set session $argv[2] + end + + env CORTEX_MULTIPLEXER=herdr CORTEX_SSH_TARGET="$target" herdr --remote "$target" --session "$session" +end diff --git a/fish/tests/w5a-hremote.fish b/fish/tests/w5a-hremote.fish new file mode 100644 index 0000000..90395f6 --- /dev/null +++ b/fish/tests/w5a-hremote.fish @@ -0,0 +1,52 @@ +#!/opt/homebrew/bin/fish + +set -l repo_root (cd (dirname (dirname (status filename))); and pwd) +set -g function_file "$repo_root/functions/hremote.fish" +set -g workspace (mktemp -d) +if test -z "$workspace" + printf 'FAIL: could not create temporary workspace\n' >&2 + exit 1 +end + +function _cleanup_workspace --on-event fish_exit + test -d "$workspace"; and rm -rf -- "$workspace" +end + +function fail + printf 'FAIL: %s\n' "$argv" >&2 + exit 1 +end + +function run_hremote + set -l bin_dir "$argv[1]" + set -e argv[1] + env HOME="$workspace/home" PATH="$bin_dir:/usr/bin:/bin" /opt/homebrew/bin/fish --no-config \ + -c 'source $argv[1]; hremote $argv[2..-1]' "$function_file" $argv +end + +set -l empty_bin "$workspace/empty-bin" +set -l fake_bin "$workspace/fake-bin" +mkdir -p "$workspace/home" "$empty_bin" "$fake_bin" + +if run_hremote "$empty_bin" >"$workspace/missing-target.out" 2>&1 + fail 'missing target must fail' +end +set -l missing_target_output (string collect <"$workspace/missing-target.out") +string match -q '*Usage: hremote [session]*' "$missing_target_output"; or fail "missing target usage: $missing_target_output" + +if run_hremote "$empty_bin" agent-dev >"$workspace/missing-herdr.out" 2>&1 + fail 'missing herdr must fail' +end +set -l missing_herdr_output (string collect <"$workspace/missing-herdr.out") +string match -q '*hremote: herdr is not available*' "$missing_herdr_output"; or fail "missing herdr error: $missing_herdr_output" + +printf '%s\n' '#!/bin/sh' 'printf "%s|%s|%s\n" "$CORTEX_MULTIPLEXER" "$CORTEX_SSH_TARGET" "$*"' >"$fake_bin/herdr" +chmod +x "$fake_bin/herdr" + +set -l default_output (run_hremote "$fake_bin" agent-dev) +test "$default_output" = 'herdr|agent-dev|--remote agent-dev --session main'; or fail 'default remote session contract' + +set -l explicit_output (run_hremote "$fake_bin" agent-dev focus) +test "$explicit_output" = 'herdr|agent-dev|--remote agent-dev --session focus'; or fail 'explicit remote session contract' + +printf 'PASS: Fish hremote parity\n' diff --git a/nix/home.nix b/nix/home.nix index 0df499a..41abd8d 100644 --- a/nix/home.nix +++ b/nix/home.nix @@ -47,5 +47,6 @@ "fish/functions/wtadd.fish".source = ../fish/functions/wtadd.fish; "fish/functions/wtlist.fish".source = ../fish/functions/wtlist.fish; "fish/functions/wtremove.fish".source = ../fish/functions/wtremove.fish; + "fish/functions/hremote.fish".source = ../fish/functions/hremote.fish; }; } From b14742dc16fb52d0ed98238c25b2b218008e28b0 Mon Sep 17 00:00:00 2001 From: Juan Barbat Date: Fri, 21 Aug 2026 15:46:20 -0300 Subject: [PATCH 07/29] fix(nix): pin valid 24.11 release inputs --- flake.lock | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ flake.nix | 2 +- 2 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 flake.lock diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..52a2fda --- /dev/null +++ b/flake.lock @@ -0,0 +1,49 @@ +{ + "nodes": { + "home-manager": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1747688870, + "narHash": "sha256-ypL9WAZfmJr5V70jEVzqGjjQzF0uCkz+AFQF7n9NmNc=", + "owner": "nix-community", + "repo": "home-manager", + "rev": "d5f1f641b289553927b3801580598d200a501863", + "type": "github" + }, + "original": { + "owner": "nix-community", + "ref": "release-24.11", + "repo": "home-manager", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1751274312, + "narHash": "sha256-/bVBlRpECLVzjV19t5KMdMFWSwKLtb5RyXdjz3LJT+g=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "50ab793786d9de88ee30ec4e4c24fb4236fc2674", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-24.11", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "home-manager": "home-manager", + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix index bfe437a..9983641 100644 --- a/flake.nix +++ b/flake.nix @@ -2,7 +2,7 @@ description = "Inert Home Manager boundary for cortex-dotfiles"; inputs = { - nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-24.11"; + nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11"; home-manager = { url = "github:nix-community/home-manager/release-24.11"; inputs.nixpkgs.follows = "nixpkgs"; From 7aaa67fab58cdecd7454e05585c95678fa54c954 Mon Sep 17 00:00:00 2001 From: Juan Barbat Date: Fri, 21 Aug 2026 15:57:35 -0300 Subject: [PATCH 08/29] test(fish): isolate core profile harness --- fish/tests/w2-core.fish | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fish/tests/w2-core.fish b/fish/tests/w2-core.fish index ce287d3..134d213 100644 --- a/fish/tests/w2-core.fish +++ b/fish/tests/w2-core.fish @@ -30,7 +30,7 @@ function assert_equal end function run_fish - env HOME="$home" XDG_CONFIG_HOME="$xdg" PATH="$bin:/usr/bin:/bin" /opt/homebrew/bin/fish $argv + env -i HOME="$home" USER=test-user LOGNAME=test-user XDG_CONFIG_HOME="$xdg" PATH="$bin:/usr/bin:/bin" TERM=xterm-256color LANG=C /opt/homebrew/bin/fish $argv end function assert_navigation_error @@ -46,7 +46,7 @@ cp "$core" "$xdg/fish/conf.d/10-core.fish" set -l defaults (run_fish -c 'string join "|" $WORKSPACE_DIR $BARBATDEV_DIR $CORTEX_HOME $CORTEX_ROOT $CORTEX_DOTFILES_DIR $CORTEX_MULTIPLEXER $CLAUDE_CODE_EFFORT_LEVEL $WORK_PROJECTS_DIR $PERSONAL_PROJECTS_DIR $INNIT_DIR $SCREENSHOTS_DIR $EDITOR $VISUAL') assert_equal "$defaults" "$home/dev|$home/dev/barbatdev|$home/.cortex|$home/dev/barbatdev/cortex/cortex|$home/dev/barbatdev/cortex/cortex-dotfiles|cmux|high|$home/dev/barbatdev/innit|$home/dev/barbatdev/products|$home/dev/barbatdev/innit|$home/Screenshots|$bin/nvim|$bin/nvim" 'defaults and editor selection' -set -l empty_defaults (env HOME="$home" XDG_CONFIG_HOME="$xdg" PATH="$bin:/usr/bin:/bin" WORKSPACE_DIR= BARBATDEV_DIR= CORTEX_HOME= CORTEX_ROOT= CORTEX_DOTFILES_DIR= CORTEX_MULTIPLEXER= CLAUDE_CODE_EFFORT_LEVEL= WORK_PROJECTS_DIR= PERSONAL_PROJECTS_DIR= INNIT_DIR= SCREENSHOTS_DIR= /opt/homebrew/bin/fish -c 'string join "|" $WORKSPACE_DIR $BARBATDEV_DIR $CORTEX_HOME $CORTEX_ROOT $CORTEX_DOTFILES_DIR $CORTEX_MULTIPLEXER $CLAUDE_CODE_EFFORT_LEVEL $WORK_PROJECTS_DIR $PERSONAL_PROJECTS_DIR $INNIT_DIR $SCREENSHOTS_DIR') +set -l empty_defaults (env -i HOME="$home" USER=test-user LOGNAME=test-user XDG_CONFIG_HOME="$xdg" PATH="$bin:/usr/bin:/bin" TERM=xterm-256color LANG=C WORKSPACE_DIR= BARBATDEV_DIR= CORTEX_HOME= CORTEX_ROOT= CORTEX_DOTFILES_DIR= CORTEX_MULTIPLEXER= CLAUDE_CODE_EFFORT_LEVEL= WORK_PROJECTS_DIR= PERSONAL_PROJECTS_DIR= INNIT_DIR= SCREENSHOTS_DIR= /opt/homebrew/bin/fish -c 'string join "|" $WORKSPACE_DIR $BARBATDEV_DIR $CORTEX_HOME $CORTEX_ROOT $CORTEX_DOTFILES_DIR $CORTEX_MULTIPLEXER $CLAUDE_CODE_EFFORT_LEVEL $WORK_PROJECTS_DIR $PERSONAL_PROJECTS_DIR $INNIT_DIR $SCREENSHOTS_DIR') assert_equal "$empty_defaults" "$home/dev|$home/dev/barbatdev|$home/.cortex|$home/dev/barbatdev/cortex/cortex|$home/dev/barbatdev/cortex/cortex-dotfiles|cmux|high|$home/dev/barbatdev/innit|$home/dev/barbatdev/products|$home/dev/barbatdev/innit|$home/Screenshots" 'empty environment values use defaults' printf 'set -gx WORKSPACE_DIR "$HOME/private-dev"\nset -gx EDITOR private-editor\nset -gx VISUAL private-editor\n' >"$xdg/fish/conf.d/99-local.fish" From f9111a6a1d492e2d5f592eb9cef47c21f41d4449 Mon Sep 17 00:00:00 2001 From: Juan Barbat Date: Fri, 21 Aug 2026 15:59:58 -0300 Subject: [PATCH 09/29] fix(nix): expose flakes and Home Manager CLI --- nix/home.nix | 3 +++ 1 file changed, 3 insertions(+) diff --git a/nix/home.nix b/nix/home.nix index 41abd8d..c79daf5 100644 --- a/nix/home.nix +++ b/nix/home.nix @@ -14,7 +14,10 @@ stateVersion = "24.11"; }; + programs.home-manager.enable = true; + xdg.configFile = { + "nix/nix.conf".text = "experimental-features = nix-command flakes\n"; "fish/conf.d/10-core.fish".source = ../fish/conf.d/10-core.fish; "fish/functions/_go_dev_dir.fish".source = ../fish/functions/_go_dev_dir.fish; "fish/functions/_go_first_existing_dir.fish".source = ../fish/functions/_go_first_existing_dir.fish; From caccfd8fabfe4863844f9e4030fa548bb3af1359 Mon Sep 17 00:00:00 2001 From: Juan Barbat Date: Fri, 21 Aug 2026 16:03:49 -0300 Subject: [PATCH 10/29] fix(nix): make preflight scripts executable --- scripts/nix-preflight.sh | 0 scripts/test-nix-preflight.sh | 0 2 files changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 scripts/nix-preflight.sh mode change 100644 => 100755 scripts/test-nix-preflight.sh diff --git a/scripts/nix-preflight.sh b/scripts/nix-preflight.sh old mode 100644 new mode 100755 diff --git a/scripts/test-nix-preflight.sh b/scripts/test-nix-preflight.sh old mode 100644 new mode 100755 From cfe7f794dcc2f898ed3f058bd5374010d779528c Mon Sep 17 00:00:00 2001 From: Juan Barbat Date: Fri, 21 Aug 2026 16:45:27 -0300 Subject: [PATCH 11/29] feat(fish): port remaining Herdr and SSH helpers --- .github/workflows/ci.yml | 1 + README.md | 2 + fish/functions/h.fish | 8 +++ fish/functions/herdr-orient.fish | 20 +++++++ fish/functions/hfocus.fish | 4 ++ fish/functions/hhere.fish | 58 ++++++++++++++++++ fish/functions/hl.fish | 4 ++ fish/functions/hmain.fish | 4 ++ fish/functions/hname.fish | 30 ++++++++++ fish/functions/hnew.fish | 4 ++ fish/functions/hrole.fish | 64 ++++++++++++++++++++ fish/functions/hs.fish | 4 ++ fish/functions/hscratch.fish | 4 ++ fish/functions/hside.fish | 4 ++ fish/functions/sshc.fish | 18 ++++++ fish/functions/sshx-doctor.fish | 28 +++++++++ fish/functions/sshx.fish | 22 +++++++ fish/functions/whereami.fish | 4 ++ fish/tests/w5b-herdr-ssh.fish | 100 +++++++++++++++++++++++++++++++ nix/home.nix | 16 +++++ 20 files changed, 399 insertions(+) create mode 100644 fish/functions/h.fish create mode 100644 fish/functions/herdr-orient.fish create mode 100644 fish/functions/hfocus.fish create mode 100644 fish/functions/hhere.fish create mode 100644 fish/functions/hl.fish create mode 100644 fish/functions/hmain.fish create mode 100644 fish/functions/hname.fish create mode 100644 fish/functions/hnew.fish create mode 100644 fish/functions/hrole.fish create mode 100644 fish/functions/hs.fish create mode 100644 fish/functions/hscratch.fish create mode 100644 fish/functions/hside.fish create mode 100644 fish/functions/sshc.fish create mode 100644 fish/functions/sshx-doctor.fish create mode 100644 fish/functions/sshx.fish create mode 100644 fish/functions/whereami.fish create mode 100644 fish/tests/w5b-herdr-ssh.fish diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bbce4ad..893d351 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -60,6 +60,7 @@ jobs: fish fish/tests/w2-core.fish fish fish/tests/w3-helpers.fish fish fish/tests/w5a-hremote.fish + fish fish/tests/w5b-herdr-ssh.fish - name: Validate JSON configs shell: bash diff --git a/README.md b/README.md index 69c4f73..be05a68 100644 --- a/README.md +++ b/README.md @@ -245,6 +245,8 @@ Editá `local/env.zsh` (gitignored) para configurar: Referencia completa: [Herdr workflow](docs/herdr-workflow.md). Atajos prácticos: [keymaps](docs/keymaps.md). +El profile Fish también expone `h`, `hs`, `hl`, `hhere`, `hmain`, `hrole`, `hnew`, `hfocus`, `hside`, `hscratch`, `hname`, `whereami`, `sshc`, `sshx` y `sshx-doctor`; Home Manager mapea cada función de forma explícita. + Usá `hremote` desde tu terminal local en macOS. No hagas `ssh` primero y después intentes levantar `herdr` dentro de esa sesión remota. - Para pegar una imagen del clipboard local en la terminal remota, usá `Ctrl+V` por defecto (no `Cmd+V`). diff --git a/fish/functions/h.fish b/fish/functions/h.fish new file mode 100644 index 0000000..6c2caa0 --- /dev/null +++ b/fish/functions/h.fish @@ -0,0 +1,8 @@ +# Run Herdr directly. Usage: h [herdr args...] +function h --description 'Run Herdr directly' + if not command -q herdr + printf 'h: herdr is not available\n' >&2 + return 127 + end + command herdr $argv +end diff --git a/fish/functions/herdr-orient.fish b/fish/functions/herdr-orient.fish new file mode 100644 index 0000000..e66a445 --- /dev/null +++ b/fish/functions/herdr-orient.fish @@ -0,0 +1,20 @@ +# Print the current host, repository, SSH, and Herdr context. Usage: herdr-orient +function herdr-orient --description 'Print the current Herdr context' + printf 'host: %s\n' (hostname -s 2>/dev/null; or hostname) + printf 'cwd: %s\n' "$PWD" + if git rev-parse --is-inside-work-tree >/dev/null 2>&1 + printf 'repo: %s\n' (git rev-parse --show-toplevel) + set -l branch (git branch --show-current 2>/dev/null) + test -n "$branch"; or set branch detached + printf 'branch: %s\n' "$branch" + end + if test -n "$SSH_CONNECTION" + set -l ssh_target $CORTEX_SSH_TARGET + test -n "$ssh_target"; or set ssh_target remote + printf 'ssh: %s\n' "$ssh_target" + end + if command -q herdr; and command -q python3 + set -l pane_id (herdr pane current --current 2>/dev/null | python3 -c 'import json,sys; print(json.load(sys.stdin)["result"]["pane"]["pane_id"])' 2>/dev/null) + test -n "$pane_id"; and printf 'herdr: %s\n' "$pane_id" + end +end diff --git a/fish/functions/hfocus.fish b/fish/functions/hfocus.fish new file mode 100644 index 0000000..0b6f5f0 --- /dev/null +++ b/fish/functions/hfocus.fish @@ -0,0 +1,4 @@ +# Enter the focused Herdr role session. Usage: hfocus [path] +function hfocus --description 'Enter the focused Herdr role session' + hrole focus $argv +end diff --git a/fish/functions/hhere.fish b/fish/functions/hhere.fish new file mode 100644 index 0000000..233224b --- /dev/null +++ b/fish/functions/hhere.fish @@ -0,0 +1,58 @@ +# Enter the main Herdr session for a path. Usage: hhere [path] +function hhere --description 'Enter the main Herdr session for a path' + if not command -q herdr + printf 'hhere: herdr is not available\n' >&2 + return 127 + end + set -l target $PWD + test (count $argv) -ge 1; and set target $argv[1] + set -l resolved (cd -- "$target" >/dev/null 2>&1; and pwd -P) + if test -z "$resolved" + printf 'Directorio no encontrado: %s\n' "$target" >&2 + return 1 + end + set -l host $HOST + if test -z "$host" + set host (hostname -s 2>/dev/null; or hostname) + end + set host (string split -m 1 . -- "$host")[1] + set -l context local + test -n "$SSH_CONNECTION$SSH_CLIENT$SSH_TTY"; and set context ssh + set -l workspace_label (basename "$resolved" | string replace -a . -) + set -l git_root (git -C "$resolved" rev-parse --show-toplevel 2>/dev/null) + if test -n "$git_root" + set -l repo_name (basename "$git_root" | string replace -a . -) + set -l parent_name (dirname "$git_root" | xargs basename | string replace -a . -) + set -l branch (git -C "$git_root" branch --show-current 2>/dev/null) + set workspace_label "$parent_name"-"$repo_name" + test -n "$branch"; and set workspace_label "$workspace_label"-(string replace -ra '[^A-Za-z0-9_.-]' - "$branch") + end + set -l session "$context"-"$host"-"$workspace_label" + if test "$HERDR_ENV" = 1 + if not command -q python3 + printf 'hhere: python3 is not available\n' >&2 + return 127 + end + set -l workspace_id (herdr workspace list 2>/dev/null | python3 -c ' +import json +import sys +label = sys.argv[1] +try: + data = json.load(sys.stdin) +except Exception: + sys.exit(0) +for workspace in data.get("result", {}).get("workspaces", []): + if workspace.get("label") == label: + print(workspace.get("workspace_id", "")) + break +' "$workspace_label" 2>/dev/null) + if test -n "$workspace_id" + herdr workspace focus "$workspace_id" >/dev/null; or return + else + herdr workspace create --cwd "$resolved" --label "$workspace_label" --focus >/dev/null; or return + end + printf 'workspace: %s\n' "$workspace_label" + return 0 + end + env CORTEX_MULTIPLEXER=herdr herdr --session "$session" +end diff --git a/fish/functions/hl.fish b/fish/functions/hl.fish new file mode 100644 index 0000000..587d57a --- /dev/null +++ b/fish/functions/hl.fish @@ -0,0 +1,4 @@ +# List Herdr workspaces. Usage: hl [herdr args...] +function hl --description 'List Herdr workspaces' + h workspace list $argv +end diff --git a/fish/functions/hmain.fish b/fish/functions/hmain.fish new file mode 100644 index 0000000..c6f592f --- /dev/null +++ b/fish/functions/hmain.fish @@ -0,0 +1,4 @@ +# Enter the main Herdr session for a path. Usage: hmain [path] +function hmain --description 'Enter the main Herdr session for a path' + hhere $argv +end diff --git a/fish/functions/hname.fish b/fish/functions/hname.fish new file mode 100644 index 0000000..93ae153 --- /dev/null +++ b/fish/functions/hname.fish @@ -0,0 +1,30 @@ +# Rename the current Herdr pane. Usage: hname [label] +function hname --description 'Rename the current Herdr pane' + if not command -q herdr + printf 'hname: herdr is not available\n' >&2 + return 127 + end + if not command -q python3 + printf 'hname: python3 is not available\n' >&2 + return 127 + end + set -l label $argv[1] + if test -z "$label" + set label (basename "$PWD" | string replace -a . -) + set -l git_root (git rev-parse --show-toplevel 2>/dev/null) + if test -n "$git_root" + set -l repo_name (basename "$git_root" | string replace -a . -) + set -l parent_name (dirname "$git_root" | xargs basename | string replace -a . -) + set -l branch (git -C "$git_root" branch --show-current 2>/dev/null) + set label "$parent_name"-"$repo_name" + test -n "$branch"; and set label "$label"-(string replace -ra '[^A-Za-z0-9_.-]' - "$branch") + end + end + set -l pane_id (herdr pane current --current 2>/dev/null | python3 -c 'import json,sys; print(json.load(sys.stdin)["result"]["pane"]["pane_id"])' 2>/dev/null) + if test -z "$pane_id" + printf 'No pude detectar el pane actual de Herdr\n' >&2 + return 1 + end + herdr pane rename "$pane_id" "$label" >/dev/null; or return + printf 'pane: %s\n' "$label" +end diff --git a/fish/functions/hnew.fish b/fish/functions/hnew.fish new file mode 100644 index 0000000..cc2200a --- /dev/null +++ b/fish/functions/hnew.fish @@ -0,0 +1,4 @@ +# Enter a fresh timestamped Herdr session. Usage: hnew [path] +function hnew --description 'Enter a fresh Herdr session' + hrole "new-"(date +%H%M%S)-(random) $argv +end diff --git a/fish/functions/hrole.fish b/fish/functions/hrole.fish new file mode 100644 index 0000000..fb6a2f4 --- /dev/null +++ b/fish/functions/hrole.fish @@ -0,0 +1,64 @@ +# Enter a named Herdr role session. Usage: hrole [path] +function hrole --description 'Enter a named Herdr role session' + if test (count $argv) -lt 1; or test -z "$argv[1]" + printf 'Uso: hrole [path]\n' >&2 + return 2 + end + if not command -q herdr + printf 'hrole: herdr is not available\n' >&2 + return 127 + end + set -l role $argv[1] + set -l target $PWD + test (count $argv) -ge 2; and set target $argv[2] + set -l resolved (cd -- "$target" >/dev/null 2>&1; and pwd -P) + if test -z "$resolved" + printf 'Directorio no encontrado: %s\n' "$target" >&2 + return 1 + end + set -l host $HOST + if test -z "$host" + set host (hostname -s 2>/dev/null; or hostname) + end + set host (string split -m 1 . -- "$host")[1] + set -l context local + test -n "$SSH_CONNECTION$SSH_CLIENT$SSH_TTY"; and set context ssh + set -l workspace_label (basename "$resolved" | string replace -a . -) + set -l git_root (git -C "$resolved" rev-parse --show-toplevel 2>/dev/null) + if test -n "$git_root" + set -l repo_name (basename "$git_root" | string replace -a . -) + set -l parent_name (dirname "$git_root" | xargs basename | string replace -a . -) + set -l branch (git -C "$git_root" branch --show-current 2>/dev/null) + set workspace_label "$parent_name"-"$repo_name" + test -n "$branch"; and set workspace_label "$workspace_label"-(string replace -ra '[^A-Za-z0-9_.-]' - "$branch") + end + set -l suffix (string replace -ra '[^A-Za-z0-9_.-]' - "$role") + set workspace_label "$workspace_label"-"$suffix" + if test "$HERDR_ENV" = 1 + if not command -q python3 + printf 'hrole: python3 is not available\n' >&2 + return 127 + end + set -l workspace_id (herdr workspace list 2>/dev/null | python3 -c ' +import json +import sys +label = sys.argv[1] +try: + data = json.load(sys.stdin) +except Exception: + sys.exit(0) +for workspace in data.get("result", {}).get("workspaces", []): + if workspace.get("label") == label: + print(workspace.get("workspace_id", "")) + break +' "$workspace_label" 2>/dev/null) + if test -n "$workspace_id" + herdr workspace focus "$workspace_id" >/dev/null; or return + else + herdr workspace create --cwd "$resolved" --label "$workspace_label" --focus >/dev/null; or return + end + printf 'workspace: %s\n' "$workspace_label" + return 0 + end + env CORTEX_MULTIPLEXER=herdr herdr --session "$context"-"$host"-"$workspace_label" +end diff --git a/fish/functions/hs.fish b/fish/functions/hs.fish new file mode 100644 index 0000000..37778df --- /dev/null +++ b/fish/functions/hs.fish @@ -0,0 +1,4 @@ +# Show Herdr status. Usage: hs [herdr args...] +function hs --description 'Show Herdr status' + h status $argv +end diff --git a/fish/functions/hscratch.fish b/fish/functions/hscratch.fish new file mode 100644 index 0000000..bdde1e1 --- /dev/null +++ b/fish/functions/hscratch.fish @@ -0,0 +1,4 @@ +# Enter the scratch Herdr role session. Usage: hscratch [path] +function hscratch --description 'Enter the scratch Herdr role session' + hrole scratch $argv +end diff --git a/fish/functions/hside.fish b/fish/functions/hside.fish new file mode 100644 index 0000000..1a4b981 --- /dev/null +++ b/fish/functions/hside.fish @@ -0,0 +1,4 @@ +# Enter the side Herdr role session. Usage: hside [path] +function hside --description 'Enter the side Herdr role session' + hrole side $argv +end diff --git a/fish/functions/sshc.fish b/fish/functions/sshc.fish new file mode 100644 index 0000000..4d85489 --- /dev/null +++ b/fish/functions/sshc.fish @@ -0,0 +1,18 @@ +# Open a conventional SSH shell with Cortex target context. Usage: sshc [ssh args...] +function sshc --description 'Open SSH with Cortex target context' + if test (count $argv) -lt 1 + printf 'Uso: sshc [ssh args...]\n' >&2 + return 2 + end + set -l target $argv[1] + if string match -q -- '-*' "$target" + printf 'Target SSH inválido: %s\n' "$target" >&2 + return 1 + end + if not command -q ssh + printf 'sshc: ssh is not available\n' >&2 + return 127 + end + set -e argv[1] + env CORTEX_SSH_TARGET="$target" ssh "$target" $argv +end diff --git a/fish/functions/sshx-doctor.fish b/fish/functions/sshx-doctor.fish new file mode 100644 index 0000000..9d06fe7 --- /dev/null +++ b/fish/functions/sshx-doctor.fish @@ -0,0 +1,28 @@ +# Check cmux and SSH host resolution without connecting. Usage: sshx-doctor +function sshx-doctor --description 'Check cmux and SSH host resolution' + if test (count $argv) -lt 1 + printf 'Uso: sshx-doctor \n' >&2 + return 2 + end + set -l target $argv[1] + if string match -q -- '-*' "$target" + printf 'Target SSH inválido: %s\n' "$target" >&2 + return 1 + end + + set -l failed 0 + if not command -q cmux + printf '✗ cmux no encontrado\n' + set failed 1 + end + if not command -q ssh + printf '✗ ssh no encontrado\n' + set failed 1 + else if ssh -G "$target" >/dev/null 2>&1 + printf '✓ SSH config resuelve: %s\n' "$target" + else + printf '✗ SSH config no resuelve: %s\n' "$target" + set failed 1 + end + return $failed +end diff --git a/fish/functions/sshx.fish b/fish/functions/sshx.fish new file mode 100644 index 0000000..37c3fe2 --- /dev/null +++ b/fish/functions/sshx.fish @@ -0,0 +1,22 @@ +# Open a persistent cmux SSH workspace. Usage: sshx [cmux ssh args...] +function sshx --description 'Open persistent cmux SSH workspace' + if test (count $argv) -lt 1 + printf 'Uso: sshx [cmux ssh args...]\n' >&2 + return 2 + end + set -l target $argv[1] + if string match -q -- '-*' "$target" + printf 'Target SSH inválido: %s\n' "$target" >&2 + return 1 + end + if not command -q cmux + printf 'cmux no está disponible; usá sshc para una conexión SSH directa\n' >&2 + return 1 + end + if not command -q ssh + printf 'sshx: ssh is not available\n' >&2 + return 127 + end + set -e argv[1] + env CORTEX_SSH_TARGET="$target" cmux ssh "$target" $argv +end diff --git a/fish/functions/whereami.fish b/fish/functions/whereami.fish new file mode 100644 index 0000000..7adcaf7 --- /dev/null +++ b/fish/functions/whereami.fish @@ -0,0 +1,4 @@ +# Print the current Herdr context. Usage: whereami +function whereami --description 'Print the current Herdr context' + herdr-orient +end diff --git a/fish/tests/w5b-herdr-ssh.fish b/fish/tests/w5b-herdr-ssh.fish new file mode 100644 index 0000000..becba65 --- /dev/null +++ b/fish/tests/w5b-herdr-ssh.fish @@ -0,0 +1,100 @@ +#!/usr/bin/env fish +set -l repo_root (cd (dirname (dirname (status filename))); and pwd) +set -g functions_dir "$repo_root/functions" +set -g workspace (mktemp -d) +test -n "$workspace"; or exit 1 +function _cleanup_workspace --on-event fish_exit + test -d "$workspace"; and rm -rf -- "$workspace" +end +function fail + printf 'FAIL: %s\n' "$argv" >&2 + exit 1 +end +function assert_equal + test "$argv[1]" = "$argv[2]"; or fail "$argv[3] (expected '$argv[2]', got '$argv[1]')" +end +function run_function + set -l bin_dir "$argv[1]" + set -e argv[1] + env HOME="$workspace/home" PATH="$bin_dir:/usr/bin:/bin" TMPDIR="$workspace/tmp" \ + HERDR_TEST_LOG="$workspace/herdr.log" /opt/homebrew/bin/fish --no-config \ + -c 'set -gx fish_function_path $argv[1]; $argv[2] $argv[3..-1]' "$functions_dir" $argv +end +set -l empty_bin "$workspace/empty-bin" +set -l fake_bin "$workspace/fake-bin" +mkdir -p "$workspace/home" "$workspace/tmp" "$workspace/project" "$empty_bin" "$fake_bin" +set -l resolved_project (cd "$workspace/project"; and pwd -P) +if run_function "$empty_bin" hhere "$workspace/project" >"$workspace/missing-herdr.out" 2>&1 + fail 'hhere without herdr must fail' +end +set -l missing_herdr (string collect <"$workspace/missing-herdr.out") +string match -q '*hhere: herdr is not available*' "$missing_herdr"; or fail "hhere missing herdr: $missing_herdr" +printf '%s\n' '#!/bin/sh' 'printf "herdr|%s|%s|%s\n" "$CORTEX_MULTIPLEXER" "$CORTEX_SSH_TARGET" "$*" >> "$HERDR_TEST_LOG"' 'case "$1 $2" in' ' "workspace list") printf "{\"result\":{\"workspaces\":[]}}\n" ;;' 'esac' >"$fake_bin/herdr" +printf '%s\n' '#!/bin/sh' 'if [ -n "$HERDR_TEST_WORKSPACE_ID" ]; then printf "%s\n" "$HERDR_TEST_WORKSPACE_ID"; elif [ -n "$HERDR_TEST_PANE_ID" ]; then printf "%s\n" "$HERDR_TEST_PANE_ID"; fi' >"$fake_bin/python3" +printf '%s\n' '#!/bin/sh' 'printf "ssh|%s|%s\n" "$CORTEX_SSH_TARGET" "$*" >> "$HERDR_TEST_LOG"' '[ "$1" = -G ] && exit 0' 'exit 99' >"$fake_bin/ssh" +printf '%s\n' '#!/bin/sh' 'printf "cmux|%s|%s\n" "$CORTEX_SSH_TARGET" "$*" >> "$HERDR_TEST_LOG"' >"$fake_bin/cmux" +printf '%s\n' '#!/bin/sh' 'printf "testhost\n"' >"$fake_bin/hostname" +printf '%s\n' '#!/bin/sh' 'printf "123456\n"' >"$fake_bin/date" +chmod +x "$fake_bin/herdr" "$fake_bin/python3" "$fake_bin/ssh" "$fake_bin/cmux" "$fake_bin/hostname" "$fake_bin/date" +set -l local_output (run_function "$fake_bin" hhere "$workspace/project") +test -z "$local_output"; or fail "hhere outer output: $local_output" +set -l herdr_log (string collect <"$workspace/herdr.log") +string match -q "*herdr|herdr||--session local-testhost-project*" "$herdr_log"; or fail "local session: $herdr_log" +printf '' >"$workspace/herdr.log" +env HOME="$workspace/home" PATH="$fake_bin:/usr/bin:/bin" TMPDIR="$workspace/tmp" HERDR_TEST_LOG="$workspace/herdr.log" /opt/homebrew/bin/fish --no-config -c 'set -gx fish_function_path $argv[1]; cd $argv[2]; hhere' "$functions_dir" "$workspace/project" +set herdr_log (string collect <"$workspace/herdr.log") +string match -q "*--session local-testhost-project*" "$herdr_log"; or fail "default hhere session: $herdr_log" +printf '' >"$workspace/herdr.log" +set -l focus_output (env HERDR_ENV=1 HERDR_TEST_WORKSPACE_ID=workspace-42 HOME="$workspace/home" PATH="$fake_bin:/usr/bin:/bin" TMPDIR="$workspace/tmp" HERDR_TEST_LOG="$workspace/herdr.log" /opt/homebrew/bin/fish --no-config -c 'set -gx fish_function_path $argv[1]; hfocus $argv[2]' "$functions_dir" "$workspace/project") +assert_equal "$focus_output" 'workspace: project-focus' 'existing workspace focus output' +set herdr_log (string collect <"$workspace/herdr.log") +string match -q '*workspace list*' "$herdr_log"; or fail "workspace list: $herdr_log" +string match -q '*workspace focus workspace-42*' "$herdr_log"; or fail "workspace focus: $herdr_log" +printf '' >"$workspace/herdr.log" +set -l scratch_output (env HERDR_ENV=1 HOME="$workspace/home" PATH="$fake_bin:/usr/bin:/bin" TMPDIR="$workspace/tmp" HERDR_TEST_LOG="$workspace/herdr.log" /opt/homebrew/bin/fish --no-config -c 'set -gx fish_function_path $argv[1]; hscratch $argv[2]' "$functions_dir" "$workspace/project") +assert_equal "$scratch_output" 'workspace: project-scratch' 'new workspace output' +set herdr_log (string collect <"$workspace/herdr.log") +string match -q "*workspace create --cwd $resolved_project --label project-scratch --focus*" "$herdr_log"; or fail "workspace create: $herdr_log" +if run_function "$fake_bin" hrole >"$workspace/hrole.out" 2>&1 + fail 'hrole without role must fail' +end +string match -q '*Uso: hrole [path]*' (string collect <"$workspace/hrole.out"); or fail 'hrole usage' +if run_function "$fake_bin" hhere "$workspace/missing" >"$workspace/path.out" 2>&1 + fail 'missing path must fail' +end +string match -q "*Directorio no encontrado: $workspace/missing*" (string collect <"$workspace/path.out"); or fail 'missing path error' +printf '' >"$workspace/herdr.log" +run_function "$fake_bin" sshc agent-dev -p 22 +set herdr_log (string collect <"$workspace/herdr.log") +assert_equal "$herdr_log" 'ssh|agent-dev|agent-dev -p 22' 'sshc routing' +printf '' >"$workspace/herdr.log" +run_function "$fake_bin" sshx agent-dev -o StrictHostKeyChecking=no +set herdr_log (string collect <"$workspace/herdr.log") +assert_equal "$herdr_log" 'cmux|agent-dev|ssh agent-dev -o StrictHostKeyChecking=no' 'sshx routing' +printf '' >"$workspace/herdr.log" +run_function "$fake_bin" sshx-doctor agent-dev; or fail 'sshx-doctor success' +set herdr_log (string collect <"$workspace/herdr.log") +assert_equal "$herdr_log" 'ssh||-G agent-dev' 'sshx-doctor must only resolve config' +printf '' >"$workspace/herdr.log" +set -l rename_output (env HERDR_TEST_PANE_ID=pane-1 HOME="$workspace/home" PATH="$fake_bin:/usr/bin:/bin" TMPDIR="$workspace/tmp" HERDR_TEST_LOG="$workspace/herdr.log" /opt/homebrew/bin/fish --no-config -c 'set -gx fish_function_path $argv[1]; hname review' "$functions_dir") +assert_equal "$rename_output" 'pane: review' 'hname output' +set herdr_log (string collect <"$workspace/herdr.log") +string match -q '*pane rename pane-1 review*' "$herdr_log"; or fail "hname routing: $herdr_log" +if run_function "$fake_bin" sshc -bad >"$workspace/invalid.out" 2>&1 + fail 'invalid ssh target must fail' +end +string match -q '*Target SSH inválido: -bad*' (string collect <"$workspace/invalid.out"); or fail 'invalid ssh target error' +if run_function "$empty_bin" sshx agent-dev >"$workspace/missing-cmux.out" 2>&1 + fail 'missing cmux must fail' +end +string match -q '*cmux no está disponible*' (string collect <"$workspace/missing-cmux.out"); or fail 'missing cmux error' +if env HOME="$workspace/home" PATH="$empty_bin" TMPDIR="$workspace/tmp" /opt/homebrew/bin/fish --no-config -c 'set -gx fish_function_path $argv[1]; sshc agent-dev' "$functions_dir" >"$workspace/missing-ssh.out" 2>&1 + fail 'missing ssh must fail' +end +string match -q '*sshc: ssh is not available*' (string collect <"$workspace/missing-ssh.out"); or fail 'missing ssh error' +if env HOME="$workspace/home" PATH="$empty_bin" TMPDIR="$workspace/tmp" /opt/homebrew/bin/fish --no-config -c 'set -gx fish_function_path $argv[1]; sshx-doctor agent-dev' "$functions_dir" >"$workspace/doctor-missing.out" 2>&1 + fail 'doctor without tools must fail' +end +set -l doctor_missing (string collect <"$workspace/doctor-missing.out") +string match -q '*cmux no encontrado*' "$doctor_missing"; and string match -q '*ssh no encontrado*' "$doctor_missing"; or fail 'doctor missing tools' +printf 'PASS: Fish W5b Herdr and SSH parity\n' diff --git a/nix/home.nix b/nix/home.nix index c79daf5..95e223c 100644 --- a/nix/home.nix +++ b/nix/home.nix @@ -51,5 +51,21 @@ "fish/functions/wtlist.fish".source = ../fish/functions/wtlist.fish; "fish/functions/wtremove.fish".source = ../fish/functions/wtremove.fish; "fish/functions/hremote.fish".source = ../fish/functions/hremote.fish; + "fish/functions/h.fish".source = ../fish/functions/h.fish; + "fish/functions/hs.fish".source = ../fish/functions/hs.fish; + "fish/functions/hl.fish".source = ../fish/functions/hl.fish; + "fish/functions/hhere.fish".source = ../fish/functions/hhere.fish; + "fish/functions/hmain.fish".source = ../fish/functions/hmain.fish; + "fish/functions/hrole.fish".source = ../fish/functions/hrole.fish; + "fish/functions/hnew.fish".source = ../fish/functions/hnew.fish; + "fish/functions/hfocus.fish".source = ../fish/functions/hfocus.fish; + "fish/functions/hside.fish".source = ../fish/functions/hside.fish; + "fish/functions/hscratch.fish".source = ../fish/functions/hscratch.fish; + "fish/functions/hname.fish".source = ../fish/functions/hname.fish; + "fish/functions/herdr-orient.fish".source = ../fish/functions/herdr-orient.fish; + "fish/functions/whereami.fish".source = ../fish/functions/whereami.fish; + "fish/functions/sshc.fish".source = ../fish/functions/sshc.fish; + "fish/functions/sshx.fish".source = ../fish/functions/sshx.fish; + "fish/functions/sshx-doctor.fish".source = ../fish/functions/sshx-doctor.fish; }; } From e186194c292517b428f0efbe0e2468dfac8153c7 Mon Sep 17 00:00:00 2001 From: Juan Barbat Date: Fri, 21 Aug 2026 16:59:11 -0300 Subject: [PATCH 12/29] feat(fish): port screenshot workflows --- .github/workflows/ci.yml | 1 + README.md | 10 ++ fish/functions/_screenshot_files.fish | 20 ++++ fish/functions/_screenshots_dir.fish | 13 +++ fish/functions/_time_ago.fish | 20 ++++ fish/functions/imgclip.fish | 21 ++++ fish/functions/last.fish | 33 ++++++ fish/functions/ss.fish | 27 +++++ fish/functions/ssd.fish | 10 ++ fish/tests/w4b-screenshots.fish | 159 ++++++++++++++++++++++++++ nix/home.nix | 7 ++ 11 files changed, 321 insertions(+) create mode 100644 fish/functions/_screenshot_files.fish create mode 100644 fish/functions/_screenshots_dir.fish create mode 100644 fish/functions/_time_ago.fish create mode 100644 fish/functions/imgclip.fish create mode 100644 fish/functions/last.fish create mode 100644 fish/functions/ss.fish create mode 100644 fish/functions/ssd.fish create mode 100644 fish/tests/w4b-screenshots.fish diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 893d351..d738ac3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -59,6 +59,7 @@ jobs: run: | fish fish/tests/w2-core.fish fish fish/tests/w3-helpers.fish + fish fish/tests/w4b-screenshots.fish fish fish/tests/w5a-hremote.fish fish fish/tests/w5b-herdr-ssh.fish diff --git a/README.md b/README.md index be05a68..75e06e5 100644 --- a/README.md +++ b/README.md @@ -141,6 +141,16 @@ Para validar sin tocar identidades reales ni la red: /opt/homebrew/bin/fish fish/tests/w3-helpers.fish ``` +## Helpers Fish: screenshots (W4b) + +W4b agrega `ss`, `last`, `ssd` e `imgclip` sin capturar la pantalla ni leer el clipboard durante la carga. `SCREENSHOTS_DIR` usa el override solo si apunta a un directorio existente; si no, conserva el fallback de macOS: `~/Screenshots` cuando existe y luego `~/Desktop`. Home Manager mapea cada función explícitamente y no administra el directorio, screenshots ni clipboard del host. + +Para validar con `HOME`, `PATH` y comandos macOS falsos aislados: + +```bash +/opt/homebrew/bin/fish fish/tests/w4b-screenshots.fish +``` + ## Estructura ``` diff --git a/fish/functions/_screenshot_files.fish b/fish/functions/_screenshot_files.fish new file mode 100644 index 0000000..c77d08b --- /dev/null +++ b/fish/functions/_screenshot_files.fish @@ -0,0 +1,20 @@ +function _screenshot_files + set -l directory (_screenshots_dir) + set -l files + + # BSD find lacks GNU -maxdepth, so retain the source helper's two-level scope in Fish. + for file in (command find "$directory" -type f -print0 2>/dev/null | string split0) + set -l relative (string replace -- "$directory/" '' "$file") + set -l components (string split / -- "$relative") + test (count $components) -le 2; or continue + + switch (path extension "$file") + case .png .jpg .jpeg .gif + set -a files "$file" + end + end + + if test (count $files) -gt 0 + command ls -t $files 2>/dev/null + end +end diff --git a/fish/functions/_screenshots_dir.fish b/fish/functions/_screenshots_dir.fish new file mode 100644 index 0000000..17cfdcc --- /dev/null +++ b/fish/functions/_screenshots_dir.fish @@ -0,0 +1,13 @@ +function _screenshots_dir + if set -q SCREENSHOTS_DIR; and test -n "$SCREENSHOTS_DIR"; and test -d "$SCREENSHOTS_DIR" + printf '%s\n' "$SCREENSHOTS_DIR" + return + end + + set -l default "$HOME/Screenshots" + if test -d "$default" + printf '%s\n' "$default" + else + printf '%s\n' "$HOME/Desktop" + end +end diff --git a/fish/functions/_time_ago.fish b/fish/functions/_time_ago.fish new file mode 100644 index 0000000..ea9236a --- /dev/null +++ b/fish/functions/_time_ago.fish @@ -0,0 +1,20 @@ +function _time_ago + set -l file "$argv[1]" + set -l now (command date +%s) + set -l mtime (command stat -f %m "$file" 2>/dev/null) + + if test $status -ne 0; or test -z "$mtime" + set mtime "$now" + end + + set -l difference (math "$now - $mtime") + if test "$difference" -lt 60 + printf 'justo ahora\n' + else if test "$difference" -lt 3600 + printf '%sm ago\n' (math "$difference / 60") + else if test "$difference" -lt 86400 + printf '%sh ago\n' (math "$difference / 3600") + else + printf '%sd ago\n' (math "$difference / 86400") + end +end diff --git a/fish/functions/imgclip.fish b/fish/functions/imgclip.fish new file mode 100644 index 0000000..beb595c --- /dev/null +++ b/fish/functions/imgclip.fish @@ -0,0 +1,21 @@ +# Usage: imgclip copies a supported image to the macOS clipboard. +function imgclip + set -l image "$argv[1]" + + if not test -f "$image" + printf '❌ Archivo no encontrado: %s\n' "$image" + return 1 + end + + set -l extension (string lower -- (string replace -r '^.*\.' '' -- "$image")) + switch "$extension" + case png jpg jpeg gif bmp tiff + set -l resolved (path resolve "$image") + command osascript -e "set the clipboard to (read (POSIX file \"$resolved\") as «class PNGf»)" 2>/dev/null + or command osascript -e "set the clipboard to POSIX file \"$resolved\"" + printf '✓ Imagen copiada al clipboard: %s\n' (command basename "$image") + case '*' + printf '❌ Formato no soportado: %s\n' "$extension" + return 1 + end +end diff --git a/fish/functions/last.fish b/fish/functions/last.fish new file mode 100644 index 0000000..5a47dab --- /dev/null +++ b/fish/functions/last.fish @@ -0,0 +1,33 @@ +# Usage: last [-c|--copy] [-o|--open] prints, copies, or opens the latest screenshot. +function last + set -l copy false + set -l open_file false + + for argument in $argv + switch "$argument" + case -c --copy + set copy true + case -o --open + set open_file true + end + end + + set -l directory (_screenshots_dir) + set -l files (_screenshot_files) + set -l file "$files[1]" + + if test -z "$file" + printf '⚠️ No se encontraron screenshots en: %s\n' "$directory" + return 1 + end + + if test "$copy" = true + printf '%s\n' "$file" | command pbcopy + printf '✓ Path copiado: %s\n' (command basename "$file") + else if test "$open_file" = true + command open "$file" + printf '✓ Abriendo: %s\n' (command basename "$file") + else + printf '%s\n' "$file" + end +end diff --git a/fish/functions/ss.fish b/fish/functions/ss.fish new file mode 100644 index 0000000..b0a32e8 --- /dev/null +++ b/fish/functions/ss.fish @@ -0,0 +1,27 @@ +# Usage: ss [count] lists the most recent screenshots. +function ss + set -l count "$argv[1]" + test -n "$count"; or set count 10 + set -l directory (_screenshots_dir) + + if not test -d "$directory" + printf '⚠️ Directorio no encontrado: %s\n' "$directory" + printf ' Configurá SCREENSHOTS_DIR en local/env.zsh\n' + return 1 + end + + printf '\n📸 Últimos %s screenshots en: %s\n\n' "$count" "$directory" + + set -l index 1 + for file in (_screenshot_files) + set -l name (command basename "$file") + set -l size (command du -sh "$file" 2>/dev/null | command cut -f1) + set -l ago (_time_ago "$file") + printf ' [%d] \033[1;37m%s\033[0m \033[90m(%s) — %s\033[0m\n' "$index" "$name" "$size" "$ago" + + set index (math "$index + 1") + test "$index" -le "$count"; or break + end + + printf '\n Usá \033[1;33mlast\033[0m para obtener el path del último screenshot\n\n' +end diff --git a/fish/functions/ssd.fish b/fish/functions/ssd.fish new file mode 100644 index 0000000..4de63cd --- /dev/null +++ b/fish/functions/ssd.fish @@ -0,0 +1,10 @@ +# Usage: ssd opens the configured screenshots directory in Finder. +function ssd + set -l directory (_screenshots_dir) + + if test -d "$directory" + command open "$directory" + else + printf '⚠️ Directorio no encontrado: %s\n' "$directory" + end +end diff --git a/fish/tests/w4b-screenshots.fish b/fish/tests/w4b-screenshots.fish new file mode 100644 index 0000000..b01ba50 --- /dev/null +++ b/fish/tests/w4b-screenshots.fish @@ -0,0 +1,159 @@ +#!/usr/bin/env fish + +set -l repo_root (cd (dirname (dirname (status filename))); and pwd) +set -g functions_dir "$repo_root/functions" +set -g workspace (mktemp -d) +test -n "$workspace"; or exit 1 + +function _cleanup_workspace --on-event fish_exit + test -d "$workspace"; and rm -rf -- "$workspace" +end + +function fail + printf 'FAIL: %s\n' "$argv" >&2 + exit 1 +end + +function assert_equal + test "$argv[1]" = "$argv[2]"; or fail "$argv[3] (expected '$argv[2]', got '$argv[1]')" +end + +function assert_contains + string match -q "*$argv[2]*" "$argv[1]"; or fail "$argv[3] (missing '$argv[2]')" +end + +function run_function + set -l bin_dir "$argv[1]" + set -e argv[1] + set -l isolated_home "$workspace/home" + test -n "$W4B_HOME"; and set isolated_home "$W4B_HOME" + env -i HOME="$isolated_home" PATH="$bin_dir:/usr/bin:/bin" \ + W4B_LOG="$workspace/commands.log" \ + W4B_SCREENSHOTS_DIR="$W4B_SCREENSHOTS_DIR" \ + W4B_OSASCRIPT_FAIL_FIRST="$W4B_OSASCRIPT_FAIL_FIRST" \ + W4B_OSASCRIPT_SEEN="$workspace/osascript.seen" \ + /opt/homebrew/bin/fish --no-config \ + -c 'set -gx fish_function_path $argv[1] $fish_function_path; if test -n "$W4B_SCREENSHOTS_DIR"; set -gx SCREENSHOTS_DIR "$W4B_SCREENSHOTS_DIR"; else; set -e SCREENSHOTS_DIR; end; $argv[2] $argv[3..-1]' \ + "$functions_dir" $argv +end + +set -l fake_bin "$workspace/fake-bin" +set -l empty_bin "$workspace/empty-bin" +set -l home "$workspace/home" +set -l screenshots "$home/Screenshots" +set -l override "$workspace/override" +mkdir -p "$fake_bin" "$empty_bin" "$screenshots/nested/deeper" "$override" + +printf '%s\n' '#!/bin/sh' 'printf "date|%s\n" "$*" >> "$W4B_LOG"' 'printf "1000\n"' >"$fake_bin/date" +printf '%s\n' '#!/bin/sh' 'printf "stat" >> "$W4B_LOG"' 'for arg in "$@"; do printf "|%s" "$arg" >> "$W4B_LOG"; done' 'printf "\n" >> "$W4B_LOG"' 'printf "995\n"' >"$fake_bin/stat" +printf '%s\n' '#!/bin/sh' 'printf "open" >> "$W4B_LOG"' 'for arg in "$@"; do printf "|%s" "$arg" >> "$W4B_LOG"; done' 'printf "\n" >> "$W4B_LOG"' >"$fake_bin/open" +printf '%s\n' '#!/bin/sh' 'input=$(cat)' 'printf "pbcopy|%s\n" "$input" >> "$W4B_LOG"' >"$fake_bin/pbcopy" +printf '%s\n' '#!/bin/sh' 'printf "osascript" >> "$W4B_LOG"' 'for arg in "$@"; do printf "|%s" "$arg" >> "$W4B_LOG"; done' 'printf "\n" >> "$W4B_LOG"' 'if [ "$W4B_OSASCRIPT_FAIL_FIRST" = 1 ] && [ ! -e "$W4B_OSASCRIPT_SEEN" ]; then : > "$W4B_OSASCRIPT_SEEN"; exit 1; fi' >"$fake_bin/osascript" +printf '%s\n' '#!/bin/sh' 'printf "screencapture|%s\n" "$*" >> "$W4B_LOG"' >"$fake_bin/screencapture" +printf '%s\n' '#!/bin/sh' 'printf "pngpaste|%s\n" "$*" >> "$W4B_LOG"' >"$fake_bin/pngpaste" +chmod +x "$fake_bin"/* + +printf 'old\n' >"$screenshots/older.png" +printf 'new\n' >"$screenshots/newer.jpg" +printf 'nested\n' >"$screenshots/nested/latest.gif" +printf 'deep\n' >"$screenshots/nested/deeper/too-deep.png" +printf 'ignored\n' >"$screenshots/ignored.txt" +printf 'override\n' >"$override/override.png" +/usr/bin/touch -t 202401010101 "$screenshots/older.png" +/usr/bin/touch -t 202401010102 "$screenshots/newer.jpg" +/usr/bin/touch -t 202401010103 "$screenshots/nested/latest.gif" +/usr/bin/touch -t 202401010104 "$screenshots/nested/deeper/too-deep.png" +/usr/bin/touch -t 202401010105 "$override/override.png" + +printf '' >"$workspace/commands.log" +set -l listed (run_function "$fake_bin" ss 2 | string collect) +assert_contains "$listed" "Últimos 2 screenshots en: $screenshots" 'ss default directory' +assert_contains "$listed" latest.gif 'ss includes nested image' +assert_contains "$listed" newer.jpg 'ss orders recent image' +assert_contains "$listed" 'justo ahora' 'ss formats recent file age' +string match -q '*older.png*' "$listed"; and fail 'ss honors count' +string match -q '*too-deep.png*' "$listed"; and fail 'ss ignores files deeper than two levels' +assert_contains (string collect <"$workspace/commands.log") "stat|-f|%m|$screenshots/nested/latest.gif" 'ss uses BSD stat arguments' + +set -gx W4B_SCREENSHOTS_DIR "$override" +set listed (run_function "$fake_bin" ss 1 | string collect) +assert_contains "$listed" "Últimos 1 screenshots en: $override" 'ss override directory' +assert_contains "$listed" override.png 'ss override image' +set -e W4B_SCREENSHOTS_DIR + +printf '' >"$workspace/commands.log" +set -l latest (run_function "$fake_bin" last) +assert_equal "$latest" "$screenshots/nested/latest.gif" 'last returns latest image path' + +printf '' >"$workspace/commands.log" +run_function "$fake_bin" last --copy ignored >/dev/null +set -l commands (string collect <"$workspace/commands.log") +assert_contains "$commands" "pbcopy|$screenshots/nested/latest.gif" 'last forwards selected path to pbcopy' +string match -q '*open|*' "$commands"; and fail 'last copy must not open' + +printf '' >"$workspace/commands.log" +run_function "$fake_bin" last --open >/dev/null +set commands (string collect <"$workspace/commands.log") +assert_contains "$commands" "open|$screenshots/nested/latest.gif" 'last forwards selected path to open' +string match -q '*pbcopy|*' "$commands"; and fail 'last open must not copy' + +set -gx W4B_SCREENSHOTS_DIR "$override" +printf '' >"$workspace/commands.log" +run_function "$fake_bin" ssd >/dev/null +set commands (string collect <"$workspace/commands.log") +assert_equal "$commands" "open|$override" 'ssd opens override directory' +set -e W4B_SCREENSHOTS_DIR + +set -gx W4B_SCREENSHOTS_DIR "$workspace/missing" +set -l fallback (run_function "$fake_bin" last) +assert_equal "$fallback" "$screenshots/nested/latest.gif" 'invalid override falls back to default screenshots directory' +set -e W4B_SCREENSHOTS_DIR + +set -gx W4B_HOME "$workspace/no-screen-home" +mkdir -p "$W4B_HOME" +if run_function "$fake_bin" ss >"$workspace/missing.out" 2>&1 + fail 'ss missing default directory must fail' +end +assert_contains (string collect <"$workspace/missing.out") "Directorio no encontrado: $W4B_HOME/Desktop" 'ss missing directory error' +if run_function "$fake_bin" last >"$workspace/last-missing.out" 2>&1 + fail 'last missing default directory must fail' +end +assert_contains (string collect <"$workspace/last-missing.out") "No se encontraron screenshots en: $W4B_HOME/Desktop" 'last missing directory error' +set -l ssd_missing (run_function "$fake_bin" ssd) +assert_contains "$ssd_missing" "Directorio no encontrado: $W4B_HOME/Desktop" 'ssd missing directory error' +mkdir -p "$W4B_HOME/Desktop" +if run_function "$fake_bin" last >"$workspace/last-empty.out" 2>&1 + fail 'last empty directory must fail' +end +assert_contains (string collect <"$workspace/last-empty.out") "No se encontraron screenshots en: $W4B_HOME/Desktop" 'last empty directory error' +set -e W4B_HOME + +set -l resolved_image (path resolve "$screenshots/newer.jpg") +printf '' >"$workspace/commands.log" +run_function "$fake_bin" imgclip "$screenshots/newer.jpg" >/dev/null +set commands (string collect <"$workspace/commands.log") +assert_contains "$commands" "osascript|-e|set the clipboard to (read (POSIX file \"$resolved_image\") as «class PNGf»)" 'imgclip forwards resolved image path' + +printf '' >"$workspace/commands.log" +set -gx W4B_OSASCRIPT_FAIL_FIRST 1 +run_function "$fake_bin" imgclip "$screenshots/newer.jpg" >/dev/null +set commands (string collect <"$workspace/commands.log") +set -l osascript_calls (string match -ra 'osascript\|' -- "$commands") +test (count $osascript_calls) -eq 2; or fail 'imgclip falls back after PNG clipboard attempt' +assert_contains "$commands" "set the clipboard to POSIX file \"$resolved_image\"" 'imgclip fallback arguments' +set -e W4B_OSASCRIPT_FAIL_FIRST + +if run_function "$fake_bin" imgclip "$screenshots/ignored.txt" >"$workspace/unsupported.out" 2>&1 + fail 'imgclip unsupported format must fail' +end +assert_contains (string collect <"$workspace/unsupported.out") 'Formato no soportado: txt' 'imgclip unsupported format error' +if run_function "$fake_bin" imgclip "$workspace/missing.png" >"$workspace/missing-image.out" 2>&1 + fail 'imgclip missing file must fail' +end +assert_contains (string collect <"$workspace/missing-image.out") "Archivo no encontrado: $workspace/missing.png" 'imgclip missing file error' + +set commands (string collect <"$workspace/commands.log") +string match -q '*screencapture|*' "$commands"; and fail 'helpers must not capture the screen' +string match -q '*pngpaste|*' "$commands"; and fail 'helpers must not read the clipboard' + +printf 'PASS: Fish W4b screenshot helpers\n' diff --git a/nix/home.nix b/nix/home.nix index 95e223c..1f056ce 100644 --- a/nix/home.nix +++ b/nix/home.nix @@ -67,5 +67,12 @@ "fish/functions/sshc.fish".source = ../fish/functions/sshc.fish; "fish/functions/sshx.fish".source = ../fish/functions/sshx.fish; "fish/functions/sshx-doctor.fish".source = ../fish/functions/sshx-doctor.fish; + "fish/functions/_screenshots_dir.fish".source = ../fish/functions/_screenshots_dir.fish; + "fish/functions/_screenshot_files.fish".source = ../fish/functions/_screenshot_files.fish; + "fish/functions/_time_ago.fish".source = ../fish/functions/_time_ago.fish; + "fish/functions/ss.fish".source = ../fish/functions/ss.fish; + "fish/functions/last.fish".source = ../fish/functions/last.fish; + "fish/functions/ssd.fish".source = ../fish/functions/ssd.fish; + "fish/functions/imgclip.fish".source = ../fish/functions/imgclip.fish; }; } From fee5b9f2d51e2236e6eab24de5ff73bc7acf6623 Mon Sep 17 00:00:00 2001 From: Juan Barbat Date: Fri, 21 Aug 2026 17:25:55 -0300 Subject: [PATCH 13/29] feat(fish): port safe agent helpers --- .github/workflows/ci.yml | 1 + README.md | 15 +++ fish/functions/_cortex_resolve_target.fish | 11 ++ fish/functions/_cortex_run_agent.fish | 11 ++ fish/functions/cc.fish | 5 + fish/functions/ccclip.fish | 43 +++++++ fish/functions/ccd.fish | 16 +++ fish/functions/ccx.fish | 14 +++ fish/functions/oc.fish | 9 ++ fish/functions/ocb.fish | 4 + fish/tests/w6-agent-helpers.fish | 139 +++++++++++++++++++++ nix/home.nix | 8 ++ 12 files changed, 276 insertions(+) create mode 100644 fish/functions/_cortex_resolve_target.fish create mode 100644 fish/functions/_cortex_run_agent.fish create mode 100644 fish/functions/cc.fish create mode 100644 fish/functions/ccclip.fish create mode 100644 fish/functions/ccd.fish create mode 100644 fish/functions/ccx.fish create mode 100644 fish/functions/oc.fish create mode 100644 fish/functions/ocb.fish create mode 100644 fish/tests/w6-agent-helpers.fish diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d738ac3..376aa8a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -62,6 +62,7 @@ jobs: fish fish/tests/w4b-screenshots.fish fish fish/tests/w5a-hremote.fish fish fish/tests/w5b-herdr-ssh.fish + fish fish/tests/w6-agent-helpers.fish - name: Validate JSON configs shell: bash diff --git a/README.md b/README.md index 75e06e5..4c7e865 100644 --- a/README.md +++ b/README.md @@ -151,6 +151,21 @@ Para validar con `HOME`, `PATH` y comandos macOS falsos aislados: /opt/homebrew/bin/fish fish/tests/w4b-screenshots.fish ``` +## Helpers Fish de agentes seguros (W6) + +W6 agrega `cc`, `oc`, `ocb`, `ccx`, `ccd` y `ccclip` como helpers Fish opt-in. Ejecutan Claude Code u OpenCode únicamente en el directorio validado; `ccx` entrega el contexto por stdin y `ccclip` escribe al clipboard solo al invocarse. No incluye `ccb`, bypasses de permisos ni auto-aprobación. + +| Área | Owner en W6 | +| --- | --- | +| Helpers y soporte privado | `fish/functions/{_cortex_resolve_target,_cortex_run_agent,cc,oc,ocb,ccx,ccd,ccclip}.fish` | +| Home Manager | Mapea cada una de esas funciones de forma explícita; no administra binarios de agentes, clipboard, worktrees, estado, historial ni configuración de proveedores | + +Para validar con agentes y clipboard falsos aislados: + +```bash +/opt/homebrew/bin/fish fish/tests/w6-agent-helpers.fish +``` + ## Estructura ``` diff --git a/fish/functions/_cortex_resolve_target.fish b/fish/functions/_cortex_resolve_target.fish new file mode 100644 index 0000000..72eeb40 --- /dev/null +++ b/fish/functions/_cortex_resolve_target.fish @@ -0,0 +1,11 @@ +function _cortex_resolve_target + set -l target "$argv[1]" + test -n "$target"; or set target . + + if not test -d "$target" + printf 'Directorio no encontrado: %s\n' "$target" >&2 + return 1 + end + + cd "$target"; and pwd +end diff --git a/fish/functions/_cortex_run_agent.fish b/fish/functions/_cortex_run_agent.fish new file mode 100644 index 0000000..ff5cefd --- /dev/null +++ b/fish/functions/_cortex_run_agent.fish @@ -0,0 +1,11 @@ +function _cortex_run_agent + set -l target "$argv[1]" + set -e argv[1] + set -l original_dir "$PWD" + + cd "$target"; or return 1 + command $argv + set -l agent_status $status + cd "$original_dir"; or return 1 + return "$agent_status" +end diff --git a/fish/functions/cc.fish b/fish/functions/cc.fish new file mode 100644 index 0000000..0d6a641 --- /dev/null +++ b/fish/functions/cc.fish @@ -0,0 +1,5 @@ +# Usage: cc [path] +function cc + set -l resolved (_cortex_resolve_target "$argv[1]"); or return 1 + _cortex_run_agent "$resolved" claude +end diff --git a/fish/functions/ccclip.fish b/fish/functions/ccclip.fish new file mode 100644 index 0000000..db8f35b --- /dev/null +++ b/fish/functions/ccclip.fish @@ -0,0 +1,43 @@ +# Usage: ccclip [-n|--line-numbers] +function ccclip + if test (count $argv) -eq 0 + printf 'Uso: ccclip [archivo2 ...] [-n|--line-numbers]\n' >&2 + return 1 + end + + set -l with_numbers false + set -l files + for arg in $argv + switch "$arg" + case -n --line-numbers + set with_numbers true + case '*' + set -a files "$arg" + end + end + + begin + for file in $files + if not test -f "$file" + printf 'Archivo no encontrado: %s\n' "$file" >&2 + continue + end + + set -l extension (path extension "$file" | string trim -c .) + printf '```%s\n// File: %s\n' "$extension" "$file" + set -l content + if test "$with_numbers" = true + set content (nl -ba "$file" | string collect) + else + set content (command cat "$file" | string collect) + end + printf '%s\n```\n\n' "$content" + end + end | pbcopy + set -l clipboard_status $pipestatus[-1] + if test "$clipboard_status" -ne 0 + return "$clipboard_status" + end + + printf 'Contexto copiado al clipboard (%d archivos)\n' (count $files) +end diff --git a/fish/functions/ccd.fish b/fish/functions/ccd.fish new file mode 100644 index 0000000..9f3fd98 --- /dev/null +++ b/fish/functions/ccd.fish @@ -0,0 +1,16 @@ +# Usage: ccd [subpath] +function ccd + set -l subpath "$argv[1]" + set -l workspace "$WORKSPACE_DIR" + test -n "$workspace"; or set workspace "$HOME/dev" + set -l target "$workspace" + test -n "$subpath"; and set target "$workspace/$subpath" + + if test -d "$target" + cd "$target" + printf 'Navegando a: %s\n' "$target" + else + printf 'Directorio no encontrado: %s\n' "$target" >&2 + return 1 + end +end diff --git a/fish/functions/ccx.fish b/fish/functions/ccx.fish new file mode 100644 index 0000000..7292837 --- /dev/null +++ b/fish/functions/ccx.fish @@ -0,0 +1,14 @@ +# Usage: ccx [path] +function ccx + set -l context "$argv[1]" + if test -z "$context" + printf 'Uso: ccx [directorio]\n' >&2 + return 1 + end + + set -l resolved (_cortex_resolve_target "$argv[2]"); or return 1 + begin + printf '%s\n' "$context" | _cortex_run_agent "$resolved" claude + return $pipestatus[-1] + end +end diff --git a/fish/functions/oc.fish b/fish/functions/oc.fish new file mode 100644 index 0000000..34f37a6 --- /dev/null +++ b/fish/functions/oc.fish @@ -0,0 +1,9 @@ +# Usage: oc [path] +function oc + set -l resolved (_cortex_resolve_target "$argv[1]"); or return 1 + set -l flags + if set -q OPENCODE_DEFAULT_FLAGS; and test -n "$OPENCODE_DEFAULT_FLAGS" + set flags (string replace -ra '[[:space:]]+' \n -- "$OPENCODE_DEFAULT_FLAGS") + end + _cortex_run_agent "$resolved" opencode $flags +end diff --git a/fish/functions/ocb.fish b/fish/functions/ocb.fish new file mode 100644 index 0000000..694b2c3 --- /dev/null +++ b/fish/functions/ocb.fish @@ -0,0 +1,4 @@ +# Usage: ocb [path] +function ocb + oc $argv +end diff --git a/fish/tests/w6-agent-helpers.fish b/fish/tests/w6-agent-helpers.fish new file mode 100644 index 0000000..a2295cc --- /dev/null +++ b/fish/tests/w6-agent-helpers.fish @@ -0,0 +1,139 @@ +#!/usr/bin/env fish + +set -l repo_root (cd (dirname (dirname (status filename))); and pwd) +set -g functions_dir "$repo_root/functions" +set -g workspace (mktemp -d) +test -n "$workspace"; or exit 1 +function _cleanup_w6 --on-event fish_exit + test -d "$workspace"; and rm -rf -- "$workspace" +end +function fail + printf 'FAIL: %s\n' "$argv" >&2 + exit 1 +end +function contains + string match -q "*$argv[2]*" -- "$argv[1]"; or fail "$argv[3]" +end + +set -g home "$workspace/home" +set -g bin "$workspace/bin" +set -g repo "$workspace/repo" +set -g target "$workspace/target dir" +set -g workspace_dir "$workspace/workspace dir" +mkdir -p "$home/dev" "$bin" "$repo" "$target" "$workspace_dir/sub dir" +printf '#!/bin/sh\nprintf "claude|%%s|cwd=%%s|stdin=" "$*" "$PWD" >> "$W6_LOG"\nwhile IFS= read -r line; do printf "%%s\\n" "$line" >> "$W6_LOG"; done\nprintf "\\n" >> "$W6_LOG"\nexit "${W6_CLAUDE_STATUS:-0}"\n' > "$bin/claude" +printf '#!/bin/sh\nprintf "opencode|%%s|cwd=%%s\\n" "$*" "$PWD" >> "$W6_LOG"\nexit "${W6_OPENCODE_STATUS:-0}"\n' > "$bin/opencode" +printf '#!/bin/sh\nprintf "pbcopy|" >> "$W6_LOG"\nwhile IFS= read -r line; do printf "%%s\\n" "$line" >> "$W6_LOG"; done\nprintf "\\n" >> "$W6_LOG"\nexit "${W6_PBCOPY_STATUS:-0}"\n' > "$bin/pbcopy" +printf '#!/bin/sh\nfor file; do while IFS= read -r line || [ -n "$line" ]; do printf "%%s\\n" "$line"; done < "$file"; done\n' > "$bin/cat" +printf '%s\n' '#!/bin/sh' 'for file; do' ' case "$file" in -*) continue ;; esac' ' i=0' ' while IFS= read -r line || [ -n "$line" ]; do' ' i=$((i + 1))' ' printf "%6s\\t%s\\n" "$i" "$line"' ' done < "$file"' 'done' > "$bin/nl" +chmod +x "$bin"/* +printf 'alpha\n' > "$target/code file.fish" +printf 'beta\n' > "$target/other.txt" +printf '' > "$target/empty.txt" + +function run_w6 + env -i HOME="$home" PATH="$bin:/usr/bin:/bin" W6_LOG="$workspace/log" \ + W6_CLAUDE_STATUS="$W6_CLAUDE_STATUS" W6_OPENCODE_STATUS="$W6_OPENCODE_STATUS" W6_PBCOPY_STATUS="$W6_PBCOPY_STATUS" OPENCODE_DEFAULT_FLAGS="$OPENCODE_DEFAULT_FLAGS" WORKSPACE_DIR="$WORKSPACE_DIR" \ + /opt/homebrew/bin/fish --no-config -c 'set -gx fish_function_path $argv[1] $fish_function_path; cd "$argv[2]"; and $argv[3] $argv[4..-1]' \ + "$functions_dir" "$repo" $argv +end +function run_w6_unavailable + env -i HOME="$home" PATH="/usr/bin:/bin" /opt/homebrew/bin/fish --no-config \ + -c 'set -gx fish_function_path $argv[1] $fish_function_path; cd "$argv[2]"; and $argv[3] $argv[4..-1]' \ + "$functions_dir" "$repo" $argv +end + +printf '' > "$workspace/log" +run_w6 cc; or fail 'cc must use the current directory without arguments' +contains (string collect <"$workspace/log") "claude||cwd=$repo" 'cc default directory' +printf '' > "$workspace/log" +run_w6 cc "$target"; or fail 'cc must succeed' +set -l log (string collect <"$workspace/log") +contains "$log" "claude||cwd=$target" 'cc target and arguments' +string match -q '*enable-auto-mode*' -- "$log"; and fail 'cc must not enable auto approval' + +printf '' > "$workspace/log" +run_w6 oc "$target"; or fail 'oc must succeed without defaults' +contains (string collect <"$workspace/log") "opencode||cwd=$target" 'oc target' +printf '' > "$workspace/log" +set -gx OPENCODE_DEFAULT_FLAGS (printf '%s\t%s\n%s' --model test --plain | string collect --no-trim-newlines) +run_w6 oc "$target"; or fail 'oc must tokenize shell whitespace in defaults' +contains (string collect <"$workspace/log") "opencode|--model test --plain|cwd=$target" 'oc tokenizes shell whitespace without eval' +set -gx OPENCODE_DEFAULT_FLAGS '--model test --plain' +run_w6 ocb "$target"; or fail 'ocb must succeed' +contains (string collect <"$workspace/log") "opencode|--model test --plain|cwd=$target" 'ocb forwards defaults' +set -e OPENCODE_DEFAULT_FLAGS +set -gx W6_OPENCODE_STATUS 24 +run_w6 oc "$target" >/dev/null 2>&1; and fail 'opencode status must propagate' +set -e W6_OPENCODE_STATUS + +printf '' > "$workspace/log" +run_w6 ccx 'initial context with spaces' "$target"; or fail 'ccx must succeed' +contains (string collect <"$workspace/log") "claude||cwd=$target|stdin=initial context with spaces" 'ccx forwards context through stdin' +if run_w6 ccx >"$workspace/ccx.out" 2>&1 + fail 'ccx without context must fail' +end +contains (string collect <"$workspace/ccx.out") 'Uso: ccx' 'ccx usage' + +if run_w6 cc "$workspace/missing" >"$workspace/missing.out" 2>&1 + fail 'missing agent target must fail' +end +contains (string collect <"$workspace/missing.out") 'Directorio no encontrado' 'missing target error' +set -gx W6_CLAUDE_STATUS 23 +run_w6 cc "$target" >/dev/null 2>&1; and fail 'claude status must propagate' +set -e W6_CLAUDE_STATUS +set -gx W6_CLAUDE_STATUS 25 +run_w6 ccx context "$target" >/dev/null 2>&1; and fail 'ccx status must propagate' +set -e W6_CLAUDE_STATUS +if run_w6_unavailable cc "$target" >"$workspace/unavailable.out" 2>&1 + fail 'unavailable claude must fail' +end +contains (string collect <"$workspace/unavailable.out") 'Unknown command' 'unavailable claude error' + +set -gx WORKSPACE_DIR '' +set -l ccd_default (run_w6 ccd) +contains "$ccd_default" "Navegando a: $home/dev" 'ccd default directory' +set -gx WORKSPACE_DIR "$workspace_dir" +set -l ccd_output (run_w6 ccd 'sub dir') +contains "$ccd_output" "Navegando a: $workspace_dir/sub dir" 'ccd uses workspace override' +source "$functions_dir/ccd.fish" +cd "$repo" +ccd 'sub dir' >/dev/null; or fail 'ccd must change the invoking Fish process directory' +test "$PWD" = "$workspace_dir/sub dir"; or fail 'ccd persists PWD in the invoking Fish process' +if run_w6 ccd missing >"$workspace/ccd.out" 2>&1 + fail 'ccd missing directory must fail' +end +contains (string collect <"$workspace/ccd.out") 'Directorio no encontrado' 'ccd validates target' +set -e WORKSPACE_DIR + +printf '' > "$workspace/log" +run_w6 ccclip "$target/code file.fish" -n; or fail 'ccclip must copy content' +set log (string collect <"$workspace/log") +contains "$log" "pbcopy|```fish" 'ccclip fences extension' +contains "$log" "// File: $target/code file.fish" 'ccclip preserves spaced path' +contains "$log" '1 alpha' 'ccclip includes line numbers' +printf '' > "$workspace/log" +run_w6 ccclip "$target/code file.fish"; or fail 'ccclip must copy newline-terminated content' +set log (string collect <"$workspace/log") +set -l expected_fence (string join \n alpha '```' | string collect) +contains "$log" "$expected_fence" 'ccclip trims trailing linefeeds before the closing fence' +printf '' > "$workspace/log" +run_w6 ccclip "$target/empty.txt"; or fail 'ccclip must copy empty files' +printf '' > "$workspace/log" +run_w6 ccclip "$workspace/missing"; or fail 'ccclip empty context preserves successful clipboard status' +contains (string collect <"$workspace/log") 'pbcopy|' 'ccclip hands empty context to clipboard' +set -gx W6_PBCOPY_STATUS 31 +run_w6 ccclip "$target/other.txt" >/dev/null 2>&1; and fail 'clipboard failure must propagate' +set -e W6_PBCOPY_STATUS +if run_w6 ccclip >"$workspace/clip.out" 2>&1 + fail 'ccclip without files must fail' +end +contains (string collect <"$workspace/clip.out") 'Uso: ccclip' 'ccclip usage' + +for file in "$functions_dir/_cortex_resolve_target.fish" "$functions_dir/_cortex_run_agent.fish" "$functions_dir/cc.fish" "$functions_dir/oc.fish" "$functions_dir/ocb.fish" "$functions_dir/ccx.fish" "$functions_dir/ccd.fish" "$functions_dir/ccclip.fish" + set -l source (string collect < "$file") + string match -rqi -- '(dangerously-skip-permissions|bypass-permissions|skip-permissions|auto-approv|--yes)' "$source"; and fail "unsafe W6 source: $file" +end +string match -rqi -- '(dangerously-skip-permissions|bypass-permissions|skip-permissions|auto-approv)' "$log"; and fail 'unsafe generated command' + +printf 'PASS: Fish W6 agent helpers\n' diff --git a/nix/home.nix b/nix/home.nix index 1f056ce..c4b0cb9 100644 --- a/nix/home.nix +++ b/nix/home.nix @@ -74,5 +74,13 @@ "fish/functions/last.fish".source = ../fish/functions/last.fish; "fish/functions/ssd.fish".source = ../fish/functions/ssd.fish; "fish/functions/imgclip.fish".source = ../fish/functions/imgclip.fish; + "fish/functions/_cortex_resolve_target.fish".source = ../fish/functions/_cortex_resolve_target.fish; + "fish/functions/_cortex_run_agent.fish".source = ../fish/functions/_cortex_run_agent.fish; + "fish/functions/cc.fish".source = ../fish/functions/cc.fish; + "fish/functions/oc.fish".source = ../fish/functions/oc.fish; + "fish/functions/ocb.fish".source = ../fish/functions/ocb.fish; + "fish/functions/ccx.fish".source = ../fish/functions/ccx.fish; + "fish/functions/ccd.fish".source = ../fish/functions/ccd.fish; + "fish/functions/ccclip.fish".source = ../fish/functions/ccclip.fish; }; } From a7868c41e271af8372403eae80f4ea0b835ce412 Mon Sep 17 00:00:00 2001 From: Juan Barbat Date: Fri, 21 Aug 2026 17:57:17 -0300 Subject: [PATCH 14/29] feat(fish): enable autonomous agent shortcuts --- README.md | 4 ++-- fish/functions/cc.fish | 2 +- fish/functions/oc.fish | 2 +- fish/tests/w6-agent-helpers.fish | 17 ++++++++--------- 4 files changed, 12 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 4c7e865..cc71b08 100644 --- a/README.md +++ b/README.md @@ -151,9 +151,9 @@ Para validar con `HOME`, `PATH` y comandos macOS falsos aislados: /opt/homebrew/bin/fish fish/tests/w4b-screenshots.fish ``` -## Helpers Fish de agentes seguros (W6) +## Helpers Fish de agentes (W6) -W6 agrega `cc`, `oc`, `ocb`, `ccx`, `ccd` y `ccclip` como helpers Fish opt-in. Ejecutan Claude Code u OpenCode únicamente en el directorio validado; `ccx` entrega el contexto por stdin y `ccclip` escribe al clipboard solo al invocarse. No incluye `ccb`, bypasses de permisos ni auto-aprobación. +**ADVERTENCIA:** por elección explícita del owner, `cc` ejecuta Claude Code con `--dangerously-skip-permissions` y `oc`/`ocb` ejecutan OpenCode con `--auto`; estos shortcuts intencionalmente omiten o autoaprueban permisos. W6 agrega `cc`, `oc`, `ocb`, `ccx`, `ccd` y `ccclip` como helpers Fish opt-in en el directorio validado; `ccx` entrega el contexto por stdin y `ccclip` escribe al clipboard solo al invocarse. No incluye `ccb`. | Área | Owner en W6 | | --- | --- | diff --git a/fish/functions/cc.fish b/fish/functions/cc.fish index 0d6a641..99531aa 100644 --- a/fish/functions/cc.fish +++ b/fish/functions/cc.fish @@ -1,5 +1,5 @@ # Usage: cc [path] function cc set -l resolved (_cortex_resolve_target "$argv[1]"); or return 1 - _cortex_run_agent "$resolved" claude + _cortex_run_agent "$resolved" claude --dangerously-skip-permissions end diff --git a/fish/functions/oc.fish b/fish/functions/oc.fish index 34f37a6..c0aaaa0 100644 --- a/fish/functions/oc.fish +++ b/fish/functions/oc.fish @@ -5,5 +5,5 @@ function oc if set -q OPENCODE_DEFAULT_FLAGS; and test -n "$OPENCODE_DEFAULT_FLAGS" set flags (string replace -ra '[[:space:]]+' \n -- "$OPENCODE_DEFAULT_FLAGS") end - _cortex_run_agent "$resolved" opencode $flags + _cortex_run_agent "$resolved" opencode --auto $flags end diff --git a/fish/tests/w6-agent-helpers.fish b/fish/tests/w6-agent-helpers.fish index a2295cc..64f9dd3 100644 --- a/fish/tests/w6-agent-helpers.fish +++ b/fish/tests/w6-agent-helpers.fish @@ -45,23 +45,23 @@ end printf '' > "$workspace/log" run_w6 cc; or fail 'cc must use the current directory without arguments' -contains (string collect <"$workspace/log") "claude||cwd=$repo" 'cc default directory' +string match -q -- "claude|--dangerously-skip-permissions|cwd=$repo|stdin=" (string collect <"$workspace/log"); or fail 'cc default command order' printf '' > "$workspace/log" run_w6 cc "$target"; or fail 'cc must succeed' set -l log (string collect <"$workspace/log") -contains "$log" "claude||cwd=$target" 'cc target and arguments' -string match -q '*enable-auto-mode*' -- "$log"; and fail 'cc must not enable auto approval' +string match -q -- "claude|--dangerously-skip-permissions|cwd=$target|stdin=" "$log"; or fail 'cc target command order' printf '' > "$workspace/log" run_w6 oc "$target"; or fail 'oc must succeed without defaults' -contains (string collect <"$workspace/log") "opencode||cwd=$target" 'oc target' +string match -q -- "opencode|--auto|cwd=$target" (string collect <"$workspace/log"); or fail 'oc default command order' printf '' > "$workspace/log" set -gx OPENCODE_DEFAULT_FLAGS (printf '%s\t%s\n%s' --model test --plain | string collect --no-trim-newlines) run_w6 oc "$target"; or fail 'oc must tokenize shell whitespace in defaults' -contains (string collect <"$workspace/log") "opencode|--model test --plain|cwd=$target" 'oc tokenizes shell whitespace without eval' +string match -q -- "opencode|--auto --model test --plain|cwd=$target" (string collect <"$workspace/log"); or fail 'oc tokenizes defaults after one auto flag' set -gx OPENCODE_DEFAULT_FLAGS '--model test --plain' +printf '' > "$workspace/log" run_w6 ocb "$target"; or fail 'ocb must succeed' -contains (string collect <"$workspace/log") "opencode|--model test --plain|cwd=$target" 'ocb forwards defaults' +string match -q -- "opencode|--auto --model test --plain|cwd=$target" (string collect <"$workspace/log"); or fail 'ocb forwards defaults after one auto flag' set -e OPENCODE_DEFAULT_FLAGS set -gx W6_OPENCODE_STATUS 24 run_w6 oc "$target" >/dev/null 2>&1; and fail 'opencode status must propagate' @@ -130,10 +130,9 @@ if run_w6 ccclip >"$workspace/clip.out" 2>&1 end contains (string collect <"$workspace/clip.out") 'Uso: ccclip' 'ccclip usage' -for file in "$functions_dir/_cortex_resolve_target.fish" "$functions_dir/_cortex_run_agent.fish" "$functions_dir/cc.fish" "$functions_dir/oc.fish" "$functions_dir/ocb.fish" "$functions_dir/ccx.fish" "$functions_dir/ccd.fish" "$functions_dir/ccclip.fish" +for file in "$functions_dir/_cortex_resolve_target.fish" "$functions_dir/_cortex_run_agent.fish" "$functions_dir/ocb.fish" "$functions_dir/ccx.fish" "$functions_dir/ccd.fish" "$functions_dir/ccclip.fish" set -l source (string collect < "$file") - string match -rqi -- '(dangerously-skip-permissions|bypass-permissions|skip-permissions|auto-approv|--yes)' "$source"; and fail "unsafe W6 source: $file" + string match -rqi -- '(dangerously-skip-permissions|bypass-permissions|skip-permissions|auto-approv|--yes)' "$source"; and fail "unexpected unsafe W6 source: $file" end -string match -rqi -- '(dangerously-skip-permissions|bypass-permissions|skip-permissions|auto-approv)' "$log"; and fail 'unsafe generated command' printf 'PASS: Fish W6 agent helpers\n' From 82226465d53a9feeb60721127506711ed38650c1 Mon Sep 17 00:00:00 2001 From: Juan Barbat Date: Sat, 22 Aug 2026 02:25:10 -0300 Subject: [PATCH 15/29] test(fish): resolve interpreter portably --- fish/tests/w2-core.fish | 7 ++++--- fish/tests/w3-helpers.fish | 3 ++- fish/tests/w4b-screenshots.fish | 3 ++- fish/tests/w5a-hremote.fish | 5 +++-- fish/tests/w5b-herdr-ssh.fish | 15 ++++++++------- fish/tests/w6-agent-helpers.fish | 5 +++-- 6 files changed, 22 insertions(+), 16 deletions(-) diff --git a/fish/tests/w2-core.fish b/fish/tests/w2-core.fish index 134d213..6eafc96 100644 --- a/fish/tests/w2-core.fish +++ b/fish/tests/w2-core.fish @@ -1,4 +1,4 @@ -#!/opt/homebrew/bin/fish +#!/usr/bin/env fish set -l repo_root (cd (dirname (dirname (status filename))); and pwd) set -l core "$repo_root/conf.d/10-core.fish" @@ -25,12 +25,13 @@ function fail exit 1 end +set -g fish_bin (status fish-path); and test -n "$fish_bin"; or fail 'could not resolve Fish interpreter' function assert_equal test "$argv[1]" = "$argv[2]"; or fail "$argv[3] (expected '$argv[2]', got '$argv[1]')" end function run_fish - env -i HOME="$home" USER=test-user LOGNAME=test-user XDG_CONFIG_HOME="$xdg" PATH="$bin:/usr/bin:/bin" TERM=xterm-256color LANG=C /opt/homebrew/bin/fish $argv + env -i HOME="$home" USER=test-user LOGNAME=test-user XDG_CONFIG_HOME="$xdg" PATH="$bin:/usr/bin:/bin" TERM=xterm-256color LANG=C "$fish_bin" $argv end function assert_navigation_error @@ -46,7 +47,7 @@ cp "$core" "$xdg/fish/conf.d/10-core.fish" set -l defaults (run_fish -c 'string join "|" $WORKSPACE_DIR $BARBATDEV_DIR $CORTEX_HOME $CORTEX_ROOT $CORTEX_DOTFILES_DIR $CORTEX_MULTIPLEXER $CLAUDE_CODE_EFFORT_LEVEL $WORK_PROJECTS_DIR $PERSONAL_PROJECTS_DIR $INNIT_DIR $SCREENSHOTS_DIR $EDITOR $VISUAL') assert_equal "$defaults" "$home/dev|$home/dev/barbatdev|$home/.cortex|$home/dev/barbatdev/cortex/cortex|$home/dev/barbatdev/cortex/cortex-dotfiles|cmux|high|$home/dev/barbatdev/innit|$home/dev/barbatdev/products|$home/dev/barbatdev/innit|$home/Screenshots|$bin/nvim|$bin/nvim" 'defaults and editor selection' -set -l empty_defaults (env -i HOME="$home" USER=test-user LOGNAME=test-user XDG_CONFIG_HOME="$xdg" PATH="$bin:/usr/bin:/bin" TERM=xterm-256color LANG=C WORKSPACE_DIR= BARBATDEV_DIR= CORTEX_HOME= CORTEX_ROOT= CORTEX_DOTFILES_DIR= CORTEX_MULTIPLEXER= CLAUDE_CODE_EFFORT_LEVEL= WORK_PROJECTS_DIR= PERSONAL_PROJECTS_DIR= INNIT_DIR= SCREENSHOTS_DIR= /opt/homebrew/bin/fish -c 'string join "|" $WORKSPACE_DIR $BARBATDEV_DIR $CORTEX_HOME $CORTEX_ROOT $CORTEX_DOTFILES_DIR $CORTEX_MULTIPLEXER $CLAUDE_CODE_EFFORT_LEVEL $WORK_PROJECTS_DIR $PERSONAL_PROJECTS_DIR $INNIT_DIR $SCREENSHOTS_DIR') +set -l empty_defaults (env -i HOME="$home" USER=test-user LOGNAME=test-user XDG_CONFIG_HOME="$xdg" PATH="$bin:/usr/bin:/bin" TERM=xterm-256color LANG=C WORKSPACE_DIR= BARBATDEV_DIR= CORTEX_HOME= CORTEX_ROOT= CORTEX_DOTFILES_DIR= CORTEX_MULTIPLEXER= CLAUDE_CODE_EFFORT_LEVEL= WORK_PROJECTS_DIR= PERSONAL_PROJECTS_DIR= INNIT_DIR= SCREENSHOTS_DIR= "$fish_bin" -c 'string join "|" $WORKSPACE_DIR $BARBATDEV_DIR $CORTEX_HOME $CORTEX_ROOT $CORTEX_DOTFILES_DIR $CORTEX_MULTIPLEXER $CLAUDE_CODE_EFFORT_LEVEL $WORK_PROJECTS_DIR $PERSONAL_PROJECTS_DIR $INNIT_DIR $SCREENSHOTS_DIR') assert_equal "$empty_defaults" "$home/dev|$home/dev/barbatdev|$home/.cortex|$home/dev/barbatdev/cortex/cortex|$home/dev/barbatdev/cortex/cortex-dotfiles|cmux|high|$home/dev/barbatdev/innit|$home/dev/barbatdev/products|$home/dev/barbatdev/innit|$home/Screenshots" 'empty environment values use defaults' printf 'set -gx WORKSPACE_DIR "$HOME/private-dev"\nset -gx EDITOR private-editor\nset -gx VISUAL private-editor\n' >"$xdg/fish/conf.d/99-local.fish" diff --git a/fish/tests/w3-helpers.fish b/fish/tests/w3-helpers.fish index d996694..3c70628 100644 --- a/fish/tests/w3-helpers.fish +++ b/fish/tests/w3-helpers.fish @@ -20,12 +20,13 @@ function fail exit 1 end +set -g fish_bin (status fish-path); and test -n "$fish_bin"; or fail 'could not resolve Fish interpreter' function assert_equal test "$argv[1]" = "$argv[2]"; or fail "$argv[3] (expected '$argv[2]', got '$argv[1]')" end function run_fish - env HOME="$home" PATH="$bin:/usr/bin:/bin" GIT_CONFIG_NOSYSTEM=1 GIT_CONFIG_GLOBAL="$workspace/global-gitconfig" /opt/homebrew/bin/fish $argv + env HOME="$home" PATH="$bin:/usr/bin:/bin" GIT_CONFIG_NOSYSTEM=1 GIT_CONFIG_GLOBAL="$workspace/global-gitconfig" "$fish_bin" $argv end printf '#!/bin/sh\nprintf "%%s\\n" "$*" >> "$W3_GIT_LOG"\n[ "$1" = clone ] && exit 0\nexec /usr/bin/git "$@"\n' >"$bin/git" diff --git a/fish/tests/w4b-screenshots.fish b/fish/tests/w4b-screenshots.fish index b01ba50..e2a1cd2 100644 --- a/fish/tests/w4b-screenshots.fish +++ b/fish/tests/w4b-screenshots.fish @@ -14,6 +14,7 @@ function fail exit 1 end +set -g fish_bin (status fish-path); and test -n "$fish_bin"; or fail 'could not resolve Fish interpreter' function assert_equal test "$argv[1]" = "$argv[2]"; or fail "$argv[3] (expected '$argv[2]', got '$argv[1]')" end @@ -32,7 +33,7 @@ function run_function W4B_SCREENSHOTS_DIR="$W4B_SCREENSHOTS_DIR" \ W4B_OSASCRIPT_FAIL_FIRST="$W4B_OSASCRIPT_FAIL_FIRST" \ W4B_OSASCRIPT_SEEN="$workspace/osascript.seen" \ - /opt/homebrew/bin/fish --no-config \ + "$fish_bin" --no-config \ -c 'set -gx fish_function_path $argv[1] $fish_function_path; if test -n "$W4B_SCREENSHOTS_DIR"; set -gx SCREENSHOTS_DIR "$W4B_SCREENSHOTS_DIR"; else; set -e SCREENSHOTS_DIR; end; $argv[2] $argv[3..-1]' \ "$functions_dir" $argv end diff --git a/fish/tests/w5a-hremote.fish b/fish/tests/w5a-hremote.fish index 90395f6..25eaf0c 100644 --- a/fish/tests/w5a-hremote.fish +++ b/fish/tests/w5a-hremote.fish @@ -1,4 +1,4 @@ -#!/opt/homebrew/bin/fish +#!/usr/bin/env fish set -l repo_root (cd (dirname (dirname (status filename))); and pwd) set -g function_file "$repo_root/functions/hremote.fish" @@ -17,10 +17,11 @@ function fail exit 1 end +set -g fish_bin (status fish-path); and test -n "$fish_bin"; or fail 'could not resolve Fish interpreter' function run_hremote set -l bin_dir "$argv[1]" set -e argv[1] - env HOME="$workspace/home" PATH="$bin_dir:/usr/bin:/bin" /opt/homebrew/bin/fish --no-config \ + env HOME="$workspace/home" PATH="$bin_dir:/usr/bin:/bin" "$fish_bin" --no-config \ -c 'source $argv[1]; hremote $argv[2..-1]' "$function_file" $argv end diff --git a/fish/tests/w5b-herdr-ssh.fish b/fish/tests/w5b-herdr-ssh.fish index becba65..f5ac69a 100644 --- a/fish/tests/w5b-herdr-ssh.fish +++ b/fish/tests/w5b-herdr-ssh.fish @@ -13,11 +13,12 @@ end function assert_equal test "$argv[1]" = "$argv[2]"; or fail "$argv[3] (expected '$argv[2]', got '$argv[1]')" end +set -g fish_bin (status fish-path); and test -n "$fish_bin"; or fail 'could not resolve Fish interpreter' function run_function set -l bin_dir "$argv[1]" set -e argv[1] env HOME="$workspace/home" PATH="$bin_dir:/usr/bin:/bin" TMPDIR="$workspace/tmp" \ - HERDR_TEST_LOG="$workspace/herdr.log" /opt/homebrew/bin/fish --no-config \ + HERDR_TEST_LOG="$workspace/herdr.log" "$fish_bin" --no-config \ -c 'set -gx fish_function_path $argv[1]; $argv[2] $argv[3..-1]' "$functions_dir" $argv end set -l empty_bin "$workspace/empty-bin" @@ -41,17 +42,17 @@ test -z "$local_output"; or fail "hhere outer output: $local_output" set -l herdr_log (string collect <"$workspace/herdr.log") string match -q "*herdr|herdr||--session local-testhost-project*" "$herdr_log"; or fail "local session: $herdr_log" printf '' >"$workspace/herdr.log" -env HOME="$workspace/home" PATH="$fake_bin:/usr/bin:/bin" TMPDIR="$workspace/tmp" HERDR_TEST_LOG="$workspace/herdr.log" /opt/homebrew/bin/fish --no-config -c 'set -gx fish_function_path $argv[1]; cd $argv[2]; hhere' "$functions_dir" "$workspace/project" +env HOME="$workspace/home" PATH="$fake_bin:/usr/bin:/bin" TMPDIR="$workspace/tmp" HERDR_TEST_LOG="$workspace/herdr.log" "$fish_bin" --no-config -c 'set -gx fish_function_path $argv[1]; cd $argv[2]; hhere' "$functions_dir" "$workspace/project" set herdr_log (string collect <"$workspace/herdr.log") string match -q "*--session local-testhost-project*" "$herdr_log"; or fail "default hhere session: $herdr_log" printf '' >"$workspace/herdr.log" -set -l focus_output (env HERDR_ENV=1 HERDR_TEST_WORKSPACE_ID=workspace-42 HOME="$workspace/home" PATH="$fake_bin:/usr/bin:/bin" TMPDIR="$workspace/tmp" HERDR_TEST_LOG="$workspace/herdr.log" /opt/homebrew/bin/fish --no-config -c 'set -gx fish_function_path $argv[1]; hfocus $argv[2]' "$functions_dir" "$workspace/project") +set -l focus_output (env HERDR_ENV=1 HERDR_TEST_WORKSPACE_ID=workspace-42 HOME="$workspace/home" PATH="$fake_bin:/usr/bin:/bin" TMPDIR="$workspace/tmp" HERDR_TEST_LOG="$workspace/herdr.log" "$fish_bin" --no-config -c 'set -gx fish_function_path $argv[1]; hfocus $argv[2]' "$functions_dir" "$workspace/project") assert_equal "$focus_output" 'workspace: project-focus' 'existing workspace focus output' set herdr_log (string collect <"$workspace/herdr.log") string match -q '*workspace list*' "$herdr_log"; or fail "workspace list: $herdr_log" string match -q '*workspace focus workspace-42*' "$herdr_log"; or fail "workspace focus: $herdr_log" printf '' >"$workspace/herdr.log" -set -l scratch_output (env HERDR_ENV=1 HOME="$workspace/home" PATH="$fake_bin:/usr/bin:/bin" TMPDIR="$workspace/tmp" HERDR_TEST_LOG="$workspace/herdr.log" /opt/homebrew/bin/fish --no-config -c 'set -gx fish_function_path $argv[1]; hscratch $argv[2]' "$functions_dir" "$workspace/project") +set -l scratch_output (env HERDR_ENV=1 HOME="$workspace/home" PATH="$fake_bin:/usr/bin:/bin" TMPDIR="$workspace/tmp" HERDR_TEST_LOG="$workspace/herdr.log" "$fish_bin" --no-config -c 'set -gx fish_function_path $argv[1]; hscratch $argv[2]' "$functions_dir" "$workspace/project") assert_equal "$scratch_output" 'workspace: project-scratch' 'new workspace output' set herdr_log (string collect <"$workspace/herdr.log") string match -q "*workspace create --cwd $resolved_project --label project-scratch --focus*" "$herdr_log"; or fail "workspace create: $herdr_log" @@ -76,7 +77,7 @@ run_function "$fake_bin" sshx-doctor agent-dev; or fail 'sshx-doctor success' set herdr_log (string collect <"$workspace/herdr.log") assert_equal "$herdr_log" 'ssh||-G agent-dev' 'sshx-doctor must only resolve config' printf '' >"$workspace/herdr.log" -set -l rename_output (env HERDR_TEST_PANE_ID=pane-1 HOME="$workspace/home" PATH="$fake_bin:/usr/bin:/bin" TMPDIR="$workspace/tmp" HERDR_TEST_LOG="$workspace/herdr.log" /opt/homebrew/bin/fish --no-config -c 'set -gx fish_function_path $argv[1]; hname review' "$functions_dir") +set -l rename_output (env HERDR_TEST_PANE_ID=pane-1 HOME="$workspace/home" PATH="$fake_bin:/usr/bin:/bin" TMPDIR="$workspace/tmp" HERDR_TEST_LOG="$workspace/herdr.log" "$fish_bin" --no-config -c 'set -gx fish_function_path $argv[1]; hname review' "$functions_dir") assert_equal "$rename_output" 'pane: review' 'hname output' set herdr_log (string collect <"$workspace/herdr.log") string match -q '*pane rename pane-1 review*' "$herdr_log"; or fail "hname routing: $herdr_log" @@ -88,11 +89,11 @@ if run_function "$empty_bin" sshx agent-dev >"$workspace/missing-cmux.out" 2>&1 fail 'missing cmux must fail' end string match -q '*cmux no está disponible*' (string collect <"$workspace/missing-cmux.out"); or fail 'missing cmux error' -if env HOME="$workspace/home" PATH="$empty_bin" TMPDIR="$workspace/tmp" /opt/homebrew/bin/fish --no-config -c 'set -gx fish_function_path $argv[1]; sshc agent-dev' "$functions_dir" >"$workspace/missing-ssh.out" 2>&1 +if env HOME="$workspace/home" PATH="$empty_bin" TMPDIR="$workspace/tmp" "$fish_bin" --no-config -c 'set -gx fish_function_path $argv[1]; sshc agent-dev' "$functions_dir" >"$workspace/missing-ssh.out" 2>&1 fail 'missing ssh must fail' end string match -q '*sshc: ssh is not available*' (string collect <"$workspace/missing-ssh.out"); or fail 'missing ssh error' -if env HOME="$workspace/home" PATH="$empty_bin" TMPDIR="$workspace/tmp" /opt/homebrew/bin/fish --no-config -c 'set -gx fish_function_path $argv[1]; sshx-doctor agent-dev' "$functions_dir" >"$workspace/doctor-missing.out" 2>&1 +if env HOME="$workspace/home" PATH="$empty_bin" TMPDIR="$workspace/tmp" "$fish_bin" --no-config -c 'set -gx fish_function_path $argv[1]; sshx-doctor agent-dev' "$functions_dir" >"$workspace/doctor-missing.out" 2>&1 fail 'doctor without tools must fail' end set -l doctor_missing (string collect <"$workspace/doctor-missing.out") diff --git a/fish/tests/w6-agent-helpers.fish b/fish/tests/w6-agent-helpers.fish index 64f9dd3..1ee090d 100644 --- a/fish/tests/w6-agent-helpers.fish +++ b/fish/tests/w6-agent-helpers.fish @@ -11,6 +11,7 @@ function fail printf 'FAIL: %s\n' "$argv" >&2 exit 1 end +set -g fish_bin (status fish-path); and test -n "$fish_bin"; or fail 'could not resolve Fish interpreter' function contains string match -q "*$argv[2]*" -- "$argv[1]"; or fail "$argv[3]" end @@ -34,11 +35,11 @@ printf '' > "$target/empty.txt" function run_w6 env -i HOME="$home" PATH="$bin:/usr/bin:/bin" W6_LOG="$workspace/log" \ W6_CLAUDE_STATUS="$W6_CLAUDE_STATUS" W6_OPENCODE_STATUS="$W6_OPENCODE_STATUS" W6_PBCOPY_STATUS="$W6_PBCOPY_STATUS" OPENCODE_DEFAULT_FLAGS="$OPENCODE_DEFAULT_FLAGS" WORKSPACE_DIR="$WORKSPACE_DIR" \ - /opt/homebrew/bin/fish --no-config -c 'set -gx fish_function_path $argv[1] $fish_function_path; cd "$argv[2]"; and $argv[3] $argv[4..-1]' \ + "$fish_bin" --no-config -c 'set -gx fish_function_path $argv[1] $fish_function_path; cd "$argv[2]"; and $argv[3] $argv[4..-1]' \ "$functions_dir" "$repo" $argv end function run_w6_unavailable - env -i HOME="$home" PATH="/usr/bin:/bin" /opt/homebrew/bin/fish --no-config \ + env -i HOME="$home" PATH="/usr/bin:/bin" "$fish_bin" --no-config \ -c 'set -gx fish_function_path $argv[1] $fish_function_path; cd "$argv[2]"; and $argv[3] $argv[4..-1]' \ "$functions_dir" "$repo" $argv end From 26b564d63e7a23825dd51b5e0be1bc011a8afe5d Mon Sep 17 00:00:00 2001 From: Juan Barbat Date: Sat, 22 Aug 2026 02:52:51 -0300 Subject: [PATCH 16/29] test(fish): make missing-agent assertion portable --- fish/tests/w6-agent-helpers.fish | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fish/tests/w6-agent-helpers.fish b/fish/tests/w6-agent-helpers.fish index 1ee090d..9874454 100644 --- a/fish/tests/w6-agent-helpers.fish +++ b/fish/tests/w6-agent-helpers.fish @@ -89,7 +89,7 @@ set -e W6_CLAUDE_STATUS if run_w6_unavailable cc "$target" >"$workspace/unavailable.out" 2>&1 fail 'unavailable claude must fail' end -contains (string collect <"$workspace/unavailable.out") 'Unknown command' 'unavailable claude error' +contains (string collect <"$workspace/unavailable.out") 'claude' 'unavailable claude identifies command' set -gx WORKSPACE_DIR '' set -l ccd_default (run_w6 ccd) From 35888f1f264e90d1aa09c8e3667483be6b1f3acd Mon Sep 17 00:00:00 2001 From: Juan Barbat Date: Sun, 23 Aug 2026 13:00:36 -0300 Subject: [PATCH 17/29] feat(yabai): reload scripting addition after Dock restart --- yabai/yabairc | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/yabai/yabairc b/yabai/yabairc index 5cde2ac..e5c71fe 100755 --- a/yabai/yabairc +++ b/yabai/yabairc @@ -1,6 +1,11 @@ #!/bin/bash -# Config gradual: no usa scripting addition ni requiere desactivar SIP. +# Config gradual con scripting addition: requiere la autorización parcial de SIP ya preparada. +# Carga la scripting addition solo si la autorización sudo no interactiva ya está instalada. +if sudo -n /opt/homebrew/bin/yabai --load-sa; then + yabai -m signal --add event=dock_did_restart action="sudo -n /opt/homebrew/bin/yabai --load-sa" +fi + yabai -m config layout bsp yabai -m config window_placement second_child yabai -m config split_ratio 0.50 From 72f93f9dc99c3f837a1c1d82b1b69e434cd2cb6d Mon Sep 17 00:00:00 2001 From: Juan Barbat Date: Sat, 26 Sep 2026 22:31:49 -0300 Subject: [PATCH 18/29] refactor(fish): remove unused gs alias to free name for gentle-shell No history usage of gs; drop alias gs 'git status' and update its test and docs. gst remains git stash, untouched. --- README.md | 2 +- fish/conf.d/10-core.fish | 1 - fish/tests/w2-core.fish | 4 ++-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index cc71b08..1c84abd 100644 --- a/README.md +++ b/README.md @@ -233,7 +233,7 @@ Seguimiento: [cortex-dotfiles #31](https://github.com/barbatdev/cortex-dotfiles/ | Comando | Descripción | | --------- | ------------- | -| `gs`, `ga`, `gc`, `gp`, `gl` | Git shortcuts | +| `ga`, `gc`, `gp`, `gl` | Git shortcuts | | `dev`, `barbat`, `cowork`, `personal`, `tools`, `worktrees` | Navegación rápida en `~/dev` | | `work`, `work-apis`, `work-mobile`, `work-webs`, `work-pcsoft` | Navegación rápida de trabajo | | `dotfiles` | Navegación rápida al repo de dotfiles | diff --git a/fish/conf.d/10-core.fish b/fish/conf.d/10-core.fish index 2ea1e16..1b9054a 100644 --- a/fish/conf.d/10-core.fish +++ b/fish/conf.d/10-core.fish @@ -42,7 +42,6 @@ end set -gx VISUAL "$EDITOR" alias g git -alias gs 'git status' alias ga 'git add' alias gc 'git commit -m' alias gp 'git push' diff --git a/fish/tests/w2-core.fish b/fish/tests/w2-core.fish index 6eafc96..5bea673 100644 --- a/fish/tests/w2-core.fish +++ b/fish/tests/w2-core.fish @@ -78,9 +78,9 @@ assert_navigation_error "set -gx BARBATDEV_DIR '$workspace/missing-barbat'; tool assert_navigation_error "set -gx HOME '$workspace/missing-home'; worktrees" "$workspace/missing-home/dev/worktrees" 'worktrees missing directory' assert_navigation_error "set -gx WORK_PROJECTS_DIR '$workspace/missing-work'; set -gx BARBATDEV_DIR '$workspace/missing-barbat'; work" "$workspace/missing-work" 'work missing directories' -set -l aliases (run_fish -c 'functions gs; functions gpl' | string collect) -string match -q '*git status*' "$aliases"; or fail 'gs alias' +set -l aliases (run_fish -c 'functions gpl' | string collect) string match -q '*git pull*' "$aliases"; or fail 'gpl alias' +run_fish -c 'functions -q gs; and exit 1; or exit 0'; or fail 'gs alias should not exist' printf '#!/bin/sh\n[ "$1" = init ] && printf "set -gx W2_STARSHIP_READY 1\\n"\n' >"$bin/starship" chmod +x "$bin/starship" From ec1e4ac745b3fb63aeee681817ca184dfca3abfe Mon Sep 17 00:00:00 2001 From: Juan Barbat Date: Mon, 28 Sep 2026 12:31:09 -0300 Subject: [PATCH 19/29] feat(herdr): define RefactorIA visual palette --- herdr/config.toml | 40 +++++++++++++++++++--------------- herdr/profiles/refactoria.toml | 34 +++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 18 deletions(-) create mode 100644 herdr/profiles/refactoria.toml diff --git a/herdr/config.toml b/herdr/config.toml index 06c622c..f2d0204 100644 --- a/herdr/config.toml +++ b/herdr/config.toml @@ -10,7 +10,7 @@ show_agent_labels_on_pane_borders = true prompt_new_tab_name = true pane_borders = true pane_gaps = true -accent = "#F97316" +accent = "#7127FF" [terminal] new_cwd = "follow" @@ -22,23 +22,27 @@ resume_agents_on_restore = true manage_ssh_config = false [theme] -name = "cortex" +# One Dark supplies structural defaults; every visible custom role is RefactorIA. +name = "one-dark" auto_switch = false [theme.custom] -background = "#0B0B0A" -panel_bg = "#27231D" -element_bg = "#39342C" -text = "#F8F1E7" -muted = "#B9AD9C" -subtle = "#7D7468" -accent = "#F97316" -accent_hover = "#FB923C" -blue = "#60A5FA" -red = "#F87171" -green = "#10B981" -yellow = "#FBBF24" -purple = "#A78BFA" -cyan = "#2DD4BF" -diff_added_bg = "#0D2A20" -diff_removed_bg = "#2C1210" +panel_bg = "#0B0714" +sidebar_bg = "#17102A" +active_row_bg = "#2C2144" +selection_bg = "#7127FF" +surface0 = "#17102A" +surface1 = "#2C2144" +surface_dim = "#0B0714" +overlay0 = "#EBEBEB" +overlay1 = "#F2EFFA" +text = "#FFFFFF" +subtext0 = "#EBEBEB" +accent = "#FFC857" +blue = "#8CC8FF" +red = "#FF6B8A" +green = "#58E6A8" +yellow = "#FFC857" +mauve = "#C7A6FF" +teal = "#66E6E6" +peach = "#FFC857" diff --git a/herdr/profiles/refactoria.toml b/herdr/profiles/refactoria.toml new file mode 100644 index 0000000..60da92b --- /dev/null +++ b/herdr/profiles/refactoria.toml @@ -0,0 +1,34 @@ +# RefactorIA Herdr profile. Loaded after the shared default config. + +[theme] +# One Dark supplies structural defaults; every visible custom role is RefactorIA. +name = "one-dark" +auto_switch = false + +[theme.custom] +panel_bg = "#0B0714" +sidebar_bg = "#17102A" +active_row_bg = "#2C2144" +selection_bg = "#7127FF" +surface0 = "#17102A" +surface1 = "#2C2144" +surface_dim = "#0B0714" +overlay0 = "#EBEBEB" +overlay1 = "#F2EFFA" +text = "#FFFFFF" +subtext0 = "#EBEBEB" +accent = "#FFC857" +blue = "#8CC8FF" +red = "#FF6B8A" +green = "#58E6A8" +yellow = "#FFC857" +mauve = "#C7A6FF" +teal = "#66E6E6" +peach = "#FFC857" + +[ui] +accent = "#7127FF" +show_agent_labels_on_pane_borders = true + +[ui.toast] +delivery = "herdr" From c9ee5edbfd0d1f03e4a2f30b5896700819052cd4 Mon Sep 17 00:00:00 2001 From: Juan Barbat Date: Mon, 28 Sep 2026 12:31:29 -0300 Subject: [PATCH 20/29] feat(herdr): merge visual roles without clobbering host config --- scripts/refactoria_herdr.py | 282 +++++++++++++++++++++++++++++++ scripts/test_refactoria_herdr.py | 191 +++++++++++++++++++++ 2 files changed, 473 insertions(+) create mode 100644 scripts/refactoria_herdr.py create mode 100644 scripts/test_refactoria_herdr.py diff --git a/scripts/refactoria_herdr.py b/scripts/refactoria_herdr.py new file mode 100644 index 0000000..567e414 --- /dev/null +++ b/scripts/refactoria_herdr.py @@ -0,0 +1,282 @@ +#!/usr/bin/env python3 +"""Herdr-specific TOML visual merge and preservation helpers.""" +from __future__ import annotations + +import json +import re +import tomllib +from typing import Any + + +class HerdrError(RuntimeError): + """A Herdr visual merge cannot be performed safely.""" + + +_HEADER = re.compile(r"^\s*\[(?!\[)(.+)\]\s*(?:#.*)?$") +_ARRAY_HEADER = re.compile(r"^\s*\[\[(.+)\]\]\s*(?:#.*)?$") +_ASSIGN = re.compile(r"^(?P[ \t]*)(?P[A-Za-z0-9_-]+|\"[^\"]+\"|'[^']+')[ \t]*=") +_VISUAL_SECTIONS = ( + ("theme", ("theme",)), + ("theme.custom", ("theme", "custom")), + ("ui", ("ui",)), +) + + +def _parse_toml(data: bytes, label: str) -> dict[str, Any]: + try: + value = tomllib.loads(data.decode("utf-8")) + except (UnicodeDecodeError, tomllib.TOMLDecodeError) as exc: + raise HerdrError(f"malformed TOML: {label}") from exc + if not isinstance(value, dict): + raise HerdrError(f"TOML root must be a table: {label}") + return value + + +def _tables(text: str) -> tuple[dict[str, int], list[tuple[int, str, bool]]]: + headers: dict[str, int] = {} + ordered: list[tuple[int, str, bool]] = [] + for index, line in enumerate(text.splitlines()): + match = _HEADER.match(line) + is_array = False + if match is None: + match = _ARRAY_HEADER.match(line) + is_array = match is not None + if match: + name = match.group(1) + if not is_array: + if name in headers: + raise HerdrError(f"duplicate TOML table: {name}") + headers[name] = index + ordered.append((index, name, is_array)) + return headers, ordered + + +def _has(data: dict[str, Any], path: tuple[str, ...]) -> bool: + value: Any = data + for key in path: + if not isinstance(value, dict) or key not in value: + return False + value = value[key] + return True + + +def _visual_values(source: dict[str, Any]) -> dict[str, dict[str, Any]]: + theme, ui = source.get("theme"), source.get("ui") + if ( + not isinstance(theme, dict) + or not isinstance(ui, dict) + or not isinstance(theme.get("custom"), dict) + or not {"name", "auto_switch"} <= theme.keys() + or "accent" not in ui + ): + raise HerdrError("source Herdr config lacks approved visual keys") + return { + "theme": {"name": theme["name"], "auto_switch": theme["auto_switch"]}, + "theme.custom": dict(theme["custom"]), + "ui": {"accent": ui["accent"]}, + } + + +def _visual_equal(data: dict[str, Any], values: dict[str, dict[str, Any]]) -> bool: + theme, ui = data.get("theme"), data.get("ui") + custom = theme.get("custom") if isinstance(theme, dict) else None + return ( + isinstance(theme, dict) + and isinstance(custom, dict) + and isinstance(ui, dict) + and theme.get("name") == values["theme"]["name"] + and theme.get("auto_switch") == values["theme"]["auto_switch"] + and ui.get("accent") == values["ui"]["accent"] + and all(custom.get(key) == value for key, value in values["theme.custom"].items()) + ) + + +def _literal(value: Any) -> str: + if isinstance(value, bool): + return "true" if value else "false" + if isinstance(value, str): + return json.dumps(value, ensure_ascii=False) + if isinstance(value, (int, float)): + return str(value).lower() + if isinstance(value, list): + return "[" + ", ".join(_literal(item) for item in value) + "]" + raise HerdrError("unsupported Herdr visual TOML value") + + +def _comment_start(text: str, start: int) -> int | None: + index = start + while index < len(text): + if text[index] == "#": + return index + if text.startswith('"""', index) or text.startswith("'''", index): + raise ValueError("multiline string") + if text[index] == '"': + index += 1 + while index < len(text): + if text[index] == "\\": + index += 2 + continue + if text[index] == '"': + index += 1 + break + index += 1 + else: + raise ValueError("unterminated string") + continue + if text[index] == "'": + index += 1 + while index < len(text) and text[index] != "'": + index += 1 + if index == len(text): + raise ValueError("unterminated string") + index += 1 + continue + index += 1 + return None + + +def _visual_value_span(content: str, match: re.Match[str], section: str, key: str) -> tuple[int, int]: + value_start = match.end() + while value_start < len(content) and content[value_start] in " \t": + value_start += 1 + try: + comment = _comment_start(content, value_start) + value_end = len(content) if comment is None else comment + while value_end > value_start and content[value_end - 1] in " \t": + value_end -= 1 + if value_start == value_end: + raise ValueError("missing value") + tomllib.loads("[check]\n" + content) + except (tomllib.TOMLDecodeError, ValueError) as exc: + raise HerdrError(f"unsupported multiline TOML visual key: {section}.{key}") from exc + return value_start, value_end + + +def _contains_multiline_string(text: str) -> bool: + index = 0 + while index < len(text): + if text[index] == "#": + newline = text.find("\n", index) + index = len(text) if newline < 0 else newline + 1 + continue + if text.startswith('"""', index) or text.startswith("'''", index): + return True + if text[index] == '"': + index += 1 + while index < len(text): + if text[index] == "\\": + index += 2 + continue + if text[index] == '"': + index += 1 + break + index += 1 + continue + if text[index] == "'": + index += 1 + while index < len(text) and text[index] != "'": + index += 1 + if index < len(text): + index += 1 + continue + index += 1 + return False + + +def _ensure_mergeable_tables(data: dict[str, Any], text: str) -> None: + if _contains_multiline_string(text): + raise HerdrError("unsupported multiline TOML visual key: ambiguous TOML construct") + headers, ordered = _tables(text) + visual_sections = tuple(section for section, _ in _VISUAL_SECTIONS) + for _, name, is_array in ordered: + if is_array and any( + name == section or name.startswith(section + ".") + for section in visual_sections + ): + raise HerdrError(f"unsupported visual array-of-tables: {name}") + for section, path in _VISUAL_SECTIONS: + if section not in headers and _has(data, path) and not any( + name.startswith(section + ".") for _, name, _ in ordered + ): + raise HerdrError(f"unsupported inline or dotted Herdr table: {section}") + + +def _merge_toml(text: str, values: dict[str, dict[str, Any]]) -> str: + lines = text.splitlines(keepends=True) + eol = next( + ("\r\n" if line.endswith("\r\n") else "\n" for line in lines if line.endswith(("\n", "\r\n"))), + "\n", + ) + headers, ordered = _tables(text) + additions: dict[int, list[str]] = {} + replacements: dict[int, str] = {} + + def section_end(start: int) -> int: + return next((index for index, _, _ in ordered if index > start), len(lines)) + + def first_child(name: str) -> int | None: + return next((index for index, section, _ in ordered if section.startswith(name + ".")), None) + + for section, wanted in values.items(): + start = headers.get(section) + if start is None: + insertion = first_child(section) + insertion = len(lines) if insertion is None else insertion + block = [f"[{section}]{eol}"] + block.extend(f"{key} = {_literal(value)}{eol}" for key, value in wanted.items()) + if insertion == len(lines) and lines and not lines[-1].endswith(("\n", "\r")): + block[0] = eol + block[0] + additions.setdefault(insertion, []).extend(block) + continue + + stop = section_end(start) + found: set[str] = set() + for index in range(start + 1, stop): + content = lines[index].rstrip("\r\n") + match = _ASSIGN.match(content) + key = match.group("key").strip("\"'") if match else None + if key not in wanted: + continue + value_start, value_end = _visual_value_span(content, match, section, key) + replacements[index] = ( + content[:value_start] + + _literal(wanted[key]) + + content[value_end:] + + lines[index][len(content):] + ) + found.add(key) + + missing = [key for key in wanted if key not in found] + if missing: + block = [f"{key} = {_literal(wanted[key])}{eol}" for key in missing] + if stop == len(lines) and lines and not lines[-1].endswith(("\n", "\r")): + block[0] = eol + block[0] + additions.setdefault(stop, []).extend(block) + + return "".join( + item + for index in range(len(lines) + 1) + for item in additions.get(index, []) + + ([replacements.get(index, lines[index])] if index < len(lines) else []) + ) + + +def plan_visual_merge( + source_data: bytes, + target_data: bytes, + *, + source_label: str = "herdr/config.toml", + target_label: str = ".config/herdr/config.toml", +) -> bytes: + """Return target bytes with only approved Herdr visual values merged.""" + values = _visual_values(_parse_toml(source_data, source_label)) + current = _parse_toml(target_data, target_label) + text = target_data.decode("utf-8") + _ensure_mergeable_tables(current, text) + if _visual_equal(current, values): + return target_data + + planned = _merge_toml(text, values).encode("utf-8") + if not _visual_equal(_parse_toml(planned, target_label), values): + raise HerdrError("Herdr visual merge did not produce the requested values") + return planned diff --git a/scripts/test_refactoria_herdr.py b/scripts/test_refactoria_herdr.py new file mode 100644 index 0000000..a571f5d --- /dev/null +++ b/scripts/test_refactoria_herdr.py @@ -0,0 +1,191 @@ +#!/usr/bin/env python3 +"""Focused tests for the Herdr TOML visual merge slice.""" +from __future__ import annotations + +import tomllib +from pathlib import Path + +import refactoria_herdr as herdr + +ROOT = Path(__file__).resolve().parents[1] +SOURCE = (ROOT / "herdr/config.toml").read_bytes() +PROFILE = (ROOT / "herdr/profiles/refactoria.toml").read_bytes() +COMPLEX_TARGET = b"""# Preserve this non-theme comment and every unrelated setting. +onboarding = true + +[keys] +prefix = "keep-key" + +[remote] +endpoint = "keep-remote" +roles = ["remote-only"] + +[ui.toast] +delivery = "terminal" + +[ui] +sidebar_width = 37 +accent = "#000000" + +[theme] +name = "old-theme" +auto_switch = true + +[theme.custom] +panel_bg = "old-panel" +remote_only = "keep-role" +""" + +ARRAY_TARGET = b"""[theme] +name = "terminal" # keep comment +auto_switch = false +[[remote]] +name = "unrelated" +[theme.custom] +panel_bg = "black" +[ui] +accent = "blue" +""" + +SPACED_COMMENT_TARGET = b"""[theme] + name = "terminal#literal" # keep name +auto_switch\t=\tfalse\t# keep switch +[theme.custom] +panel_bg = "#000000#literal" # keep panel +[ui] +accent\t=\t"#000000#literal"\t# keep accent +""" + +CUSTOM_ARRAY_TARGET = b"""[theme] +name = "terminal" +auto_switch = false +[theme.custom] +panel_bg = "black" +[[remote]] +name = "unrelated" +[ui] +accent = "blue" +""" + + +def expect_error(source: bytes, target: bytes, text: str) -> None: + try: + herdr.plan_visual_merge(source, target) + except herdr.HerdrError as exc: + assert text in str(exc), str(exc) + else: + raise AssertionError(f"expected HerdrError containing {text!r}") + + +def test_portable_profile_matches_mac_visual_roles() -> None: + assert herdr.plan_visual_merge( + PROFILE, COMPLEX_TARGET, source_label="herdr/profiles/refactoria.toml" + ) == herdr.plan_visual_merge(SOURCE, COMPLEX_TARGET) + + +def test_complex_target_preservation_and_idempotency() -> None: + planned = herdr.plan_visual_merge(SOURCE, COMPLEX_TARGET) + parsed = tomllib.loads(planned.decode("utf-8")) + source = tomllib.loads(SOURCE.decode("utf-8")) + + assert parsed["onboarding"] is True + assert parsed["keys"]["prefix"] == "keep-key" + assert parsed["remote"] == {"endpoint": "keep-remote", "roles": ["remote-only"]} + assert parsed["ui"]["toast"]["delivery"] == "terminal" + assert parsed["ui"]["sidebar_width"] == 37 + assert parsed["theme"]["custom"]["remote_only"] == "keep-role" + assert parsed["theme"]["name"] == source["theme"]["name"] + assert parsed["theme"]["auto_switch"] == source["theme"]["auto_switch"] + assert parsed["theme"]["custom"]["panel_bg"] == source["theme"]["custom"]["panel_bg"] + assert b"# Preserve this non-theme comment" in planned + assert b'endpoint = "keep-remote"' in planned + assert b'delivery = "terminal"' in planned + + assert herdr.plan_visual_merge(SOURCE, planned) == planned + + +def test_array_headers_boundaries_and_comments_are_preserved() -> None: + planned = herdr.plan_visual_merge(SOURCE, ARRAY_TARGET) + parsed = tomllib.loads(planned.decode("utf-8")) + + assert parsed["remote"] == [{"name": "unrelated"}] + assert b'[[remote]]\nname = "unrelated"\n' in planned + assert b'name = "one-dark" # keep comment\n' in planned + + +def test_visual_value_spacing_and_quoted_hashes_are_preserved() -> None: + planned = herdr.plan_visual_merge(SOURCE, SPACED_COMMENT_TARGET) + + assert b' name = "one-dark" # keep name\n' in planned + assert b"auto_switch\t=\tfalse\t# keep switch\n" in planned + assert b'panel_bg = "#0B0714" # keep panel\n' in planned + assert b'accent\t=\t"#7127FF"\t# keep accent\n' in planned + + +def test_missing_custom_roles_are_inserted_before_following_array() -> None: + planned = herdr.plan_visual_merge(SOURCE, CUSTOM_ARRAY_TARGET) + parsed = tomllib.loads(planned.decode("utf-8")) + array_start = planned.index(b"[[remote]]") + + for key in tomllib.loads(SOURCE.decode("utf-8"))["theme"]["custom"]: + assert planned.index(f"{key} = ".encode("utf-8")) < array_start + assert parsed["remote"] == [{"name": "unrelated"}] + + +def test_visual_array_tables_and_multiline_values_fail_closed() -> None: + for target in ( + b'[[theme]]\nname = "old-theme"\nauto_switch = true\n', + b'[theme]\nname = "old-theme"\nauto_switch = true\n[[theme.custom]]\npanel_bg = "old"\n', + b'[theme]\nname = "old-theme"\nauto_switch = true\n[theme.custom]\npanel_bg = "old"\n[[ui]]\naccent = "#000"\n', + ): + expect_error(SOURCE, target, "unsupported visual array-of-tables") + + expect_error( + SOURCE, + b'[theme]\nname = """old-theme\ncontinued"""\nauto_switch = true\n', + "unsupported multiline TOML visual key", + ) + + +def test_malformed_inline_and_dotted_inputs_fail_closed() -> None: + expect_error(b"[theme\n", COMPLEX_TARGET, "malformed TOML: herdr/config.toml") + expect_error(SOURCE, b"[theme\n", "malformed TOML: .config/herdr/config.toml") + expect_error( + SOURCE, + b'theme = {name = "old-theme", auto_switch = true, custom = {panel_bg = "old"}}\nui = {accent = "#000000"}\n', + "unsupported inline or dotted Herdr table: theme", + ) + expect_error( + SOURCE, + b'theme.name = "old-theme"\ntheme.auto_switch = true\ntheme.custom.panel_bg = "old"\nui.accent = "#000000"\n', + "unsupported inline or dotted Herdr table: theme", + ) + expect_error( + SOURCE, + b'[theme]\nname = "old-theme"\nauto_switch = true\ncustom.panel_bg = "old"\n[ui]\naccent = "#000000"\n', + "unsupported inline or dotted Herdr table: theme.custom", + ) + + +def test_source_visual_keys_are_required() -> None: + expect_error( + b'[theme]\nname = "only-name"\n[theme.custom]\npanel_bg = "#000"\n', + COMPLEX_TARGET, + "source Herdr config lacks approved visual keys", + ) + + +def main() -> None: + test_portable_profile_matches_mac_visual_roles() + test_complex_target_preservation_and_idempotency() + test_array_headers_boundaries_and_comments_are_preserved() + test_visual_value_spacing_and_quoted_hashes_are_preserved() + test_missing_custom_roles_are_inserted_before_following_array() + test_visual_array_tables_and_multiline_values_fail_closed() + test_malformed_inline_and_dotted_inputs_fail_closed() + test_source_visual_keys_are_required() + print("PASS RefactorIA Herdr merge tests") + + +if __name__ == "__main__": + main() From c82b2dfd9551bccae5baad2e60e0669e83a82677 Mon Sep 17 00:00:00 2001 From: Juan Barbat Date: Mon, 28 Sep 2026 12:31:44 -0300 Subject: [PATCH 21/29] feat(fish): add RefactorIA theme greeting and help --- fish/functions/fish_greeting.fish | 116 ++++++++++++++++ fish/functions/help-profile.fish | 224 ++++++++++++++++++++++++++++++ fish/themes/refactoria.fish | 41 ++++++ 3 files changed, 381 insertions(+) create mode 100644 fish/functions/fish_greeting.fish create mode 100644 fish/functions/help-profile.fish create mode 100644 fish/themes/refactoria.fish diff --git a/fish/functions/fish_greeting.fish b/fish/functions/fish_greeting.fish new file mode 100644 index 0000000..b8dde47 --- /dev/null +++ b/fish/functions/fish_greeting.fish @@ -0,0 +1,116 @@ +# Usage: fish_greeting — show the interactive Fish welcome banner. +function fish_greeting + # Presentation roles: success #58E6A8, info #8CC8FF, violet #A477FF, secondary #EBEBEB. + set -l success (set_color --bold '#58E6A8') + set -l info (set_color --bold '#8CC8FF') + set -l violet (set_color --bold '#A477FF') + set -l secondary (set_color '#EBEBEB') + set -l reset (set_color normal) + + set -l terminal_width 80 + if set -q COLUMNS + set -l candidate "$COLUMNS" + if string match -qr '^[1-9][0-9]*$' -- "$candidate" + set terminal_width "$candidate" + end + else if command -q tput + set -l detected_width (tput cols 2>/dev/null) + if string match -qr '^[1-9][0-9]*$' -- "$detected_width" + set terminal_width "$detected_width" + end + end + + set -l title 'Dev Environment' + set -l status_text '⚡ Fish listo' + set -l clock (date '+%H:%M:%S') + + printf '\n' + + if test "$terminal_width" -lt 28 + printf '%s%s%s\n' "$violet" "$title" "$reset" + printf '%s%s%s\n' "$success" "$status_text" "$reset" + printf '%s%s%s\n' "$info" "🕐 $clock" "$reset" + printf '%sEscribí%s\n' "$secondary" "$reset" + printf ' %shelp-profile%s\n' "$violet" "$reset" + if test "$terminal_width" -ge 17 + printf '%spara ver comandos%s\n' "$secondary" "$reset" + printf '%sdisponibles%s\n' "$secondary" "$reset" + else + printf '%spara%s\n' "$secondary" "$reset" + printf '%sver%s\n' "$secondary" "$reset" + printf '%scomandos%s\n' "$secondary" "$reset" + printf '%sdisponibles%s\n' "$secondary" "$reset" + end + printf '\n' + return 0 + end + + set -l frame_width 40 + if test "$terminal_width" -lt 44 + set frame_width (math "$terminal_width - 4") + end + set -l content_width (math "$frame_width - 2") + set -l rule (string repeat -n (math "$frame_width - 2") '═') + + set -l title_length (string length --visible -- "$title") + set -l title_left (math "floor(($content_width - $title_length) / 2)") + test "$title_left" -gt 0; or set title_left 1 + set -l title_right (math "$content_width - $title_length - $title_left") + + set -l status_length (string length --visible -- "$status_text") + set -l status_left (math "floor(($content_width - $status_length) / 2)") + test "$status_left" -gt 0; or set status_left 1 + set -l status_right (math "$content_width - $status_length - $status_left") + + set -l clock_text "🕐 $clock" + set -l clock_length (string length --visible -- "$clock_text") + set -l clock_left (math "floor(($content_width - $clock_length) / 2)") + test "$clock_left" -gt 0; or set clock_left 1 + set -l clock_right (math "$content_width - $clock_length - $clock_left") + + printf '%s ╔%s╗%s\n' "$secondary" "$rule" "$reset" + printf '%s ║%s%s%s%s%s%s║%s\n' \ + "$secondary" \ + (string repeat -n "$title_left" ' ') \ + "$violet" "$title" "$reset" \ + (string repeat -n "$title_right" ' ') \ + "$secondary" "$reset" + printf '%s ╠%s╣%s\n' "$secondary" "$rule" "$reset" + printf '%s ║%s%s%s%s%s%s║%s\n' \ + "$secondary" \ + (string repeat -n "$status_left" ' ') \ + "$success" "$status_text" "$reset" \ + (string repeat -n "$status_right" ' ') \ + "$secondary" "$reset" + printf '%s ║%s%s%s%s%s%s║%s\n' \ + "$secondary" \ + (string repeat -n "$clock_left" ' ') \ + "$info" "$clock_text" "$reset" \ + (string repeat -n "$clock_right" ' ') \ + "$secondary" "$reset" + printf '%s ╚%s╝%s\n' "$secondary" "$rule" "$reset" + printf '\n' + + if test "$terminal_width" -ge 58 + printf ' %sEscribí%s %shelp-profile%s %spara ver comandos disponibles%s\n' \ + "$secondary" "$reset" "$violet" "$reset" "$secondary" "$reset" + else if test "$terminal_width" -ge 40 + printf ' %sEscribí%s %shelp-profile%s\n' "$secondary" "$reset" "$violet" "$reset" + printf ' %spara ver comandos disponibles%s\n' "$secondary" "$reset" + else + printf ' %sEscribí%s\n' "$secondary" "$reset" + printf ' %shelp-profile%s\n' "$violet" "$reset" + if test "$terminal_width" -ge 31 + printf ' %spara ver comandos disponibles%s\n' "$secondary" "$reset" + else if test "$terminal_width" -ge 19 + printf ' %spara ver comandos%s\n' "$secondary" "$reset" + printf ' %sdisponibles%s\n' "$secondary" "$reset" + else + printf ' %spara%s\n' "$secondary" "$reset" + printf ' %sver%s\n' "$secondary" "$reset" + printf ' %scomandos%s\n' "$secondary" "$reset" + printf ' %sdisponibles%s\n' "$secondary" "$reset" + end + end + printf '\n' +end diff --git a/fish/functions/help-profile.fish b/fish/functions/help-profile.fish new file mode 100644 index 0000000..9fc615b --- /dev/null +++ b/fish/functions/help-profile.fish @@ -0,0 +1,224 @@ +# Usage: help-profile — show the confirmed Fish helper commands. +function help-profile + # Presentation roles: info #8CC8FF, violet #A477FF, secondary #EBEBEB, reset normal. + set -l info (set_color --bold '#8CC8FF') + set -l violet (set_color --bold '#A477FF') + set -l secondary (set_color '#EBEBEB') + set -l reset (set_color normal) + + set -l terminal_width 80 + if set -q COLUMNS + set -l candidate "$COLUMNS" + if string match -qr '^[1-9][0-9]*$' -- "$candidate" + set terminal_width "$candidate" + end + else if command -q tput + set -l detected_width (tput cols 2>/dev/null) + if string match -qr '^[1-9][0-9]*$' -- "$detected_width" + set terminal_width "$detected_width" + end + end + + set -l heading_rule '' + set -l heading_indent ' ' + if test "$terminal_width" -ge 26 + set -l rule_width 41 + if test "$terminal_width" -lt 45 + set heading_indent '' + set rule_width (math "$terminal_width - 2") + end + set heading_rule (string repeat -n "$rule_width" '━') + end + + printf '\n' + if test "$terminal_width" -lt 26 + printf '%sFish%s\n' "$violet" "$reset" + if test "$terminal_width" -ge 19 + printf '%s— Referencia rápida%s\n' "$violet" "$reset" + else + printf '%s— Referencia%s\n' "$violet" "$reset" + printf '%srápida%s\n' "$violet" "$reset" + end + else + printf '%s%s%s\n' "$violet" "$heading_rule" "$reset" + printf '%s%sFish — Referencia rápida%s\n' "$violet" "$heading_indent" "$reset" + printf '%s%s%s\n' "$violet" "$heading_rule" "$reset" + end + + printf '\n%sGit identidades%s\n' "$violet" "$reset" + _help_profile_print_entries "$info" "$secondary" "$reset" "$terminal_width" \ + git-workdev 'Setear identidad de trabajo en repo actual' \ + git-personaldev 'Setear identidad personal en repo actual' \ + git-whoami 'Ver identidad y remote configurados' \ + clone-workdev 'Clonar repo de cuenta de trabajo' \ + clone-personaldev 'Clonar repo de cuenta personal' + + printf '\n%sNavegación%s\n' "$violet" "$reset" + _help_profile_print_entries "$info" "$secondary" "$reset" "$terminal_width" \ + dev 'Ir al workspace de desarrollo' \ + barbat 'Ir al workspace de Barbatdev' \ + cowork 'Ir a los proyectos de trabajo compartidos' \ + personal 'Ir a los proyectos personales' \ + tools 'Ir al directorio de herramientas' \ + worktrees 'Ir a los worktrees de desarrollo' \ + work 'Ir a los proyectos de trabajo' \ + innit 'Ir al workspace de InnIT' \ + innit-apis 'Ir al área de APIs de InnIT o alternativa' \ + innit-mobile 'Ir al área mobile de InnIT o alternativa' \ + innit-webs 'Ir al área web de InnIT o alternativa' \ + innit-pcsoft 'Ir al área PCSoft de InnIT o alternativa' \ + dotfiles 'Ir a este repositorio de dotfiles' + + if test "$terminal_width" -ge 17 + printf '\n%sClaude / OpenCode%s\n' "$violet" "$reset" + else + printf '\n%sClaude /%s\n' "$violet" "$reset" + printf '%sOpenCode%s\n' "$violet" "$reset" + end + _help_profile_print_entries "$info" "$secondary" "$reset" "$terminal_width" \ + 'cc [path]' 'Abrir Claude Code en un directorio' \ + 'ccx [path]' 'Abrir Claude Code con contexto inicial' \ + 'ccd [subpath]' 'Ir al workspace de Claude' \ + 'ccclip ' 'Copiar archivos como contexto Markdown' \ + 'oc [path]' 'Abrir OpenCode en un directorio' \ + 'ocb [path]' 'Abrir OpenCode mediante el helper de Fish' \ + herdr-orient 'Ver contexto de host, repo, SSH y Herdr' + + printf '\n' +end + +# Private renderer for command/description pairs; it wraps descriptions on narrow terminals. +function _help_profile_print_entries + set -l info $argv[1] + set -l secondary $argv[2] + set -l reset $argv[3] + set -l terminal_width $argv[4] + set -l entries $argv[5..-1] + set -l description_width (math "$terminal_width - 6") + test "$description_width" -gt 1; or set description_width 1 + + set -l index 1 + set -l entry_count (count $entries) + while test "$index" -lt "$entry_count" + set -l command $entries[$index] + set -l description $entries[(math "$index + 1")] + set -l command_name (string match -r '^[^[:space:]]+' -- "$command") + if not type -q "$command_name" + set index (math "$index + 2") + continue + end + if test "$terminal_width" -ge 72 + printf ' %s%-22s%s %s%s%s\n' "$info" "$command" "$reset" "$secondary" "$description" "$reset" + else + _help_profile_print_command "$info" "$reset" "$terminal_width" "$command" + set -l line '' + for word in (string split ' ' -- "$description") + if test -z "$line" + set line "$word" + else + set -l candidate "$line $word" + if test (string length -- "$candidate") -le "$description_width" + set line "$candidate" + else + printf ' %s%s%s\n' "$secondary" "$line" "$reset" + set line "$word" + end + end + end + test -z "$line"; or printf ' %s%s%s\n' "$secondary" "$line" "$reset" + end + set index (math "$index + 2") + end +end + +# Private renderer for command signatures; it wraps at token and hyphen boundaries. +function _help_profile_print_command + set -l info $argv[1] + set -l reset $argv[2] + set -l terminal_width $argv[3] + set -l command $argv[4] + set -l command_length (string length --visible -- "$command") + + if test (math "$command_length + 2") -le "$terminal_width" + printf ' %s%s%s\n' "$info" "$command" "$reset" + return 0 + end + + set -l line '' + set -l indent ' ' + set -l continuation_indent ' ' + set -l chunk_width (math "$terminal_width - 4") + test "$chunk_width" -gt 0; or set chunk_width 1 + + for token in (string split ' ' -- "$command") + set -l indent_width (string length --visible -- "$indent") + set -l line_length (string length --visible -- "$line") + set -l token_length (string length --visible -- "$token") + set -l separator_width 0 + test -z "$line"; or set separator_width 1 + + if test (math "$indent_width + $line_length + $separator_width + $token_length") -le "$terminal_width" + if test -n "$line" + set line "$line $token" + else + set line "$token" + end + continue + end + + if test -n "$line" + printf '%s%s%s%s\n' "$indent" "$info" "$line" "$reset" + set line '' + set indent "$continuation_indent" + end + + set indent_width (string length --visible -- "$indent") + set token_length (string length --visible -- "$token") + if test (math "$indent_width + $token_length") -le "$terminal_width" + set line "$token" + continue + end + + set -l token_chunks + set -l hyphen_parts (string split '-' -- "$token") + if test (count $hyphen_parts) -gt 1 + set -l part_index 1 + set -l part_count (count $hyphen_parts) + for part in $hyphen_parts + if test "$part_index" -lt "$part_count" + set -a token_chunks "$part-" + else + set -a token_chunks "$part" + end + set part_index (math "$part_index + 1") + end + else + set token_chunks "$token" + end + + set -l safe_chunks + for chunk in $token_chunks + set -l remaining "$chunk" + while test (string length --visible -- "$remaining") -gt "$chunk_width" + set -a safe_chunks (string sub -l "$chunk_width" -- "$remaining") + set remaining (string sub -s (math "$chunk_width + 1") -- "$remaining") + end + set -a safe_chunks "$remaining" + end + + set -l chunk_index 1 + set -l chunk_count (count $safe_chunks) + for chunk in $safe_chunks + if test "$chunk_index" -lt "$chunk_count" + printf '%s%s%s%s\n' "$indent" "$info" "$chunk" "$reset" + set indent "$continuation_indent" + else + set line "$chunk" + set indent "$continuation_indent" + end + set chunk_index (math "$chunk_index + 1") + end + end + + test -z "$line"; or printf '%s%s%s%s\n' "$indent" "$info" "$line" "$reset" +end diff --git a/fish/themes/refactoria.fish b/fish/themes/refactoria.fish new file mode 100644 index 0000000..20e66d5 --- /dev/null +++ b/fish/themes/refactoria.fish @@ -0,0 +1,41 @@ +# RefactorIA Fish colors. + +set -l base "#0B0714" +set -l panel "#17102A" +set -l text "#FFFFFF" +set -l selection_text "#FFFFFF" +set -l active_violet "#A477FF" +set -l bright_lavender "#C7A6FF" +set -l bright_amber "#FFE08A" +set -l amber "#FFC857" +set -l bright_blue "#8CC8FF" +set -l error "#FF6B8A" +set -l muted "#B8AEC8" +set -l dim "#A39AB5" +set -l selection "#7127FF" +set -l search_background "#2C2144" + +# Syntax highlighting colors. +set -g fish_color_normal $text +set -g fish_color_command --bold $active_violet +set -g fish_color_keyword --bold $bright_lavender +set -g fish_color_quote $bright_amber +set -g fish_color_redirection $bright_blue +set -g fish_color_end $amber +set -g fish_color_error --bold $error +set -g fish_color_param $bright_lavender +set -g fish_color_comment $dim +set -g fish_color_selection --background=$selection $selection_text +set -g fish_color_search_match --background=$search_background $selection_text +set -g fish_color_operator $bright_blue +set -g fish_color_escape $active_violet +set -g fish_color_autosuggestion $muted +set -g fish_color_option $bright_lavender + +# Completion pager colors. +set -g fish_pager_color_progress $dim +set -g fish_pager_color_prefix --bold $active_violet +set -g fish_pager_color_completion $text +set -g fish_pager_color_description $muted +set -g fish_pager_color_selected_background --background=$selection $selection_text +set -g fish_pager_color_secondary_background --background=$panel From 918e009631326f9fe29d47240385d28962540f28 Mon Sep 17 00:00:00 2001 From: Juan Barbat Date: Mon, 28 Sep 2026 12:31:53 -0300 Subject: [PATCH 22/29] feat(claude): add portable RefactorIA theme and statusline --- claude/statusline-command.sh | 99 +++++++++++++ claude/themes/refactoria.json | 52 +++++++ scripts/test-refactoria-assets.py | 223 ++++++++++++++++++++++++++++++ 3 files changed, 374 insertions(+) create mode 100755 claude/statusline-command.sh create mode 100644 claude/themes/refactoria.json create mode 100644 scripts/test-refactoria-assets.py diff --git a/claude/statusline-command.sh b/claude/statusline-command.sh new file mode 100755 index 0000000..4795618 --- /dev/null +++ b/claude/statusline-command.sh @@ -0,0 +1,99 @@ +#!/bin/bash +# Minimal, low-color Claude Code statusline. +# +# Segments (left -> right, most -> least important): +# 1. context window usage — ASCII bar + % (bold; red accent only when high) +# 2. model in use (display_name), with reasoning effort level appended +# when the payload reports one (".effort.level" is only present for +# models that support reasoning effort — it is never fabricated here) +# 3. cwd, short (basename only) +# 4. git branch, with a single "*" marker if the worktree is dirty +# (no ahead/behind, staged/modified/untracked counts — judged noise +# and dropped in favor of a minimal "has changes" cue) +# 5. 5h rate-limit usage (Claude.ai subscription window), when available — +# the one added datum that changes throughout the day and isn't +# redundant with the per-conversation context percentage +# +# No per-segment color theme (Starship-palette replication stays dropped). +# Secondary text uses an explicit gray (not ANSI "dim", which is close to +# unreadable in many terminals). Red accent is reserved for context usage +# only, applied consistently to both the bar and the number. + +input=$(cat) +cwd=$(echo "$input" | jq -r '.workspace.current_dir // .cwd') +used_pct=$(echo "$input" | jq -r '.context_window.used_percentage // empty') +five_hour_pct=$(echo "$input" | jq -r '.rate_limits.five_hour.used_percentage // empty') +model_name=$(echo "$input" | jq -r '.model.display_name // .model.id // empty') +effort_level=$(echo "$input" | jq -r '.effort.level // empty') + +bold="\033[1m" +gray="\033[38;2;235;235;235m" +accent="\033[1;38;2;255;107;138m" +reset="\033[0m" +bar_width=8 + +parts=() + +# --- 1. context used: ASCII bar + % --- +if [ -n "$used_pct" ]; then + pct_int=$(printf '%.0f' "$used_pct") + filled=$(( (pct_int * bar_width + 50) / 100 )) + [ "$filled" -gt "$bar_width" ] && filled=$bar_width + empty=$(( bar_width - filled )) + + if [ "$pct_int" -ge 80 ]; then + fill_color="$accent" + else + fill_color="$bold" + fi + + filled_str=$(printf '%*s' "$filled" '') + filled_str=${filled_str// /█} + empty_str=$(printf '%*s' "$empty" '') + empty_str=${empty_str// /░} + bar="${gray}[${reset}${fill_color}${filled_str}${reset}${gray}${empty_str}]${reset}" + + parts+=("${bar} ${fill_color}${pct_int}% ctx${reset}") +else + empty_str=$(printf '%*s' "$bar_width" '') + empty_str=${empty_str// /░} + parts+=("${gray}[${empty_str}] ctx --${reset}") +fi + +# --- 2. model in use (+ reasoning effort, only when the payload reports it) --- +if [ -n "$model_name" ]; then + if [ -n "$effort_level" ]; then + parts+=("${gray}${model_name} (${effort_level})${reset}") + else + parts+=("${gray}${model_name}${reset}") + fi +fi + +# --- 3. directory, short --- +parts+=("${gray}$(basename "$cwd")${reset}") + +# --- 4. git branch (+ minimal dirty marker), --no-optional-locks --- +if git -C "$cwd" --no-optional-locks rev-parse --is-inside-work-tree >/dev/null 2>&1; then + branch=$(git -C "$cwd" --no-optional-locks branch --show-current 2>/dev/null) + if [ -n "$branch" ]; then + dirty="" + if [ -n "$(git -C "$cwd" --no-optional-locks status --porcelain 2>/dev/null)" ]; then + dirty="*" + fi + parts+=("${gray}${branch}${dirty}${reset}") + fi +fi + +# --- 5. 5h rate-limit usage, when the field is present --- +if [ -n "$five_hour_pct" ]; then + five_hour_int=$(printf '%.0f' "$five_hour_pct") + parts+=("${gray}5h ${five_hour_int}%${reset}") +fi + +out="" +for i in "${!parts[@]}"; do + [ "$i" -gt 0 ] && out="${out} ${gray}·${reset} " + out="${out}${parts[$i]}" +done + +printf "%b" "$out" diff --git a/claude/themes/refactoria.json b/claude/themes/refactoria.json new file mode 100644 index 0000000..86495a9 --- /dev/null +++ b/claude/themes/refactoria.json @@ -0,0 +1,52 @@ +{ + "name": "RefactorIA", + "base": "dark-daltonized", + "overrides": { + "claude": "#A477FF", + "claudeShimmer": "#A477FF", + "text": "#FFFFFF", + "inverseText": "#0B0714", + "inactive": "#B6B2C0", + "inactiveShimmer": "#E9D5FF", + "subtle": "#918C9C", + "suggestion": "#F8F8F8", + "permission": "#9B7BFF", + "permissionShimmer": "#9B7BFF", + "remember": "#7DD3FC", + "success": "#7DD3FC", + "error": "#EF4444", + "warning": "#FBBF24", + "warningShimmer": "#FFE08A", + "merged": "#C4B5FD", + "promptBorder": "#9B7BFF", + "promptBorderShimmer": "#E9D5FF", + "planMode": "#9B7BFF", + "autoAccept": "#7DD3FC", + "bashBorder": "#FBBF24", + "ide": "#7DD3FC", + "fastMode": "#7DD3FC", + "fastModeShimmer": "#BAE6FD", + "diffAdded": "#0F1D26", + "diffRemoved": "#2A1014", + "diffAddedDimmed": "#0E0D12", + "diffRemovedDimmed": "#0E0D12", + "diffAddedWord": "#7DD3FC", + "diffRemovedWord": "#EF4444", + "userMessageBackground": "#2C2144", + "userMessageBackgroundHover": "#7127FF", + "messageActionsBackground": "#17151D", + "bashMessageBackgroundColor": "#0E0D12", + "memoryBackgroundColor": "#111016", + "selectionBg": "#744BED", + "rate_limit_fill": "#9B7BFF", + "rate_limit_empty": "#17151D", + "briefLabelYou": "#9B7BFF", + "briefLabelClaude": "#9B7BFF", + "blue_FOR_SUBAGENTS_ONLY": "#BAE6FD", + "green_FOR_SUBAGENTS_ONLY": "#7DD3FC", + "cyan_FOR_SUBAGENTS_ONLY": "#7DD3FC", + "yellow_FOR_SUBAGENTS_ONLY": "#FBBF24", + "red_FOR_SUBAGENTS_ONLY": "#EF4444", + "purple_FOR_SUBAGENTS_ONLY": "#C4B5FD" + } +} diff --git a/scripts/test-refactoria-assets.py b/scripts/test-refactoria-assets.py new file mode 100644 index 0000000..c1249b4 --- /dev/null +++ b/scripts/test-refactoria-assets.py @@ -0,0 +1,223 @@ +#!/usr/bin/env python3 +"""Focused checks for the portable RefactorIA CLI assets.""" + +from __future__ import annotations + +import hashlib +import json +import os +import re +import shutil +import subprocess +import tempfile +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +STATUSLINE = ROOT / "claude/statusline-command.sh" +HELP_PROFILE = ROOT / "fish/functions/help-profile.fish" +EXPECTED_STATUSLINE_SHA = "8b3524b8f1c9279f19986dbef7be0e3230d9044868d6f21b44b5ad85c40e454c" +ANSI_SGR = re.compile(r"\x1b\[[0-9;]*m") +ACCENT = "\033[1;38;2;255;107;138m" +BOLD = "\033[1m" + + +class TestFailure(RuntimeError): + """Raised when a focused asset check fails.""" + + +def require_tool(name: str) -> str: + path = shutil.which(name) + if path is None: + raise TestFailure(f"required command not found: {name}") + return path + + +def run_checked(command: list[str], *, env: dict[str, str], input_text: str = "") -> subprocess.CompletedProcess[str]: + result = subprocess.run( + command, + input=input_text, + capture_output=True, + check=False, + cwd=ROOT, + env=env, + text=True, + encoding="utf-8", + ) + if result.returncode != 0: + raise TestFailure( + f"command failed ({result.returncode}): {' '.join(command)}\n" + f"stdout: {result.stdout}\nstderr: {result.stderr}" + ) + return result + + +def statusline_payload(workspace: Path, used_percentage: int | None) -> str: + context_window: dict[str, int] = {} + if used_percentage is not None: + context_window["used_percentage"] = used_percentage + return json.dumps( + { + "workspace": {"current_dir": str(workspace)}, + "context_window": context_window, + "rate_limits": {"five_hour": {"used_percentage": 42}}, + "model": {"display_name": "claude-test"}, + "effort": {"level": "high"}, + } + ) + + +def run_statusline(bash: str, workspace: Path, used_percentage: int | None, env: dict[str, str]) -> str: + result = run_checked( + [bash, str(STATUSLINE)], + env=env, + input_text=statusline_payload(workspace, used_percentage), + ) + return result.stdout + + +def strip_ansi(value: str) -> str: + return ANSI_SGR.sub("", value) + + +def assert_statusline_case( + bash: str, + workspace: Path, + used_percentage: int | None, + env: dict[str, str], +) -> None: + output = run_statusline(bash, workspace, used_percentage, env) + clean = strip_ansi(output) + segments = clean.split(" · ") + if len(segments) != 5: + raise TestFailure(f"expected five statusline segments, got {len(segments)}: {clean!r}") + + if used_percentage is None: + expected_bar = "[░░░░░░░░] ctx --" + expected_filled = 0 + if ACCENT in output or BOLD in output: + raise TestFailure("missing context unexpectedly used a threshold color") + else: + expected_filled = (used_percentage * 8 + 50) // 100 + expected_bar = ( + "[" + + "█" * expected_filled + + "░" * (8 - expected_filled) + + f"] {used_percentage}% ctx" + ) + expected_color = ACCENT if used_percentage >= 80 else BOLD + if expected_color not in output: + raise TestFailure(f"missing expected ANSI threshold color at {used_percentage}%") + unexpected_color = BOLD if expected_color == ACCENT else ACCENT + if unexpected_color in output: + raise TestFailure(f"unexpected ANSI threshold color at {used_percentage}%") + + if segments != [expected_bar, "claude-test (high)", "workspace", "main*", "5h 42%"]: + raise TestFailure(f"unexpected statusline segments at {used_percentage!r}: {segments!r}") + if output.count("█") != expected_filled: + raise TestFailure(f"expected {expected_filled} filled glyphs at {used_percentage!r}") + if output.count("░") != 8 - expected_filled: + raise TestFailure(f"expected {8 - expected_filled} empty glyphs at {used_percentage!r}") + + +def test_statusline() -> None: + if not STATUSLINE.is_file(): + raise TestFailure(f"missing statusline source: {STATUSLINE}") + actual_sha = hashlib.sha256(STATUSLINE.read_bytes()).hexdigest() + if actual_sha != EXPECTED_STATUSLINE_SHA: + raise TestFailure(f"statusline SHA mismatch: expected {EXPECTED_STATUSLINE_SHA}, got {actual_sha}") + source = STATUSLINE.read_text(encoding="utf-8") + for forbidden in ("/Users/", "/home/", "SECRET", "TOKEN"): + if forbidden in source: + raise TestFailure(f"statusline source contains forbidden host/secret marker: {forbidden}") + + bash = require_tool("bash") + require_tool("git") + require_tool("jq") + with tempfile.TemporaryDirectory(prefix="refactoria-statusline-") as temporary_directory: + temporary_root = Path(temporary_directory) + home = temporary_root / "home" + workspace = temporary_root / "workspace" + home.mkdir() + workspace.mkdir() + + env = os.environ.copy() + env.update( + { + "HOME": str(home), + "LC_ALL": "C.UTF-8", + "LANG": "C.UTF-8", + "TERM": "dumb", + "GIT_CONFIG_NOSYSTEM": "1", + "GIT_CONFIG_GLOBAL": str(home / "gitconfig"), + } + ) + run_checked([require_tool("git"), "init", "-q", "-b", "main", str(workspace)], env=env) + (workspace / "dirty.txt").write_text("dirty\n", encoding="utf-8") + + for used_percentage in (None, 0, 25, 79, 80, 95, 100): + assert_statusline_case(bash, workspace, used_percentage, env) + + +def command_line_present(lines: list[str], command: str) -> bool: + return any(line.strip().startswith(command) for line in lines) + + +def test_dynamic_help_filter() -> None: + fish = shutil.which("fish") + if fish is None: + print("SKIP dynamic Fish help filter: fish binary not found") + return + + with tempfile.TemporaryDirectory(prefix="refactoria-help-") as temporary_directory: + temporary_root = Path(temporary_directory) + home = temporary_root / "home" + bin_directory = temporary_root / "bin" + home.mkdir() + bin_directory.mkdir() + for command in ("git-workdev", "dev", "cc"): + executable = bin_directory / command + executable.write_text("#!/bin/sh\nexit 0\n", encoding="utf-8") + executable.chmod(0o755) + + env = os.environ.copy() + env.update( + { + "HOME": str(home), + "PATH": str(bin_directory), + "COLUMNS": "80", + "TERM": "dumb", + "LC_ALL": "C.UTF-8", + "LANG": "C.UTF-8", + } + ) + result = run_checked( + [ + fish, + "--no-config", + "-c", + 'source "$argv[1]"; help-profile', + str(HELP_PROFILE), + ], + env=env, + ) + lines = strip_ansi(result.stdout).splitlines() + for command in ("git-workdev", "dev", "cc [path]"): + if not command_line_present(lines, command): + raise TestFailure(f"available helper missing from Fish help: {command}") + for command in ("git-personaldev", "clone-workdev", "barbat", "oc [path]", "herdr-orient"): + if command_line_present(lines, command): + raise TestFailure(f"unavailable helper was displayed in Fish help: {command}") + + +def main() -> None: + test_statusline() + test_dynamic_help_filter() + print("PASS RefactorIA asset checks") + + +if __name__ == "__main__": + try: + main() + except TestFailure as error: + raise SystemExit(f"FAIL: {error}") from error From fa852d98f27f5490caf1b1b0e8dafdc253bda0a0 Mon Sep 17 00:00:00 2001 From: Juan Barbat Date: Mon, 28 Sep 2026 12:32:02 -0300 Subject: [PATCH 23/29] feat(pi): add RefactorIA opt-in theme --- pi/themes/refactoria.json | 96 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 pi/themes/refactoria.json diff --git a/pi/themes/refactoria.json b/pi/themes/refactoria.json new file mode 100644 index 0000000..92cb79f --- /dev/null +++ b/pi/themes/refactoria.json @@ -0,0 +1,96 @@ +{ + "$schema": "https://raw.githubusercontent.com/earendil-works/pi-mono/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json", + "name": "refactoria", + "vars": { + "bg": "#0B0714", + "bgPanel": "#17102A", + "bgElement": "#2C2144", + "bgSubtle": "#17102A", + "border": "#2C2144", + "borderSubtle": "#6D627C", + "text": "#F8F8F8", + "muted": "#B6B2C0", + "mutedBright": "#D9D6E1", + "dim": "#918C9C", + "brandViolet": "#744BED", + "brandBright": "#9B7BFF", + "textAccent": "#A477FF", + "lavender": "#C4B5FD", + "brightLavender": "#E9D5FF", + "deepViolet": "#5A32A3", + "blue": "#7DD3FC", + "brightBlue": "#BAE6FD", + "warning": "#FBBF24", + "brightAmber": "#FFE08A", + "error": "#EF4444", + "selection": "#744BED", + "userBg": "#2C2144", + "userText": "#FFFFFF", + "toolPendingBg": "#121019", + "toolSuccessBg": "#0F1D26", + "toolErrorBg": "#2A1014", + "infoBg": "#17102A" + }, + "colors": { + "accent": "textAccent", + "border": "border", + "borderAccent": "brandBright", + "borderMuted": "borderSubtle", + "success": "blue", + "error": "error", + "warning": "warning", + "muted": "muted", + "dim": "dim", + "text": "text", + "thinkingText": "muted", + "selectedBg": "selection", + "scrollbarThumb": "brandViolet", + "searchMatchBg": "brandViolet", + "searchMatchText": "userText", + "userMessageBg": "userBg", + "userMessageText": "userText", + "customMessageBg": "bgSubtle", + "customMessageText": "text", + "customMessageLabel": "lavender", + "toolPendingBg": "toolPendingBg", + "toolSuccessBg": "toolSuccessBg", + "toolErrorBg": "toolErrorBg", + "toolTitle": "lavender", + "toolOutput": "text", + "mdHeading": "brightLavender", + "mdLink": "brightBlue", + "mdLinkUrl": "mutedBright", + "mdCode": "brightAmber", + "mdCodeBlock": "text", + "mdCodeBlockBorder": "mutedBright", + "mdQuote": "mutedBright", + "mdQuoteBorder": "lavender", + "mdHr": "muted", + "mdListBullet": "brightLavender", + "toolDiffAdded": "blue", + "toolDiffRemoved": "error", + "toolDiffContext": "muted", + "syntaxComment": "mutedBright", + "syntaxKeyword": "brightLavender", + "syntaxFunction": "brightBlue", + "syntaxVariable": "text", + "syntaxString": "brightAmber", + "syntaxNumber": "brightLavender", + "syntaxType": "brightBlue", + "syntaxOperator": "brightLavender", + "syntaxPunctuation": "mutedBright", + "thinkingOff": "dim", + "thinkingMinimal": "muted", + "thinkingLow": "brightBlue", + "thinkingMedium": "lavender", + "thinkingHigh": "warning", + "thinkingXhigh": "brandBright", + "thinkingMax": "brightLavender", + "bashMode": "warning" + }, + "export": { + "pageBg": "bg", + "cardBg": "bgElement", + "infoBg": "infoBg" + } +} From 26141c5e7a7c67d21fe5b1d1e61cb1b0f230d013 Mon Sep 17 00:00:00 2001 From: Juan Barbat Date: Mon, 28 Sep 2026 12:32:15 -0300 Subject: [PATCH 24/29] feat(opencode): add RefactorIA theme --- opencode/themes/refactoria.json | 73 +++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 opencode/themes/refactoria.json diff --git a/opencode/themes/refactoria.json b/opencode/themes/refactoria.json new file mode 100644 index 0000000..86ee4d2 --- /dev/null +++ b/opencode/themes/refactoria.json @@ -0,0 +1,73 @@ +{ + "$schema": "https://opencode.ai/theme.json", + "defs": { + "bg": "#0B0714", + "panel": "#17102A", + "element": "#2C2144", + "text": "#FFFFFF", + "muted": "#B6B2C0", + "subtle": "#918C9C", + "logoViolet": "#744BED", + "activeViolet": "#9B7BFF", + "textAccent": "#A477FF", + "lavender": "#C4B5FD", + "brightLavender": "#E9D5FF", + "cyan": "#7DD3FC", + "brightBlue": "#BAE6FD", + "amber": "#FBBF24", + "brightAmber": "#FFE08A", + "red": "#F05A5A" + }, + "theme": { + "primary": "textAccent", + "secondary": "cyan", + "accent": "textAccent", + "error": "red", + "warning": "amber", + "success": "cyan", + "info": "brightBlue", + "text": "text", + "textMuted": "muted", + "background": "bg", + "backgroundPanel": "panel", + "backgroundElement": "element", + "border": "muted", + "borderActive": "activeViolet", + "borderSubtle": "subtle", + "diffAdded": "cyan", + "diffRemoved": "red", + "diffContext": "muted", + "diffHunkHeader": "brightBlue", + "diffHighlightAdded": "brightBlue", + "diffHighlightRemoved": "red", + "diffAddedBg": "#0F1D26", + "diffRemovedBg": "#2A1014", + "diffContextBg": "panel", + "diffLineNumber": "muted", + "diffAddedLineNumberBg": "#0F1D26", + "diffRemovedLineNumberBg": "#2A1014", + "markdownText": "text", + "markdownHeading": "brightLavender", + "markdownLink": "brightBlue", + "markdownLinkText": "lavender", + "markdownCode": "brightAmber", + "markdownBlockQuote": "muted", + "markdownEmph": "lavender", + "markdownStrong": "text", + "markdownHorizontalRule": "muted", + "markdownListItem": "cyan", + "markdownListEnumeration": "lavender", + "markdownImage": "brightBlue", + "markdownImageText": "lavender", + "markdownCodeBlock": "text", + "syntaxComment": "muted", + "syntaxKeyword": "brightLavender", + "syntaxFunction": "brightBlue", + "syntaxVariable": "text", + "syntaxString": "brightAmber", + "syntaxNumber": "lavender", + "syntaxType": "cyan", + "syntaxOperator": "lavender", + "syntaxPunctuation": "muted" + } +} From 7310b896285bc84d202cb5d22011fdf0c623eca5 Mon Sep 17 00:00:00 2001 From: Juan Barbat Date: Mon, 28 Sep 2026 12:32:23 -0300 Subject: [PATCH 25/29] feat(starship): apply RefactorIA prompt palette --- starship/starship.toml | 89 +++++++++++++++++++++--------------------- 1 file changed, 45 insertions(+), 44 deletions(-) diff --git a/starship/starship.toml b/starship/starship.toml index 6bfa982..d098b20 100644 --- a/starship/starship.toml +++ b/starship/starship.toml @@ -1,12 +1,12 @@ # Formato general format = """ ${custom.repo}\ -[|](fg:color_orange)\ +[|](fg:color_brand)\ $directory\ -[|](fg:color_yellow)\ +[|](fg:color_brand)\ $git_branch\ $git_status\ -[|](fg:color_aqua)\ +[|](fg:color_brand)\ ${custom.sdd_auto}\ ${custom.sdd_interactive}\ $nodejs\ @@ -17,23 +17,24 @@ $line_break$character""" # Módulos alineados a la derecha right_format = """$cmd_duration$time""" -palette = 'innit_tech_slate' - -[palettes.innit_tech_slate] -color_fg0 = '#F6F8FA' -color_bg1 = '#1C2128' -color_bg3 = '#2D333B' -color_blue = '#0054ff' -color_aqua = '#58A6FF' -color_green = '#3FB950' -color_orange = '#0054ff' -color_purple = '#8B5CF6' -color_red = '#F85149' -color_yellow = '#D29922' +palette = 'refactoria' + +[palettes.refactoria] +color_fg0 = '#FFFFFF' +color_base = '#0B0714' +color_surface = '#17102A' +color_panel = '#2C2144' +color_brand = '#7127FF' +color_accent = '#A477FF' +color_success = '#58E6A8' +color_info = '#8CC8FF' +color_warning = '#FFC857' +color_beard = '#FFDE70' +color_error = '#FF6B8A' [os] disabled = true -style = "bg:color_orange fg:color_fg0" +style = "bg:color_surface fg:color_fg0" [os.symbols] Macos = "macOS" @@ -43,12 +44,12 @@ Linux = "Linux" [username] show_always = false -style_user = "bg:color_orange fg:color_fg0" -style_root = "bg:color_orange fg:color_fg0" +style_user = "bg:color_surface fg:color_fg0" +style_root = "bg:color_surface fg:color_fg0" format = '[ $user ]($style)' [directory] -style = "fg:color_fg0 bg:color_yellow" +style = "fg:color_fg0 bg:color_panel" format = "[ $path ]($style)" truncation_length = 3 truncation_symbol = "…/" @@ -63,14 +64,14 @@ truncation_symbol = "…/" [git_branch] symbol = "" -style = "bg:color_aqua" -format = '[[ $branch ](fg:color_fg0 bg:color_aqua)]($style)' +style = "bg:color_info" +format = '[[ $branch ](fg:color_base bg:color_info)]($style)' truncation_length = 24 truncation_symbol = "…" [git_status] -style = "bg:color_aqua" -format = '[[($all_status$ahead_behind )](fg:color_fg0 bg:color_aqua)]($style)' +style = "bg:color_info" +format = '[[($all_status$ahead_behind )](fg:color_base bg:color_info)]($style)' ahead = '↑${count}' behind = '↓${count}' diverged = '⇅↑${ahead_count}↓${behind_count}' @@ -87,7 +88,7 @@ description = "Nombre corto del repo actual" when = "git rev-parse --is-inside-work-tree >/dev/null 2>&1" shell = ["bash", "--noprofile", "--norc"] command = "basename \"$(git rev-parse --show-toplevel 2>/dev/null)\" | cut -c1-24" -style = "fg:color_fg0 bg:color_orange" +style = "fg:color_fg0 bg:color_brand" format = '[ $output ]($style)' [custom.sdd_auto] @@ -95,7 +96,7 @@ description = "Estado SDD auto del proyecto" when = "git rev-parse --is-inside-work-tree >/dev/null 2>&1 && test -f .ai/sdd-session.json && (jq -r '.execution_mode // empty' .ai/sdd-session.json 2>/dev/null || python3 -c \"import json; print(json.load(open('.ai/sdd-session.json')).get('execution_mode',''))\" 2>/dev/null) | grep -qx 'auto'" shell = ["bash", "--noprofile", "--norc"] command = "printf 'sdd:auto'" -style = "fg:color_fg0 bg:color_green" +style = "fg:color_success bg:color_surface" format = '[ $output ]($style)' [custom.sdd_interactive] @@ -103,20 +104,20 @@ description = "Estado SDD interactivo del proyecto" when = "git rev-parse --is-inside-work-tree >/dev/null 2>&1 && test -f .ai/sdd-session.json && (jq -r '.execution_mode // empty' .ai/sdd-session.json 2>/dev/null || python3 -c \"import json; print(json.load(open('.ai/sdd-session.json')).get('execution_mode',''))\" 2>/dev/null) | grep -qx 'interactive'" shell = ["bash", "--noprofile", "--norc"] command = "printf 'sdd:interactive'" -style = "fg:color_fg0 bg:color_purple" +style = "fg:color_accent bg:color_surface" format = '[ $output ]($style)' [nodejs] symbol = "" -style = "bg:color_blue" -format = '[[ node($version) ](fg:color_fg0 bg:color_blue)]($style)' +style = "bg:color_panel" +format = '[[ node($version) ](fg:color_info bg:color_panel)]($style)' detect_files = ["package.json", ".node-version", ".nvmrc"] detect_folders = ["node_modules"] [python] symbol = "" -style = "bg:color_blue" -format = '[[ py($version) ](fg:color_fg0 bg:color_blue)]($style)' +style = "bg:color_panel" +format = '[[ py($version) ](fg:color_info bg:color_panel)]($style)' detect_files = ["pyproject.toml", "requirements.txt", "requirements-dev.txt", ".python-version", "uv.lock", "Pipfile"] detect_folders = [".venv", "venv"] @@ -125,20 +126,20 @@ disabled = true [status] disabled = false -style = "fg:color_red" +style = "fg:color_error bg:color_base" symbol = 'x' format = '[ $symbol$status ]($style)' [cmd_duration] min_time = 500 -style = "bg:color_bg1" -format = '[[ $duration ](fg:color_yellow bg:color_bg1)]($style)' +style = "bg:color_surface" +format = '[[ $duration ](fg:color_warning bg:color_surface)]($style)' [time] disabled = false time_format = "%R" -style = "bg:color_bg1" -format = '[[ $time ](fg:color_fg0 bg:color_bg1)]($style)' +style = "bg:color_surface" +format = '[[ $time ](fg:color_fg0 bg:color_surface)]($style)' [battery] full_symbol = '󰁹' @@ -148,20 +149,20 @@ format = '[$symbol$percentage ]($style)' [[battery.display]] threshold = 20 -style = "bold red" +style = "bold color_error" [[battery.display]] threshold = 50 -style = "bold yellow" +style = "bold color_warning" [line_break] disabled = false [character] disabled = false -success_symbol = '[󰼀](fg:color_fg0) ' -error_symbol = '[󰼀](fg:color_red) ' -vimcmd_symbol = '[󰼀](fg:color_fg0) ' -vimcmd_replace_one_symbol = '[󰼀](fg:color_purple) ' -vimcmd_replace_symbol = '[󰼀](fg:color_purple) ' -vimcmd_visual_symbol = '[󰼀](fg:color_yellow) ' +success_symbol = '[󰼀](fg:color_beard) ' +error_symbol = '[󰼀](fg:color_error) ' +vimcmd_symbol = '[󰼀](fg:color_info) ' +vimcmd_replace_one_symbol = '[󰼀](fg:color_accent) ' +vimcmd_replace_symbol = '[󰼀](fg:color_accent) ' +vimcmd_visual_symbol = '[󰼀](fg:color_warning) ' From 61f1082238af06dd06f4e6579f01d78e0087c4d6 Mon Sep 17 00:00:00 2001 From: Juan Barbat Date: Mon, 28 Sep 2026 12:33:03 -0300 Subject: [PATCH 26/29] docs(refactoria): record guarded Linux rollout and recovery --- docs/refactoria-agent-gentle-rollout.md | 71 +++++++++++++++++++ .../refactoria-reusable-cli-agent-gentle.md | 27 +++++++ 2 files changed, 98 insertions(+) create mode 100644 docs/refactoria-agent-gentle-rollout.md create mode 100644 odd/tasks/refactoria-reusable-cli-agent-gentle.md diff --git a/docs/refactoria-agent-gentle-rollout.md b/docs/refactoria-agent-gentle-rollout.md new file mode 100644 index 0000000..2c56be6 --- /dev/null +++ b/docs/refactoria-agent-gentle-rollout.md @@ -0,0 +1,71 @@ +# RefactorIA CLI visuals on agent-gentle + +The Linux/Fish rollout is **applied and visually approved** on `agent-gentle` (2026-09-27). This is a per-tool manual recipe and recovery record, **not** an all-tool installer. Pi's guarded batch installed only the theme for `pi --use-theme refactoria`. After visual use, its saved default changed from the initially preserved `Gentleman-Sexy` to `refactoria`; the user explicitly chose to **keep RefactorIA as the saved default**. We did not write Pi settings in the guarded batch. No Zsh, Ghostty, Neovim, SketchyBar, Herdr binary patch, package installation, app restart, or broad `install.sh` was involved. + +> **Source availability:** Use the selected feature-branch revision containing the nine Linux replay inputs below. Until those commits are published, an ordinary remote clean clone will not contain them. The Mac `herdr/config.toml` is a separate visual-only change: local onboarding/keys/UI settings must not be copied or staged wholesale. Do not take assets from `main` by assumption, and do not run the rejected `scripts/refactoria_linux.py` or the superseded all-tool draft. + +## Quick path for a future manual replay + +1. Select an **explicit** source tree containing the nine replay files and SHA-256s below. Confirm the destination is the intended Linux account/worktree and use the existing pinned SSH alias only: `ssh -o HostKeyAlias=agent-gentle -o StrictHostKeyChecking=yes -o UpdateHostKeys=no agent-gentle`. +2. Work **one tool at a time** in the order below. Inspect every destination's `lstat` type, mode, owner, SHA-256 or absence; refuse symlinked ancestors and any unexpected baseline. Stage only that tool's source files in a new owned `0700` directory beneath `~/.local/state/refactoria-staging/` and verify source hashes there. +3. Before a target write, create a unique owned `0700` directory beneath `~/.local/state/refactoria-backups/`. Copy each existing exact target as a `0600` file and write `0600` absence markers for targets that do not exist. Check backup hashes, then recheck the entire live baseline. Never print a settings file or its secrets. +4. Generate the candidate for that tool, compare **nonvisual** settings semantically with the backed-up original, parse it and run its isolated checker. Write only approved target paths. Create absent files exclusively (same-directory unique temporary file plus no-clobber link); replace existing files with a same-directory temporary file and rename, preserving the original mode/owner. Recheck immediately before each write. A failed or drifted step stops the tool batch; do not advance automatically. +5. Verify hashes, permissions, parsers, tool-specific runtime checks and a **fresh human visual check** before the next tool. Keep backup and staging intact. To restore, first inspect the live hashes: restore an existing original only when live bytes are the recorded candidate, and remove a newly created file only when its bytes still match the recorded source; skip drifted paths for manual review. Never restore by wildcard or from an unverified backup. A created but now empty `profiles/` or `themes/` directory may intentionally remain after file rollback. + +This procedure is deliberately operator-driven: a script's passing rehearsal does not authorize replacing a different baseline. The current host-specific staged one-off scripts are recovery aids for the **exact** snapshots below, not a portable installer. + +## Approved Linux replay inputs (nine files) + +| Repository path | SHA-256 | +|---|---| +| `herdr/profiles/refactoria.toml` | `db5fb9e9c2cfa2b2c71ec389eaf7dff35596cfeee655740e237013c0d7816e33` | +| `pi/themes/refactoria.json` | `add53fd1744a052af7630508257b0cd6cfc7e53739088370207408f5918347ee` | +| `claude/themes/refactoria.json` | `8f2c3994416a352da5c121ce1831eb65cdbdb59f85324ec8542a2b0489e1020e` | +| `claude/statusline-command.sh` | `8b3524b8f1c9279f19986dbef7be0e3230d9044868d6f21b44b5ad85c40e454c` | +| `opencode/themes/refactoria.json` | `612dd0a82986d8ea9f5478867aed8d6f29e525c3ee37cf2662749a0d2bd6276a` | +| `starship/starship.toml` | `e03def24fa28f3dacf43918f0604e096220f3e02cf4d6c9324c17fd25415acf5` | +| `fish/themes/refactoria.fish` | `3b04a53612cf0c0f76dea3b9e6c08f4485c24a451eac46a332ede9d00b5b9583` | +| `fish/functions/fish_greeting.fish` | `15450023712b8c517cd1203e8de636425c97b669d6f3b9f1af403184ef904f6c` | +| `fish/functions/help-profile.fish` | `b079bdc6110cfb55e129cd84654f93951d014b1a7d817df682df21f904a22626` | + +## Applied destinations and recovery boundaries + +All paths below are relative to `/home/jbarbat`; installed files are regular and owned by uid/gid `1000/1000`. The backup root for each row group is `~/.local/state/refactoria-backups//` (directory `0700`, saved files/markers `0600`). **Absent** means no pre-rollout file existed. The approved after-hashes bind a guided restore; never use a backup for a different target identity. + +| Tool / backup `` | Exact destination | Before | Approved after | +|---|---|---|---| +| Herdr / `herdr-_dvfiir9` | `.config/herdr/config.toml` | `d618349e…bc95ca4` | `cc60b9668a7f162516a94bbee3b23c3e3aa8bcfaebc9e90b6eea5e3e2403b945` (0644) | +| | `.config/herdr/profiles/refactoria.toml` | absent | `db5fb9e9…6e33` (0644) | +| Pi / `pi.oZ39kbUB` | `.pi/agent/themes/refactoria.json` | absent | `add53fd17…347ee` (0644) | +| Claude / `claude.7rr9KBTd` | `.claude/settings.json` | `7fe09aff…2323f6` | `3c4e1bbf8ee61dfd7d957193e92683a599b6da81eddd73b84536dae8dd7338ef` (0644) | +| | `.claude/themes/refactoria.json` | absent | `8f2c3994…e1020e` (0644) | +| | `.claude/statusline-command.sh` | absent | `8b3524b8…e454c` (0755) | +| OpenCode / `opencode.qqD2OtS1` | `.config/opencode/opencode.json` | `2f9a1c89…32b3d` | `1dbba72622cb6459b6c75906fc1d9ac352f1c7917d3c039f4ca079ae23315fd6` (0644) | +| | `.config/opencode/tui.json` | `fe85e04e…cb38c8c` | `c450eaa8533ee5a06570dabeacba164e2f51627ebf9ba10a3863409b42bc1c21` (0644) | +| | `.config/opencode/themes/refactoria.json` | absent | `612dd0a8…6276a` (0644) | +| Starship / `starship.M761Rlpy` | `.config/starship.toml` | `978c7203…fc1db` | `e03def24fa28f3dacf43918f0604e096220f3e02cf4d6c9324c17fd25415acf5` (0644) | +| Fish / `fish.aT7Q5jpk` | `.config/fish/themes/refactoria.fish` | absent | `3b04a536…b9583` (0644) | +| | `.config/fish/conf.d/zz-refactoria-theme.fish` | **`8ebfb078e66c0c0109dbc1708a4060a7f4a93f13fa2494d41164218c89fdedfb`** | `b0cb6463e40437d772afc12e0b3577428d540528eb5ab2dafb706c8b2a086ea8` (0644) | +| | `.config/fish/functions/fish_greeting.fish` | absent | `15450023…4f6c` (0644) | +| | `.config/fish/functions/help-profile.fish` | absent | `b079bdc6…2626` (0644) | + +**Post-rollout Claude drift:** `.claude/settings.json` now has SHA `cea2d41181e38bd439c919b64fa039d73fae6fa99c41525f94ecc1c69120787e`, not its guarded after-hash in the table. The approved `theme`/`statusLine` remain exact, the five original keys still deep-equal the private backup, and `skipDangerousModePermissionPrompt` was added later (cause unknown). **The staged byte-guarded settings rollback must stop on this drift**; do not restore the old JSON over that key. Any desired reversal of the two visual selectors requires a separately backed-up, reviewed semantic merge. + +**Guarded during our applications:** `.config/fish/config.fish` SHA `9d784d88e78620d459d01c8e385851df6f46d675f28fcfe6eb2db3c7facdfeb5` retains host setup. We wrote **14** target files; `.pi/agent/settings.json` was not one of them and remained SHA `3f3b6489…b900` with `Gentleman-Sexy` immediately after our Pi batch. After visual use, its current SHA is `e41d483e74d48a3ff02594ac4b60be09160110a65284f2880a0078cc965f0067` with `theme="refactoria"`. The user explicitly chose to keep this current default. On another host, choosing a saved default is a **separate guarded settings backup/semantic merge**, not implied by copying the theme; the original current-host settings bytes were not backed up by the Pi theme batch. + +## Per-tool decisions and checks + +| Tool | Candidate construction and verification | Visual result | +|---|---|---| +| Herdr 0.9.1 | Use the portable `herdr/profiles/refactoria.toml` as visual input for `scripts/refactoria_herdr.py:plan_visual_merge`; it modifies only `[theme]` name/auto_switch/custom roles and `[ui] accent` in the existing config, preserving unrelated TOML/comment lines including `[[remote]]`. Validate under private `XDG_CONFIG_HOME` with `herdr config check`, then after apply. Copy the same profile separately. The profile and current Mac config have identical merge input roles; do not take Mac-only settings as Linux input. | Approved in a fresh Herdr session. | +| Pi 0.87.1 | Copy the user theme first; `pi --use-theme refactoria` proves the theme without a settings change. The user later selected `refactoria` as the **saved default**. For a future host, separately back up and semantic-merge just the `theme` key if that default is desired, preserving all other settings; do not assume Pi application itself edits the selector. | Approved in a fresh Pi invocation; saved RefactorIA default retained by user choice. | +| Claude Code 2.1.283 | Deep-copy JSON settings; add `theme="custom:refactoria"` and `statusLine={"type":"command","command":"bash ~/.claude/statusline-command.sh"}` only. Preserve all five other keys; copy theme and executable minimal statusline. `bash -n`, `jq` availability and a synthetic 80%/UTF-8 fixture passed. | Approved in a fresh Claude session. | +| OpenCode 1.18.32 | In `opencode.json`, change `theme="system"` to `"refactoria"`; in `tui.json`, add only `theme="refactoria"`. Preserve all other JSON values and copy theme. | Approved in a fresh OpenCode session. | +| Starship 1.26.0 | Replace **only** the recognized `978c…fc1db` baseline with the source TOML. `starship print-config` and `starship prompt` passed against both an isolated candidate and the live config. | Approved in a fresh Fish shell. | +| Fish 4.9.2 | Copy three sources; replace the exact old full-theme activation with the one-line wrapper `source "$HOME/.config/fish/themes/refactoria.fish"`. Preserve `config.fish`. `fish -n` and an isolated sourced greeting/help invocation passed; unavailable helpers are filtered by `type -q`. | Approved in a fresh Fish shell with `help-profile`. | + +## Unfinished delivery and cleanup + +- At the time of host rollout, no commit, push, merge or full installer ran. The user later authorized **scoped work-unit commits only** on the feature branch. Push/PR/merge remain separate decisions. Repository-local dirty work predates this rollout; do not stage it wholesale. +- Private host backups and per-tool staging remain on `agent-gentle` for recovery. The two misplaced Starship rehearsal fixtures at `/home/jbarbat/dev/refactoria-rehearsal-fixture` and `...fixture2` were removed after explicit user approval and fresh exact-tree validation; the real Starship backup/stage and other isolated private-stage fixtures were retained. +- The rejected all-tool auto script and 323-line Herdr transaction draft, plus their two paired tests, were removed as **four exact untracked files** after the user authorized cleanup. Other drafts and generated bytecode were deliberately left untouched. The only executable rollback helpers for this host are the private staged one-off scripts bound to the backup IDs above; first run their preview and compare hashes, then use the explicit confirmation route if recovery is actually needed. diff --git a/odd/tasks/refactoria-reusable-cli-agent-gentle.md b/odd/tasks/refactoria-reusable-cli-agent-gentle.md new file mode 100644 index 0000000..bdea504 --- /dev/null +++ b/odd/tasks/refactoria-reusable-cli-agent-gentle.md @@ -0,0 +1,27 @@ +# Reusable RefactorIA CLI visuals for agent-gentle + +## Goal and authority +The user chose a reproducible visual-only recipe first, then application on `agent-gentle`. The approved `agent-dev` presentation is the reference; each tool keeps its own palette hierarchy. Scope on this Linux/Fish host: Herdr, Pi, Claude Code (including the corrected minimal statusline), OpenCode, Starship and Fish greeting/help/theme. Zsh is absent, so do not install or configure it. Exclude Ghostty, Neovim, SketchyBar, Herdr binary patch, broader packages/services, and the broad `install.sh`. No commits, push or session restarts without a separate decision. Preserve unrelated local and host work. + +## Target discovery and safety boundary +- `agent-gentle` is Linux, login Fish, `/home/jbarbat`; reachable only using the pre-existing SSH host key under `HostKeyAlias=agent-gentle`, with strict checking and no key updates. The initial alias invocation failed because `known_hosts` pins the alias rather than the numeric HostName. No key was accepted or changed. +- No cortex-dotfiles checkout is present in bounded likely locations. All relevant existing configs are regular host files. Pi selects `omarchy-system`, OpenCode main config selects `system`, Claude has no selected theme/statusLine, Herdr has terminal/blue with `herdr 0.8.2`, Starship and Fish have older visual sources; some Fish helper commands in the approved help are unavailable here. No existing symlink is to be replaced without explicit scope review. +- Sources in the current Mac worktree and approved `agent-dev` remote remain uncommitted. Local `main` and `origin/main` advanced together to `0c0c0665`, while this dirty feature worktree remains at `72f93f9d`; `agent-dev` stays at `678d897f`. Main changed Fish core/installer helpers, not the approved RefactorIA theme sources. Do not fetch, merge, switch branches, or infer a deployment source from `main`. Reproducible here means a checked-in-ready Linux visual recipe and source assets that can be rerun from an explicitly selected source tree; a clean clone still requires separately authorized versioning. +- Before any target config write: run isolated tests and a no-write target plan, capture type/mode/hash/absence of every exact destination in a private backup manifest, prewrite recheck, use atomic replacement, and retain bounded rollback. Keep settings data private; report only targeted selectors/roles. Preserve Herdr non-theme values and unrelated JSON settings. Do not deploy if Herdr 0.8.2 rejects the candidate. + +## Tasks +- [x] **T1 — Canonical portable assets.** Repo `claude/statusline-command.sh` matches approved agent-dev SHA `8b3524b8...e454c` (five segments, Linux-safe Unicode and 80% threshold); Fish help filters unavailable helper signatures without altering layout. Independent asset tests, Bash/Fish syntax and diff check passed. Rich `claude/statusline.sh` remains untouched. No commit per user constraint. +- [ ] **T2 — Replace oversized draft with an acotada Linux recipe.** User chose simplification after the general planner/backup/test draft reached 2,113 lines before apply. The first 'narrow' `scripts/refactoria_linux.py` grew to 663 lines plus 558 tests even after repairing verified backup-root, plan-drift, manifest and partial-failure defects; it failed the user's simplification goal despite 29 passing local tests. The obsolete generic draft was removed; the large narrow draft remains untracked and is NOT selected for host execution. A read-only negative-control and candidate check under temporary XDG_CONFIG_HOME proved Herdr 0.8.2 accepts the proposed theme (`config: ok`); the live config stayed untouched. The user selected a short manual backed-up playbook with explicit guided rollback instead of the larger automatic transaction engine. The large narrow draft is not a deployment asset; remove it and its tests only after the manual procedure is documented and safely rehearsed. No target config writes yet. +- [ ] **T3 — Document and rehearse manual recipe.** Record exact 15 destination manifest, source staging, no-write inventory, private backups, per-tool guarded copies/selector merges, parser checks and guided rollback in a concise runbook. A first 360-line draft (`docs/refactoria-agent-gentle-rollout.md`) is explicitly marked UNSAFE/DO NOT RUN: it would reject the actual Fish activation only after earlier writes, widen existing settings modes, fail on absent profile/theme parent dirs, miss some symlink ancestors, and double-process rollback rows. A second shortened draft still failed independent adversarial review: staging-parent symlink escape, predictable write-temp overwrite, composite Claude `statusLine` printed, selector/candidate expectations not enforced, rollback's human gate does not pause and can follow symlinks, and unpreserved mode/owner drift. The document is now explicitly UNSAFE/DO NOT RUN design notes. The user selected a revised manual rollout **one tool at a time** after both all-in-one drafts failed review: stage/plan and private backup, isolated rehearsal, guarded apply, static verification, then stop for a check before the next tool. The unsafe all-in-one document remains DO NOT RUN and must not be used as instructions. Start with Herdr only. For its two paths, a small Herdr-specific command helper was explored, but the first implementation (`scripts/refactoria_herdr_rollout.py`, 323 lines + 190 tests) is **REJECTED / DO NOT RUN** despite 9 passing tests: independent review found a post-replace/pre-journal crash gap, non-resumable partial rollback, and concurrent drift overwrite; it also exceeds the per-tool review budget. Do not stage it on agent-gentle. Replace with a genuinely short exact-path manual backup/apply/restore procedure, rehearsed in isolation and guarded by the two pinned candidate hashes. Subsequent Pi/Claude/OpenCode/Starship/Fish batches each need their own read-only inventory, explicit allowlist, backup and checks. No target config writes before the corresponding per-tool rehearsal, no `install.sh`. Herdr read-only plan (2026-09-27): target config regular 0644 uid/gid 1000/1000 SHA `d618349e3a17b84a530e5c4b41429aae2e98d6fd06601cd59f0386965bc95ca4`, profile absent; approved source hashes `7134a7e7...a158`/`db5fb9e9...6e33`; candidate `cc60b9668a7f162516a94bbee3b23c3e3aa8bcfaebc9e90b6eea5e3e2403b945` matches prior isolated Herdr 0.8.2 check, TOML parses, 87 nonvisual lines and parsed nonvisual subtrees retained, merge idempotent. Focused 8 helper tests and an isolated fixture passed; `python3 -m unittest scripts.test_refactoria_herdr` failed only because the test imports the bare module, while canonical `python3 scripts/test_refactoria_herdr.py` passes. A generic scout gave unsafe false `/tmp`/known_hosts instructions and is not an execution source. No target config writes; backup/apply/restore still pending. +- [ ] **T4 — Pilot on agent-gentle.** Herdr-only phase applied on 2026-09-27: real `~/.config/herdr/config.toml` SHA changed from `d618349e…bc95ca4` to `cc60b966…03b945` (regular 0644 uid/gid1000/1000); new `~/.config/herdr/profiles/refactoria.toml` SHA `db5fb9e9…6e33` regular 0644. Private exact-path backup `/home/jbarbat/.local/state/refactoria-backups/herdr-_dvfiir9/` mode0700, `config-original.toml` and `profile-absent` mode0600. Private staging `/home/jbarbat/.local/state/refactoria-staging/herdr.bu4M9Wm4/` mode0700 contains candidate, profile and a one-host `herdr-manual.py` SHA `704f0310937033076d5587179393829b28978d85f998c075e9b68f88fc843da2`; this is **not** the rejected 323-line repo helper or the unsafe all-tool recipe. The short private helper was rehearsed through backup/apply/rollback in isolated target HOME under `umask 077`, plus partial-state rollback; independently reviewed, with remaining same-user race/power-loss caveats. Prewrite backup/stage/live hashes and modes passed direct readback; `herdr config check` returned `config: ok` before and after apply; direct postwrite SHA/mode/owner/parsed nonvisual preservation checks passed. No other target config files changed; no app restart. **Human visual check of Herdr passed:** the user opened a fresh session and selected “Se ve bien.” Pi may be planned as the next separate guarded tool batch. Fresh read-only Pi inventory found unexpected selector drift: `~/.pi/agent/settings.json` regular0644 uid/gid1000 SHA `3f3b64893878a353f6f55016c8db34f3a02388673d645ead3e5a1815ecd9b900` now has `theme="Gentleman-Sexy"` (previously `omarchy-system`), while `~/.pi/agent/themes/refactoria.json` is absent; Pi version 0.87.1. User selected installing the RefactorIA Pi theme **without changing** `Gentleman-Sexy` default; `pi --use-theme refactoria` is the opt-in invocation. Only `~/.pi/agent/themes/refactoria.json` may be created, guarded by its absent pre-state; settings.json must remain byte-for-byte SHA `3f3b64893878a353f6f55016c8db34f3a02388673d645ead3e5a1815ecd9b900`. Pi opt-in install completed on 2026-09-27: `~/.pi/agent/themes/refactoria.json` regular0644 uid/gid1000/1000 SHA `add53fd1744a052af7630508257b0cd6cfc7e53739088370207408f5918347ee`; `settings.json` remains byte-for-byte SHA `3f3b64893878a353f6f55016c8db34f3a02388673d645ead3e5a1815ecd9b900` with `theme="Gentleman-Sexy"`. Private absent-marker backup `/home/jbarbat/.local/state/refactoria-backups/pi.oZ39kbUB/` mode0700 (`theme-absent` 0600); staging `/home/jbarbat/.local/state/refactoria-staging/pi.D3r5sdH3/` private. Isolated Linux HOME rehearsal passed backup/apply/hash-guarded exact-file removal; independent live static checks passed. Opt in with `pi --use-theme refactoria`; the user ran a fresh invocation and selected “Se ve bien,” closing the Pi visual gate. Claude may be planned next. No existing Pi settings were written. If rollback is needed, exact backup and separately confirmed `rollback` phase in the private staged helper accept only baseline/candidate hashes; it intentionally retains an empty `profiles/` directory after restoring the absent profile. Claude batch applied on 2026-09-27 after separate private HOME full+partial rollback rehearsal: `~/.claude/settings.json` SHA `3c4e1bbf8ee61dfd7d957193e92683a599b6da81eddd73b84536dae8dd7338ef` regular0644 (from backup SHA `7fe09aff…2323f6`), `~/.claude/themes/refactoria.json` SHA `8f2c3994…e1020e` regular0644 and `~/.claude/statusline-command.sh` SHA `8b3524b8…e454c` regular0755. Unrelated five settings keys are deep-equal to backup; selector `custom:refactoria` and `statusLine` command match approved agent-dev setup. Private backup `/home/jbarbat/.local/state/refactoria-backups/claude.7rr9KBTd/` (0700, original settings and absent markers 0600) and stage `/home/jbarbat/.local/state/refactoria-staging/claude.zxE1uNZ7/` retained. Live Bash syntax and UTF-8/80% fixture passed, jq/Bash/Git/locale available. An independent verifier confused SHA256 with SHA3 and missed the theme file; direct parent readback confirmed all three exact paths and hashes. The user opened a fresh Claude session and selected “Se ven bien” for theme and statusline, closing the Claude visual gate; OpenCode may be planned next. OpenCode batch applied on 2026-09-27 after full and partial isolated Linux HOME rollback rehearsal: `~/.config/opencode/opencode.json` regular0644 SHA `1dbba72622cb6459b6c75906fc1d9ac352f1c7917d3c039f4ca079ae23315fd6`, `tui.json` regular0644 SHA `c450eaa8533ee5a06570dabeacba164e2f51627ebf9ba10a3863409b42bc1c21`, new `themes/refactoria.json` regular0644 SHA `612dd0a82986d8ea9f5478867aed8d6f29e525c3ee37cf2662749a0d2bd6276a`; both selectors `theme="refactoria"`, all non-theme keys deep-equal to backup. Private backup `/home/jbarbat/.local/state/refactoria-backups/opencode.qqD2OtS1/` (0700; original configs and absent marker0600), stage `/home/jbarbat/.local/state/refactoria-staging/opencode.bcxQWM2d/` retained. Direct parent postwrite hash/mode/owner/JSON checks passed, no app restart. The user opened a fresh OpenCode session and selected “Se ve bien,” closing the OpenCode visual gate; Starship may be planned next. Starship candidate source SHA `e03def24fa28f3dacf43918f0604e096220f3e02cf4d6c9324c17fd25415acf5` passed TOML parse and Starship 1.26 isolated `print-config`/`prompt` under private stage `/home/jbarbat/.local/state/refactoria-staging/starship.VtNWyyNJ/`; current live config is regular0644 uid/gid1000 SHA `978c72035a6ee71099322ee4797df86f7c2c0d73fab2cd80cfcfb52c67afc1db`. Backup/apply/rollback rehearsal passed, but the verifier placed two 0700 owned fixtures under `/home/jbarbat/dev/refactoria-rehearsal-fixture{,2}` rather than requested stage. Parent direct read-only inspection found only bounded fake HOME config/backup files, no symlinks/foreign owners; **retain, do not `rm -rf`** until explicit cleanup decision. The incident was separately diagnosed and fixtures retained. After fresh strict-SSH stage/live readback, Starship was applied on 2026-09-27: `/home/jbarbat/.config/starship.toml` regular0644 uid/gid1000/1000 SHA `e03def24fa28f3dacf43918f0604e096220f3e02cf4d6c9324c17fd25415acf5`. Private exact-path backup `/home/jbarbat/.local/state/refactoria-backups/starship.M761Rlpy/` (0700; original file0600 SHA `978c72035a6ee71099322ee4797df86f7c2c0d73fab2cd80cfcfb52c67afc1db`, apply-intent0600) and stage retained. Direct parent remote readback confirmed SHA/mode/owner/TOML palette; live `starship print-config` and `starship prompt` exit0. A delegated verifier mistakenly checked the Mac (`/Users/jbarbat`) instead of the explicit Linux SSH target and falsely reported the backup missing; its report was discarded. The user opened a fresh Fish shell and selected “Se ve bien” for the Starship prompt, closing that visual gate; Fish visuals may be planned next. Fish batch applied on 2026-09-27 after corrected baseline gate and isolated full+partial rollback rehearsal: new `~/.config/fish/themes/refactoria.fish` regular0644 SHA `3b04a53612cf0c0f76dea3b9e6c08f4485c24a451eac46a332ede9d00b5b9583`, `functions/fish_greeting.fish` SHA `15450023712b8c517cd1203e8de636425c97b669d6f3b9f1af403184ef904f6c`, `functions/help-profile.fish` SHA `b079bdc6110cfb55e129cd84654f93951d014b1a7d817df682df21f904a22626`, and `conf.d/zz-refactoria-theme.fish` wrapper SHA `b0cb6463e40437d772afc12e0b3577428d540528eb5ab2dafb706c8b2a086ea8` replacing old full-theme SHA **`8ebfb078e66c0c0109dbc1708a4060a7f4a93f13fa2494d41164218c89fdedfb`** (64 hex). `config.fish` remains byte-for-byte SHA `9d784d88e78620d459d01c8e385851df6f46d675f28fcfe6eb2db3c7facdfeb5`. Private backup `/home/jbarbat/.local/state/refactoria-backups/fish.aT7Q5jpk/` mode0700 (old activation and absent markers0600), stage `/home/jbarbat/.local/state/refactoria-staging/fish.PoXzl17V/` retained. Direct strict-SSH postwrite hashes/modes/owner, Fish4.9.2 `fish -n` and no-config sourced greeting/help runtime passed. The user opened a fresh Fish shell and selected “Se ven y funcionan bien” for greeting, colors and `help-profile`, closing the last visual gate. Actual target writes total 14, not 15: Pi `settings.json` was intentionally preserved as `Gentleman-Sexy`. The initial recorded activation SHA carried an erroneous 65th trailing `5`; the first fixture setup failed closed before target writes, then scripts were corrected and re-rehearsed. A concise, nonautomatic 70-line per-tool runbook now replaces the UNSAFE all-tool draft at `docs/refactoria-agent-gentle-rollout.md`; its source SHA table was checked directly. **Pi default follow-up resolved:** our Pi application preserved `settings.json` SHA `3f3b6489…b900` with `Gentleman-Sexy`, but a later read-only audit after fresh visual sessions found current SHA `e41d483e74d48a3ff02594ac4b60be09160110a65284f2880a0078cc965f0067` and `theme="refactoria"`; actor/cause unknown. The user explicitly chose **RefactorIA as the saved default** and no settings write was needed. The runbook distinguishes our 14 guarded target writes from this subsequent host setting. A later direct strict-SSH audit also found `~/.claude/settings.json` now SHA `cea2d41181e38bd439c919b64fa039d73fae6fa99c41525f94ecc1c69120787e`, rather than guarded after SHA `3c4e1bbf…38ef`; approved theme/statusLine remain exact and all five backed-up original keys deep-equal, with only new `skipDangerousModePermissionPrompt` as an additional key. The old byte-guarded rollback must refuse this drift; preserve the extra setting and use a new reviewed semantic merge if visual selectors ever need removal. An independent verifier mistakenly reported four local source files missing from a different cwd and six new Claude keys; direct parent readback corrected both. The user authorized exact-path cleanup: both misplaced `~/dev/refactoria-rehearsal-fixture{,2}` roots were revalidated and removed without touching private actual backups; an initial explicit Python removal stopped after the first fixture's files/inner dirs due incorrect directory ordering, then fresh direct inventory confirmed only two empty child dirs and an intact second fixture before the corrected deepest-first removal completed. A delegated incident report was stale and not used for mutation. Four rejected local untracked drafts (`scripts/refactoria_linux.py`, `scripts/test_refactoria_linux.py`, `scripts/refactoria_herdr_rollout.py`, `scripts/test_refactoria_herdr_rollout.py`) were SHA-guarded and removed. Other drafts/caches/dirty work remain. The user separately authorized **reviewable scoped commits without push**, pending exact-file slice planning and tests. + +## Superseded draft checkpoint +The general planner (`scripts/refactoria_visual.py` and Herdr module) and backup library passed independent isolated tests, including a corrected `[[remote]]` TOML corruption regression. They have **not** applied anything to `agent-gentle`. Keep those local draft files until a tested narrow replacement is ready, then remove only the unused draft modules/tests. The preexisting dirty `install.sh` (Aug29; SHA `86d005...c9e0`) was not touched. + +## Authorized commit slices (no push) +The user authorized scoped work-unit commits after the six visual gates, plus one ~472-line Herdr helper+tests review-budget exception. Planned order: (1) Herdr visual-only hunk of tracked `herdr/config.toml` plus portable profile, leaving unrelated onboarding/keys/status/sound unstaged; (2) Herdr merge helper plus focused tests (exception); (3) Fish theme/greeting/help; (4) Claude theme/minimal statusline plus portable asset checks (also exercises Fish help already present); (5) Pi theme; (6) OpenCode theme; (7) Starship palette; (8) Linux manual runbook and ODD evidence. Verify each index diff and focused checks before committing, record commit IDs here, and never stage unrelated dirty work. The Linux runbook uses **nine** portable replay inputs: the Herdr profile supplies identical visual merge roles without depending on the Mac `herdr/config.toml`'s unrelated local settings. Push/PR/merge remain separate decisions. + +**Commit evidence (feature branch, no push):** `ec1e4ac745b3fb63aeee681817ca184dfca3abfe` Herdr visual-only config/profile (index diff/check clean; nonvisual worktree changes remain); `c9ee5edbfd0d1f03e4a2f30b5896700819052cd4` Herdr helper+191-line focused tests, 473 added lines under the explicit exception (`python3 scripts/test_refactoria_herdr.py` PASS); `c82b2dfd9551bccae5baad2e60e0669e83a82677` Fish theme/greeting/help (`fish -n` and no-config sourced runtime PASS); `918e009631326f9fe29d47240385d28962540f28` Claude theme/statusline plus cross-surface asset test (`python3 scripts/test-refactoria-assets.py`, `bash -n`, JSON parse PASS); `fa852d98f27f5490caf1b1b0e8dafdc253bda0a0` Pi theme (JSON parse PASS); `26141c5e7a7c67d21fe5b1d1e61cb1b0f230d013` OpenCode theme (JSON parse PASS); `7310b896285bc84d202cb5d22011fdf0c623eca5` Starship palette (Starship 1.26 `print-config` PASS). Every commit staged only named paths, passed `git diff --cached --check`, and left the index empty; the manual runbook/task-evidence commit is pending. The no-config Fish runtime check and all six human visual approvals cover live behavior, but a fresh remote clean clone cannot access the branch until the user separately authorizes push. + +## Review and delivery gates +Prefer one serialized writer and independent bounded verification per source unit. Keep each reviewable unit below a reasonable authored diff; if a cohesive unit exceeds 400 authored lines, surface workload before widening scope, not code-golf. Record failed/skipped checks honestly. Implementation outcomes may be checked after observed tests, but work-unit delivery/ODD commit closure remains deferred: commit identities are unavailable because commit/push were not authorized. Do not commit to satisfy a checkbox. The native review switch is off. Neither an on-disk selector nor static validation proves a visible theme; user observation remains the final display check. From 3174df71a42334c3dd680381be50bf98abbf5099 Mon Sep 17 00:00:00 2001 From: Juan Barbat Date: Mon, 28 Sep 2026 12:37:11 -0300 Subject: [PATCH 27/29] docs(odd): close guarded RefactorIA rollout tasks --- odd/tasks/refactoria-reusable-cli-agent-gentle.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/odd/tasks/refactoria-reusable-cli-agent-gentle.md b/odd/tasks/refactoria-reusable-cli-agent-gentle.md index bdea504..1940179 100644 --- a/odd/tasks/refactoria-reusable-cli-agent-gentle.md +++ b/odd/tasks/refactoria-reusable-cli-agent-gentle.md @@ -11,9 +11,10 @@ The user chose a reproducible visual-only recipe first, then application on `age ## Tasks - [x] **T1 — Canonical portable assets.** Repo `claude/statusline-command.sh` matches approved agent-dev SHA `8b3524b8...e454c` (five segments, Linux-safe Unicode and 80% threshold); Fish help filters unavailable helper signatures without altering layout. Independent asset tests, Bash/Fish syntax and diff check passed. Rich `claude/statusline.sh` remains untouched. No commit per user constraint. -- [ ] **T2 — Replace oversized draft with an acotada Linux recipe.** User chose simplification after the general planner/backup/test draft reached 2,113 lines before apply. The first 'narrow' `scripts/refactoria_linux.py` grew to 663 lines plus 558 tests even after repairing verified backup-root, plan-drift, manifest and partial-failure defects; it failed the user's simplification goal despite 29 passing local tests. The obsolete generic draft was removed; the large narrow draft remains untracked and is NOT selected for host execution. A read-only negative-control and candidate check under temporary XDG_CONFIG_HOME proved Herdr 0.8.2 accepts the proposed theme (`config: ok`); the live config stayed untouched. The user selected a short manual backed-up playbook with explicit guided rollback instead of the larger automatic transaction engine. The large narrow draft is not a deployment asset; remove it and its tests only after the manual procedure is documented and safely rehearsed. No target config writes yet. -- [ ] **T3 — Document and rehearse manual recipe.** Record exact 15 destination manifest, source staging, no-write inventory, private backups, per-tool guarded copies/selector merges, parser checks and guided rollback in a concise runbook. A first 360-line draft (`docs/refactoria-agent-gentle-rollout.md`) is explicitly marked UNSAFE/DO NOT RUN: it would reject the actual Fish activation only after earlier writes, widen existing settings modes, fail on absent profile/theme parent dirs, miss some symlink ancestors, and double-process rollback rows. A second shortened draft still failed independent adversarial review: staging-parent symlink escape, predictable write-temp overwrite, composite Claude `statusLine` printed, selector/candidate expectations not enforced, rollback's human gate does not pause and can follow symlinks, and unpreserved mode/owner drift. The document is now explicitly UNSAFE/DO NOT RUN design notes. The user selected a revised manual rollout **one tool at a time** after both all-in-one drafts failed review: stage/plan and private backup, isolated rehearsal, guarded apply, static verification, then stop for a check before the next tool. The unsafe all-in-one document remains DO NOT RUN and must not be used as instructions. Start with Herdr only. For its two paths, a small Herdr-specific command helper was explored, but the first implementation (`scripts/refactoria_herdr_rollout.py`, 323 lines + 190 tests) is **REJECTED / DO NOT RUN** despite 9 passing tests: independent review found a post-replace/pre-journal crash gap, non-resumable partial rollback, and concurrent drift overwrite; it also exceeds the per-tool review budget. Do not stage it on agent-gentle. Replace with a genuinely short exact-path manual backup/apply/restore procedure, rehearsed in isolation and guarded by the two pinned candidate hashes. Subsequent Pi/Claude/OpenCode/Starship/Fish batches each need their own read-only inventory, explicit allowlist, backup and checks. No target config writes before the corresponding per-tool rehearsal, no `install.sh`. Herdr read-only plan (2026-09-27): target config regular 0644 uid/gid 1000/1000 SHA `d618349e3a17b84a530e5c4b41429aae2e98d6fd06601cd59f0386965bc95ca4`, profile absent; approved source hashes `7134a7e7...a158`/`db5fb9e9...6e33`; candidate `cc60b9668a7f162516a94bbee3b23c3e3aa8bcfaebc9e90b6eea5e3e2403b945` matches prior isolated Herdr 0.8.2 check, TOML parses, 87 nonvisual lines and parsed nonvisual subtrees retained, merge idempotent. Focused 8 helper tests and an isolated fixture passed; `python3 -m unittest scripts.test_refactoria_herdr` failed only because the test imports the bare module, while canonical `python3 scripts/test_refactoria_herdr.py` passes. A generic scout gave unsafe false `/tmp`/known_hosts instructions and is not an execution source. No target config writes; backup/apply/restore still pending. -- [ ] **T4 — Pilot on agent-gentle.** Herdr-only phase applied on 2026-09-27: real `~/.config/herdr/config.toml` SHA changed from `d618349e…bc95ca4` to `cc60b966…03b945` (regular 0644 uid/gid1000/1000); new `~/.config/herdr/profiles/refactoria.toml` SHA `db5fb9e9…6e33` regular 0644. Private exact-path backup `/home/jbarbat/.local/state/refactoria-backups/herdr-_dvfiir9/` mode0700, `config-original.toml` and `profile-absent` mode0600. Private staging `/home/jbarbat/.local/state/refactoria-staging/herdr.bu4M9Wm4/` mode0700 contains candidate, profile and a one-host `herdr-manual.py` SHA `704f0310937033076d5587179393829b28978d85f998c075e9b68f88fc843da2`; this is **not** the rejected 323-line repo helper or the unsafe all-tool recipe. The short private helper was rehearsed through backup/apply/rollback in isolated target HOME under `umask 077`, plus partial-state rollback; independently reviewed, with remaining same-user race/power-loss caveats. Prewrite backup/stage/live hashes and modes passed direct readback; `herdr config check` returned `config: ok` before and after apply; direct postwrite SHA/mode/owner/parsed nonvisual preservation checks passed. No other target config files changed; no app restart. **Human visual check of Herdr passed:** the user opened a fresh session and selected “Se ve bien.” Pi may be planned as the next separate guarded tool batch. Fresh read-only Pi inventory found unexpected selector drift: `~/.pi/agent/settings.json` regular0644 uid/gid1000 SHA `3f3b64893878a353f6f55016c8db34f3a02388673d645ead3e5a1815ecd9b900` now has `theme="Gentleman-Sexy"` (previously `omarchy-system`), while `~/.pi/agent/themes/refactoria.json` is absent; Pi version 0.87.1. User selected installing the RefactorIA Pi theme **without changing** `Gentleman-Sexy` default; `pi --use-theme refactoria` is the opt-in invocation. Only `~/.pi/agent/themes/refactoria.json` may be created, guarded by its absent pre-state; settings.json must remain byte-for-byte SHA `3f3b64893878a353f6f55016c8db34f3a02388673d645ead3e5a1815ecd9b900`. Pi opt-in install completed on 2026-09-27: `~/.pi/agent/themes/refactoria.json` regular0644 uid/gid1000/1000 SHA `add53fd1744a052af7630508257b0cd6cfc7e53739088370207408f5918347ee`; `settings.json` remains byte-for-byte SHA `3f3b64893878a353f6f55016c8db34f3a02388673d645ead3e5a1815ecd9b900` with `theme="Gentleman-Sexy"`. Private absent-marker backup `/home/jbarbat/.local/state/refactoria-backups/pi.oZ39kbUB/` mode0700 (`theme-absent` 0600); staging `/home/jbarbat/.local/state/refactoria-staging/pi.D3r5sdH3/` private. Isolated Linux HOME rehearsal passed backup/apply/hash-guarded exact-file removal; independent live static checks passed. Opt in with `pi --use-theme refactoria`; the user ran a fresh invocation and selected “Se ve bien,” closing the Pi visual gate. Claude may be planned next. No existing Pi settings were written. If rollback is needed, exact backup and separately confirmed `rollback` phase in the private staged helper accept only baseline/candidate hashes; it intentionally retains an empty `profiles/` directory after restoring the absent profile. Claude batch applied on 2026-09-27 after separate private HOME full+partial rollback rehearsal: `~/.claude/settings.json` SHA `3c4e1bbf8ee61dfd7d957193e92683a599b6da81eddd73b84536dae8dd7338ef` regular0644 (from backup SHA `7fe09aff…2323f6`), `~/.claude/themes/refactoria.json` SHA `8f2c3994…e1020e` regular0644 and `~/.claude/statusline-command.sh` SHA `8b3524b8…e454c` regular0755. Unrelated five settings keys are deep-equal to backup; selector `custom:refactoria` and `statusLine` command match approved agent-dev setup. Private backup `/home/jbarbat/.local/state/refactoria-backups/claude.7rr9KBTd/` (0700, original settings and absent markers 0600) and stage `/home/jbarbat/.local/state/refactoria-staging/claude.zxE1uNZ7/` retained. Live Bash syntax and UTF-8/80% fixture passed, jq/Bash/Git/locale available. An independent verifier confused SHA256 with SHA3 and missed the theme file; direct parent readback confirmed all three exact paths and hashes. The user opened a fresh Claude session and selected “Se ven bien” for theme and statusline, closing the Claude visual gate; OpenCode may be planned next. OpenCode batch applied on 2026-09-27 after full and partial isolated Linux HOME rollback rehearsal: `~/.config/opencode/opencode.json` regular0644 SHA `1dbba72622cb6459b6c75906fc1d9ac352f1c7917d3c039f4ca079ae23315fd6`, `tui.json` regular0644 SHA `c450eaa8533ee5a06570dabeacba164e2f51627ebf9ba10a3863409b42bc1c21`, new `themes/refactoria.json` regular0644 SHA `612dd0a82986d8ea9f5478867aed8d6f29e525c3ee37cf2662749a0d2bd6276a`; both selectors `theme="refactoria"`, all non-theme keys deep-equal to backup. Private backup `/home/jbarbat/.local/state/refactoria-backups/opencode.qqD2OtS1/` (0700; original configs and absent marker0600), stage `/home/jbarbat/.local/state/refactoria-staging/opencode.bcxQWM2d/` retained. Direct parent postwrite hash/mode/owner/JSON checks passed, no app restart. The user opened a fresh OpenCode session and selected “Se ve bien,” closing the OpenCode visual gate; Starship may be planned next. Starship candidate source SHA `e03def24fa28f3dacf43918f0604e096220f3e02cf4d6c9324c17fd25415acf5` passed TOML parse and Starship 1.26 isolated `print-config`/`prompt` under private stage `/home/jbarbat/.local/state/refactoria-staging/starship.VtNWyyNJ/`; current live config is regular0644 uid/gid1000 SHA `978c72035a6ee71099322ee4797df86f7c2c0d73fab2cd80cfcfb52c67afc1db`. Backup/apply/rollback rehearsal passed, but the verifier placed two 0700 owned fixtures under `/home/jbarbat/dev/refactoria-rehearsal-fixture{,2}` rather than requested stage. Parent direct read-only inspection found only bounded fake HOME config/backup files, no symlinks/foreign owners; **retain, do not `rm -rf`** until explicit cleanup decision. The incident was separately diagnosed and fixtures retained. After fresh strict-SSH stage/live readback, Starship was applied on 2026-09-27: `/home/jbarbat/.config/starship.toml` regular0644 uid/gid1000/1000 SHA `e03def24fa28f3dacf43918f0604e096220f3e02cf4d6c9324c17fd25415acf5`. Private exact-path backup `/home/jbarbat/.local/state/refactoria-backups/starship.M761Rlpy/` (0700; original file0600 SHA `978c72035a6ee71099322ee4797df86f7c2c0d73fab2cd80cfcfb52c67afc1db`, apply-intent0600) and stage retained. Direct parent remote readback confirmed SHA/mode/owner/TOML palette; live `starship print-config` and `starship prompt` exit0. A delegated verifier mistakenly checked the Mac (`/Users/jbarbat`) instead of the explicit Linux SSH target and falsely reported the backup missing; its report was discarded. The user opened a fresh Fish shell and selected “Se ve bien” for the Starship prompt, closing that visual gate; Fish visuals may be planned next. Fish batch applied on 2026-09-27 after corrected baseline gate and isolated full+partial rollback rehearsal: new `~/.config/fish/themes/refactoria.fish` regular0644 SHA `3b04a53612cf0c0f76dea3b9e6c08f4485c24a451eac46a332ede9d00b5b9583`, `functions/fish_greeting.fish` SHA `15450023712b8c517cd1203e8de636425c97b669d6f3b9f1af403184ef904f6c`, `functions/help-profile.fish` SHA `b079bdc6110cfb55e129cd84654f93951d014b1a7d817df682df21f904a22626`, and `conf.d/zz-refactoria-theme.fish` wrapper SHA `b0cb6463e40437d772afc12e0b3577428d540528eb5ab2dafb706c8b2a086ea8` replacing old full-theme SHA **`8ebfb078e66c0c0109dbc1708a4060a7f4a93f13fa2494d41164218c89fdedfb`** (64 hex). `config.fish` remains byte-for-byte SHA `9d784d88e78620d459d01c8e385851df6f46d675f28fcfe6eb2db3c7facdfeb5`. Private backup `/home/jbarbat/.local/state/refactoria-backups/fish.aT7Q5jpk/` mode0700 (old activation and absent markers0600), stage `/home/jbarbat/.local/state/refactoria-staging/fish.PoXzl17V/` retained. Direct strict-SSH postwrite hashes/modes/owner, Fish4.9.2 `fish -n` and no-config sourced greeting/help runtime passed. The user opened a fresh Fish shell and selected “Se ven y funcionan bien” for greeting, colors and `help-profile`, closing the last visual gate. Actual target writes total 14, not 15: Pi `settings.json` was intentionally preserved as `Gentleman-Sexy`. The initial recorded activation SHA carried an erroneous 65th trailing `5`; the first fixture setup failed closed before target writes, then scripts were corrected and re-rehearsed. A concise, nonautomatic 70-line per-tool runbook now replaces the UNSAFE all-tool draft at `docs/refactoria-agent-gentle-rollout.md`; its source SHA table was checked directly. **Pi default follow-up resolved:** our Pi application preserved `settings.json` SHA `3f3b6489…b900` with `Gentleman-Sexy`, but a later read-only audit after fresh visual sessions found current SHA `e41d483e74d48a3ff02594ac4b60be09160110a65284f2880a0078cc965f0067` and `theme="refactoria"`; actor/cause unknown. The user explicitly chose **RefactorIA as the saved default** and no settings write was needed. The runbook distinguishes our 14 guarded target writes from this subsequent host setting. A later direct strict-SSH audit also found `~/.claude/settings.json` now SHA `cea2d41181e38bd439c919b64fa039d73fae6fa99c41525f94ecc1c69120787e`, rather than guarded after SHA `3c4e1bbf…38ef`; approved theme/statusLine remain exact and all five backed-up original keys deep-equal, with only new `skipDangerousModePermissionPrompt` as an additional key. The old byte-guarded rollback must refuse this drift; preserve the extra setting and use a new reviewed semantic merge if visual selectors ever need removal. An independent verifier mistakenly reported four local source files missing from a different cwd and six new Claude keys; direct parent readback corrected both. The user authorized exact-path cleanup: both misplaced `~/dev/refactoria-rehearsal-fixture{,2}` roots were revalidated and removed without touching private actual backups; an initial explicit Python removal stopped after the first fixture's files/inner dirs due incorrect directory ordering, then fresh direct inventory confirmed only two empty child dirs and an intact second fixture before the corrected deepest-first removal completed. A delegated incident report was stale and not used for mutation. Four rejected local untracked drafts (`scripts/refactoria_linux.py`, `scripts/test_refactoria_linux.py`, `scripts/refactoria_herdr_rollout.py`, `scripts/test_refactoria_herdr_rollout.py`) were SHA-guarded and removed. Other drafts/caches/dirty work remain. The user separately authorized **reviewable scoped commits without push**, pending exact-file slice planning and tests. +- [x] **T2 — Replace oversized draft with a bounded Linux recipe.** User chose simplification after the general planner/backup/test draft reached 2,113 lines before apply. The first 'narrow' `scripts/refactoria_linux.py` grew to 663 lines plus 558 tests even after repairing verified backup-root, plan-drift, manifest and partial-failure defects; it failed the user's simplification goal despite 29 passing local tests. The obsolete generic draft was removed; the large narrow draft was never selected for host execution and was later removed after explicit authorization. A read-only negative-control and candidate check under temporary XDG_CONFIG_HOME proved Herdr 0.8.2 accepts the proposed theme (`config: ok`); the live config stayed untouched. The user selected a short manual backed-up playbook with explicit guided rollback instead of the larger automatic transaction engine. The large narrow draft is not a deployment asset; remove it and its tests only after the manual procedure is documented and safely rehearsed. No target config writes yet. +- [x] **T3 — Document and rehearse manual recipe.** Record the 14 guarded target writes and the separately selected Pi default, source staging, no-write inventory, private backups, per-tool guarded copies/selector merges, parser checks and guided rollback in a concise runbook. A first 360-line draft (`docs/refactoria-agent-gentle-rollout.md`) is explicitly marked UNSAFE/DO NOT RUN: it would reject the actual Fish activation only after earlier writes, widen existing settings modes, fail on absent profile/theme parent dirs, miss some symlink ancestors, and double-process rollback rows. A second shortened draft still failed independent adversarial review: staging-parent symlink escape, predictable write-temp overwrite, composite Claude `statusLine` printed, selector/candidate expectations not enforced, rollback's human gate does not pause and can follow symlinks, and unpreserved mode/owner drift. The document is now explicitly UNSAFE/DO NOT RUN design notes. The user selected a revised manual rollout **one tool at a time** after both all-in-one drafts failed review: stage/plan and private backup, isolated rehearsal, guarded apply, static verification, then stop for a check before the next tool. The unsafe all-in-one document remains DO NOT RUN and must not be used as instructions. Start with Herdr only. For its two paths, a small Herdr-specific command helper was explored, but the first implementation (`scripts/refactoria_herdr_rollout.py`, 323 lines + 190 tests) is **REJECTED / DO NOT RUN** despite 9 passing tests: independent review found a post-replace/pre-journal crash gap, non-resumable partial rollback, and concurrent drift overwrite; it also exceeds the per-tool review budget. Do not stage it on agent-gentle. Replace with a genuinely short exact-path manual backup/apply/restore procedure, rehearsed in isolation and guarded by the two pinned candidate hashes. Subsequent Pi/Claude/OpenCode/Starship/Fish batches each need their own read-only inventory, explicit allowlist, backup and checks. No target config writes before the corresponding per-tool rehearsal, no `install.sh`. Herdr read-only plan (2026-09-27): target config regular 0644 uid/gid 1000/1000 SHA `d618349e3a17b84a530e5c4b41429aae2e98d6fd06601cd59f0386965bc95ca4`, profile absent; approved source hashes `7134a7e7...a158`/`db5fb9e9...6e33`; candidate `cc60b9668a7f162516a94bbee3b23c3e3aa8bcfaebc9e90b6eea5e3e2403b945` matches prior isolated Herdr 0.8.2 check, TOML parses, 87 nonvisual lines and parsed nonvisual subtrees retained, merge idempotent. Focused 8 helper tests and an isolated fixture passed; `python3 -m unittest scripts.test_refactoria_herdr` failed only because the test imports the bare module, while canonical `python3 scripts/test_refactoria_herdr.py` passes. A generic scout gave unsafe false `/tmp`/known_hosts instructions and is not an execution source. No target config writes; backup/apply/restore still pending. + **T3 closure:** the unsafe drafts were superseded by `docs/refactoria-agent-gentle-rollout.md` (manual 71-line per-tool record); all six tool batches were independently rehearsed and backed up, checked statically, then approved visually. Earlier DO NOT RUN references above describe historical rejected revisions, not the committed guide. +- [x] **T4 — Pilot on agent-gentle.** Herdr-only phase applied on 2026-09-27: real `~/.config/herdr/config.toml` SHA changed from `d618349e…bc95ca4` to `cc60b966…03b945` (regular 0644 uid/gid1000/1000); new `~/.config/herdr/profiles/refactoria.toml` SHA `db5fb9e9…6e33` regular 0644. Private exact-path backup `/home/jbarbat/.local/state/refactoria-backups/herdr-_dvfiir9/` mode0700, `config-original.toml` and `profile-absent` mode0600. Private staging `/home/jbarbat/.local/state/refactoria-staging/herdr.bu4M9Wm4/` mode0700 contains candidate, profile and a one-host `herdr-manual.py` SHA `704f0310937033076d5587179393829b28978d85f998c075e9b68f88fc843da2`; this is **not** the rejected 323-line repo helper or the unsafe all-tool recipe. The short private helper was rehearsed through backup/apply/rollback in isolated target HOME under `umask 077`, plus partial-state rollback; independently reviewed, with remaining same-user race/power-loss caveats. Prewrite backup/stage/live hashes and modes passed direct readback; `herdr config check` returned `config: ok` before and after apply; direct postwrite SHA/mode/owner/parsed nonvisual preservation checks passed. No other target config files changed; no app restart. **Human visual check of Herdr passed:** the user opened a fresh session and selected “Se ve bien.” Pi may be planned as the next separate guarded tool batch. Fresh read-only Pi inventory found unexpected selector drift: `~/.pi/agent/settings.json` regular0644 uid/gid1000 SHA `3f3b64893878a353f6f55016c8db34f3a02388673d645ead3e5a1815ecd9b900` now has `theme="Gentleman-Sexy"` (previously `omarchy-system`), while `~/.pi/agent/themes/refactoria.json` is absent; Pi version 0.87.1. User selected installing the RefactorIA Pi theme **without changing** `Gentleman-Sexy` default; `pi --use-theme refactoria` is the opt-in invocation. Only `~/.pi/agent/themes/refactoria.json` may be created, guarded by its absent pre-state; settings.json must remain byte-for-byte SHA `3f3b64893878a353f6f55016c8db34f3a02388673d645ead3e5a1815ecd9b900`. Pi opt-in install completed on 2026-09-27: `~/.pi/agent/themes/refactoria.json` regular0644 uid/gid1000/1000 SHA `add53fd1744a052af7630508257b0cd6cfc7e53739088370207408f5918347ee`; `settings.json` remains byte-for-byte SHA `3f3b64893878a353f6f55016c8db34f3a02388673d645ead3e5a1815ecd9b900` with `theme="Gentleman-Sexy"`. Private absent-marker backup `/home/jbarbat/.local/state/refactoria-backups/pi.oZ39kbUB/` mode0700 (`theme-absent` 0600); staging `/home/jbarbat/.local/state/refactoria-staging/pi.D3r5sdH3/` private. Isolated Linux HOME rehearsal passed backup/apply/hash-guarded exact-file removal; independent live static checks passed. Opt in with `pi --use-theme refactoria`; the user ran a fresh invocation and selected “Se ve bien,” closing the Pi visual gate. Claude may be planned next. No existing Pi settings were written. If rollback is needed, exact backup and separately confirmed `rollback` phase in the private staged helper accept only baseline/candidate hashes; it intentionally retains an empty `profiles/` directory after restoring the absent profile. Claude batch applied on 2026-09-27 after separate private HOME full+partial rollback rehearsal: `~/.claude/settings.json` SHA `3c4e1bbf8ee61dfd7d957193e92683a599b6da81eddd73b84536dae8dd7338ef` regular0644 (from backup SHA `7fe09aff…2323f6`), `~/.claude/themes/refactoria.json` SHA `8f2c3994…e1020e` regular0644 and `~/.claude/statusline-command.sh` SHA `8b3524b8…e454c` regular0755. Unrelated five settings keys are deep-equal to backup; selector `custom:refactoria` and `statusLine` command match approved agent-dev setup. Private backup `/home/jbarbat/.local/state/refactoria-backups/claude.7rr9KBTd/` (0700, original settings and absent markers 0600) and stage `/home/jbarbat/.local/state/refactoria-staging/claude.zxE1uNZ7/` retained. Live Bash syntax and UTF-8/80% fixture passed, jq/Bash/Git/locale available. An independent verifier confused SHA256 with SHA3 and missed the theme file; direct parent readback confirmed all three exact paths and hashes. The user opened a fresh Claude session and selected “Se ven bien” for theme and statusline, closing the Claude visual gate; OpenCode may be planned next. OpenCode batch applied on 2026-09-27 after full and partial isolated Linux HOME rollback rehearsal: `~/.config/opencode/opencode.json` regular0644 SHA `1dbba72622cb6459b6c75906fc1d9ac352f1c7917d3c039f4ca079ae23315fd6`, `tui.json` regular0644 SHA `c450eaa8533ee5a06570dabeacba164e2f51627ebf9ba10a3863409b42bc1c21`, new `themes/refactoria.json` regular0644 SHA `612dd0a82986d8ea9f5478867aed8d6f29e525c3ee37cf2662749a0d2bd6276a`; both selectors `theme="refactoria"`, all non-theme keys deep-equal to backup. Private backup `/home/jbarbat/.local/state/refactoria-backups/opencode.qqD2OtS1/` (0700; original configs and absent marker0600), stage `/home/jbarbat/.local/state/refactoria-staging/opencode.bcxQWM2d/` retained. Direct parent postwrite hash/mode/owner/JSON checks passed, no app restart. The user opened a fresh OpenCode session and selected “Se ve bien,” closing the OpenCode visual gate; Starship may be planned next. Starship candidate source SHA `e03def24fa28f3dacf43918f0604e096220f3e02cf4d6c9324c17fd25415acf5` passed TOML parse and Starship 1.26 isolated `print-config`/`prompt` under private stage `/home/jbarbat/.local/state/refactoria-staging/starship.VtNWyyNJ/`; current live config is regular0644 uid/gid1000 SHA `978c72035a6ee71099322ee4797df86f7c2c0d73fab2cd80cfcfb52c67afc1db`. Backup/apply/rollback rehearsal passed, but the verifier placed two 0700 owned fixtures under `/home/jbarbat/dev/refactoria-rehearsal-fixture{,2}` rather than requested stage. Parent direct read-only inspection found only bounded fake HOME config/backup files, no symlinks/foreign owners; **retain, do not `rm -rf`** until explicit cleanup decision. The incident was separately diagnosed and fixtures retained. After fresh strict-SSH stage/live readback, Starship was applied on 2026-09-27: `/home/jbarbat/.config/starship.toml` regular0644 uid/gid1000/1000 SHA `e03def24fa28f3dacf43918f0604e096220f3e02cf4d6c9324c17fd25415acf5`. Private exact-path backup `/home/jbarbat/.local/state/refactoria-backups/starship.M761Rlpy/` (0700; original file0600 SHA `978c72035a6ee71099322ee4797df86f7c2c0d73fab2cd80cfcfb52c67afc1db`, apply-intent0600) and stage retained. Direct parent remote readback confirmed SHA/mode/owner/TOML palette; live `starship print-config` and `starship prompt` exit0. A delegated verifier mistakenly checked the Mac (`/Users/jbarbat`) instead of the explicit Linux SSH target and falsely reported the backup missing; its report was discarded. The user opened a fresh Fish shell and selected “Se ve bien” for the Starship prompt, closing that visual gate; Fish visuals may be planned next. Fish batch applied on 2026-09-27 after corrected baseline gate and isolated full+partial rollback rehearsal: new `~/.config/fish/themes/refactoria.fish` regular0644 SHA `3b04a53612cf0c0f76dea3b9e6c08f4485c24a451eac46a332ede9d00b5b9583`, `functions/fish_greeting.fish` SHA `15450023712b8c517cd1203e8de636425c97b669d6f3b9f1af403184ef904f6c`, `functions/help-profile.fish` SHA `b079bdc6110cfb55e129cd84654f93951d014b1a7d817df682df21f904a22626`, and `conf.d/zz-refactoria-theme.fish` wrapper SHA `b0cb6463e40437d772afc12e0b3577428d540528eb5ab2dafb706c8b2a086ea8` replacing old full-theme SHA **`8ebfb078e66c0c0109dbc1708a4060a7f4a93f13fa2494d41164218c89fdedfb`** (64 hex). `config.fish` remains byte-for-byte SHA `9d784d88e78620d459d01c8e385851df6f46d675f28fcfe6eb2db3c7facdfeb5`. Private backup `/home/jbarbat/.local/state/refactoria-backups/fish.aT7Q5jpk/` mode0700 (old activation and absent markers0600), stage `/home/jbarbat/.local/state/refactoria-staging/fish.PoXzl17V/` retained. Direct strict-SSH postwrite hashes/modes/owner, Fish4.9.2 `fish -n` and no-config sourced greeting/help runtime passed. The user opened a fresh Fish shell and selected “Se ven y funcionan bien” for greeting, colors and `help-profile`, closing the last visual gate. Actual target writes total 14, not 15: Pi `settings.json` was intentionally preserved as `Gentleman-Sexy`. The initial recorded activation SHA carried an erroneous 65th trailing `5`; the first fixture setup failed closed before target writes, then scripts were corrected and re-rehearsed. A concise, nonautomatic 70-line per-tool runbook now replaces the UNSAFE all-tool draft at `docs/refactoria-agent-gentle-rollout.md`; its source SHA table was checked directly. **Pi default follow-up resolved:** our Pi application preserved `settings.json` SHA `3f3b6489…b900` with `Gentleman-Sexy`, but a later read-only audit after fresh visual sessions found current SHA `e41d483e74d48a3ff02594ac4b60be09160110a65284f2880a0078cc965f0067` and `theme="refactoria"`; actor/cause unknown. The user explicitly chose **RefactorIA as the saved default** and no settings write was needed. The runbook distinguishes our 14 guarded target writes from this subsequent host setting. A later direct strict-SSH audit also found `~/.claude/settings.json` now SHA `cea2d41181e38bd439c919b64fa039d73fae6fa99c41525f94ecc1c69120787e`, rather than guarded after SHA `3c4e1bbf…38ef`; approved theme/statusLine remain exact and all five backed-up original keys deep-equal, with only new `skipDangerousModePermissionPrompt` as an additional key. The old byte-guarded rollback must refuse this drift; preserve the extra setting and use a new reviewed semantic merge if visual selectors ever need removal. An independent verifier mistakenly reported four local source files missing from a different cwd and six new Claude keys; direct parent readback corrected both. The user authorized exact-path cleanup: both misplaced `~/dev/refactoria-rehearsal-fixture{,2}` roots were revalidated and removed without touching private actual backups; an initial explicit Python removal stopped after the first fixture's files/inner dirs due incorrect directory ordering, then fresh direct inventory confirmed only two empty child dirs and an intact second fixture before the corrected deepest-first removal completed. A delegated incident report was stale and not used for mutation. Four rejected local untracked drafts (`scripts/refactoria_linux.py`, `scripts/test_refactoria_linux.py`, `scripts/refactoria_herdr_rollout.py`, `scripts/test_refactoria_herdr_rollout.py`) were SHA-guarded and removed. Other drafts/caches/dirty work remain. The user separately authorized **reviewable scoped commits without push**, pending exact-file slice planning and tests. ## Superseded draft checkpoint The general planner (`scripts/refactoria_visual.py` and Herdr module) and backup library passed independent isolated tests, including a corrected `[[remote]]` TOML corruption regression. They have **not** applied anything to `agent-gentle`. Keep those local draft files until a tested narrow replacement is ready, then remove only the unused draft modules/tests. The preexisting dirty `install.sh` (Aug29; SHA `86d005...c9e0`) was not touched. @@ -21,7 +22,7 @@ The general planner (`scripts/refactoria_visual.py` and Herdr module) and backup ## Authorized commit slices (no push) The user authorized scoped work-unit commits after the six visual gates, plus one ~472-line Herdr helper+tests review-budget exception. Planned order: (1) Herdr visual-only hunk of tracked `herdr/config.toml` plus portable profile, leaving unrelated onboarding/keys/status/sound unstaged; (2) Herdr merge helper plus focused tests (exception); (3) Fish theme/greeting/help; (4) Claude theme/minimal statusline plus portable asset checks (also exercises Fish help already present); (5) Pi theme; (6) OpenCode theme; (7) Starship palette; (8) Linux manual runbook and ODD evidence. Verify each index diff and focused checks before committing, record commit IDs here, and never stage unrelated dirty work. The Linux runbook uses **nine** portable replay inputs: the Herdr profile supplies identical visual merge roles without depending on the Mac `herdr/config.toml`'s unrelated local settings. Push/PR/merge remain separate decisions. -**Commit evidence (feature branch, no push):** `ec1e4ac745b3fb63aeee681817ca184dfca3abfe` Herdr visual-only config/profile (index diff/check clean; nonvisual worktree changes remain); `c9ee5edbfd0d1f03e4a2f30b5896700819052cd4` Herdr helper+191-line focused tests, 473 added lines under the explicit exception (`python3 scripts/test_refactoria_herdr.py` PASS); `c82b2dfd9551bccae5baad2e60e0669e83a82677` Fish theme/greeting/help (`fish -n` and no-config sourced runtime PASS); `918e009631326f9fe29d47240385d28962540f28` Claude theme/statusline plus cross-surface asset test (`python3 scripts/test-refactoria-assets.py`, `bash -n`, JSON parse PASS); `fa852d98f27f5490caf1b1b0e8dafdc253bda0a0` Pi theme (JSON parse PASS); `26141c5e7a7c67d21fe5b1d1e61cb1b0f230d013` OpenCode theme (JSON parse PASS); `7310b896285bc84d202cb5d22011fdf0c623eca5` Starship palette (Starship 1.26 `print-config` PASS). Every commit staged only named paths, passed `git diff --cached --check`, and left the index empty; the manual runbook/task-evidence commit is pending. The no-config Fish runtime check and all six human visual approvals cover live behavior, but a fresh remote clean clone cannot access the branch until the user separately authorizes push. +**Commit evidence (feature branch, no push):** `ec1e4ac745b3fb63aeee681817ca184dfca3abfe` Herdr visual-only config/profile (index diff/check clean; nonvisual worktree changes remain); `c9ee5edbfd0d1f03e4a2f30b5896700819052cd4` Herdr helper+191-line focused tests, 473 added lines under the explicit exception (`python3 scripts/test_refactoria_herdr.py` PASS); `c82b2dfd9551bccae5baad2e60e0669e83a82677` Fish theme/greeting/help (`fish -n` and no-config sourced runtime PASS); `918e009631326f9fe29d47240385d28962540f28` Claude theme/statusline plus cross-surface asset test (`python3 scripts/test-refactoria-assets.py`, `bash -n`, JSON parse PASS); `fa852d98f27f5490caf1b1b0e8dafdc253bda0a0` Pi theme (JSON parse PASS); `26141c5e7a7c67d21fe5b1d1e61cb1b0f230d013` OpenCode theme (JSON parse PASS); `7310b896285bc84d202cb5d22011fdf0c623eca5` Starship palette (Starship 1.26 `print-config` PASS). Every commit staged only named paths, passed `git diff --cached --check`, and left the index empty; `61f1082238af06dd06f4e6579f01d78e0087c4d6` manual runbook/task evidence (nine committed replay SHA checks PASS). A clean `git archive` of that HEAD passed Herdr and asset tests, Fish/Bash syntax, JSON/TOML parses and Starship print-config; direct strict-SSH readback confirmed 13 byte-pinned visual files, both app selectors, six private backup roots and absence of both misplaced fixtures. The final task checkbox reconciliation follows in its own evidence-only commit. The no-config Fish runtime check and all six human visual approvals cover live behavior, but a fresh remote clean clone cannot access the branch until the user separately authorizes push. ## Review and delivery gates -Prefer one serialized writer and independent bounded verification per source unit. Keep each reviewable unit below a reasonable authored diff; if a cohesive unit exceeds 400 authored lines, surface workload before widening scope, not code-golf. Record failed/skipped checks honestly. Implementation outcomes may be checked after observed tests, but work-unit delivery/ODD commit closure remains deferred: commit identities are unavailable because commit/push were not authorized. Do not commit to satisfy a checkbox. The native review switch is off. Neither an on-disk selector nor static validation proves a visible theme; user observation remains the final display check. +Prefer one serialized writer and independent bounded verification per source unit. Keep each reviewable unit below a reasonable authored diff; if a cohesive unit exceeds 400 authored lines, surface workload before widening scope, not code-golf. Record failed/skipped checks honestly. All four task outcomes, focused checks and eight scoped work-unit commit identities are recorded above after the user's explicit commit authorization; no push/PR/merge was authorized. Do not stage the remaining dirty work to satisfy a checkbox. The native review switch is off. Neither an on-disk selector nor static validation proves a visible theme; user observation remains the final display check. From a1a47212b4c681ca820ee56f9a0c9a2004b2b161 Mon Sep 17 00:00:00 2001 From: Juan Barbat Date: Thu, 8 Oct 2026 12:23:09 -0300 Subject: [PATCH 28/29] fix(navigation): graceful ancestor fallback and simplified workspace targets - _go_first_existing_dir/_go_dev_dir now fall back to the nearest existing ancestor with a notice instead of hard-failing on hosts that lack part of the workspace layout (Linux agents). - innit/work/cowork default to ~/dev/innit-sas (real workspace) instead of the nonexistent ~/dev/barbatdev/innit on every host. - new commands: refactoria -> $REFACTORIA_DIR (~/dev/refactoria) and gentle -> $GENTLE_DIR (~/dev/gentle). - zsh: rename the RefactorIA logo command to refactoria-logo to free the refactoria name for navigation. - tests updated to the new contract. --- fish/conf.d/10-core.fish | 12 +++++-- fish/functions/_go_dev_dir.fish | 7 +--- fish/functions/_go_first_existing_dir.fish | 12 +++++++ fish/functions/cowork.fish | 2 +- fish/functions/gentle.fish | 4 +++ fish/functions/innit.fish | 2 +- fish/functions/refactoria.fish | 4 +++ fish/functions/work.fish | 2 +- fish/tests/w2-core.fish | 42 ++++++++++++---------- zsh/zshrc | 38 +++++++++++++------- 10 files changed, 83 insertions(+), 42 deletions(-) create mode 100644 fish/functions/gentle.fish create mode 100644 fish/functions/refactoria.fish diff --git a/fish/conf.d/10-core.fish b/fish/conf.d/10-core.fish index 1b9054a..ea1bef7 100644 --- a/fish/conf.d/10-core.fish +++ b/fish/conf.d/10-core.fish @@ -20,13 +20,21 @@ if not set -q CORTEX_MULTIPLEXER; or test -z "$CORTEX_MULTIPLEXER" end set -gx CLAUDE_CODE_EFFORT_LEVEL high if not set -q WORK_PROJECTS_DIR; or test -z "$WORK_PROJECTS_DIR" - set -gx WORK_PROJECTS_DIR "$BARBATDEV_DIR/innit" + set -gx WORK_PROJECTS_DIR "$WORKSPACE_DIR/innit-sas" end if not set -q PERSONAL_PROJECTS_DIR; or test -z "$PERSONAL_PROJECTS_DIR" set -gx PERSONAL_PROJECTS_DIR "$BARBATDEV_DIR/products" end if not set -q INNIT_DIR; or test -z "$INNIT_DIR" - set -gx INNIT_DIR "$BARBATDEV_DIR/innit" + set -gx INNIT_DIR "$WORKSPACE_DIR/innit-sas" +end + +if not set -q REFACTORIA_DIR; or test -z "$REFACTORIA_DIR" + set -gx REFACTORIA_DIR "$WORKSPACE_DIR/refactoria" +end + +if not set -q GENTLE_DIR; or test -z "$GENTLE_DIR" + set -gx GENTLE_DIR "$WORKSPACE_DIR/gentle" end if not set -q SCREENSHOTS_DIR; or test -z "$SCREENSHOTS_DIR" set -gx SCREENSHOTS_DIR "$HOME/Screenshots" diff --git a/fish/functions/_go_dev_dir.fish b/fish/functions/_go_dev_dir.fish index b12d0e3..2b9a7ad 100644 --- a/fish/functions/_go_dev_dir.fish +++ b/fish/functions/_go_dev_dir.fish @@ -1,8 +1,3 @@ function _go_dev_dir --argument-names target - if test -d "$target" - cd "$target" - return 0 - end - printf 'Directory not found: %s\n' "$target" >&2 - return 1 + _go_first_existing_dir "$target" end diff --git a/fish/functions/_go_first_existing_dir.fish b/fish/functions/_go_first_existing_dir.fish index 47f0e94..446741e 100644 --- a/fish/functions/_go_first_existing_dir.fish +++ b/fish/functions/_go_first_existing_dir.fish @@ -5,6 +5,18 @@ function _go_first_existing_dir return 0 end end + # No candidate exists: walk up to the nearest existing ancestor of the + # first target so navigation commands degrade gracefully on hosts that + # do not have this part of the workspace layout. + set -l probe "$argv[1]" + while test -n "$probe"; and not test -d "$probe" + set probe (dirname "$probe") + end + if test -n "$probe"; and test -d "$probe" + printf 'Directory not found: %s — nearest existing: %s\n' "$argv[1]" "$probe" >&2 + cd "$probe" + return 0 + end printf 'Directory not found: %s\n' "$argv[1]" >&2 return 1 end diff --git a/fish/functions/cowork.fish b/fish/functions/cowork.fish index 42fda9d..03f87a2 100644 --- a/fish/functions/cowork.fish +++ b/fish/functions/cowork.fish @@ -1,4 +1,4 @@ # Usage: cowork — change to the work projects directory. function cowork - _go_first_existing_dir "$WORK_PROJECTS_DIR" "$BARBATDEV_DIR/innit" + _go_first_existing_dir "$WORK_PROJECTS_DIR" "$WORKSPACE_DIR/innit-sas" end diff --git a/fish/functions/gentle.fish b/fish/functions/gentle.fish new file mode 100644 index 0000000..37bfa91 --- /dev/null +++ b/fish/functions/gentle.fish @@ -0,0 +1,4 @@ +# Usage: gentle — change to the Gentle workspace. +function gentle + _go_dev_dir "$GENTLE_DIR" +end diff --git a/fish/functions/innit.fish b/fish/functions/innit.fish index 96815c9..33affbe 100644 --- a/fish/functions/innit.fish +++ b/fish/functions/innit.fish @@ -1,4 +1,4 @@ # Usage: innit — change to the first available InnIT directory. function innit - _go_first_existing_dir "$INNIT_DIR" "$BARBATDEV_DIR/innit" + _go_first_existing_dir "$INNIT_DIR" "$WORKSPACE_DIR/innit-sas" end diff --git a/fish/functions/refactoria.fish b/fish/functions/refactoria.fish new file mode 100644 index 0000000..022c59b --- /dev/null +++ b/fish/functions/refactoria.fish @@ -0,0 +1,4 @@ +# Usage: refactoria — change to the RefactorIA workspace. +function refactoria + _go_dev_dir "$REFACTORIA_DIR" +end diff --git a/fish/functions/work.fish b/fish/functions/work.fish index 3b11466..23c1c6e 100644 --- a/fish/functions/work.fish +++ b/fish/functions/work.fish @@ -1,4 +1,4 @@ # Usage: work — change to the first available work projects directory. function work - _go_first_existing_dir "$WORK_PROJECTS_DIR" "$BARBATDEV_DIR/innit" + _go_first_existing_dir "$WORK_PROJECTS_DIR" "$WORKSPACE_DIR/innit-sas" end diff --git a/fish/tests/w2-core.fish b/fish/tests/w2-core.fish index 5bea673..d507628 100644 --- a/fish/tests/w2-core.fish +++ b/fish/tests/w2-core.fish @@ -34,49 +34,53 @@ function run_fish env -i HOME="$home" USER=test-user LOGNAME=test-user XDG_CONFIG_HOME="$xdg" PATH="$bin:/usr/bin:/bin" TERM=xterm-256color LANG=C "$fish_bin" $argv end -function assert_navigation_error +function assert_navigation_fallback set -l command "$argv[1]" - set -l expected "$argv[2]" - set -l description "$argv[3]" - - run_fish -c "set -gx fish_function_path '$functions_dir' \$fish_function_path; $command" >/dev/null 2>"$workspace/error"; and fail "$description must fail" - grep -Fq "Directory not found: $expected" "$workspace/error"; or fail "$description error message" + set -l missing "$argv[2]" + set -l expected_pwd "$argv[3]" + set -l description "$argv[4]" + + set -l pwd_out (run_fish -c "set -gx fish_function_path '$functions_dir' \$fish_function_path; $command; pwd" | string collect) + test "$pwd_out" = "$expected_pwd"; or fail "$description must land on nearest existing ancestor (expected '$expected_pwd', got '$pwd_out')" + run_fish -c "set -gx fish_function_path '$functions_dir' \$fish_function_path; $command" 2>"$workspace/error"; and true + grep -Fq "Directory not found: $missing" "$workspace/error"; or fail "$description must warn about the missing directory" + grep -Fq "nearest existing" "$workspace/error"; or fail "$description must name the nearest existing ancestor" end cp "$core" "$xdg/fish/conf.d/10-core.fish" set -l defaults (run_fish -c 'string join "|" $WORKSPACE_DIR $BARBATDEV_DIR $CORTEX_HOME $CORTEX_ROOT $CORTEX_DOTFILES_DIR $CORTEX_MULTIPLEXER $CLAUDE_CODE_EFFORT_LEVEL $WORK_PROJECTS_DIR $PERSONAL_PROJECTS_DIR $INNIT_DIR $SCREENSHOTS_DIR $EDITOR $VISUAL') -assert_equal "$defaults" "$home/dev|$home/dev/barbatdev|$home/.cortex|$home/dev/barbatdev/cortex/cortex|$home/dev/barbatdev/cortex/cortex-dotfiles|cmux|high|$home/dev/barbatdev/innit|$home/dev/barbatdev/products|$home/dev/barbatdev/innit|$home/Screenshots|$bin/nvim|$bin/nvim" 'defaults and editor selection' +assert_equal "$defaults" "$home/dev|$home/dev/barbatdev|$home/.cortex|$home/dev/barbatdev/cortex/cortex|$home/dev/barbatdev/cortex/cortex-dotfiles|cmux|high|$home/dev/innit-sas|$home/dev/barbatdev/products|$home/dev/innit-sas|$home/Screenshots|$bin/nvim|$bin/nvim" 'defaults and editor selection' set -l empty_defaults (env -i HOME="$home" USER=test-user LOGNAME=test-user XDG_CONFIG_HOME="$xdg" PATH="$bin:/usr/bin:/bin" TERM=xterm-256color LANG=C WORKSPACE_DIR= BARBATDEV_DIR= CORTEX_HOME= CORTEX_ROOT= CORTEX_DOTFILES_DIR= CORTEX_MULTIPLEXER= CLAUDE_CODE_EFFORT_LEVEL= WORK_PROJECTS_DIR= PERSONAL_PROJECTS_DIR= INNIT_DIR= SCREENSHOTS_DIR= "$fish_bin" -c 'string join "|" $WORKSPACE_DIR $BARBATDEV_DIR $CORTEX_HOME $CORTEX_ROOT $CORTEX_DOTFILES_DIR $CORTEX_MULTIPLEXER $CLAUDE_CODE_EFFORT_LEVEL $WORK_PROJECTS_DIR $PERSONAL_PROJECTS_DIR $INNIT_DIR $SCREENSHOTS_DIR') -assert_equal "$empty_defaults" "$home/dev|$home/dev/barbatdev|$home/.cortex|$home/dev/barbatdev/cortex/cortex|$home/dev/barbatdev/cortex/cortex-dotfiles|cmux|high|$home/dev/barbatdev/innit|$home/dev/barbatdev/products|$home/dev/barbatdev/innit|$home/Screenshots" 'empty environment values use defaults' +assert_equal "$empty_defaults" "$home/dev|$home/dev/barbatdev|$home/.cortex|$home/dev/barbatdev/cortex/cortex|$home/dev/barbatdev/cortex/cortex-dotfiles|cmux|high|$home/dev/innit-sas|$home/dev/barbatdev/products|$home/dev/innit-sas|$home/Screenshots" 'empty environment values use defaults' printf 'set -gx WORKSPACE_DIR "$HOME/private-dev"\nset -gx EDITOR private-editor\nset -gx VISUAL private-editor\n' >"$xdg/fish/conf.d/99-local.fish" set -l overrides (run_fish -c 'string join "|" $WORKSPACE_DIR $EDITOR $VISUAL') assert_equal "$overrides" "$home/private-dev|private-editor|private-editor" '99-local override precedence' -mkdir -p "$home/dev" "$home/dev/barbatdev/innit" "$workspace/apis" "$workspace/mobile" "$workspace/webs" "$workspace/pcsoft" "$workspace/dotfiles" +mkdir -p "$home/dev" "$home/dev/barbatdev" "$home/dev/innit-sas" "$workspace/apis" "$workspace/mobile" "$workspace/webs" "$workspace/pcsoft" "$workspace/dotfiles" set -l navigation (run_fish -c "set -gx fish_function_path '$functions_dir' \$fish_function_path; set -gx INNIT_APIS_DIR '$workspace/apis'; set -gx INNIT_MOBILE_DIR '$workspace/mobile'; set -gx INNIT_WEBS_DIR '$workspace/webs'; set -gx INNIT_PCSOFT_DIR '$workspace/pcsoft'; set -gx _DOTFILES_DIR '$workspace/dotfiles'; dev; pwd; barbat; pwd; innit; pwd; innit-apis; pwd; innit-mobile; pwd; innit-webs; pwd; innit-pcsoft; pwd; dotfiles; pwd" | string collect) -set -l expected_navigation (printf '%s\n' "$home/dev" "$home/dev/barbatdev" "$home/dev/barbatdev/innit" "$workspace/apis" "$workspace/mobile" "$workspace/webs" "$workspace/pcsoft" "$workspace/dotfiles" | string collect) +set -l expected_navigation (printf '%s\n' "$home/dev" "$home/dev/barbatdev" "$home/dev/innit-sas" "$workspace/apis" "$workspace/mobile" "$workspace/webs" "$workspace/pcsoft" "$workspace/dotfiles" | string collect) assert_equal "$navigation" "$expected_navigation" 'navigation destinations' set -l fallback (run_fish -c "set -gx fish_function_path '$functions_dir' \$fish_function_path; set -gx INNIT_APIS_DIR '$workspace/missing'; innit-apis; pwd") -assert_equal "$fallback" "$home/dev/barbatdev/innit" 'navigation fallback' -assert_navigation_error "set -gx BARBATDEV_DIR '$workspace/missing'; barbat" "$workspace/missing" 'barbat missing directory' +assert_equal "$fallback" "$home/dev/innit-sas" 'navigation fallback' +assert_navigation_fallback "set -gx BARBATDEV_DIR '$workspace/missing'; barbat" "$workspace/missing" "$workspace" 'barbat missing directory' -mkdir -p "$workspace/cowork" "$workspace/personal" "$workspace/barbatdev/tools" "$workspace/barbatdev/innit" "$workspace/barbatdev/products" "$home/dev/worktrees" +mkdir -p "$workspace/cowork" "$workspace/personal" "$workspace/barbatdev/tools" "$workspace/innit-sas" "$workspace/barbatdev/products" "$home/dev/worktrees" set -l extended_navigation (run_fish -c "set -gx fish_function_path '$functions_dir' \$fish_function_path; set -gx WORK_PROJECTS_DIR '$workspace/cowork'; set -gx PERSONAL_PROJECTS_DIR '$workspace/personal'; set -gx BARBATDEV_DIR '$workspace/barbatdev'; cowork; pwd; personal; pwd; tools; pwd; worktrees; pwd; work; pwd" | string collect) set -l expected_extended_navigation (printf '%s\n' "$workspace/cowork" "$workspace/personal" "$workspace/barbatdev/tools" "$home/dev/worktrees" "$workspace/cowork" | string collect) assert_equal "$extended_navigation" "$expected_extended_navigation" 'extended navigation destinations' set -l extended_fallback (run_fish -c "set -gx fish_function_path '$functions_dir' \$fish_function_path; set -gx WORK_PROJECTS_DIR '$workspace/missing-work'; set -gx PERSONAL_PROJECTS_DIR '$workspace/missing-personal'; set -gx BARBATDEV_DIR '$workspace/barbatdev'; cowork; pwd; work; pwd; personal; pwd" | string collect) -set -l expected_extended_fallback (printf '%s\n' "$workspace/barbatdev/innit" "$workspace/barbatdev/innit" "$workspace/barbatdev/products" | string collect) +set -l expected_extended_fallback (printf '%s\n' "$workspace" "$workspace" "$workspace/barbatdev/products" | string collect) assert_equal "$extended_fallback" "$expected_extended_fallback" 'extended navigation fallbacks' -assert_navigation_error "set -gx WORK_PROJECTS_DIR '$workspace/missing-work'; set -gx BARBATDEV_DIR '$workspace/missing-barbat'; cowork" "$workspace/missing-work" 'cowork missing directories' -assert_navigation_error "set -gx PERSONAL_PROJECTS_DIR '$workspace/missing-personal'; set -gx BARBATDEV_DIR '$workspace/missing-barbat'; personal" "$workspace/missing-personal" 'personal missing directories' -assert_navigation_error "set -gx BARBATDEV_DIR '$workspace/missing-barbat'; tools" "$workspace/missing-barbat/tools" 'tools missing directory' -assert_navigation_error "set -gx HOME '$workspace/missing-home'; worktrees" "$workspace/missing-home/dev/worktrees" 'worktrees missing directory' -assert_navigation_error "set -gx WORK_PROJECTS_DIR '$workspace/missing-work'; set -gx BARBATDEV_DIR '$workspace/missing-barbat'; work" "$workspace/missing-work" 'work missing directories' +assert_navigation_fallback "set -gx WORK_PROJECTS_DIR '$workspace/missing-work'; set -gx BARBATDEV_DIR '$workspace/missing-barbat'; cowork" "$workspace/missing-work" "$workspace" 'cowork missing directories' +assert_navigation_fallback "set -gx PERSONAL_PROJECTS_DIR '$workspace/missing-personal'; set -gx BARBATDEV_DIR '$workspace/missing-barbat'; personal" "$workspace/missing-personal" "$workspace" 'personal missing directories' +assert_navigation_fallback "set -gx BARBATDEV_DIR '$workspace/missing-barbat'; tools" "$workspace/missing-barbat/tools" "$workspace" 'tools missing directory' +assert_navigation_fallback "set -gx HOME '$workspace/missing-home'; worktrees" "$workspace/missing-home/dev/worktrees" "$workspace" 'worktrees missing directory' +assert_navigation_fallback "set -gx WORK_PROJECTS_DIR '$workspace/missing-work'; set -gx BARBATDEV_DIR '$workspace/missing-barbat'; work" "$workspace/missing-work" "$workspace" 'work missing directories' set -l aliases (run_fish -c 'functions gpl' | string collect) string match -q '*git pull*' "$aliases"; or fail 'gpl alias' diff --git a/zsh/zshrc b/zsh/zshrc index f30e8c8..8960edb 100644 --- a/zsh/zshrc +++ b/zsh/zshrc @@ -64,9 +64,9 @@ export CORTEX_DOTFILES_DIR="${CORTEX_DOTFILES_DIR:-$BARBATDEV_DIR/cortex/cortex- export CORTEX_MULTIPLEXER="${CORTEX_MULTIPLEXER:-cmux}" # Proyectos de la organización -export WORK_PROJECTS_DIR="${WORK_PROJECTS_DIR:-$BARBATDEV_DIR/innit}" +export WORK_PROJECTS_DIR="${WORK_PROJECTS_DIR:-$WORKSPACE_DIR/innit-sas}" export PERSONAL_PROJECTS_DIR="${PERSONAL_PROJECTS_DIR:-$BARBATDEV_DIR/products}" -export INNIT_DIR="${INNIT_DIR:-$BARBATDEV_DIR/innit}" +export INNIT_DIR="${INNIT_DIR:-$WORKSPACE_DIR/innit-sas}" # Screenshots (configurable en local/env.zsh) export SCREENSHOTS_DIR="${SCREENSHOTS_DIR:-$HOME/Screenshots}" @@ -103,25 +103,39 @@ _go_dev_dir() { } _go_first_existing_dir() { - local target + local target probe for target in "$@"; do if [[ -d "$target" ]]; then cd "$target" return fi done + # No candidate exists: walk up to the nearest existing ancestor of the + # first target so navigation commands degrade gracefully on hosts that + # do not have this part of the workspace layout. + probe="$1" + while [[ -n "$probe" && ! -d "$probe" ]]; do + probe="${probe:h}" + done + if [[ -n "$probe" && -d "$probe" ]]; then + echo "⚠️ Directorio no encontrado: $1 — usando el ancestro más cercano: $probe" + cd "$probe" + return + fi echo "❌ Directorio no encontrado: $1" return 1 } dev() { _go_dev_dir "$HOME/dev"; } barbat() { _go_dev_dir "$BARBATDEV_DIR"; } -cowork() { _go_first_existing_dir "$WORK_PROJECTS_DIR" "$BARBATDEV_DIR/innit"; } +cowork() { _go_first_existing_dir "$WORK_PROJECTS_DIR" "$WORKSPACE_DIR/innit-sas"; } personal() { _go_first_existing_dir "$PERSONAL_PROJECTS_DIR" "$BARBATDEV_DIR/products"; } tools() { _go_dev_dir "$BARBATDEV_DIR/tools"; } worktrees() { _go_dev_dir "$HOME/dev/worktrees"; } -work() { _go_first_existing_dir "$WORK_PROJECTS_DIR" "$BARBATDEV_DIR/innit"; } -innit() { _go_first_existing_dir "$INNIT_DIR" "$BARBATDEV_DIR/innit"; } +work() { _go_first_existing_dir "$WORK_PROJECTS_DIR" "$WORKSPACE_DIR/innit-sas"; } +innit() { _go_first_existing_dir "$INNIT_DIR" "$WORKSPACE_DIR/innit-sas"; } +refactoria() { _go_dev_dir "${REFACTORIA_DIR:-$WORKSPACE_DIR/refactoria}"; } +gentle() { _go_dev_dir "${GENTLE_DIR:-$WORKSPACE_DIR/gentle}"; } innit-apis() { _go_first_existing_dir "${INNIT_APIS_DIR:-$INNIT_DIR}" "$INNIT_DIR"; } innit-mobile(){ _go_first_existing_dir "${INNIT_MOBILE_DIR:-$INNIT_DIR}" "$INNIT_DIR"; } innit-webs() { _go_first_existing_dir "${INNIT_WEBS_DIR:-$INNIT_DIR}" "$INNIT_DIR"; } @@ -180,7 +194,7 @@ myip() { } # Mostrar el logo de RefactorIA en Braille Unicode bajo demanda. -refactoria() { +refactoria-logo() { local logo="${_DOTFILES_DIR:-$HOME/dev/personal/cortex-dotfiles}/assets/refactoria-braille.txt" if [[ -f "$logo" ]]; then cat "$logo" @@ -283,17 +297,17 @@ _welcome() { echo " ╔══════════════════════════════════════╗" echo " ║ Dev Environment ║" echo " ╠══════════════════════════════════════╣" - printf " ║ ⚡ Cargado en \033[1;32m%sms\033[0m\n" "$ms" - printf " ║ 🕐 \033[1;34m%s\033[0m\n" "$(date '+%H:%M:%S')" + printf " ║ ⚡ Cargado en \033[1;38;2;88;230;168m%sms\033[0m\n" "$ms" + printf " ║ 🕐 \033[1;38;2;140;200;255m%s\033[0m\n" "$(date '+%H:%M:%S')" echo " ╚══════════════════════════════════════╝" echo "" - echo " \033[90mescribí\033[0m \033[1;33mhelp-profile\033[0m \033[90mpara ver comandos disponibles\033[0m" + echo " \033[38;2;235;235;235mescribí\033[0m \033[1;38;2;164;119;255mhelp-profile\033[0m \033[38;2;235;235;235mpara ver comandos disponibles\033[0m" echo "" } help-profile() { - local T="\033[1;33m" # título (amarillo) - local C="\033[1;36m" # comando (cyan) + local T="\033[1;38;2;164;119;255m" # título/acción (violeta de marca) + local C="\033[1;38;2;140;200;255m" # comando (cyan) local R="\033[0m" # reset echo "" From e5232cf55c50d9ea0579a6d97039d0795d40ea64 Mon Sep 17 00:00:00 2001 From: Juan Barbat Date: Thu, 8 Oct 2026 13:29:20 -0300 Subject: [PATCH 29/29] feat(zsh): port navigation fallback and refactoria/gentle into extracted profile Apply the same navigation contract already deployed on mac (worktree dotfiles-live) to main's extracted zsh structure: - ancestor fallback in _go_dev_dir/_go_first_existing_dir - INNIT_DIR/WORK_PROJECTS_DIR default to $WORKSPACE_DIR/innit-sas - new refactoria/gentle commands + REFACTORIA_DIR/GENTLE_DIR defaults - braille logo command renamed to refactoria-logo - register refactoria.fish/gentle.fish in nix/home.nix --- nix/home.nix | 2 ++ zsh/cortex-dotfiles.zsh | 23 +++++++++++++++++++---- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/nix/home.nix b/nix/home.nix index 777379d..2810e44 100644 --- a/nix/home.nix +++ b/nix/home.nix @@ -18,6 +18,8 @@ let "fish/functions/innit-webs.fish".source = ../fish/functions/innit-webs.fish; "fish/functions/innit-pcsoft.fish".source = ../fish/functions/innit-pcsoft.fish; "fish/functions/dotfiles.fish".source = ../fish/functions/dotfiles.fish; + "fish/functions/refactoria.fish".source = ../fish/functions/refactoria.fish; + "fish/functions/gentle.fish".source = ../fish/functions/gentle.fish; "fish/functions/_parse_github_repo.fish".source = ../fish/functions/_parse_github_repo.fish; "fish/functions/_git_config_identity.fish".source = ../fish/functions/_git_config_identity.fish; "fish/functions/clone-workdev.fish".source = ../fish/functions/clone-workdev.fish; diff --git a/zsh/cortex-dotfiles.zsh b/zsh/cortex-dotfiles.zsh index 413b9c6..c29910e 100644 --- a/zsh/cortex-dotfiles.zsh +++ b/zsh/cortex-dotfiles.zsh @@ -82,19 +82,30 @@ _go_dev_dir() { if [[ -d "$target" ]]; then cd "$target" else - echo "❌ Directorio no encontrado: $target" - return 1 + _go_first_existing_dir "$target" fi } _go_first_existing_dir() { - local target + local target probe for target in "$@"; do if [[ -d "$target" ]]; then cd "$target" return fi done + # No candidate exists: walk up to the nearest existing ancestor of the + # first target so navigation commands degrade gracefully on hosts that + # do not have this part of the workspace layout. + probe="$1" + while [[ -n "$probe" && ! -d "$probe" ]]; do + probe="${probe:h}" + done + if [[ -n "$probe" && -d "$probe" ]]; then + echo "⚠️ Directorio no encontrado: $1 — usando el ancestro más cercano: $probe" + cd "$probe" + return + fi echo "❌ Directorio no encontrado: $1" return 1 } @@ -107,6 +118,8 @@ tools() { _go_dev_dir "$TOOLS_DIR"; } worktrees() { _go_dev_dir "$WORKTREES_DIR"; } work() { _go_dev_dir "$WORK_PROJECTS_DIR"; } innit() { _go_dev_dir "$INNIT_DIR"; } +refactoria() { _go_dev_dir "${REFACTORIA_DIR:-$WORKSPACE_DIR/refactoria}"; } +gentle() { _go_dev_dir "${GENTLE_DIR:-$WORKSPACE_DIR/gentle}"; } innit-apis() { _go_dev_dir "$INNIT_APIS_DIR"; } innit-mobile(){ _go_dev_dir "$INNIT_MOBILE_DIR"; } innit-webs() { _go_dev_dir "$INNIT_WEBS_DIR"; } @@ -165,7 +178,7 @@ myip() { } # Mostrar el logo de RefactorIA en Braille Unicode bajo demanda. -refactoria() { +refactoria-logo() { local logo="${_DOTFILES_DIR:-$HOME/dev/personal/cortex-dotfiles}/assets/refactoria-braille.txt" if [[ -f "$logo" ]]; then cat "$logo" @@ -249,6 +262,8 @@ export PERSONAL_PROJECTS_DIR="${PERSONAL_PROJECTS_DIR:-$WORKSPACE_DIR/local}" export TOOLS_DIR="${TOOLS_DIR:-$PERSONAL_PROJECTS_DIR/tools}" export WORKTREES_DIR="${WORKTREES_DIR:-$WORKSPACE_DIR/worktrees}" export INNIT_DIR="${INNIT_DIR:-$WORK_PROJECTS_DIR}" +export REFACTORIA_DIR="${REFACTORIA_DIR:-$WORKSPACE_DIR/refactoria}" +export GENTLE_DIR="${GENTLE_DIR:-$WORKSPACE_DIR/gentle}" export INNIT_APIS_DIR="${INNIT_APIS_DIR:-$INNIT_DIR/apis}" export INNIT_MOBILE_DIR="${INNIT_MOBILE_DIR:-$INNIT_DIR/mobile}" export INNIT_WEBS_DIR="${INNIT_WEBS_DIR:-$INNIT_DIR/webs}"