From 7977acfb8c07ad5daa6536c1e18be8ee02f7e77d Mon Sep 17 00:00:00 2001 From: Juan Barbat Date: Wed, 21 Oct 2026 09:12:52 -0300 Subject: [PATCH 1/9] fix(install): seed opencode tui.json as a copy instead of a symlink gentle-ai sync refuses to read symlinks and aborts (and fails rollback) on ~/.config/opencode/tui.json. Seed it once, convert our old symlink in place, and keep the file once a tool owns it. Refs #69 --- install.sh | 11 +++++++--- scripts/check-install-symlinks.sh | 34 +++++++++++++++++++++++++++++++ scripts/seed-file.sh | 33 ++++++++++++++++++++++++++++++ 3 files changed, 75 insertions(+), 3 deletions(-) create mode 100644 scripts/seed-file.sh diff --git a/install.sh b/install.sh index 9ab406b..08c43b5 100755 --- a/install.sh +++ b/install.sh @@ -224,7 +224,10 @@ PY check_symlink_target "$DOTFILES/fish/functions/gbarbatdev.fish" "$HOME/.config/fish/functions/gbarbatdev.fish" check_symlink_target "$DOTFILES/fish/functions/grefactoria.fish" "$HOME/.config/fish/functions/grefactoria.fish" fi - check_symlink_target "$DOTFILES/opencode/tui.json" "$HOME/.config/opencode/tui.json" + check_file "$DOTFILES/opencode/tui.json" + if [[ -L "$HOME/.config/opencode/tui.json" ]]; then + warn "opencode tui.json is a symlink; gentle-ai sync refuses it (install.sh converts it to a copy)" + fi check_symlink_target "$DOTFILES/opencode/themes" "$HOME/.config/opencode/themes" check_symlink_target "$DOTFILES/tmux/tmux.conf" "$HOME/.tmux.conf" check_symlink_target "$DOTFILES/claude/statusline.sh" "$HOME/.claude/statusline.sh" @@ -500,7 +503,6 @@ backup_if_exists "$HOME/.config/starship.toml" backup_if_exists "$HOME/.config/herdr/config.toml" backup_if_exists "$HOME/.config/ghostty/config" backup_if_exists "$HOME/.config/herdr/config.toml" -backup_if_exists "$HOME/.config/opencode/tui.json" backup_if_exists "$HOME/.config/opencode/themes" backup_if_exists "$HOME/.tmux.conf" backup_if_exists "$HOME/.claude/statusline.sh" @@ -566,7 +568,10 @@ if [[ "$PLATFORM" == "Darwin" ]]; then create_symlink "$DOTFILES/fish/functions/gbarbatdev.fish" "$HOME/.config/fish/functions/gbarbatdev.fish" create_symlink "$DOTFILES/fish/functions/grefactoria.fish" "$HOME/.config/fish/functions/grefactoria.fish" fi -create_symlink "$DOTFILES/opencode/tui.json" "$HOME/.config/opencode/tui.json" +# gentle-ai sync edits tui.json and refuses symlinks: seed a copy instead. +# shellcheck source=scripts/seed-file.sh +source "$DOTFILES/scripts/seed-file.sh" +seed_file "$DOTFILES/opencode/tui.json" "$HOME/.config/opencode/tui.json" create_symlink "$DOTFILES/opencode/themes" "$HOME/.config/opencode/themes" create_symlink "$DOTFILES/tmux/tmux.conf" "$HOME/.tmux.conf" run_or_plan "chmod +x $DOTFILES/claude/statusline.sh" chmod +x "$DOTFILES/claude/statusline.sh" diff --git a/scripts/check-install-symlinks.sh b/scripts/check-install-symlinks.sh index 606835b..103bf38 100755 --- a/scripts/check-install-symlinks.sh +++ b/scripts/check-install-symlinks.sh @@ -55,3 +55,37 @@ if backup_if_exists "$collision" >/dev/null 2>&1; then fi [[ "$(<"$collision")" == original ]] [[ "$(<"$collision.bak_test")" == existing ]] + +# seed_file: tool-owned configs (gentle-ai refuses symlinks) are copied once. +# shellcheck source=scripts/seed-file.sh +source "$repo_root/scripts/seed-file.sh" +seed_src="$tmp_dir/seed-src" +printf '%s\n' seed > "$seed_src" + +seed_absent="$tmp_dir/seed/absent.json" +seed_file "$seed_src" "$seed_absent" >/dev/null +[[ -f "$seed_absent" && ! -L "$seed_absent" && "$(<"$seed_absent")" == seed ]] + +seed_link="$tmp_dir/seed-link.json" +ln -s "$seed_src" "$seed_link" +seed_file "$seed_src" "$seed_link" >/dev/null +[[ -f "$seed_link" && ! -L "$seed_link" && "$(<"$seed_link")" == seed ]] +[[ "$(<"$seed_src")" == seed ]] + +seed_owned="$tmp_dir/seed-owned.json" +printf '%s\n' tool-edited > "$seed_owned" +seed_file "$seed_src" "$seed_owned" >/dev/null +[[ "$(<"$seed_owned")" == tool-edited ]] + +seed_foreign="$tmp_dir/seed-foreign.json" +ln -s "$old_target" "$seed_foreign" +seed_file "$seed_src" "$seed_foreign" >/dev/null +[[ -L "$seed_foreign" && "$(readlink "$seed_foreign")" == "$old_target" ]] + +DRY_RUN=true +seed_dry="$tmp_dir/seed-dry.json" +seed_file "$seed_src" "$seed_dry" >/dev/null +[[ ! -e "$seed_dry" ]] +DRY_RUN=false + +echo "install symlink checks passed" diff --git a/scripts/seed-file.sh b/scripts/seed-file.sh new file mode 100644 index 0000000..a57fe32 --- /dev/null +++ b/scripts/seed-file.sh @@ -0,0 +1,33 @@ +#!/bin/bash + +# seed_file SRC DST — install a config that another tool owns after first run. +# Some managers (gentle-ai sync) refuse to read or write symlinks, so these +# files are copied, never linked: +# - absent DST: copy SRC +# - DST is our old symlink to SRC: replace it with a regular copy +# - DST is a regular file: keep it; the owning tool may have edited it +# - DST is a symlink elsewhere: keep it and warn +seed_file() { + local src="$1" + local dst="$2" + + if [[ -L "$dst" && "$(readlink "$dst")" != "$src" ]]; then + echo " ! $dst is a foreign symlink; left untouched" >&2 + return 0 + fi + if [[ -e "$dst" && ! -L "$dst" ]]; then + echo " = $dst kept (tool-owned)" + return 0 + fi + if [[ "$DRY_RUN" == true ]]; then + echo " → Would seed $dst from $src" + return 0 + fi + mkdir -p "$(dirname "$dst")" + local tmp + tmp="$(mktemp "$dst.seed.XXXXXX")" + cp "$src" "$tmp" + # mv over a symlink replaces the link itself, never its target. + mv -f "$tmp" "$dst" + echo " ✓ $dst seeded from $src" +} From 19424b3803c6ac4bbbb47a01e9cebd8c11c0ac61 Mon Sep 17 00:00:00 2001 From: Juan Barbat Date: Wed, 21 Oct 2026 09:12:52 -0300 Subject: [PATCH 2/9] fix(fish): put gentle-ai managed launchers first on PATH gentle-ai cannot persist PATH for fish login shells, so the OpenCode background launcher in ~/.gentle-ai/bin never took effect. Refs #69 --- fish/conf.d/10-core.fish | 6 ++++++ fish/tests/w7-gentle-ai-path.fish | 28 ++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 fish/tests/w7-gentle-ai-path.fish diff --git a/fish/conf.d/10-core.fish b/fish/conf.d/10-core.fish index ea1bef7..1392862 100644 --- a/fish/conf.d/10-core.fish +++ b/fish/conf.d/10-core.fish @@ -75,6 +75,12 @@ else alias la 'ls -lah -A' end +# gentle-ai's managed launchers (OpenCode background subagents) must win on PATH; +# gentle-ai cannot persist PATH for fish login shells by itself. +if test -d "$HOME/.gentle-ai/bin" + fish_add_path --path --move "$HOME/.gentle-ai/bin" +end + if status is-interactive; and command -q starship starship init fish | source end diff --git a/fish/tests/w7-gentle-ai-path.fish b/fish/tests/w7-gentle-ai-path.fish new file mode 100644 index 0000000..37cba5d --- /dev/null +++ b/fish/tests/w7-gentle-ai-path.fish @@ -0,0 +1,28 @@ +#!/usr/bin/env fish +# gentle-ai's managed launcher dir (~/.gentle-ai/bin) must lead PATH when present, +# because gentle-ai cannot persist PATH for fish login shells itself. + +set -l repo_root (cd (dirname (dirname (status filename))); and pwd) +set -l core "$repo_root/conf.d/10-core.fish" +set -l fish_bin (status fish-path) +set -l ws (mktemp -d) + +function fail + printf 'FAIL: %s\n' "$argv" >&2 + exit 1 +end + +set -l probe 'source $argv[1]; string join "|" $PATH' + +mkdir -p "$ws/with/.gentle-ai/bin" "$ws/without" +set -l with (env -i HOME="$ws/with" PATH=/usr/bin:/bin "$fish_bin" --no-config -c "$probe" "$core") +string match -q "$ws/with/.gentle-ai/bin|*" -- "$with"; or fail "launcher dir not first in PATH: $with" + +set -l twice (env -i HOME="$ws/with" PATH="$ws/with/.gentle-ai/bin:/usr/bin:/bin" "$fish_bin" --no-config -c "$probe" "$core") +test (count (string match -a -- "$ws/with/.gentle-ai/bin" (string split '|' -- "$twice"))) -eq 1; or fail "launcher dir duplicated: $twice" + +set -l without (env -i HOME="$ws/without" PATH=/usr/bin:/bin "$fish_bin" --no-config -c "$probe" "$core") +string match -q '*gentle-ai*' -- "$without"; and fail "missing launcher dir added to PATH: $without" + +rm -rf -- "$ws" +printf 'gentle-ai path checks passed\n' From b647f504fe8c287910af26331118623034388174 Mon Sep 17 00:00:00 2001 From: Juan Barbat Date: Wed, 21 Oct 2026 09:12:52 -0300 Subject: [PATCH 3/9] feat(gs): run gentle-ai sync after updating binaries gs sync only rebuilt binaries, so managed configs drifted per host. It now runs gentle-ai sync (scoped by GS_SYNC_AGENTS, skippable with GS_SKIP_AI_SYNC=1), reports failures without aborting, and flags legacy upgrade scripts so gs sync is the single entry point. Closes #69 --- bin/gs | 23 +++++++++- fish/conf.d/99-local.fish.example | 4 ++ local/env.zsh.example | 5 +++ scripts/test-gs-sync.sh | 74 +++++++++++++++++++++++++++++++ 4 files changed, 104 insertions(+), 2 deletions(-) create mode 100644 scripts/test-gs-sync.sh diff --git a/bin/gs b/bin/gs index 392b909..bf88509 100755 --- a/bin/gs +++ b/bin/gs @@ -29,8 +29,11 @@ gs - gentle-shell on main, plus one disposable worktree per PR gs review Check out someone else's PR to test or review it gs base Clean origin/main worktree, to compare failures gs run [pi args...] Run gentle-shell --link with this worktree as --package-root - gs sync Bring gentle-shell, gentle-ai and engram to main and check - compatibility (workshop: local clones; elsewhere: GitHub) + gs sync Bring gentle-shell, gentle-ai and engram to main, run + gentle-ai sync for managed configs, and check compatibility + (workshop: local clones; elsewhere: GitHub) + GS_SYNC_AGENTS=a,b scopes gentle-ai sync to those runtimes; + GS_SKIP_AI_SYNC=1 skips it (binaries only) gs ls List open worktrees gs done Remove the current worktree and its merged local branch (drops odd/tasks records; refuses if real work would be lost) @@ -342,6 +345,22 @@ cmd_sync() { ln -sfn "$ai_bin" "$HOME/.local/bin/gentle-ai" ln -sfn "$bin/engram" "$HOME/.local/bin/engram" + # New binaries are not enough: gentle-ai sync materializes the managed configs + # (agents, skills, plugins, engram instructions) every runtime reads. + # GS_SYNC_AGENTS scopes runtimes per host (local env); empty = gentle-ai default. + if [ "${GS_SKIP_AI_SYNC:-}" = 1 ]; then + printf ' %-34s skipped (GS_SKIP_AI_SYNC=1)\n' 'gentle-ai sync' + else + local ai_sync=(sync) + [ -n "${GS_SYNC_AGENTS:-}" ] && ai_sync+=(--agents "$GS_SYNC_AGENTS") + run_quiet 'gentle-ai sync' "$ai_bin" "${ai_sync[@]}" + fi + # gs sync is the only upgrade path; older homemade scripts drift the hosts apart. + for name in "$HOME/.local/bin/gentle-maintainer-upgrade" "$HOME/.local/bin/gentle-sync.sh" \ + /usr/bin/gentle-sync.sh /usr/local/bin/gentle-upgrade.sh /bin/gentle-upgrade.sh; do + [ -e "$name" ] && printf 'WARN: legacy upgrade script %s; use gs sync only and remove it\n' "$name" + done + # Versions after the sync decide the closing message: restart only on change. local shell_after ai_after engram_after changed shell_after=$(shell_version); ai_after=$(ai_version); engram_after=$(engram_version) diff --git a/fish/conf.d/99-local.fish.example b/fish/conf.d/99-local.fish.example index 1e46392..15f6c5c 100644 --- a/fish/conf.d/99-local.fish.example +++ b/fish/conf.d/99-local.fish.example @@ -8,3 +8,7 @@ # set -gx GIT_WORKDEV_EMAIL "you@company.invalid" # set -gx GIT_PERSONALDEV_NAME "Your personal name" # set -gx GIT_PERSONALDEV_EMAIL "you@personal.invalid" +# gs sync runs gentle-ai sync after updating binaries. Scope it to this host's runtimes: +# set -gx GS_SYNC_AGENTS claude-code,opencode,pi +# Binaries only, no managed-config sync: +# set -gx GS_SKIP_AI_SYNC 1 diff --git a/local/env.zsh.example b/local/env.zsh.example index d759cc6..4766b42 100644 --- a/local/env.zsh.example +++ b/local/env.zsh.example @@ -48,3 +48,8 @@ # export CORTEX_BRAINS_OLLAMA_URL="http://:11434" # export CORTEX_BRAINS_PG_HOST="" # export CORTEX_BRAINS_PG_PORT="5432" + +# gs sync runs gentle-ai sync after updating binaries. Scope it to this host's runtimes: +# export GS_SYNC_AGENTS="claude-code,opencode,pi" +# Binaries only, no managed-config sync: +# export GS_SKIP_AI_SYNC=1 diff --git a/scripts/test-gs-sync.sh b/scripts/test-gs-sync.sh new file mode 100644 index 0000000..067b58a --- /dev/null +++ b/scripts/test-gs-sync.sh @@ -0,0 +1,74 @@ +#!/bin/bash +# Run the real `gs sync` (consumer mode) with an isolated HOME and PATH shims: +# no network, no host installs. Checks that the binaries step is followed by +# `gentle-ai sync`, scoped by GS_SYNC_AGENTS and skippable with GS_SKIP_AI_SYNC. +set -euo pipefail + +repo_root="$(cd "$(dirname "$0")/.." && pwd)" +ws="$(mktemp -d)" +trap 'rm -rf "$ws"' EXIT + +fail() { printf 'FAIL: %s\n%s\n' "$1" "${2:-}" >&2; exit 1; } + +home="$ws/home" +shims="$ws/shims" +log="$ws/calls.log" +mkdir -p "$home/.pi/gentle-ai" "$shims" + +# One shim script, dispatched by name; every call is logged. +cat >"$shims/_shim" <>"$log" +case "\$name \$1" in + "gentle-shell --version") echo "gentle-shell 4.2.0" ;; + "go install") + for arg in "\$@"; do case "\$arg" in *cmd/engram@*) + printf '#!/bin/sh\necho "engram 3.0.0"\n' >"\$GOBIN/engram"; chmod +x "\$GOBIN/engram" ;; + esac; done ;; + "gentle-ai --version") echo "gentle-ai 4.2.1" ;; + "gentle-ai sync") [ -n "\${FAKE_AI_SYNC_FAIL:-}" ] && { echo "Error: partial sync"; exit 1; }; echo "managed sync executed" ;; +esac +exit 0 +EOF +chmod +x "$shims/_shim" +for name in gentle-shell pi go; do ln -s _shim "$shims/$name"; done +ai_dir="$ws/ai"; mkdir -p "$ai_dir"; ln -s "$shims/_shim" "$ai_dir/gentle-ai" +printf '{"schema":"gentle-pi.dev-binary/v1","path":"%s"}\n' "$ai_dir/gentle-ai" >"$home/.pi/gentle-ai/dev-binary.json" + +run_sync() { + : >"$log" + env -i HOME="$home" PATH="$shims:/usr/bin:/bin" TERM=dumb "$@" \ + bash "$repo_root/bin/gs" sync 2>&1 +} +ai_sync_calls() { grep '^gentle-ai|sync' "$log" || true; } + +# 1. Default: gentle-ai sync runs once, unscoped, after the binaries step. +out=$(run_sync) || fail 'gs sync exited non-zero' "$out" +[[ "$(ai_sync_calls)" == 'gentle-ai|sync' ]] || fail 'expected one unscoped gentle-ai sync' "$(cat "$log")" +[[ "$out" == *'gentle-ai sync'*'ok'* ]] || fail 'missing gentle-ai sync status line' "$out" +engram_line=$(grep -n 'cmd/engram@main' "$log" | cut -d: -f1) +sync_line=$(grep -n '^gentle-ai|sync' "$log" | cut -d: -f1) +(( engram_line < sync_line )) || fail 'gentle-ai sync ran before the binaries step' "$(cat "$log")" + +# 2. GS_SYNC_AGENTS scopes the runtimes for this host. +out=$(run_sync GS_SYNC_AGENTS=claude-code,opencode,pi) || fail 'scoped gs sync exited non-zero' "$out" +[[ "$(ai_sync_calls)" == 'gentle-ai|sync --agents claude-code,opencode,pi' ]] || fail 'agents scope not forwarded' "$(cat "$log")" + +# 3. GS_SKIP_AI_SYNC=1 keeps the binaries-only behavior. +out=$(run_sync GS_SKIP_AI_SYNC=1) || fail 'skipped gs sync exited non-zero' "$out" +[[ -z "$(ai_sync_calls)" ]] || fail 'gentle-ai sync ran despite GS_SKIP_AI_SYNC=1' "$(cat "$log")" +[[ "$out" == *'gentle-ai sync'*'skipped (GS_SKIP_AI_SYNC=1)'* ]] || fail 'missing skip status line' "$out" + +# 4. A failing gentle-ai sync is reported with its output, and gs sync goes on. +out=$(run_sync FAKE_AI_SYNC_FAIL=1) || fail 'gs sync aborted on gentle-ai sync failure' "$out" +[[ "$out" == *'gentle-ai sync'*'FAILED'*'Error: partial sync'* ]] || fail 'failure output not surfaced' "$out" +[[ "$out" == *'versions:'* ]] || fail 'sync stopped before the closing report' "$out" + +# 5. Legacy upgrade scripts are flagged so only gs sync remains. +mkdir -p "$home/.local/bin"; printf '#!/bin/sh\n' >"$home/.local/bin/gentle-maintainer-upgrade" +out=$(run_sync) || fail 'gs sync exited non-zero with legacy script' "$out" +[[ "$out" == *"WARN: legacy upgrade script $home/.local/bin/gentle-maintainer-upgrade"* ]] || fail 'legacy script not flagged' "$out" +[[ -f "$home/.local/bin/gentle-maintainer-upgrade" ]] || fail 'legacy script was removed' "$out" + +echo "gs sync checks passed" From 6ad22866d5f9316e7fe8fe048c5b8e3847ddf0ec Mon Sep 17 00:00:00 2001 From: Juan Barbat Date: Wed, 21 Oct 2026 09:18:14 -0300 Subject: [PATCH 4/9] ci: restore Linux Home Manager README contract and run gs sync test The README restore in dc56614 dropped the Linux rows the nix preflight check requires, which kept main red and blocked merges. Refs #69 --- .github/workflows/ci.yml | 3 +++ README.md | 2 ++ 2 files changed, 5 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8a10fd3..3ffa252 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -83,6 +83,9 @@ jobs: - name: Check installer symlink safety run: ./scripts/check-install-symlinks.sh + - name: Check gs sync runs gentle-ai sync + run: bash scripts/test-gs-sync.sh + - name: Check Nix Home Manager preflight contracts run: bash scripts/test-nix-preflight.sh diff --git a/README.md b/README.md index 365de2e..b5d3f81 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,8 @@ El preflight es de solo lectura y sin red: usa `--offline` y `--no-write-lock-fi | 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 | +| Linux | La configuración pura para Linux es `homeConfigurations.jbarbat-linux`: usa `x86_64-linux`, `/home/jbarbat` y declara solo `fish` y `starship` como runtime mínimo. | +| Fish compartido | Home Manager mapea cada archivo declarado de forma explícita; no gestiona el directorio completo, historial ni variables Fish. | 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: From e937dfbd6a37e68c016413dadcea6423a79e820c Mon Sep 17 00:00:00 2001 From: Juan Barbat Date: Wed, 21 Oct 2026 09:21:21 -0300 Subject: [PATCH 5/9] fix(install): add profile files install.sh already links (#72) * fix(install): add profile files install.sh already links install.sh on main links herdr, ghostty, fish and pi profile files that were never committed, so install.sh --check fails and CI stays red. Refs #69 * docs: restore Linux Home Manager rows the nix preflight requires Dropped by the README restore in dc56614. Refs #69 --- README.md | 2 + fish/functions/gbarbatdev.fish | 54 ++++++++++++++++++ fish/functions/grefactoria.fish | 54 ++++++++++++++++++ fish/functions/hbarbatdev.fish | 48 ++++++++++++++++ fish/functions/hrefactoria.fish | 48 ++++++++++++++++ fish/themes/barbatdev.fish | 41 ++++++++++++++ ghostty/profiles/barbatdev.conf | 25 +++++++++ ghostty/profiles/refactoria.conf | 27 +++++++++ herdr/config-base.toml | 15 +++++ herdr/profiles/barbatdev.toml | 31 +++++++++++ pi/themes/barbatdev.json | 94 ++++++++++++++++++++++++++++++++ 11 files changed, 439 insertions(+) create mode 100644 fish/functions/gbarbatdev.fish create mode 100644 fish/functions/grefactoria.fish create mode 100644 fish/functions/hbarbatdev.fish create mode 100644 fish/functions/hrefactoria.fish create mode 100644 fish/themes/barbatdev.fish create mode 100644 ghostty/profiles/barbatdev.conf create mode 100644 ghostty/profiles/refactoria.conf create mode 100644 herdr/config-base.toml create mode 100644 herdr/profiles/barbatdev.toml create mode 100644 pi/themes/barbatdev.json diff --git a/README.md b/README.md index 365de2e..b5d3f81 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,8 @@ El preflight es de solo lectura y sin red: usa `--offline` y `--no-write-lock-fi | 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 | +| Linux | La configuración pura para Linux es `homeConfigurations.jbarbat-linux`: usa `x86_64-linux`, `/home/jbarbat` y declara solo `fish` y `starship` como runtime mínimo. | +| Fish compartido | Home Manager mapea cada archivo declarado de forma explícita; no gestiona el directorio completo, historial ni variables Fish. | 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: diff --git a/fish/functions/gbarbatdev.fish b/fish/functions/gbarbatdev.fish new file mode 100644 index 0000000..77d5f31 --- /dev/null +++ b/fish/functions/gbarbatdev.fish @@ -0,0 +1,54 @@ +# Open an isolated Ghostty instance with the Barbatdev profile. Usage: gbarbatdev [ghostty args...] +function gbarbatdev --description 'Open Ghostty with the Barbatdev profile' + if not command -q open + printf 'gbarbatdev: macOS open command is not available\n' >&2 + return 127 + end + + set -l ghostty /Applications/Ghostty.app/Contents/MacOS/ghostty + if not test -x "$ghostty" + printf 'gbarbatdev: Ghostty.app is not installed in /Applications\n' >&2 + return 1 + end + + set -l base "$HOME/.config/ghostty/config" + set -l profile "$HOME/.config/ghostty/profiles/barbatdev.conf" + if not test -r "$base" + printf 'gbarbatdev: missing readable config: %s\n' "$base" >&2 + return 1 + end + if not test -r "$profile" + printf 'gbarbatdev: missing readable config: %s\n' "$profile" >&2 + return 1 + end + + set -l tmp_dir (command mktemp -d) + set -l mktemp_status $status + if test $mktemp_status -ne 0 + return $mktemp_status + end + if test -z "$tmp_dir" + return 1 + end + + set -l config "$tmp_dir/config.ghostty" + printf 'config-file = "%s"\nconfig-file = "%s"\n' "$base" "$profile" > "$config" + set -l compose_status $status + if test $compose_status -ne 0 + command rm -rf -- "$tmp_dir" + return $compose_status + end + + "$ghostty" +validate-config --config-file="$config" + set -l validate_status $status + if test $validate_status -ne 0 + printf 'gbarbatdev: composed Ghostty config is invalid\n' >&2 + command rm -rf -- "$tmp_dir" + return $validate_status + end + + open -W -na Ghostty --args --config-default-files=false --config-file="$config" --working-directory="$PWD" $argv + set -l ghostty_status $status + command rm -rf -- "$tmp_dir" + return $ghostty_status +end diff --git a/fish/functions/grefactoria.fish b/fish/functions/grefactoria.fish new file mode 100644 index 0000000..905a37b --- /dev/null +++ b/fish/functions/grefactoria.fish @@ -0,0 +1,54 @@ +# Open an isolated Ghostty instance with the RefactorIA profile. Usage: grefactoria [ghostty args...] +function grefactoria --description 'Open Ghostty with the RefactorIA profile' + if not command -q open + printf 'grefactoria: macOS open command is not available\n' >&2 + return 127 + end + + set -l ghostty /Applications/Ghostty.app/Contents/MacOS/ghostty + if not test -x "$ghostty" + printf 'grefactoria: Ghostty.app is not installed in /Applications\n' >&2 + return 1 + end + + set -l base "$HOME/.config/ghostty/config" + set -l profile "$HOME/.config/ghostty/profiles/refactoria.conf" + if not test -r "$base" + printf 'grefactoria: missing readable config: %s\n' "$base" >&2 + return 1 + end + if not test -r "$profile" + printf 'grefactoria: missing readable config: %s\n' "$profile" >&2 + return 1 + end + + set -l tmp_dir (command mktemp -d) + set -l mktemp_status $status + if test $mktemp_status -ne 0 + return $mktemp_status + end + if test -z "$tmp_dir" + return 1 + end + + set -l config "$tmp_dir/config.ghostty" + printf 'config-file = "%s"\nconfig-file = "%s"\n' "$base" "$profile" > "$config" + set -l compose_status $status + if test $compose_status -ne 0 + command rm -rf -- "$tmp_dir" + return $compose_status + end + + "$ghostty" +validate-config --config-file="$config" + set -l validate_status $status + if test $validate_status -ne 0 + printf 'grefactoria: composed Ghostty config is invalid\n' >&2 + command rm -rf -- "$tmp_dir" + return $validate_status + end + + open -W -na Ghostty --args --config-default-files=false --config-file="$config" --working-directory="$PWD" $argv + set -l ghostty_status $status + command rm -rf -- "$tmp_dir" + return $ghostty_status +end diff --git a/fish/functions/hbarbatdev.fish b/fish/functions/hbarbatdev.fish new file mode 100644 index 0000000..737c1ba --- /dev/null +++ b/fish/functions/hbarbatdev.fish @@ -0,0 +1,48 @@ +# Run Herdr with the Barbatdev profile. Usage: hbarbatdev [herdr args...] +function hbarbatdev --description 'Run Herdr with the Barbatdev profile' + if set -q HERDR_ENV + printf 'hbarbatdev: launch this preview from an external Fish shell or window, not from within Herdr\n' >&2 + return 1 + end + + if not command -q herdr + printf 'hbarbatdev: herdr is not available\n' >&2 + return 127 + end + + set -l base "$HOME/.config/herdr/config-base.toml" + set -l profile "$HOME/.config/herdr/profiles/barbatdev.toml" + if not test -r "$base" + printf 'hbarbatdev: missing readable config: %s\n' "$base" >&2 + return 1 + end + if not test -r "$profile" + printf 'hbarbatdev: missing readable config: %s\n' "$profile" >&2 + return 1 + end + + set -l tmp_dir (command mktemp -d) + set -l mktemp_status $status + if test $mktemp_status -ne 0 + return $mktemp_status + end + if test -z "$tmp_dir" + return 1 + end + + set -l config "$tmp_dir/config.toml" + set -l server_socket "$tmp_dir/herdr.sock" + set -l client_socket "$tmp_dir/herdr-client.sock" + + command cat -- "$base" "$profile" > "$config" + set -l cat_status $status + if test $cat_status -ne 0 + command rm -rf -- "$tmp_dir" + return $cat_status + end + + env HERDR_CONFIG_PATH="$config" HERDR_SOCKET_PATH="$server_socket" HERDR_CLIENT_SOCKET_PATH="$client_socket" herdr --no-session $argv + set -l herdr_status $status + command rm -rf -- "$tmp_dir" + return $herdr_status +end diff --git a/fish/functions/hrefactoria.fish b/fish/functions/hrefactoria.fish new file mode 100644 index 0000000..4375135 --- /dev/null +++ b/fish/functions/hrefactoria.fish @@ -0,0 +1,48 @@ +# Run Herdr with the RefactorIA profile. Usage: hrefactoria [herdr args...] +function hrefactoria --description 'Run Herdr with the RefactorIA profile' + if set -q HERDR_ENV + printf 'hrefactoria: launch this preview from an external Fish shell or window, not from within Herdr\n' >&2 + return 1 + end + + if not command -q herdr + printf 'hrefactoria: herdr is not available\n' >&2 + return 127 + end + + set -l base "$HOME/.config/herdr/config-base.toml" + set -l profile "$HOME/.config/herdr/profiles/refactoria.toml" + if not test -r "$base" + printf 'hrefactoria: missing readable config: %s\n' "$base" >&2 + return 1 + end + if not test -r "$profile" + printf 'hrefactoria: missing readable config: %s\n' "$profile" >&2 + return 1 + end + + set -l tmp_dir (command mktemp -d) + set -l mktemp_status $status + if test $mktemp_status -ne 0 + return $mktemp_status + end + if test -z "$tmp_dir" + return 1 + end + + set -l config "$tmp_dir/config.toml" + set -l server_socket "$tmp_dir/herdr.sock" + set -l client_socket "$tmp_dir/herdr-client.sock" + + command cat -- "$base" "$profile" > "$config" + set -l cat_status $status + if test $cat_status -ne 0 + command rm -rf -- "$tmp_dir" + return $cat_status + end + + env HERDR_CONFIG_PATH="$config" HERDR_SOCKET_PATH="$server_socket" HERDR_CLIENT_SOCKET_PATH="$client_socket" herdr --no-session $argv + set -l herdr_status $status + command rm -rf -- "$tmp_dir" + return $herdr_status +end diff --git a/fish/themes/barbatdev.fish b/fish/themes/barbatdev.fish new file mode 100644 index 0000000..bbd9feb --- /dev/null +++ b/fish/themes/barbatdev.fish @@ -0,0 +1,41 @@ +# Barbatdev Fish colors. + +set -l base "#0B0B0A" +set -l panel "#171512" +set -l text "#F8F1E7" +set -l selection_text "#FFF7ED" +set -l accent "#F97316" +set -l active_orange "#FB923C" +set -l gold "#FBBF24" +set -l soft_orange "#FDBA74" +set -l bright_blue "#BAE6FD" +set -l error "#EF4444" +set -l muted "#B9AD9C" +set -l dim "#8C8173" +set -l selection "#7A3500" +set -l search_background "#3A1D0C" + +# Syntax highlighting colors. +set -g fish_color_normal $text +set -g fish_color_command --bold $accent +set -g fish_color_keyword --bold $active_orange +set -g fish_color_quote $gold +set -g fish_color_redirection $bright_blue +set -g fish_color_end $soft_orange +set -g fish_color_error --bold $error +set -g fish_color_param $soft_orange +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_orange +set -g fish_color_autosuggestion $muted +set -g fish_color_option $soft_orange + +# Completion pager colors. +set -g fish_pager_color_progress $dim +set -g fish_pager_color_prefix --bold $accent +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 diff --git a/ghostty/profiles/barbatdev.conf b/ghostty/profiles/barbatdev.conf new file mode 100644 index 0000000..9ddeeb0 --- /dev/null +++ b/ghostty/profiles/barbatdev.conf @@ -0,0 +1,25 @@ +# Barbatdev terminal palette. Loaded after the shared Ghostty config. +background = #0B0B0A +foreground = #F8F1E7 +cursor-color = #F97316 +cursor-text = #0B0B0A +selection-background = #7C2D12 +selection-foreground = #F8F1E7 + +# Accessible ANSI colors for the graphite background. +palette = 0=#8B8378 +palette = 1=#F87171 +palette = 2=#34D399 +palette = 3=#FBBF24 +palette = 4=#60A5FA +palette = 5=#C084FC +palette = 6=#2DD4BF +palette = 7=#F8F1E7 +palette = 8=#B8AEA0 +palette = 9=#FCA5A5 +palette = 10=#6EE7B7 +palette = 11=#FCD34D +palette = 12=#93C5FD +palette = 13=#D8B4FE +palette = 14=#5EEAD4 +palette = 15=#FFFFFF diff --git a/ghostty/profiles/refactoria.conf b/ghostty/profiles/refactoria.conf new file mode 100644 index 0000000..3ad7e07 --- /dev/null +++ b/ghostty/profiles/refactoria.conf @@ -0,0 +1,27 @@ +# RefactorIA terminal palette. Loaded after the shared Ghostty config. +background = #0B0714 +foreground = #FFFFFF +cursor-color = #7127FF +cursor-text = #0B0714 +selection-background = #FFFFFF +selection-foreground = #0B0714 + +# ANSI semantic colors are adapted for contrast; they are not RefactorIA brand tokens. +palette = 0=#0B0714 +palette = 1=#FF6B8A +palette = 2=#58E6A8 +palette = 3=#FFC857 +palette = 4=#8CC8FF +palette = 5=#C7A6FF +palette = 6=#66E6E6 +palette = 7=#FFFFFF + +# Bright ANSI variants retain the same semantic roles. +palette = 8=#8F839D +palette = 9=#FF9CB0 +palette = 10=#8FF0C3 +palette = 11=#FFD98A +palette = 12=#B8DDFF +palette = 13=#E1CFFF +palette = 14=#A8F2F2 +palette = 15=#FFFFFF diff --git a/herdr/config-base.toml b/herdr/config-base.toml new file mode 100644 index 0000000..a6c9038 --- /dev/null +++ b/herdr/config-base.toml @@ -0,0 +1,15 @@ +# Shared Herdr settings managed by Gentleman.Dots. + +[keys] +# Match tmux/Zellij-style prefix muscle memory. +prefix = "ctrl+a" + +# Move between agents in the sidebar. +# Avoid Herdr's built-in prefix+shift+j/k pane-swap bindings. +previous_agent = "prefix+alt+k" +next_agent = "prefix+alt+j" + +# Jump straight to agent N (1..9) as listed in the sidebar. +# Use Ctrl after the prefix: Alt+number is owned by skhd/yabai spaces, +# and Shift+number did not reach Herdr reliably in the current terminal setup. +focus_agent = "prefix+ctrl+1..9" diff --git a/herdr/profiles/barbatdev.toml b/herdr/profiles/barbatdev.toml new file mode 100644 index 0000000..953c8fa --- /dev/null +++ b/herdr/profiles/barbatdev.toml @@ -0,0 +1,31 @@ +# Barbatdev/Cortex orange Herdr profile. + +[theme] +# One Dark supplies stable structural defaults outside Herdr's custom palette. +name = "one-dark" +auto_switch = false + +[theme.custom] +panel_bg = "#27231D" +surface0 = "#17130F" +surface1 = "#27231D" +surface_dim = "#11100E" +overlay0 = "#8E8375" +overlay1 = "#B9AD9C" +text = "#F8F1E7" +subtext0 = "#B9AD9C" +accent = "#F97316" +blue = "#60A5FA" +red = "#F87171" +green = "#34D399" +yellow = "#FBBF24" +mauve = "#A78BFA" +teal = "#2DD4BF" +peach = "#FB923C" + +[ui] +accent = "#F97316" +show_agent_labels_on_pane_borders = true + +[ui.toast] +delivery = "herdr" diff --git a/pi/themes/barbatdev.json b/pi/themes/barbatdev.json new file mode 100644 index 0000000..27473c4 --- /dev/null +++ b/pi/themes/barbatdev.json @@ -0,0 +1,94 @@ +{ + "$schema": "https://raw.githubusercontent.com/earendil-works/pi-mono/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json", + "name": "barbatdev", + "vars": { + "bg": "#0B0B0A", + "bgPanel": "#171512", + "bgElement": "#211F1A", + "bgSubtle": "#171512", + "border": "#7C2D12", + "borderSubtle": "#3A2114", + "text": "#F8F1E7", + "muted": "#B9AD9C", + "mutedBright": "#D8CBB8", + "dim": "#8C8173", + "accent": "#F97316", + "deepOrange": "#C2410C", + "activeOrange": "#FB923C", + "gold": "#FBBF24", + "softOrange": "#FDBA74", + "blue": "#60A5FA", + "brightBlue": "#BAE6FD", + "successBlue": "#7DD3FC", + "error": "#EF4444", + "selection": "#3A1D0C", + "userBg": "#7A3500", + "userText": "#FFF7ED", + "toolPendingBg": "#171512", + "toolSuccessBg": "#2A1A0D", + "toolErrorBg": "#2A0E0E", + "infoBg": "#171512" + }, + "colors": { + "accent": "accent", + "border": "border", + "borderAccent": "activeOrange", + "borderMuted": "borderSubtle", + "success": "successBlue", + "error": "error", + "warning": "gold", + "muted": "muted", + "dim": "dim", + "text": "text", + "thinkingText": "muted", + "selectedBg": "selection", + "scrollbarThumb": "border", + "searchMatchBg": "deepOrange", + "searchMatchText": "text", + "userMessageBg": "userBg", + "userMessageText": "userText", + "customMessageBg": "bgSubtle", + "customMessageText": "text", + "customMessageLabel": "gold", + "toolPendingBg": "toolPendingBg", + "toolSuccessBg": "toolSuccessBg", + "toolErrorBg": "toolErrorBg", + "toolTitle": "gold", + "toolOutput": "text", + "mdHeading": "gold", + "mdLink": "brightBlue", + "mdLinkUrl": "mutedBright", + "mdCode": "gold", + "mdCodeBlock": "text", + "mdCodeBlockBorder": "mutedBright", + "mdQuote": "mutedBright", + "mdQuoteBorder": "gold", + "mdHr": "muted", + "mdListBullet": "gold", + "toolDiffAdded": "successBlue", + "toolDiffRemoved": "error", + "toolDiffContext": "muted", + "syntaxComment": "mutedBright", + "syntaxKeyword": "gold", + "syntaxFunction": "brightBlue", + "syntaxVariable": "text", + "syntaxString": "gold", + "syntaxNumber": "softOrange", + "syntaxType": "softOrange", + "syntaxOperator": "brightBlue", + "syntaxPunctuation": "mutedBright", + "thinkingOff": "dim", + "thinkingMinimal": "dim", + "thinkingLow": "muted", + "thinkingMedium": "blue", + "thinkingHigh": "gold", + "thinkingXhigh": "activeOrange", + "thinkingMax": "accent", + "bashMode": "gold" + }, + "export": { + "pageBg": "bg", + "cardBg": "bgElement", + "infoBg": "infoBg" + } +} From 6ec421ce1f28eb5a64d062be29f7f1e7a47b0504 Mon Sep 17 00:00:00 2001 From: Juan Barbat Date: Wed, 21 Oct 2026 09:12:52 -0300 Subject: [PATCH 6/9] fix(install): seed opencode tui.json as a copy instead of a symlink gentle-ai sync refuses to read symlinks and aborts (and fails rollback) on ~/.config/opencode/tui.json. Seed it once, convert our old symlink in place, and keep the file once a tool owns it. Refs #69 --- install.sh | 11 +++++++--- scripts/check-install-symlinks.sh | 34 +++++++++++++++++++++++++++++++ scripts/seed-file.sh | 33 ++++++++++++++++++++++++++++++ 3 files changed, 75 insertions(+), 3 deletions(-) create mode 100644 scripts/seed-file.sh diff --git a/install.sh b/install.sh index 9ab406b..08c43b5 100755 --- a/install.sh +++ b/install.sh @@ -224,7 +224,10 @@ PY check_symlink_target "$DOTFILES/fish/functions/gbarbatdev.fish" "$HOME/.config/fish/functions/gbarbatdev.fish" check_symlink_target "$DOTFILES/fish/functions/grefactoria.fish" "$HOME/.config/fish/functions/grefactoria.fish" fi - check_symlink_target "$DOTFILES/opencode/tui.json" "$HOME/.config/opencode/tui.json" + check_file "$DOTFILES/opencode/tui.json" + if [[ -L "$HOME/.config/opencode/tui.json" ]]; then + warn "opencode tui.json is a symlink; gentle-ai sync refuses it (install.sh converts it to a copy)" + fi check_symlink_target "$DOTFILES/opencode/themes" "$HOME/.config/opencode/themes" check_symlink_target "$DOTFILES/tmux/tmux.conf" "$HOME/.tmux.conf" check_symlink_target "$DOTFILES/claude/statusline.sh" "$HOME/.claude/statusline.sh" @@ -500,7 +503,6 @@ backup_if_exists "$HOME/.config/starship.toml" backup_if_exists "$HOME/.config/herdr/config.toml" backup_if_exists "$HOME/.config/ghostty/config" backup_if_exists "$HOME/.config/herdr/config.toml" -backup_if_exists "$HOME/.config/opencode/tui.json" backup_if_exists "$HOME/.config/opencode/themes" backup_if_exists "$HOME/.tmux.conf" backup_if_exists "$HOME/.claude/statusline.sh" @@ -566,7 +568,10 @@ if [[ "$PLATFORM" == "Darwin" ]]; then create_symlink "$DOTFILES/fish/functions/gbarbatdev.fish" "$HOME/.config/fish/functions/gbarbatdev.fish" create_symlink "$DOTFILES/fish/functions/grefactoria.fish" "$HOME/.config/fish/functions/grefactoria.fish" fi -create_symlink "$DOTFILES/opencode/tui.json" "$HOME/.config/opencode/tui.json" +# gentle-ai sync edits tui.json and refuses symlinks: seed a copy instead. +# shellcheck source=scripts/seed-file.sh +source "$DOTFILES/scripts/seed-file.sh" +seed_file "$DOTFILES/opencode/tui.json" "$HOME/.config/opencode/tui.json" create_symlink "$DOTFILES/opencode/themes" "$HOME/.config/opencode/themes" create_symlink "$DOTFILES/tmux/tmux.conf" "$HOME/.tmux.conf" run_or_plan "chmod +x $DOTFILES/claude/statusline.sh" chmod +x "$DOTFILES/claude/statusline.sh" diff --git a/scripts/check-install-symlinks.sh b/scripts/check-install-symlinks.sh index 606835b..103bf38 100755 --- a/scripts/check-install-symlinks.sh +++ b/scripts/check-install-symlinks.sh @@ -55,3 +55,37 @@ if backup_if_exists "$collision" >/dev/null 2>&1; then fi [[ "$(<"$collision")" == original ]] [[ "$(<"$collision.bak_test")" == existing ]] + +# seed_file: tool-owned configs (gentle-ai refuses symlinks) are copied once. +# shellcheck source=scripts/seed-file.sh +source "$repo_root/scripts/seed-file.sh" +seed_src="$tmp_dir/seed-src" +printf '%s\n' seed > "$seed_src" + +seed_absent="$tmp_dir/seed/absent.json" +seed_file "$seed_src" "$seed_absent" >/dev/null +[[ -f "$seed_absent" && ! -L "$seed_absent" && "$(<"$seed_absent")" == seed ]] + +seed_link="$tmp_dir/seed-link.json" +ln -s "$seed_src" "$seed_link" +seed_file "$seed_src" "$seed_link" >/dev/null +[[ -f "$seed_link" && ! -L "$seed_link" && "$(<"$seed_link")" == seed ]] +[[ "$(<"$seed_src")" == seed ]] + +seed_owned="$tmp_dir/seed-owned.json" +printf '%s\n' tool-edited > "$seed_owned" +seed_file "$seed_src" "$seed_owned" >/dev/null +[[ "$(<"$seed_owned")" == tool-edited ]] + +seed_foreign="$tmp_dir/seed-foreign.json" +ln -s "$old_target" "$seed_foreign" +seed_file "$seed_src" "$seed_foreign" >/dev/null +[[ -L "$seed_foreign" && "$(readlink "$seed_foreign")" == "$old_target" ]] + +DRY_RUN=true +seed_dry="$tmp_dir/seed-dry.json" +seed_file "$seed_src" "$seed_dry" >/dev/null +[[ ! -e "$seed_dry" ]] +DRY_RUN=false + +echo "install symlink checks passed" diff --git a/scripts/seed-file.sh b/scripts/seed-file.sh new file mode 100644 index 0000000..a57fe32 --- /dev/null +++ b/scripts/seed-file.sh @@ -0,0 +1,33 @@ +#!/bin/bash + +# seed_file SRC DST — install a config that another tool owns after first run. +# Some managers (gentle-ai sync) refuse to read or write symlinks, so these +# files are copied, never linked: +# - absent DST: copy SRC +# - DST is our old symlink to SRC: replace it with a regular copy +# - DST is a regular file: keep it; the owning tool may have edited it +# - DST is a symlink elsewhere: keep it and warn +seed_file() { + local src="$1" + local dst="$2" + + if [[ -L "$dst" && "$(readlink "$dst")" != "$src" ]]; then + echo " ! $dst is a foreign symlink; left untouched" >&2 + return 0 + fi + if [[ -e "$dst" && ! -L "$dst" ]]; then + echo " = $dst kept (tool-owned)" + return 0 + fi + if [[ "$DRY_RUN" == true ]]; then + echo " → Would seed $dst from $src" + return 0 + fi + mkdir -p "$(dirname "$dst")" + local tmp + tmp="$(mktemp "$dst.seed.XXXXXX")" + cp "$src" "$tmp" + # mv over a symlink replaces the link itself, never its target. + mv -f "$tmp" "$dst" + echo " ✓ $dst seeded from $src" +} From 1a1f5c83d8676279d94c1e65444988c77e3312ed Mon Sep 17 00:00:00 2001 From: Juan Barbat Date: Wed, 21 Oct 2026 09:12:52 -0300 Subject: [PATCH 7/9] fix(fish): put gentle-ai managed launchers first on PATH gentle-ai cannot persist PATH for fish login shells, so the OpenCode background launcher in ~/.gentle-ai/bin never took effect. Refs #69 --- fish/conf.d/10-core.fish | 6 ++++++ fish/tests/w7-gentle-ai-path.fish | 28 ++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 fish/tests/w7-gentle-ai-path.fish diff --git a/fish/conf.d/10-core.fish b/fish/conf.d/10-core.fish index ea1bef7..1392862 100644 --- a/fish/conf.d/10-core.fish +++ b/fish/conf.d/10-core.fish @@ -75,6 +75,12 @@ else alias la 'ls -lah -A' end +# gentle-ai's managed launchers (OpenCode background subagents) must win on PATH; +# gentle-ai cannot persist PATH for fish login shells by itself. +if test -d "$HOME/.gentle-ai/bin" + fish_add_path --path --move "$HOME/.gentle-ai/bin" +end + if status is-interactive; and command -q starship starship init fish | source end diff --git a/fish/tests/w7-gentle-ai-path.fish b/fish/tests/w7-gentle-ai-path.fish new file mode 100644 index 0000000..37cba5d --- /dev/null +++ b/fish/tests/w7-gentle-ai-path.fish @@ -0,0 +1,28 @@ +#!/usr/bin/env fish +# gentle-ai's managed launcher dir (~/.gentle-ai/bin) must lead PATH when present, +# because gentle-ai cannot persist PATH for fish login shells itself. + +set -l repo_root (cd (dirname (dirname (status filename))); and pwd) +set -l core "$repo_root/conf.d/10-core.fish" +set -l fish_bin (status fish-path) +set -l ws (mktemp -d) + +function fail + printf 'FAIL: %s\n' "$argv" >&2 + exit 1 +end + +set -l probe 'source $argv[1]; string join "|" $PATH' + +mkdir -p "$ws/with/.gentle-ai/bin" "$ws/without" +set -l with (env -i HOME="$ws/with" PATH=/usr/bin:/bin "$fish_bin" --no-config -c "$probe" "$core") +string match -q "$ws/with/.gentle-ai/bin|*" -- "$with"; or fail "launcher dir not first in PATH: $with" + +set -l twice (env -i HOME="$ws/with" PATH="$ws/with/.gentle-ai/bin:/usr/bin:/bin" "$fish_bin" --no-config -c "$probe" "$core") +test (count (string match -a -- "$ws/with/.gentle-ai/bin" (string split '|' -- "$twice"))) -eq 1; or fail "launcher dir duplicated: $twice" + +set -l without (env -i HOME="$ws/without" PATH=/usr/bin:/bin "$fish_bin" --no-config -c "$probe" "$core") +string match -q '*gentle-ai*' -- "$without"; and fail "missing launcher dir added to PATH: $without" + +rm -rf -- "$ws" +printf 'gentle-ai path checks passed\n' From f3ac409727641209c13ca16ee12d701452cfa346 Mon Sep 17 00:00:00 2001 From: Juan Barbat Date: Wed, 21 Oct 2026 09:12:52 -0300 Subject: [PATCH 8/9] feat(gs): run gentle-ai sync after updating binaries gs sync only rebuilt binaries, so managed configs drifted per host. It now runs gentle-ai sync (scoped by GS_SYNC_AGENTS, skippable with GS_SKIP_AI_SYNC=1), reports failures without aborting, and flags legacy upgrade scripts so gs sync is the single entry point. Closes #69 --- bin/gs | 23 +++++++++- fish/conf.d/99-local.fish.example | 4 ++ local/env.zsh.example | 5 +++ scripts/test-gs-sync.sh | 74 +++++++++++++++++++++++++++++++ 4 files changed, 104 insertions(+), 2 deletions(-) create mode 100644 scripts/test-gs-sync.sh diff --git a/bin/gs b/bin/gs index 392b909..bf88509 100755 --- a/bin/gs +++ b/bin/gs @@ -29,8 +29,11 @@ gs - gentle-shell on main, plus one disposable worktree per PR gs review Check out someone else's PR to test or review it gs base Clean origin/main worktree, to compare failures gs run [pi args...] Run gentle-shell --link with this worktree as --package-root - gs sync Bring gentle-shell, gentle-ai and engram to main and check - compatibility (workshop: local clones; elsewhere: GitHub) + gs sync Bring gentle-shell, gentle-ai and engram to main, run + gentle-ai sync for managed configs, and check compatibility + (workshop: local clones; elsewhere: GitHub) + GS_SYNC_AGENTS=a,b scopes gentle-ai sync to those runtimes; + GS_SKIP_AI_SYNC=1 skips it (binaries only) gs ls List open worktrees gs done Remove the current worktree and its merged local branch (drops odd/tasks records; refuses if real work would be lost) @@ -342,6 +345,22 @@ cmd_sync() { ln -sfn "$ai_bin" "$HOME/.local/bin/gentle-ai" ln -sfn "$bin/engram" "$HOME/.local/bin/engram" + # New binaries are not enough: gentle-ai sync materializes the managed configs + # (agents, skills, plugins, engram instructions) every runtime reads. + # GS_SYNC_AGENTS scopes runtimes per host (local env); empty = gentle-ai default. + if [ "${GS_SKIP_AI_SYNC:-}" = 1 ]; then + printf ' %-34s skipped (GS_SKIP_AI_SYNC=1)\n' 'gentle-ai sync' + else + local ai_sync=(sync) + [ -n "${GS_SYNC_AGENTS:-}" ] && ai_sync+=(--agents "$GS_SYNC_AGENTS") + run_quiet 'gentle-ai sync' "$ai_bin" "${ai_sync[@]}" + fi + # gs sync is the only upgrade path; older homemade scripts drift the hosts apart. + for name in "$HOME/.local/bin/gentle-maintainer-upgrade" "$HOME/.local/bin/gentle-sync.sh" \ + /usr/bin/gentle-sync.sh /usr/local/bin/gentle-upgrade.sh /bin/gentle-upgrade.sh; do + [ -e "$name" ] && printf 'WARN: legacy upgrade script %s; use gs sync only and remove it\n' "$name" + done + # Versions after the sync decide the closing message: restart only on change. local shell_after ai_after engram_after changed shell_after=$(shell_version); ai_after=$(ai_version); engram_after=$(engram_version) diff --git a/fish/conf.d/99-local.fish.example b/fish/conf.d/99-local.fish.example index 1e46392..15f6c5c 100644 --- a/fish/conf.d/99-local.fish.example +++ b/fish/conf.d/99-local.fish.example @@ -8,3 +8,7 @@ # set -gx GIT_WORKDEV_EMAIL "you@company.invalid" # set -gx GIT_PERSONALDEV_NAME "Your personal name" # set -gx GIT_PERSONALDEV_EMAIL "you@personal.invalid" +# gs sync runs gentle-ai sync after updating binaries. Scope it to this host's runtimes: +# set -gx GS_SYNC_AGENTS claude-code,opencode,pi +# Binaries only, no managed-config sync: +# set -gx GS_SKIP_AI_SYNC 1 diff --git a/local/env.zsh.example b/local/env.zsh.example index d759cc6..4766b42 100644 --- a/local/env.zsh.example +++ b/local/env.zsh.example @@ -48,3 +48,8 @@ # export CORTEX_BRAINS_OLLAMA_URL="http://:11434" # export CORTEX_BRAINS_PG_HOST="" # export CORTEX_BRAINS_PG_PORT="5432" + +# gs sync runs gentle-ai sync after updating binaries. Scope it to this host's runtimes: +# export GS_SYNC_AGENTS="claude-code,opencode,pi" +# Binaries only, no managed-config sync: +# export GS_SKIP_AI_SYNC=1 diff --git a/scripts/test-gs-sync.sh b/scripts/test-gs-sync.sh new file mode 100644 index 0000000..067b58a --- /dev/null +++ b/scripts/test-gs-sync.sh @@ -0,0 +1,74 @@ +#!/bin/bash +# Run the real `gs sync` (consumer mode) with an isolated HOME and PATH shims: +# no network, no host installs. Checks that the binaries step is followed by +# `gentle-ai sync`, scoped by GS_SYNC_AGENTS and skippable with GS_SKIP_AI_SYNC. +set -euo pipefail + +repo_root="$(cd "$(dirname "$0")/.." && pwd)" +ws="$(mktemp -d)" +trap 'rm -rf "$ws"' EXIT + +fail() { printf 'FAIL: %s\n%s\n' "$1" "${2:-}" >&2; exit 1; } + +home="$ws/home" +shims="$ws/shims" +log="$ws/calls.log" +mkdir -p "$home/.pi/gentle-ai" "$shims" + +# One shim script, dispatched by name; every call is logged. +cat >"$shims/_shim" <>"$log" +case "\$name \$1" in + "gentle-shell --version") echo "gentle-shell 4.2.0" ;; + "go install") + for arg in "\$@"; do case "\$arg" in *cmd/engram@*) + printf '#!/bin/sh\necho "engram 3.0.0"\n' >"\$GOBIN/engram"; chmod +x "\$GOBIN/engram" ;; + esac; done ;; + "gentle-ai --version") echo "gentle-ai 4.2.1" ;; + "gentle-ai sync") [ -n "\${FAKE_AI_SYNC_FAIL:-}" ] && { echo "Error: partial sync"; exit 1; }; echo "managed sync executed" ;; +esac +exit 0 +EOF +chmod +x "$shims/_shim" +for name in gentle-shell pi go; do ln -s _shim "$shims/$name"; done +ai_dir="$ws/ai"; mkdir -p "$ai_dir"; ln -s "$shims/_shim" "$ai_dir/gentle-ai" +printf '{"schema":"gentle-pi.dev-binary/v1","path":"%s"}\n' "$ai_dir/gentle-ai" >"$home/.pi/gentle-ai/dev-binary.json" + +run_sync() { + : >"$log" + env -i HOME="$home" PATH="$shims:/usr/bin:/bin" TERM=dumb "$@" \ + bash "$repo_root/bin/gs" sync 2>&1 +} +ai_sync_calls() { grep '^gentle-ai|sync' "$log" || true; } + +# 1. Default: gentle-ai sync runs once, unscoped, after the binaries step. +out=$(run_sync) || fail 'gs sync exited non-zero' "$out" +[[ "$(ai_sync_calls)" == 'gentle-ai|sync' ]] || fail 'expected one unscoped gentle-ai sync' "$(cat "$log")" +[[ "$out" == *'gentle-ai sync'*'ok'* ]] || fail 'missing gentle-ai sync status line' "$out" +engram_line=$(grep -n 'cmd/engram@main' "$log" | cut -d: -f1) +sync_line=$(grep -n '^gentle-ai|sync' "$log" | cut -d: -f1) +(( engram_line < sync_line )) || fail 'gentle-ai sync ran before the binaries step' "$(cat "$log")" + +# 2. GS_SYNC_AGENTS scopes the runtimes for this host. +out=$(run_sync GS_SYNC_AGENTS=claude-code,opencode,pi) || fail 'scoped gs sync exited non-zero' "$out" +[[ "$(ai_sync_calls)" == 'gentle-ai|sync --agents claude-code,opencode,pi' ]] || fail 'agents scope not forwarded' "$(cat "$log")" + +# 3. GS_SKIP_AI_SYNC=1 keeps the binaries-only behavior. +out=$(run_sync GS_SKIP_AI_SYNC=1) || fail 'skipped gs sync exited non-zero' "$out" +[[ -z "$(ai_sync_calls)" ]] || fail 'gentle-ai sync ran despite GS_SKIP_AI_SYNC=1' "$(cat "$log")" +[[ "$out" == *'gentle-ai sync'*'skipped (GS_SKIP_AI_SYNC=1)'* ]] || fail 'missing skip status line' "$out" + +# 4. A failing gentle-ai sync is reported with its output, and gs sync goes on. +out=$(run_sync FAKE_AI_SYNC_FAIL=1) || fail 'gs sync aborted on gentle-ai sync failure' "$out" +[[ "$out" == *'gentle-ai sync'*'FAILED'*'Error: partial sync'* ]] || fail 'failure output not surfaced' "$out" +[[ "$out" == *'versions:'* ]] || fail 'sync stopped before the closing report' "$out" + +# 5. Legacy upgrade scripts are flagged so only gs sync remains. +mkdir -p "$home/.local/bin"; printf '#!/bin/sh\n' >"$home/.local/bin/gentle-maintainer-upgrade" +out=$(run_sync) || fail 'gs sync exited non-zero with legacy script' "$out" +[[ "$out" == *"WARN: legacy upgrade script $home/.local/bin/gentle-maintainer-upgrade"* ]] || fail 'legacy script not flagged' "$out" +[[ -f "$home/.local/bin/gentle-maintainer-upgrade" ]] || fail 'legacy script was removed' "$out" + +echo "gs sync checks passed" From 2c8f8dc7c0ed01ba297210977fa7baefaba6e9de Mon Sep 17 00:00:00 2001 From: Juan Barbat Date: Wed, 21 Oct 2026 09:18:14 -0300 Subject: [PATCH 9/9] ci: restore Linux Home Manager README contract and run gs sync test The README restore in dc56614 dropped the Linux rows the nix preflight check requires, which kept main red and blocked merges. Refs #69 --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8a10fd3..3ffa252 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -83,6 +83,9 @@ jobs: - name: Check installer symlink safety run: ./scripts/check-install-symlinks.sh + - name: Check gs sync runs gentle-ai sync + run: bash scripts/test-gs-sync.sh + - name: Check Nix Home Manager preflight contracts run: bash scripts/test-nix-preflight.sh