From 58e5dd8321dfd5f532b460e038dd5a7fa1c693ff Mon Sep 17 00:00:00 2001 From: Justin Puah Date: Mon, 20 Jul 2026 15:09:27 +1000 Subject: [PATCH 1/2] feat(setup): add herdr module with agent-state integration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Complete the herdr module (terminal workspace manager for AI agents): - Install-Herdr / install_herdr symlink herdr/config.toml to ~/.config/herdr/config.toml (herdr uses ~/.config on Windows too) and run `herdr integration install ` for each agent on PATH. The installs are idempotent and herdr owns the generated files, so nothing is vendored — setup regenerates the per-machine (absolute-path) hooks. claude + codex on both OSes; pi on Linux only (Windows unsupported). - Order herdr LAST in the `-Module all` array: its claude integration writes into ~/.claude/settings.json, which the claude module symlinks to the repo. Running before claude would create a real settings.json that the later symlink backs up and replaces, dropping the hook block. - codex/config.toml gains `[features] hooks = true` (config.toml is copied, not symlinked, so the flag must be tracked to survive a copy-overwrite regardless of module order). - herdr/config.toml tracks real prefs (onboarding, pane_history, ui, catppuccin theme) plus default_shell = pwsh. - Tests: dry-run coverage for -Module herdr. Docs: herdr/README.md and the CLAUDE.md herdr section + module lists. --- CLAUDE.md | 18 ++++++++++-- codex/config.toml | 8 ++++++ herdr/README.md | 54 +++++++++++++++++++++++++++++++++++ herdr/config.toml | 31 +++++++++++++++++++++ setup.ps1 | 65 +++++++++++++++++++++++++++++++++++++++++-- setup.sh | 40 ++++++++++++++++++++++++-- tests/setup.Tests.ps1 | 26 +++++++++++++++++ 7 files changed, 234 insertions(+), 8 deletions(-) create mode 100644 herdr/README.md create mode 100644 herdr/config.toml diff --git a/CLAUDE.md b/CLAUDE.md index fb433ada..b1f41698 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Repository scope -Personal dotfiles spanning Windows (PowerShell 7) and Linux/WSL (bash, Neovim, tmux, i3/bspwm). Active development is on the **Windows side**: the PowerShell profile under `powershell/`, the prompt in `powershell/Profile/Set-Prompt.ps1`, the Neovim Lua config under `nvim/`, the Zellij config under `zellij/`, the Yazi config under `yazi/`, and the git config under `git/`. Configs are organized into per-tool directories: `git/`, `nvim/`, `vim/`, `bash/`, `powershell/`, `tig/`, `tmux/`, `zellij/`, `psmux/`, `yazi/`, `fzf/`, `curl/`, `claude/`, `codex/`, `windowsterminal/`, `lazygit/`, `bat/`, `vscode/`, `winget/`, `ripgrep/`, `eza/`. The Linux setup (`config/bspwm`, `config/sxhkd`, `i3/`, `tmux/`, `bash/`, `vim/`, etc.) is a legacy snapshot — touch only when explicitly asked. +Personal dotfiles spanning Windows (PowerShell 7) and Linux/WSL (bash, Neovim, tmux, i3/bspwm). Active development is on the **Windows side**: the PowerShell profile under `powershell/`, the prompt in `powershell/Profile/Set-Prompt.ps1`, the Neovim Lua config under `nvim/`, the Zellij config under `zellij/`, the Yazi config under `yazi/`, and the git config under `git/`. Configs are organized into per-tool directories: `git/`, `nvim/`, `vim/`, `bash/`, `powershell/`, `tig/`, `tmux/`, `zellij/`, `psmux/`, `herdr/`, `yazi/`, `fzf/`, `curl/`, `claude/`, `codex/`, `windowsterminal/`, `lazygit/`, `bat/`, `vscode/`, `winget/`, `ripgrep/`, `eza/`. The Linux setup (`config/bspwm`, `config/sxhkd`, `i3/`, `tmux/`, `bash/`, `vim/`, etc.) is a legacy snapshot — touch only when explicitly asked. Note the two `CLAUDE.md` files: this root one is **project instructions for the dotfiles repo**; `claude/CLAUDE.md` is the **global user instructions** that `setup.ps1 -Module claude` installs to `~/.claude/CLAUDE.md`. They are unrelated — don't merge them. See `claude/README.md` for the Claude Code module (settings, statusline, skills, agents); its files are **symlinked** into `~/.claude` on both Windows and Linux (Windows file symlinks need Developer Mode; skill dirs are junctioned on Windows), so the live files and the repo never drift. @@ -21,8 +21,8 @@ The PowerShell profile (`powershell/Microsoft.PowerShell_profile.ps1`) is a sing | Script | Target | Notes | |---|---|---| -| `setup.ps1` | Windows | Module-based installer. `-Module neovim,vim,powershell,git,bash,tig,tmux,zellij,psmux,yazi,curl,claude,codex,serena,context7,fastmail,lazygit,windowsterminal,bat,vscode,winget` or `-Module all`. Supports `-DryRun`. | -| `setup.sh` | Linux / WSL | Module-based installer. `-m neovim,vim,powershell,git,bash,tig,tmux,zellij,curl,claude,lazygit,windowsterminal` or `-m all`. Supports `--dry-run`. (No `codex` module yet — Windows only.) | +| `setup.ps1` | Windows | Module-based installer. `-Module neovim,vim,powershell,git,bash,tig,tmux,zellij,psmux,herdr,yazi,curl,claude,codex,serena,context7,fastmail,lazygit,windowsterminal,bat,vscode,winget` or `-Module all`. Supports `-DryRun`. | +| `setup.sh` | Linux / WSL | Module-based installer. `-m neovim,vim,powershell,git,bash,tig,tmux,zellij,herdr,curl,claude,lazygit,windowsterminal` or `-m all`. Supports `--dry-run`. (No `codex` module yet — Windows only.) | `setup.ps1` argument handling has Pester tests in `tests/setup.Tests.ps1` (run `Invoke-Pester -Path tests`); they invoke the installer in `-DryRun` so nothing is mutated. @@ -200,6 +200,18 @@ Single config file `psmux/psmux.conf`, symlinked to `~/.psmux.conf` via `setup.p - **Session fzf helpers** (`Enter-/Remove-/Get-PsmuxSession` + native tab-completer) live in the PowerShell profile **Phase 2b**, beside the zellij/az completers. The three functions must be declared `function global:` — a bare `function` inside the Phase-2b `Initialize-DeferredProfileSecondary` is scoped local and vanishes when it returns (the tab-completer survives only because `Register-ArgumentCompleter` is a global side-effect). - **`tests/psmux.Tests.ps1`**: (1) a **config parse test** — loads `psmux.conf` via `psmux -f` on a private socket + throwaway HOME and asserts no parse warning (guards the trailing-comment class of bug; skips when psmux is absent, as CI has none); and (2) a **`save.ps1` zero-session guard test** — drives `save.ps1` with a PATH-shim fake `psmux` (empty-output and stderr-error cases) and asserts the previous `last` snapshot is kept and the guard's skip message + exit code fire (so it can't false-green). The save test needs no psmux install, so it runs in CI. +## Herdr (`herdr/`) + +[Herdr](https://herdr.dev) is a terminal workspace manager for AI coding agents — it tracks each pane's live agent session. Single config file `herdr/config.toml`, installed via `setup.ps1 -Module herdr` / `setup.sh -m herdr`. See `herdr/README.md`. + +### Key decisions (do not reverse without asking) + +- **Config is at `~/.config/herdr/config.toml` on every platform — including Windows** (verified from herdr's own server-log path output; it does **not** use `%APPDATA%`). Only `config.toml` is symlinked; the rest of that directory is herdr runtime state (`session.json`, `herdr.sock`, `*.log`) and must stay untracked — so the file is linked, not the whole dir (unlike Zellij/Yazi). +- **Agent integrations are (re)generated by setup, not vendored.** `Install-Herdr` runs `herdr integration install ` for each agent on PATH (`claude`, `codex`). It is **idempotent** (a re-run is a verified no-op) and writes files herdr manages itself — the hook script (`~/.claude/hooks/herdr-agent-state.ps1`, `~/.codex/herdr-agent-state.ps1`), `~/.codex/hooks.json`, and the hook's registration in the agent's settings. Nothing is committed to the repo because the generated blocks carry machine-absolute paths; regenerating per machine keeps them correct. This mirrors the codex module's `claude mcp add` pattern. +- **`herdr` must run AFTER `claude`/`codex` — the `-Module all` array orders it last.** `claude/settings.json` is **symlinked**, so `herdr integration install claude` writes its SessionStart block straight through into the repo file (the same self-mutate path Claude Code's own settings-writes use) — **but only once the symlink exists**. If herdr ran first on a fresh machine, that write would create a *real* `settings.json`, then the claude module's `New-FileSymlink` would back it up and replace it, silently dropping the block. `codex` differs: its registration lives in a separate `~/.codex/hooks.json` the codex module never touches, plus `[features] hooks = true` — and because `codex/config.toml` is **copied**, that flag is tracked in the repo copy so a copy-overwrite can't drop it. So codex survives any order; **claude survives only if herdr is last** (hence the array order, and the caveat: installing `claude` *after* `herdr` needs a `-Module herdr` re-run). Consequence: each `-Module herdr` run rewrites the block into the tracked `settings.json`, i.e. expected uncommitted drift — consistent with the self-mutate behaviour, and why the block is not itself committed. +- **`pi` integration is Windows-unsupported** (`herdr integration install pi` → "not supported on Windows"), so `setup.ps1` omits it; `setup.sh` includes `pi` in the wired agents for Linux/WSL. +- **`config.toml` tracks real prefs** (`onboarding=false`, `[experimental] pane_history`, `[ui]`/`[ui.toast]`, `[theme]` catppuccin) plus `[terminal] default_shell = "pwsh"`. Dry-run coverage is in `tests/setup.Tests.ps1` (asserts the config symlink line always, and the integration dry-run line when herdr + an agent are present). + ## Yazi (`yazi/`) Three config files: `yazi.toml` (manager/opener/preview settings), `keymap.toml` (keybinding additions via `prepend_keymap`), `theme.toml` (flavor reference). Installed via `setup.ps1 -Module yazi` (Windows — junction of the whole `yazi/` dir to `%AppData%\yazi\config\`). diff --git a/codex/config.toml b/codex/config.toml index dc7a4ef7..3a4c6198 100644 --- a/codex/config.toml +++ b/codex/config.toml @@ -18,3 +18,11 @@ sandbox_mode = "workspace-write" # (sourced from dotfiles claude/AGENTS.md). Cap the instruction chain generously so the # full conventions file is always read. project_doc_max_bytes = 32768 + +# Enable Codex's hook system so herdr's agent-state integration can report session +# activity to a running herdr server. `setup.ps1 -Module herdr` runs `herdr integration +# install codex`, which writes the hook script + ~/.codex/hooks.json; this flag turns the +# hook system on. It lives here (not left to the integration) because this file is COPIED, +# not symlinked — so a fresh -Module codex run after herdr would otherwise drop it. +[features] +hooks = true diff --git a/herdr/README.md b/herdr/README.md new file mode 100644 index 00000000..40c755c5 --- /dev/null +++ b/herdr/README.md @@ -0,0 +1,54 @@ +# herdr + +[Herdr](https://herdr.dev) is a terminal workspace manager for AI coding agents. It +tracks each pane's live agent session so you can see, at a glance, which agent is running +where. This module installs the shared `config.toml` and wires herdr's agent-state hooks +into the AI agents you have installed. + +## Install + +```powershell +setup.ps1 -Module herdr # Windows +``` +```bash +./setup.sh -m herdr # Linux / WSL +``` + +herdr itself is installed separately (`https://herdr.dev`); the module warns if it is not +on PATH. + +## What the module does + +1. **Symlinks `config.toml`** to `~/.config/herdr/config.toml` (herdr uses `~/.config` on + every platform, including Windows — it does **not** use `%APPDATA%`). Only this file is + linked; herdr keeps its runtime state (`session.json`, `herdr.sock`, `*.log`) in the same + directory and that must stay untracked. +2. **Wires agent integrations** by running `herdr integration install ` for each AI + agent found on PATH: + - **claude** and **codex** on Windows and Linux. + - **pi** on Linux/WSL only — `herdr integration install pi` reports "not supported on + Windows", so `setup.ps1` omits it. + + These installs are **idempotent** — re-running is a no-op. herdr owns the files they + generate (the `herdr-agent-state.ps1` hook script, `~/.codex/hooks.json`, and the hook's + registration in each agent's settings), which is why they are not committed to this repo: + the generated blocks carry machine-absolute paths, so regenerating them per machine keeps + them correct. `codex/config.toml` carries `[features] hooks = true` (needed to turn on + Codex's hook system) because that file is copied, not symlinked. + +## config.toml + +Only deliberate overrides live in `config.toml`; everything else stays on herdr's defaults. +Run `herdr --default-config` for the full annotated template. After editing: + +``` +herdr config check # validate +herdr server reload-config # apply to a running server +``` + +## Verify + +``` +herdr integration status # per-agent hook version + path +herdr config check +``` diff --git a/herdr/config.toml b/herdr/config.toml new file mode 100644 index 00000000..3e8333e3 --- /dev/null +++ b/herdr/config.toml @@ -0,0 +1,31 @@ +# herdr configuration — see `herdr --default-config` for the full annotated template. +# Only deliberate overrides live here; everything else stays on herdr's defaults. +# +# Installed by `setup.ps1 -Module herdr` / `setup.sh -m herdr` as a symlink to +# ~/.config/herdr/config.toml (herdr uses ~/.config on Windows too, not %APPDATA%). +# Only this file is linked — the rest of that directory is herdr runtime state +# (session.json, sockets, logs) and must not be tracked. +# +# Validate with `herdr config check`; apply to a running server with +# `herdr server reload-config`. + +# Skip the first-run onboarding flow. +onboarding = false + +[terminal] +# Resolved from PATH. Herdr otherwise falls back to $SHELL, then /bin/sh. +default_shell = "pwsh" + +[experimental] +# Keep per-pane scrollback history across sessions. +pane_history = true + +[ui] +show_agent_labels_on_pane_borders = true + +[ui.toast] +delivery = "herdr" + +[theme] +name = "catppuccin" +auto_switch = false diff --git a/setup.ps1 b/setup.ps1 index cc6a6bfc..62e3022f 100644 --- a/setup.ps1 +++ b/setup.ps1 @@ -9,7 +9,7 @@ before being replaced. .PARAMETER Module - One or more modules to install: neovim, vim, powershell, git, bash, tig, tmux, zellij, psmux, yazi, curl, claude, codex, serena, context7, fastmail, lazygit, windowsterminal, bat, vscode, winget, all. + One or more modules to install: neovim, vim, powershell, git, bash, tig, tmux, zellij, psmux, herdr, yazi, curl, claude, codex, serena, context7, fastmail, lazygit, windowsterminal, bat, vscode, winget, all. Optional when -CleanBackups is specified. .PARAMETER DryRun @@ -66,7 +66,13 @@ if (-not $env:USERPROFILE) { $env:USERPROFILE = $HOME } # Expand 'all' if ($Module -contains 'all') { - $Module = @('neovim', 'vim', 'powershell', 'git', 'bash', 'tig', 'tmux', 'zellij', 'psmux', 'yazi', 'curl', 'claude', 'codex', 'serena', 'context7', 'fastmail', 'lazygit', 'windowsterminal', 'bat', 'vscode', 'winget') + # 'herdr' MUST come after 'claude' and 'codex': it runs `herdr integration install `, + # which writes a hook registration into each agent's settings. claude's registration lands in + # ~/.claude/settings.json — a file the claude module SYMLINKS to the repo. If herdr ran first, + # that write would create a real settings.json, then the claude module's symlink would back it + # up and replace it, silently dropping the herdr block. Running herdr last means it writes + # through the already-established symlink into the repo file, so the block survives. + $Module = @('neovim', 'vim', 'powershell', 'git', 'bash', 'tig', 'tmux', 'zellij', 'psmux', 'yazi', 'curl', 'claude', 'codex', 'serena', 'context7', 'fastmail', 'lazygit', 'windowsterminal', 'bat', 'vscode', 'winget', 'herdr') } # @() wrapper: `Select-Object -Unique` over an empty array yields $null (and a # single value yields a scalar), so without it the `$Module.Count` guard below @@ -541,6 +547,60 @@ function Install-Psmux { } } +function Install-Herdr { + Write-Host '' + Write-Info '=== Herdr (terminal workspace manager for AI agents) ===' + # Herdr reads ~/.config/herdr/config.toml on every platform including Windows + # (verified from the server log's own path output) — it does not use %APPDATA%. + # Only config.toml is linked, NOT the whole directory: herdr keeps its runtime + # state (session.json, herdr.sock, *.log) alongside it, which must stay untracked. + $herdrConfig = Join-Path $env:USERPROFILE '.config\herdr' + # -Backup is a reverse-sync (live -> repo) and must never mutate the live side; + # New-FileSymlink skips under -Backup, so guard the mkdir the same way (mirrors Install-Zellij). + if (-not (Test-Path $herdrConfig) -and -not $Backup) { + if ($DryRun) { + Write-Info "[DRY RUN] would create: $herdrConfig" + } else { + New-Item -ItemType Directory -Path $herdrConfig -Force | Out-Null + Write-Info "Created: $herdrConfig" + } + } + $params = @{ + Link = Join-Path $herdrConfig 'config.toml' + Target = Join-Path $Dotfiles 'herdr\config.toml' + } + New-FileSymlink @params + + if (-not (Get-Command -Name herdr -ErrorAction Ignore)) { + Write-Warn 'herdr not found on PATH. Install from https://herdr.dev' + } else { + Write-Ok 'herdr is installed.' + Write-Info 'Validate with: herdr config check Apply live with: herdr server reload-config' + + # Wire herdr's agent-state hooks into the installed AI agents so herdr can track each + # pane's live agent session. `herdr integration install ` is idempotent (a re-run + # is a verified no-op) and writes agent-side files herdr manages itself — the hook script + # (~/.claude/hooks, ~/.codex) plus its registration in that agent's settings — so nothing + # is vendored here; setup regenerates it per machine with correct paths. Only agents + # actually on PATH are wired. pi is omitted: `herdr integration install pi` reports "not + # supported on Windows" (it is wired on Linux by setup.sh instead). + foreach ($agent in @('claude', 'codex')) { + if (-not (Get-Command -Name $agent -ErrorAction Ignore)) { continue } + if ($Backup) { continue } # reverse-sync must never mutate the live side + if ($DryRun) { + Write-Info "[DRY RUN] herdr integration install $agent" + continue + } + herdr integration install $agent *>$null + if ($LASTEXITCODE -eq 0) { + Write-Ok "Integration: $agent wired to herdr" + } else { + Write-Warn "herdr integration install $agent failed (exit $LASTEXITCODE)" + } + } + } +} + function Install-Yazi { Write-Host '' Write-Info '=== Yazi ===' @@ -1114,6 +1174,7 @@ foreach ($m in $Module) { 'tmux' { Install-Tmux } 'zellij' { Install-Zellij } 'psmux' { Install-Psmux } + 'herdr' { Install-Herdr } 'yazi' { Install-Yazi } 'curl' { Install-Curl } 'winget' { Install-Winget } diff --git a/setup.sh b/setup.sh index 113998dc..c299f535 100644 --- a/setup.sh +++ b/setup.sh @@ -5,7 +5,7 @@ # ./setup.sh -m neovim,vim # ./setup.sh -m all --dry-run # -# Modules: neovim, vim, powershell, git, bash, tig, tmux, zellij, curl, claude, lazygit, windowsterminal, all +# Modules: neovim, vim, powershell, git, bash, tig, tmux, zellij, herdr, curl, claude, lazygit, windowsterminal, all set -euo pipefail @@ -20,7 +20,7 @@ MAX_BACKUP_AGE_DAYS=0 usage() { echo "Usage: $0 -m [--dry-run] [--clean-backups [--keep-backups N] [--max-backup-age-days N]]" - echo " Modules: neovim, vim, powershell, git, bash, tig, tmux, zellij, curl, claude, lazygit, windowsterminal, all" + echo " Modules: neovim, vim, powershell, git, bash, tig, tmux, zellij, herdr, curl, claude, lazygit, windowsterminal, all" echo " --clean-backups remove old .bak.TIMESTAMP files from previous runs" echo " --keep-backups N keep N most recent backups per file (default: 5, 0 = no limit)" echo " --max-backup-age-days N delete backups older than N days (default: 0 = disabled)" @@ -55,7 +55,12 @@ done # Expand 'all' for m in "${MODULES[@]}"; do if [[ "$m" == "all" ]]; then - MODULES=(neovim vim powershell git bash tig tmux zellij curl claude lazygit windowsterminal) + # 'herdr' MUST come after 'claude': install_herdr runs `herdr integration install claude`, + # which writes a hook registration into ~/.claude/settings.json — a file the claude module + # symlinks to the repo. If herdr ran first it would create a real settings.json, then the + # claude symlink would replace it, dropping the herdr block. Running herdr last writes + # through the established symlink so the block survives (same reasoning as setup.ps1). + MODULES=(neovim vim powershell git bash tig tmux zellij curl claude lazygit windowsterminal herdr) break fi done @@ -224,6 +229,34 @@ install_zellij() { make_symlink "$DOTFILES/zellij" "$xdg_config/zellij" } +install_herdr() { + echo '' + info '=== Herdr ===' + local xdg_config="${XDG_CONFIG_HOME:-$HOME/.config}" + # Link only config.toml, not the directory: herdr keeps its runtime state + # (session.json, herdr.sock, *.log) in the same dir and that must stay untracked. + make_symlink "$DOTFILES/herdr/config.toml" "$xdg_config/herdr/config.toml" + + # Wire herdr's agent-state hooks into installed AI agents so herdr can track each pane's + # live agent session. `herdr integration install ` is idempotent and writes files + # herdr manages itself, so nothing is vendored — setup regenerates it per machine. Only + # agents on PATH are wired. Unlike Windows, pi's integration is supported here. + if command -v herdr >/dev/null 2>&1; then + for agent in claude codex pi; do + command -v "$agent" >/dev/null 2>&1 || continue + if [[ $DRY_RUN -eq 1 ]]; then + info "[DRY RUN] herdr integration install $agent" + elif herdr integration install "$agent" >/dev/null 2>&1; then + ok "Integration: $agent wired to herdr" + else + warn "herdr integration install $agent failed" + fi + done + else + warn 'herdr not found on PATH. Install from https://herdr.dev' + fi +} + install_curl() { echo '' info '=== Curl ===' @@ -394,6 +427,7 @@ for module in "${MODULES[@]}"; do tig) install_tig ;; tmux) install_tmux ;; zellij) install_zellij ;; + herdr) install_herdr ;; curl) install_curl ;; claude) install_claude ;; lazygit) install_lazygit ;; diff --git a/tests/setup.Tests.ps1 b/tests/setup.Tests.ps1 index a8fd0702..73d24cb9 100644 --- a/tests/setup.Tests.ps1 +++ b/tests/setup.Tests.ps1 @@ -196,6 +196,32 @@ Describe 'setup.ps1 claude module output styles' { } } +Describe 'setup.ps1 herdr module' { + It 'dry-run links config.toml and, when herdr + an agent are present, wires the integration' { + $tmpHome = Join-Path ([IO.Path]::GetTempPath()) ('setup-herdr-dryrun-' + [guid]::NewGuid()) + New-Item -ItemType Directory -Path $tmpHome -Force | Out-Null + $origUP = $env:USERPROFILE + try { + $env:USERPROFILE = $tmpHome + $output = & pwsh -NoProfile -File $script:SetupScript -Module herdr -DryRun 2>&1 | Out-String + $LASTEXITCODE | Should -Be 0 + $output | Should -Match '\[DRY RUN\] symlink .*herdr\\config\.toml' + # The agent-integration wiring only runs when herdr itself is on PATH; assert its + # dry-run line only then, so CI (which has no herdr) still exercises the symlink path. + if (Get-Command -Name herdr -ErrorAction Ignore) { + foreach ($agent in @('claude', 'codex')) { + if (Get-Command -Name $agent -ErrorAction Ignore) { + $output | Should -Match "\[DRY RUN\] herdr integration install $agent" + } + } + } + } finally { + $env:USERPROFILE = $origUP + Remove-Item -Path $tmpHome -Recurse -Force -ErrorAction SilentlyContinue + } + } +} + Describe 'setup.ps1 -Module all' { It 'runs the full module set in -DryRun without error (Windows-only)' -Skip:(-not $IsWindows) { # Non-Windows hits [Environment]::GetFolderPath('MyDocuments') returning an empty string From 0a6d7eae6a56870e454a5f5e87db0991fde57b79 Mon Sep 17 00:00:00 2001 From: Justin Puah Date: Mon, 20 Jul 2026 17:26:53 +1000 Subject: [PATCH 2/2] test(setup): make module tests pass on Linux CI The Ubuntu Pester job asserted Windows-specific output on Linux: - herdr test: relax the config.toml symlink path pattern to match either separator (herdr[\/]config.toml) so it passes on Linux too. - codex-skills (x2) + output-styles tests assert Windows junction paths (junctions are a Windows concept); guard them -Skip:(-not $IsWindows), matching the existing -Module all test. These three were already failing the Ubuntu job on main (pre-existing), not introduced here. --- tests/setup.Tests.ps1 | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/setup.Tests.ps1 b/tests/setup.Tests.ps1 index 73d24cb9..57641826 100644 --- a/tests/setup.Tests.ps1 +++ b/tests/setup.Tests.ps1 @@ -130,7 +130,7 @@ Describe 'setup.ps1 dry-run directory-creation cosmetics' { } Describe 'setup.ps1 codex module shared claude skills' { - It 'junctions claude/skills subdirectories into ~/.codex/skills, minus the Claude-harness-coupled denylist' { + It 'junctions claude/skills subdirectories into ~/.codex/skills, minus the Claude-harness-coupled denylist' -Skip:(-not $IsWindows) { $tmpHome = Join-Path ([IO.Path]::GetTempPath()) ('setup-codex-skills-' + [guid]::NewGuid()) New-Item -ItemType Directory -Path $tmpHome -Force | Out-Null $origUP = $env:USERPROFILE @@ -153,7 +153,7 @@ Describe 'setup.ps1 codex module shared claude skills' { } } - It 'junctions only the Codex-native flavour when a codex/skills name collides with a shared skill' { + It 'junctions only the Codex-native flavour when a codex/skills name collides with a shared skill' -Skip:(-not $IsWindows) { # A name in both sources must yield ONE junction (the codex/skills one) — junctioning # the shared dir first and letting the native one replace it would back up and re-create # the junction on every run, accumulating stale .bak.* junctions. @@ -180,7 +180,7 @@ Describe 'setup.ps1 codex module shared claude skills' { } Describe 'setup.ps1 claude module output styles' { - It 'junctions claude/output-styles into ~/.claude/output-styles' { + It 'junctions claude/output-styles into ~/.claude/output-styles' -Skip:(-not $IsWindows) { $tmpHome = Join-Path ([IO.Path]::GetTempPath()) ('setup-claude-styles-' + [guid]::NewGuid()) New-Item -ItemType Directory -Path $tmpHome -Force | Out-Null $origUP = $env:USERPROFILE @@ -205,7 +205,7 @@ Describe 'setup.ps1 herdr module' { $env:USERPROFILE = $tmpHome $output = & pwsh -NoProfile -File $script:SetupScript -Module herdr -DryRun 2>&1 | Out-String $LASTEXITCODE | Should -Be 0 - $output | Should -Match '\[DRY RUN\] symlink .*herdr\\config\.toml' + $output | Should -Match '\[DRY RUN\] symlink .*herdr[\\/]config\.toml' # The agent-integration wiring only runs when herdr itself is on PATH; assert its # dry-run line only then, so CI (which has no herdr) still exercises the symlink path. if (Get-Command -Name herdr -ErrorAction Ignore) {