From 63b07165d40e17829c23811c73f6ec3f4657ca5e Mon Sep 17 00:00:00 2001 From: Manu Lorente Date: Mon, 18 May 2026 11:35:34 -0600 Subject: [PATCH] fix(setup,aliases): detect new standalone Copilot CLI v2 (BUG-003) Empirical discovery on the admin Windows machine while validating BUG-001 (#40): GitHub shipped a new standalone `copilot` CLI (winget GitHub.Copilot) that is agentic in nature (closer to Claude Code than to the legacy gh-copilot extension's suggest/explain wrappers). The dotfiles setup only knew about the v1 extension path, so on machines with the new CLI installed and operative it would log "extension not installed, skipping" and never deploy copilot-instructions.md -- the AI-013 pointer-style refactor was functionally inert on those machines. Changes: - setup-windows.ps1: drop `gh extension list` detection; detect via `Get-Command copilot`; auto-install GitHub.Copilot via winget in the dev tools block; refresh PATH after the winget loop so newly-installed tools (Copilot included) are visible to subsequent blocks of the same run. - setup-linux.sh: same detection swap (`command -v copilot`); no auto-install (distros vary); idempotent cleanup of the stale `eval "$(gh copilot alias -- bash)"` line that the previous setup added to .zshrc/.bashrc (the subcommand does not exist in the v2 CLI and errors on every shell startup). - powershell/profile.ps1 + .zsh/aliases.zsh: rename ghcs/ghce to cop/cops. Reason: old aliases wrapped `gh copilot suggest/explain` which no longer exist; same names with different semantics would be a cognitive trap. cop = interactive agent (tool use confirmed); cops = single-shot non-interactive with --allow-all-tools (required by the v2 CLI for -p mode). - env-contract.json: add `copilot` to optional_binaries, update `gh` purpose string to reflect that Copilot is no longer a gh extension. - tests: parity asserts in setup-windows.bats and aliases.bats lock in the new detection + alias names + absence of legacy references. Verification (empirical, this machine, 2026-05-18): - winget GitHub.Copilot recognises pre-installed v1.0.48 (no re-install) - PATH refresh exposes `copilot.exe` to the subsequent setup block - `Get-Command copilot` returns the WinGet packages path; deploy succeeds; copilot-instructions.md verified pointer to AGENTS.md - WIN-003 SessionStart hook self-heal and BUG-002 CLAUDE/GEMINI verify-strings regression-free in same setup run Out of scope (deferred): - AWS Copilot CLI (Amazon.CopilotCLI) name collision -- documented in inline comment; <1% population, BUG-004 if it surfaces. - Linux auto-install -- distros vary; manual install documented in the detect-and-act info message. --- .zsh/aliases.zsh | 6 ++++ env-contract.json | 3 +- powershell/profile.ps1 | 12 +++++--- setup-linux.sh | 46 ++++++++++++++-------------- setup-windows.ps1 | 66 +++++++++++++++++++--------------------- tests/aliases.bats | 38 +++++++++++++++++++++++ tests/setup-windows.bats | 26 ++++++++++++++++ 7 files changed, 134 insertions(+), 63 deletions(-) diff --git a/.zsh/aliases.zsh b/.zsh/aliases.zsh index 9701b63..cf958eb 100644 --- a/.zsh/aliases.zsh +++ b/.zsh/aliases.zsh @@ -35,6 +35,12 @@ alias cl="changelog-gen.sh" # Regenerate CHANGELOG.md from alias oc="opencode" # TUI: opencode Go subscription alias oclog='tail -F "$(ls -t ~/.local/share/opencode/log/*.log | head -1)" | grep --line-buffered -vE "file\.watcher\.updated|bus type=message\.part\.delta"' # live tail of newest opencode log, filtered +# GitHub Copilot CLI v2 (BUG-003: standalone agentic CLI, replaces ghcs/ghce wrappers) +# cop -> interactive agent (tool use requires confirmation, safe default) +# cops -> single-shot non-interactive prompt with --allow-all-tools (required by CLI for -p mode) +alias cop="copilot" +cops() { copilot -p "$*" --allow-all-tools -s; } + # tmux session management alias tx='tmux new -A -s' # attach-or-create by name: tx dotfiles alias txl='tmux ls' # list sessions diff --git a/env-contract.json b/env-contract.json index 87acaa3..94bc0f1 100644 --- a/env-contract.json +++ b/env-contract.json @@ -64,7 +64,8 @@ { "name": "npx", "purpose": "MCP server stdio transports" }, { "name": "npm", "purpose": "claude-mem-heal zod install" }, { "name": "age", "purpose": "secrets encryption" }, - { "name": "gh", "purpose": "GitHub CLI + Copilot extension" }, + { "name": "gh", "purpose": "GitHub CLI (PR/issue management; Copilot CLI is the standalone `copilot` binary, not a gh extension since BUG-003)" }, + { "name": "copilot", "purpose": "GitHub Copilot CLI (agentic assistant; winget GitHub.Copilot on Windows, manual install on Linux)" }, { "name": "uv", "purpose": "Python tool installs (hive-vault, general Python workflows)" } ] } diff --git a/powershell/profile.ps1 b/powershell/profile.ps1 index 3d00b87..94e2fe2 100644 --- a/powershell/profile.ps1 +++ b/powershell/profile.ps1 @@ -67,10 +67,14 @@ function gd { git diff } function gl { git log --oneline -10 } function gp { git pull } -# GitHub Copilot CLI aliases -if (Get-Command gh -ErrorAction SilentlyContinue) { - function ghcs { gh copilot suggest @args } - function ghce { gh copilot explain @args } +# GitHub Copilot CLI v2 aliases (BUG-003: standalone agentic `copilot`, not gh extension) +# - cop -> interactive agent (default safe: tool use needs confirmation) +# - cops -> single-shot non-interactive prompt; --allow-all-tools is REQUIRED by +# the CLI for non-interactive mode and gives the agent edit/exec power. +# Use 'cop' instead when you want per-tool confirmation. +if (Get-Command copilot -ErrorAction SilentlyContinue) { + Set-Alias -Name cop -Value copilot + function cops { copilot -p "$args" --allow-all-tools -s } } # Enhanced listing (requires eza) diff --git a/setup-linux.sh b/setup-linux.sh index fed37d9..c3d4821 100755 --- a/setup-linux.sh +++ b/setup-linux.sh @@ -501,33 +501,33 @@ else log_warning "ghostty config source missing: $GHOSTTY_CONFIG_SRC" fi -# GitHub Copilot CLI (detect-and-act: deploy config only when the extension -# is actually installed; the script does NOT auto-install the extension on -# Linux — gh is widely used for PR/issue management without Copilot intent. -# To enable: gh extension install github/gh-copilot, then re-run setup). -# Verification string updated for AI-013 pointer-style copilot-instructions.md. -if command -v gh >/dev/null 2>&1; then - if gh extension list 2>/dev/null | grep -q "github/gh-copilot"; then - log_info "GitHub Copilot CLI extension detected, deploying configuration..." - ensure_directory "$HOME/.copilot" - cp -rf "$CURRENT_DIR/ai/copilot/"* "$HOME/.copilot/" 2>/dev/null || true - if [ -f "$HOME/.copilot/copilot-instructions.md" ] && grep -q 'First, read `AGENTS.md`' "$HOME/.copilot/copilot-instructions.md"; then - log_success "copilot-instructions.md deployed successfully (verified pointer to AGENTS.md)" - else - log_warning "copilot-instructions.md deployment failed verification (expected pointer to AGENTS.md)" - fi - log_success "GitHub Copilot CLI configured" +# GitHub Copilot CLI (BUG-003: standalone agentic CLI, drops legacy gh-copilot +# extension path). Linux side is detect-and-act -- no auto-install (distros vary; +# user installs via snap/apt/curl per https://docs.github.com/copilot/how-tos/copilot-cli). +# Verification string set by AI-013 (pointer-style copilot-instructions.md). +# +# Cleanup (idempotent): the previous setup added 'eval "$(gh copilot alias -- bash)"' +# to .zshrc/.bashrc. That subcommand does not exist in the new standalone CLI, so +# the line errors silently on every shell startup. Strip it if present. +for rc in "$HOME/.zshrc" "$HOME/.bashrc"; do + if [ -f "$rc" ] && grep -qF 'eval "$(gh copilot alias -- bash)"' "$rc"; then + sed -i '/eval "$(gh copilot alias -- bash)"/d' "$rc" + log_info "Removed stale gh-copilot eval line from $rc" + fi +done - # Aliases - log_info "Adding Copilot aliases to .zshrc/.bashrc..." - COPILOT_SUGGEST='eval "$(gh copilot alias -- bash)"' - [ -f "$HOME/.zshrc" ] && ensure_line_in_file "$HOME/.zshrc" "$COPILOT_SUGGEST" - [ -f "$HOME/.bashrc" ] && ensure_line_in_file "$HOME/.bashrc" "$COPILOT_SUGGEST" +if command -v copilot >/dev/null 2>&1; then + log_info "GitHub Copilot CLI detected, deploying configuration..." + ensure_directory "$HOME/.copilot" + cp -rf "$CURRENT_DIR/ai/copilot/"* "$HOME/.copilot/" 2>/dev/null || true + if [ -f "$HOME/.copilot/copilot-instructions.md" ] && grep -q 'First, read `AGENTS.md`' "$HOME/.copilot/copilot-instructions.md"; then + log_success "copilot-instructions.md deployed successfully (verified pointer to AGENTS.md)" else - log_info "GitHub Copilot CLI extension not installed, skipping Copilot config (install with: gh extension install github/gh-copilot)" + log_warning "copilot-instructions.md deployment failed verification (expected pointer to AGENTS.md)" fi + log_success "GitHub Copilot CLI configured (aliases cop/cops in .zsh/aliases.zsh)" else - log_warning "GitHub CLI (gh) not found, skipping Copilot setup" + log_info "GitHub Copilot CLI not installed, skipping Copilot config (install via snap/apt/curl: https://docs.github.com/copilot/how-tos/copilot-cli)" fi # Register MCP servers (requires Claude Code CLI, Node.js, jq) diff --git a/setup-windows.ps1 b/setup-windows.ps1 index d9f6880..3190220 100644 --- a/setup-windows.ps1 +++ b/setup-windows.ps1 @@ -104,7 +104,8 @@ if ($wingetCmd) { @{ Name = "eza"; Cmd = "eza"; Id = "eza-community.eza" }, @{ Name = "jq"; Cmd = "jq"; Id = "jqlang.jq" }, @{ Name = "GitHub CLI"; Cmd = "gh"; Id = "GitHub.cli" }, - @{ Name = "zoxide"; Cmd = "zoxide"; Id = "ajeetdsouza.zoxide" } + @{ Name = "zoxide"; Cmd = "zoxide"; Id = "ajeetdsouza.zoxide" }, + @{ Name = "GitHub Copilot CLI"; Cmd = "copilot"; Id = "GitHub.Copilot" } ) foreach ($tool in $tools) { if (-not (Get-Command $tool.Cmd -ErrorAction SilentlyContinue)) { @@ -119,6 +120,11 @@ if ($wingetCmd) { Write-Info "$($tool.Name) already installed" } } + # Refresh PATH so freshly-installed winget tools are visible to subsequent + # blocks of this same setup run (otherwise Get-Command misses them until + # the next shell start; first introduced for BUG-003 so the Copilot config + # deploy block sees the just-installed `copilot` binary). + $env:PATH = [Environment]::GetEnvironmentVariable("PATH", "Machine") + ";" + [Environment]::GetEnvironmentVariable("PATH", "User") } else { Write-Warn "winget not found, skipping developer tools installation" } @@ -792,43 +798,33 @@ if (Test-Path $ClaudeSettings) { Write-Info "Setting up GitHub Copilot CLI..." -# Detect-and-act: deploy config only when gh AND the gh-copilot extension -# are actually installed. The script does NOT auto-install the extension -- -# gh is widely used for PR/issue management on machines that do not want -# Copilot. To enable: gh extension install github/gh-copilot, then re-run. -# Verification string updated for AI-013 pointer-style copilot-instructions.md. -$ghCmd = Get-Command gh -ErrorAction SilentlyContinue -if ($ghCmd) { - $copilotInstalled = $false - try { - $extensions = & gh extension list 2>$null - $copilotInstalled = ($extensions -match 'github/gh-copilot') - } catch { - $copilotInstalled = $false - } - - if ($copilotInstalled) { - Write-Info "GitHub Copilot CLI extension detected, deploying configuration..." - $CopilotHome = "$env:USERPROFILE\.copilot" - Ensure-Directory $CopilotHome - - $copilotSource = "$DotfilesDir\ai\copilot" - if (Test-Path $copilotSource) { - Copy-Item "$copilotSource\*" "$CopilotHome\" -Recurse -Force -ErrorAction SilentlyContinue - if ((Test-Path "$CopilotHome\copilot-instructions.md") -and - (Select-String -Path "$CopilotHome\copilot-instructions.md" -Pattern 'First, read `AGENTS.md`' -SimpleMatch -Quiet)) { - Write-Success "copilot-instructions.md deployed successfully (verified pointer to AGENTS.md)" - } else { - Write-Warn "copilot-instructions.md deployment failed verification (expected pointer to AGENTS.md)" - } +# BUG-003: detect the new standalone `copilot` CLI (winget GitHub.Copilot, +# agentic interface, closer to Claude Code than to the legacy gh-copilot +# extension's suggest/explain wrappers). The dev tools winget block above +# auto-installs it; this block deploys config when the binary is on PATH. +# Note: AWS Copilot CLI (Amazon.CopilotCLI) also exposes itself as `copilot`. +# If both are installed, Get-Command resolves to the first on PATH. Out-of- +# scope to disambiguate here; <1% population. +$copilotCmd = Get-Command copilot -ErrorAction SilentlyContinue +if ($copilotCmd) { + Write-Info "GitHub Copilot CLI detected at $($copilotCmd.Source), deploying configuration..." + $CopilotHome = "$env:USERPROFILE\.copilot" + Ensure-Directory $CopilotHome + + $copilotSource = "$DotfilesDir\ai\copilot" + if (Test-Path $copilotSource) { + Copy-Item "$copilotSource\*" "$CopilotHome\" -Recurse -Force -ErrorAction SilentlyContinue + if ((Test-Path "$CopilotHome\copilot-instructions.md") -and + (Select-String -Path "$CopilotHome\copilot-instructions.md" -Pattern 'First, read `AGENTS.md`' -SimpleMatch -Quiet)) { + Write-Success "copilot-instructions.md deployed successfully (verified pointer to AGENTS.md)" + } else { + Write-Warn "copilot-instructions.md deployment failed verification (expected pointer to AGENTS.md)" } - - Write-Success "GitHub Copilot CLI configured (aliases ghcs/ghce in profile.ps1)" - } else { - Write-Info "GitHub Copilot CLI extension not installed, skipping Copilot config (install with: gh extension install github/gh-copilot)" } + + Write-Success "GitHub Copilot CLI configured (aliases cop/cops in profile.ps1)" } else { - Write-Warn "GitHub CLI (gh) not found, skipping Copilot setup" + Write-Info "GitHub Copilot CLI not installed; the dev tools block above attempts auto-install via winget GitHub.Copilot. Re-run setup or open a new shell if the binary was just installed and PATH needs refresh." } # Weekly vault maintenance scheduled task (Sundays 10:07 AM) diff --git a/tests/aliases.bats b/tests/aliases.bats index c608de6..036a3c5 100644 --- a/tests/aliases.bats +++ b/tests/aliases.bats @@ -49,6 +49,28 @@ setup() { ! grep -qE '^alias (ai|aic|aia)=' "$ALIASES_FILE" } +# --- Copilot CLI v2 aliases (BUG-003: rename from ghcs/ghce) --- +# The old aliases wrapped 'gh copilot suggest/explain' which do not exist in +# the new standalone 'copilot' CLI. Rename to 'cop' (interactive agentic CLI) +# and 'cops' (single-shot non-interactive prompt with --allow-all-tools). + +@test "aliases.zsh defines cop alias for copilot" { + grep -qE '^alias cop="copilot"' "$ALIASES_FILE" +} + +@test "aliases.zsh defines cops function for single-shot copilot prompt" { + grep -qE '^cops\(\)' "$ALIASES_FILE" + grep -qF 'copilot -p' "$ALIASES_FILE" + grep -qF -- '--allow-all-tools' "$ALIASES_FILE" +} + +@test "aliases.zsh no longer defines ghcs/ghce (renamed in BUG-003)" { + # Anchor to start-of-line + alias/function definition forms only -- comments + # mentioning the old names (e.g. "replaces ghcs/ghce wrappers") are fine. + ! grep -qE '^(alias ghcs|ghcs\(\)|function ghcs)' "$ALIASES_FILE" + ! grep -qE '^(alias ghce|ghce\(\)|function ghce)' "$ALIASES_FILE" +} + # --- Knowledge aliases --- @test "aliases.zsh defines kc alias" { @@ -107,6 +129,22 @@ setup() { grep -q 'function gp' "$ps_file" } +@test "parity: cop and cops exist in both aliases.zsh and profile.ps1" { + ps_file="$DOTFILES_DIR/powershell/profile.ps1" + grep -qE '^alias cop="copilot"' "$ALIASES_FILE" + grep -qE 'Set-Alias -Name cop -Value copilot' "$ps_file" + grep -qE '^cops\(\)' "$ALIASES_FILE" + grep -qE 'function cops' "$ps_file" +} + +@test "parity: ghcs/ghce removed from both aliases.zsh and profile.ps1" { + ps_file="$DOTFILES_DIR/powershell/profile.ps1" + ! grep -qE '^(alias ghcs|ghcs\(\)|function ghcs)' "$ALIASES_FILE" + ! grep -qE '^(alias ghce|ghce\(\)|function ghce)' "$ALIASES_FILE" + ! grep -qE '^\s*function ghcs' "$ps_file" + ! grep -qE '^\s*function ghce' "$ps_file" +} + # --- tmux aliases --- @test "aliases.zsh defines tx alias (attach-or-create with -A)" { diff --git a/tests/setup-windows.bats b/tests/setup-windows.bats index 7743bcb..aa80380 100644 --- a/tests/setup-windows.bats +++ b/tests/setup-windows.bats @@ -57,6 +57,24 @@ setup() { grep -q 'Setting up GitHub Copilot CLI' "$PS1_SCRIPT" } +# --- BUG-003: drop gh-copilot extension path, detect new copilot CLI v2 --- +# The legacy 'gh extension install github/gh-copilot' product was the v1 wrapper +# around suggest/explain. The new 'copilot' standalone CLI (winget GitHub.Copilot, +# binary `copilot`) is agentic (closer to Claude Code). Setup detects the new one +# via 'Get-Command copilot' and stops referencing the old extension entirely. + +@test "setup-windows.ps1 detects copilot via Get-Command, not gh extension list" { + grep -qF "Get-Command copilot" "$PS1_SCRIPT" + ! grep -qF "gh extension list" "$PS1_SCRIPT" + ! grep -qF "github/gh-copilot" "$PS1_SCRIPT" +} + +@test "setup-windows.ps1 auto-installs GitHub.Copilot via winget" { + grep -qF "GitHub.Copilot" "$PS1_SCRIPT" + # In the dev tools winget block, the binary check uses `copilot` + grep -qE 'Name = "GitHub Copilot CLI"; Cmd = "copilot"; Id = "GitHub\.Copilot"' "$PS1_SCRIPT" +} + @test "setup-windows.ps1 deploys init-project.ps1" { grep -q 'init-project.ps1' "$PS1_SCRIPT" } @@ -245,6 +263,14 @@ setup() { ! grep -qF -- '-Pattern "CORE PRINCIPLE"' "$PS1_SCRIPT" } +@test "parity: both setup scripts detect copilot via binary, not gh extension" { + grep -qF "command -v copilot" "$DOTFILES_DIR/setup-linux.sh" + grep -qF "Get-Command copilot" "$PS1_SCRIPT" + # Neither should still reference the legacy extension path + ! grep -qF "github/gh-copilot" "$DOTFILES_DIR/setup-linux.sh" + ! grep -qF "github/gh-copilot" "$PS1_SCRIPT" +} + # --- PSScriptAnalyzer --- @test "setup-windows.ps1 passes PSScriptAnalyzer (if pwsh available)" {