diff --git a/.copilot/skills/changelog-fold-completeness/SKILL.md b/.copilot/skills/changelog-fold-completeness/SKILL.md index e64612e0..5beddb1e 100644 --- a/.copilot/skills/changelog-fold-completeness/SKILL.md +++ b/.copilot/skills/changelog-fold-completeness/SKILL.md @@ -10,12 +10,12 @@ source: "earned (issues #399, Sprints 15/16/17 -- 3 consecutive releases, binary The `CHANGELOG.md` file follows Keep a Changelog format. The `[Unreleased]` block is supposed to accumulate entries as PRs merge to develop throughout a sprint. In practice, -only the first agent to land a feature PR adds their entry. Subsequent agents either +only the first contributor to land a feature PR adds their entry. Subsequent contributors either skip the update (single-PR scope feels too small) or defer it to "someone else." By release time, the `[Unreleased]` block contains only the first-lander's entry -- typically 1 of N contributions. -The release agent (Mickey) folds `[Unreleased]` to `[X.Y.Z]` and ships. The missing +The release process folds `[Unreleased]` to `[X.Y.Z]` and ships. The missing entries are gone from the release notes permanently. This pattern has repeated identically in Sprints 15, 16, and 17. The fix is a @@ -118,39 +118,39 @@ gh issue list --state closed ` inside the release transaction, not a best-effort beforehand. - **Step 8 (restore empty block) immediately after fold.** If the fold succeeds but the restore is skipped, the next sprint starts without a `[Unreleased]` block, which - causes the first agent to manually edit CHANGELOG structure instead of appending + causes the first contributor to manually edit CHANGELOG structure instead of appending to a predictable location. ## Examples **Sprint 17 release (0.9.7, PR #393, commit 71d2ffe):** At release time, `[Unreleased]` contained only the `#382` (sprint-end label automation) -entry -- 1 of 6 total PRs merged to develop that sprint. Mickey-14 ran the completeness +entry -- 1 of 6 total PRs merged to develop that sprint. The release process ran the completeness check and added 5 missing entries (PRs #383, #384, #385, #388, #390 plus issues #371, #381) before folding. Without this check, 0.9.7 release notes would have been ~85% incomplete. Cited in Sprint 17 retro under "Key learnings." **Sprint 16 release (0.9.6, PR #372, commit 7172ae7):** -`[Unreleased]` had sparse coverage -- only the first-lander entry. Mickey-12 added +`[Unreleased]` had sparse coverage -- only the first-lander entry. The release process added missing Sprint 16 entries (PRs #368, #369, #370, #363, #365, #367) before folding to `[0.9.6] - 2026-05-17 -- Sprint 16: Skill formalization + hygiene gate review`. **Sprint 15 release (0.9.5, PR #360, commit 0c8d710):** -Same pattern. Only 1 entry in `[Unreleased]` at release time. Mickey added the missing +Same pattern. Only 1 entry in `[Unreleased]` at release time. The release process added the missing Sprint 15 PR entries (#355 normalization, #356 ASCII sweep, #357-#359 Doc wave) before folding. Sprint 15 retro noted this as a process gap; it was not fixed structurally until this skill was formalized. **Pattern summary:** Three consecutive releases (0.9.5, 0.9.6, 0.9.7) had incomplete `[Unreleased]` blocks at release time. The merge PR template asks contributors to -update CHANGELOG.md, but agents on single-PR scopes routinely skip it. The live block -contains only the first agent's entry by release time. The completeness check has +update CHANGELOG.md, but contributors on single-PR scopes routinely skip it. The live block +contains only the first contributor's entry by release time. The completeness check has never failed to find missing entries when applied. ## Anti-Patterns - **Trusting that `[Unreleased]` is complete.** It never is by release time. The - pattern is binary: the first lander adds one entry; subsequent agents skip. Every + pattern is binary: the first lander adds one entry; subsequent contributors skip. Every sprint, every release. - **Skipping Steps 3-5 because the sprint was "small."** Sprint 17 had 6 PRs; only 1 entry was in `[Unreleased]`. Sprint 16 had 7 Sprint PRs; same pattern. There is no @@ -159,34 +159,31 @@ never failed to find missing entries when applied. to it requires a second commit amending the release block -- noisy and confusing for readers of the git log. - **Folding without restoring `[Unreleased]`.** Leaves CHANGELOG.md with no top-level - Unreleased block. First agent next sprint either forgets to add the block and + Unreleased block. First contributor next sprint either forgets to add the block and appends under the versioned section, or adds the block incorrectly. - **Relying on the PR template alone.** The template asks for a CHANGELOG update on - every PR. Agents on single-PR scope treat it as optional. The release gate is the + every PR. Contributors on single-PR scope treat it as optional. The release gate is the only reliable enforcement point. ## Placement decision -This skill lives in `.copilot/skills/` (coordinator level) rather than `.squad/skills/` -(team level). Rationale: the fold step is performed by Mickey (or the coordinator -directly) during a release cut -- it is release-process governance, not day-to-day -agent workflow. The coordinator is responsible for ensuring the fold is gated on -completeness. If Mickey is the release agent, the coordinator must include this skill -reference in Mickey's release spawn prompt. +This skill lives in `.copilot/skills/` (process automation level) rather than project-specific +workflow. Rationale: the fold step is performed during a release cut -- it is release-process +governance, not day-to-day workflow. The release automation is responsible for ensuring the +fold is gated on completeness. -Compare with `.squad/skills/history-md-pre-size-check/SKILL.md`, which governs -individual agent appends and therefore lives at the team level. +Compare with other append-based hygiene checks which govern individual contributions. ## Related Skills -- `.squad/skills/gh-pr-base-develop/SKILL.md` -- every squad PR must pass +- `.squad/skills/gh-pr-base-develop/SKILL.md` -- every PR must pass `--base develop`; if any PR was misrouted (base=main), the gh queries in Steps 4-5 will miss it; verify develop ancestry with `git log $LAST_TAG..develop --merges` as the authoritative source. - `.copilot/skills/release-process/SKILL.md` -- the full release runbook; this skill is a pre-step for the fold phase of that runbook. - `.squad/skills/history-md-pre-size-check/SKILL.md` -- companion hygiene check - (agent-level); ensures agents' own history files are within gate before the sprint + (contributor-level); ensures history files are within gate before the sprint closes and before the release audit. ## References diff --git a/.copilot/skills/distributed-mesh/SKILL.md b/.copilot/skills/distributed-mesh/SKILL.md deleted file mode 100644 index 56782ccf..00000000 --- a/.copilot/skills/distributed-mesh/SKILL.md +++ /dev/null @@ -1,287 +0,0 @@ ---- -name: "distributed-mesh" -description: "How to coordinate with squads on different machines using git as transport" -domain: "distributed-coordination" -confidence: "high" -source: "multi-model-consensus (Opus 4.6, Sonnet 4.5, GPT-5.4)" ---- - -## SCOPE - -**[x] THIS SKILL PRODUCES (exactly these, nothing more):** - -1. **`mesh.json`** -- Generated from user answers about zones and squads (which squads participate, what zone each is in, paths/URLs for each), using `mesh.json.example` in this skill's directory as the schema template -2. **`sync-mesh.sh` and `sync-mesh.ps1`** -- Copied from this skill's directory into the project root (these are bundled resources, NOT generated code) -3. **Zone 2 state repo initialization** (if applicable) -- If the user specified a Zone 2 shared state repo, run `sync-mesh.sh --init` to scaffold the state repo structure -4. **A decision entry** in `.squad/decisions/inbox/` documenting the mesh configuration for team awareness - -**[ ] THIS SKILL DOES NOT PRODUCE:** - -- **No application code** -- No validators, libraries, or modules of any kind -- **No test files** -- No test suites, test cases, or test scaffolding -- **No GENERATING sync scripts** -- They are bundled with this skill as pre-built resources. COPY them, don't generate them. -- **No daemons or services** -- No background processes, servers, or persistent runtimes -- **No modifications to existing squad files** beyond the decision entry (no changes to team.md, routing.md, agent charters, etc.) - -**Your role:** Configure the mesh topology and install the bundled sync scripts. Nothing more. - -## Context - -When squads are on different machines (developer laptops, CI runners, cloud VMs, partner orgs), the local file-reading convention still works -- but remote files need to arrive on your disk first. This skill teaches the pattern for distributed squad communication. - -**When this applies:** -- Squads span multiple machines, VMs, or CI runners -- Squads span organizations or companies -- An agent needs context from a squad whose files aren't on the local filesystem - -**When this does NOT apply:** -- All squads are on the same machine (just read the files directly) - -## Patterns - -### The Core Principle - -> "The filesystem is the mesh, and git is how the mesh crosses machine boundaries." - -The agent interface never changes. Agents always read local files. The distributed layer's only job is to make remote files appear locally before the agent reads them. - -### Three Zones of Communication - -**Zone 1 -- Local:** Same filesystem. Read files directly. Zero transport. - -**Zone 2 -- Remote-Trusted:** Different host, same org, shared git auth. Transport: `git pull` from a shared repo. This collapses Zone 2 into Zone 1 -- files materialize on disk, agent reads them normally. - -**Zone 3 -- Remote-Opaque:** Different org, no shared auth. Transport: `curl` to fetch published contracts (SUMMARY.md). One-way visibility -- you see only what they publish. - -### Agent Lifecycle (Distributed) - -``` -1. SYNC: git pull (Zone 2) + curl (Zone 3) -- materialize remote state -2. READ: cat .mesh/**/state.md -- all files are local now -3. WORK: do their assigned work (the agent's normal task, NOT mesh-building) -4. WRITE: update own billboard, log, drops -5. PUBLISH: git add + commit + push -- share state with remote peers -``` - -Steps 2-4 are identical to local-only. Steps 1 and 5 are the entire distributed extension. **Note:** "WORK" means the agent performs its normal squad duties -- it does NOT mean "build mesh infrastructure." - -### The mesh.json Config - -```json -{ - "squads": { - "auth-squad": { "zone": "local", "path": "../auth-squad/.mesh" }, - "ci-squad": { - "zone": "remote-trusted", - "source": "git@github.com:our-org/ci-squad.git", - "ref": "main", - "sync_to": ".mesh/remotes/ci-squad" - }, - "partner-fraud": { - "zone": "remote-opaque", - "source": "https://partner.dev/squad-contracts/fraud/SUMMARY.md", - "sync_to": ".mesh/remotes/partner-fraud", - "auth": "bearer" - } - } -} -``` - -Three zone types, one file. Local squads need only a path. Remote-trusted need a git URL. Remote-opaque need an HTTP URL. - -### Write Partitioning - -Each squad writes only to its own directory (`boards/{self}.md`, `squads/{self}/*`, `drops/{date}-{self}-*.md`). No two squads write to the same file. Git push/pull never conflicts. If push fails ("branch is behind"), the fix is always `git pull --rebase && git push`. - -### Trust Boundaries - -Trust maps to git permissions: -- **Same repo access** = full mesh visibility -- **Read-only access** = can observe, can't write -- **No access** = invisible (correct behavior) - -For selective visibility, use separate repos per audience (internal, partner, public). Git permissions ARE the trust negotiation. - -### Phased Rollout - -- **Phase 0:** Convention only -- document zones, agree on mesh.json fields, manually run `git pull`/`git push`. Zero new code. -- **Phase 1:** Sync script (~30 lines bash or PowerShell) when manual sync gets tedious. -- **Phase 2:** Published contracts + curl fetch when a Zone 3 partner appears. -- **Phase 3:** Never. No MCP federation, A2A, service discovery, message queues. - -**Important:** Phases are NOT auto-advanced. These are project-level decisions -- you start at Phase 0 (manual sync) and only move forward when the team decides complexity is justified. - -### Mesh State Repo - -The shared mesh state repo is a plain git repository -- NOT a Squad project. It holds: -- One directory per participating squad -- Each directory contains at minimum a SUMMARY.md with the squad's current state -- A root README explaining what the repo is and who participates - -No `.squad/` folder, no agents, no automation. Write partitioning means each squad only pushes to its own directory. The repo is a rendezvous point, not an intelligent system. - -If you want a squad that *observes* mesh health, that's a separate Squad project that lists the state repo as a Zone 2 remote in its `mesh.json` -- it does NOT live inside the state repo. - -## Examples - -### Developer Laptop + CI Squad (Zone 2) - -Auth-squad agent wakes up. `git pull` brings ci-squad's latest results. Agent reads: "3 test failures in auth module." Adjusts work. Pushes results when done. **Overhead: one `git pull`, one `git push`.** - -### Two Orgs Collaborating (Zone 3) - -Payment-squad fetches partner's published SUMMARY.md via curl. Reads: "Risk scoring v3 API deprecated April 15. New field `device_fingerprint` required." The consuming agent (in payment-squad's team) reads this information and uses it to inform its work -- for example, updating payment integration code to include the new field. Partner can't see payment-squad's internals. - -### Same Org, Shared Mesh Repo (Zone 2) - -Three squads on different machines. One shared git repo holds the mesh. Each squad: `git pull` before work, `git push` after. Write partitioning ensures zero merge conflicts. - -## AGENT WORKFLOW (Deterministic Setup) - -When a user invokes this skill to set up a distributed mesh, follow these steps **exactly, in order:** - -### Step 1: ASK the user for mesh topology - -Ask these questions (adapt phrasing naturally, but get these answers): - -1. **Which squads are participating?** (List of squad names) -2. **For each squad, which zone is it in?** - - `local` -- same filesystem (just need a path) - - `remote-trusted` -- different machine, same org, shared git access (need git URL + ref) - - `remote-opaque` -- different org, no shared auth (need HTTPS URL to published contract) -3. **For each squad, what's the connection info?** - - Local: relative or absolute path to their `.mesh/` directory - - Remote-trusted: git URL (SSH or HTTPS), ref (branch/tag), and where to sync it to locally - - Remote-opaque: HTTPS URL to their SUMMARY.md, where to sync it, and auth type (none/bearer) -4. **Where should the shared state live?** (For Zone 2 squads: git repo URL for the mesh state, or confirm each squad syncs independently) - -### Step 2: GENERATE `mesh.json` - -Using the answers from Step 1, create a `mesh.json` file at the project root. Use `mesh.json.example` from THIS skill's directory (`.squad/skills/distributed-mesh/mesh.json.example`) as the schema template. - -Structure: - -```json -{ - "squads": { - "": { "zone": "local", "path": "" }, - "": { - "zone": "remote-trusted", - "source": "", - "ref": "", - "sync_to": ".mesh/remotes/" - }, - "": { - "zone": "remote-opaque", - "source": "", - "sync_to": ".mesh/remotes/", - "auth": "" - } - } -} -``` - -Write this file to the project root. Do NOT write any other code. - -### Step 3: COPY sync scripts - -Copy the bundled sync scripts from THIS skill's directory into the project root: - -- **Source:** `.squad/skills/distributed-mesh/sync-mesh.sh` -- **Destination:** `sync-mesh.sh` (project root) - -- **Source:** `.squad/skills/distributed-mesh/sync-mesh.ps1` -- **Destination:** `sync-mesh.ps1` (project root) - -These are bundled resources. Do NOT generate them -- COPY them directly. - -### Step 4: RUN `--init` (if Zone 2 state repo exists) - -If the user specified a Zone 2 shared state repo in Step 1, run the initialization: - -**On Unix/Linux/macOS:** -```bash -bash sync-mesh.sh --init -``` - -**On Windows:** -```powershell -.\sync-mesh.ps1 -Init -``` - -This scaffolds the state repo structure (squad directories, placeholder SUMMARY.md files, root README). - -**Skip this step if:** -- No Zone 2 squads are configured (local/opaque only) -- The state repo already exists and is initialized - -### Step 5: WRITE a decision entry - -Create a decision file at `.squad/decisions/inbox/-mesh-setup.md` with this content: - -```markdown -### : Mesh configuration - -**By:** (via distributed-mesh skill) - -**What:** Configured distributed mesh with squads across zones - -**Squads:** -- `` -- Zone -- -- `` -- Zone -- -- ... - -**State repo:** - -**Why:** -``` - -Write this file. The Scribe will merge it into the main decisions file later. - -### Step 6: STOP - -**You are done.** Do not: -- Generate sync scripts (they're bundled with this skill -- COPY them) -- Write validator code -- Write test files -- Create any other modules, libraries, or application code -- Modify existing squad files (team.md, routing.md, charters) -- Auto-advance to Phase 2 or Phase 3 - -Output a simple completion message: - -``` -[x] Mesh configured. Created: -- mesh.json ( squads) -- sync-mesh.sh and sync-mesh.ps1 (copied from skill bundle) -- Decision entry: .squad/decisions/inbox/ - -Run `bash sync-mesh.sh` (or `.\sync-mesh.ps1` on Windows) before agents start to materialize remote state. -``` - ---- - -## Anti-Patterns - -**[ ] Code generation anti-patterns:** -- Writing `mesh-config-validator.js` or any validator module -- Writing test files for mesh configuration -- Generating sync scripts instead of copying the bundled ones from this skill's directory -- Creating library modules or utilities -- Building any code that "runs the mesh" -- the mesh is read by agents, not executed - -**[ ] Architectural anti-patterns:** -- Building a federation protocol -- Git push/pull IS federation -- Running a sync daemon or server -- Agents are not persistent. Sync at startup, publish at shutdown -- Real-time notifications -- Agents don't need real-time. They need "recent enough." `git pull` is recent enough -- Schema validation for markdown -- The LLM reads markdown. If the format changes, it adapts -- Service discovery protocol -- mesh.json is a file with 10 entries. Not a "discovery problem" -- Auth framework -- Git SSH keys and HTTPS tokens. Not a framework. Already configured -- Message queues / event buses -- Agents wake, read, work, write, sleep. Nobody's home to receive events -- Any component requiring a running process -- That's the line. Don't cross it - -**[ ] Scope creep anti-patterns:** -- Auto-advancing phases without user decision -- Modifying agent charters or routing rules -- Setting up CI/CD pipelines for mesh sync -- Creating dashboards or monitoring tools diff --git a/.copilot/skills/distributed-mesh/mesh.json.example b/.copilot/skills/distributed-mesh/mesh.json.example deleted file mode 100644 index 7f5730a8..00000000 --- a/.copilot/skills/distributed-mesh/mesh.json.example +++ /dev/null @@ -1,30 +0,0 @@ -{ - "squads": { - "auth-squad": { - "zone": "local", - "path": "../auth-squad/.mesh" - }, - "api-squad": { - "zone": "local", - "path": "../api-squad/.mesh" - }, - "ci-squad": { - "zone": "remote-trusted", - "source": "git@github.com:our-org/ci-squad.git", - "ref": "main", - "sync_to": ".mesh/remotes/ci-squad" - }, - "data-squad": { - "zone": "remote-trusted", - "source": "git@github.com:our-org/data-pipeline.git", - "ref": "main", - "sync_to": ".mesh/remotes/data-squad" - }, - "partner-fraud": { - "zone": "remote-opaque", - "source": "https://partner.example.com/squad-contracts/fraud/SUMMARY.md", - "sync_to": ".mesh/remotes/partner-fraud", - "auth": "bearer" - } - } -} diff --git a/.copilot/skills/distributed-mesh/sync-mesh.ps1 b/.copilot/skills/distributed-mesh/sync-mesh.ps1 deleted file mode 100644 index 5f409ef3..00000000 --- a/.copilot/skills/distributed-mesh/sync-mesh.ps1 +++ /dev/null @@ -1,111 +0,0 @@ -# sync-mesh.ps1 — Materialize remote squad state locally -# -# Reads mesh.json, fetches remote squads into local directories. -# Run before agent reads. No daemon. No service. ~40 lines. -# -# Usage: .\sync-mesh.ps1 [path-to-mesh.json] -# .\sync-mesh.ps1 -Init [path-to-mesh.json] -# Requires: git -param( - [switch]$Init, - [string]$MeshJson = "mesh.json" -) -$ErrorActionPreference = "Stop" - -# Handle -Init mode -if ($Init) { - if (-not (Test-Path $MeshJson)) { - Write-Host "❌ $MeshJson not found" - exit 1 - } - - Write-Host "🚀 Initializing mesh state repository..." - $config = Get-Content $MeshJson -Raw | ConvertFrom-Json - $squads = $config.squads.PSObject.Properties.Name - - # Create squad directories with placeholder SUMMARY.md - foreach ($squad in $squads) { - if (-not (Test-Path $squad)) { - New-Item -ItemType Directory -Path $squad | Out-Null - Write-Host " ✓ Created $squad/" - } else { - Write-Host " • $squad/ exists (skipped)" - } - - $summaryPath = "$squad/SUMMARY.md" - if (-not (Test-Path $summaryPath)) { - "# $squad`n`n_No state published yet._" | Set-Content $summaryPath - Write-Host " ✓ Created $summaryPath" - } else { - Write-Host " • $summaryPath exists (skipped)" - } - } - - # Generate root README.md - if (-not (Test-Path "README.md")) { - $readme = @" -# Squad Mesh State Repository - -This repository tracks published state from participating squads. - -## Participating Squads - -"@ - foreach ($squad in $squads) { - $zone = $config.squads.$squad.zone - $readme += "- **$squad** (Zone: $zone)`n" - } - $readme += @" - -Each squad directory contains a ``SUMMARY.md`` with their latest published state. -State is synchronized using ``sync-mesh.sh`` or ``sync-mesh.ps1``. -"@ - $readme | Set-Content "README.md" - Write-Host " ✓ Created README.md" - } else { - Write-Host " • README.md exists (skipped)" - } - - Write-Host "" - Write-Host "✅ Mesh state repository initialized" - exit 0 -} - -$config = Get-Content $MeshJson -Raw | ConvertFrom-Json - -# Zone 2: Remote-trusted — git clone/pull -foreach ($entry in $config.squads.PSObject.Properties | Where-Object { $_.Value.zone -eq "remote-trusted" }) { - $squad = $entry.Name - $source = $entry.Value.source - $ref = if ($entry.Value.ref) { $entry.Value.ref } else { "main" } - $target = $entry.Value.sync_to - - if (Test-Path "$target/.git") { - git -C $target pull --rebase --quiet 2>$null - if ($LASTEXITCODE -ne 0) { Write-Host "⚠ ${squad}: pull failed (using stale)" } - } else { - New-Item -ItemType Directory -Force -Path (Split-Path $target -Parent) | Out-Null - git clone --quiet --depth 1 --branch $ref $source $target 2>$null - if ($LASTEXITCODE -ne 0) { Write-Host "⚠ ${squad}: clone failed (unavailable)" } - } -} - -# Zone 3: Remote-opaque — fetch published contracts -foreach ($entry in $config.squads.PSObject.Properties | Where-Object { $_.Value.zone -eq "remote-opaque" }) { - $squad = $entry.Name - $source = $entry.Value.source - $target = $entry.Value.sync_to - $auth = $entry.Value.auth - - New-Item -ItemType Directory -Force -Path $target | Out-Null - $params = @{ Uri = $source; OutFile = "$target/SUMMARY.md"; UseBasicParsing = $true } - if ($auth -eq "bearer") { - $tokenVar = ($squad.ToUpper() -replace '-', '_') + "_TOKEN" - $token = [Environment]::GetEnvironmentVariable($tokenVar) - if ($token) { $params.Headers = @{ Authorization = "Bearer $token" } } - } - try { Invoke-WebRequest @params -ErrorAction Stop } - catch { "# ${squad} — unavailable ($(Get-Date))" | Set-Content "$target/SUMMARY.md" } -} - -Write-Host "✓ Mesh sync complete" diff --git a/.copilot/skills/distributed-mesh/sync-mesh.sh b/.copilot/skills/distributed-mesh/sync-mesh.sh deleted file mode 100644 index 802fd2d8..00000000 --- a/.copilot/skills/distributed-mesh/sync-mesh.sh +++ /dev/null @@ -1,104 +0,0 @@ -#!/bin/bash -# sync-mesh.sh — Materialize remote squad state locally -# -# Reads mesh.json, fetches remote squads into local directories. -# Run before agent reads. No daemon. No service. ~40 lines. -# -# Usage: ./sync-mesh.sh [path-to-mesh.json] -# ./sync-mesh.sh --init [path-to-mesh.json] -# Requires: jq (https://github.com/jqlang/jq), git, curl - -set -euo pipefail - -# Handle --init mode -if [ "${1:-}" = "--init" ]; then - MESH_JSON="${2:-mesh.json}" - - if [ ! -f "$MESH_JSON" ]; then - echo "❌ $MESH_JSON not found" - exit 1 - fi - - echo "🚀 Initializing mesh state repository..." - squads=$(jq -r '.squads | keys[]' "$MESH_JSON") - - # Create squad directories with placeholder SUMMARY.md - for squad in $squads; do - if [ ! -d "$squad" ]; then - mkdir -p "$squad" - echo " ✓ Created $squad/" - else - echo " • $squad/ exists (skipped)" - fi - - if [ ! -f "$squad/SUMMARY.md" ]; then - echo -e "# $squad\n\n_No state published yet._" > "$squad/SUMMARY.md" - echo " ✓ Created $squad/SUMMARY.md" - else - echo " • $squad/SUMMARY.md exists (skipped)" - fi - done - - # Generate root README.md - if [ ! -f "README.md" ]; then - { - echo "# Squad Mesh State Repository" - echo "" - echo "This repository tracks published state from participating squads." - echo "" - echo "## Participating Squads" - echo "" - for squad in $squads; do - zone=$(jq -r ".squads.\"$squad\".zone" "$MESH_JSON") - echo "- **$squad** (Zone: $zone)" - done - echo "" - echo "Each squad directory contains a \`SUMMARY.md\` with their latest published state." - echo "State is synchronized using \`sync-mesh.sh\` or \`sync-mesh.ps1\`." - } > README.md - echo " ✓ Created README.md" - else - echo " • README.md exists (skipped)" - fi - - echo "" - echo "✅ Mesh state repository initialized" - exit 0 -fi - -MESH_JSON="${1:-mesh.json}" - -# Zone 2: Remote-trusted — git clone/pull -for squad in $(jq -r '.squads | to_entries[] | select(.value.zone == "remote-trusted") | .key' "$MESH_JSON"); do - source=$(jq -r ".squads.\"$squad\".source" "$MESH_JSON") - ref=$(jq -r ".squads.\"$squad\".ref // \"main\"" "$MESH_JSON") - target=$(jq -r ".squads.\"$squad\".sync_to" "$MESH_JSON") - - if [ -d "$target/.git" ]; then - git -C "$target" pull --rebase --quiet 2>/dev/null \ - || echo "⚠ $squad: pull failed (using stale)" - else - mkdir -p "$(dirname "$target")" - git clone --quiet --depth 1 --branch "$ref" "$source" "$target" 2>/dev/null \ - || echo "⚠ $squad: clone failed (unavailable)" - fi -done - -# Zone 3: Remote-opaque — fetch published contracts -for squad in $(jq -r '.squads | to_entries[] | select(.value.zone == "remote-opaque") | .key' "$MESH_JSON"); do - source=$(jq -r ".squads.\"$squad\".source" "$MESH_JSON") - target=$(jq -r ".squads.\"$squad\".sync_to" "$MESH_JSON") - auth=$(jq -r ".squads.\"$squad\".auth // \"\"" "$MESH_JSON") - - mkdir -p "$target" - auth_flag="" - if [ "$auth" = "bearer" ]; then - token_var="$(echo "${squad}" | tr '[:lower:]-' '[:upper:]_')_TOKEN" - [ -n "${!token_var:-}" ] && auth_flag="--header \"Authorization: Bearer ${!token_var}\"" - fi - - eval curl --silent --fail $auth_flag "$source" -o "$target/SUMMARY.md" 2>/dev/null \ - || echo "# ${squad} — unavailable ($(date))" > "$target/SUMMARY.md" -done - -echo "✓ Mesh sync complete" diff --git a/.copilot/skills/economy-mode/SKILL.md b/.copilot/skills/economy-mode/SKILL.md deleted file mode 100644 index 66fa0496..00000000 --- a/.copilot/skills/economy-mode/SKILL.md +++ /dev/null @@ -1,114 +0,0 @@ ---- -name: "economy-mode" -description: "Shifts Layer 3 model selection to cost-optimized alternatives when economy mode is active." -domain: "model-selection" -confidence: "low" -source: "manual" ---- - -## SCOPE - -[x] THIS SKILL PRODUCES: -- A modified Layer 3 model selection table applied when economy mode is active -- `economyMode: true` written to `.squad/config.json` when activated persistently -- Spawn acknowledgments with `?` indicator when economy mode is active - -[ ] THIS SKILL DOES NOT PRODUCE: -- Code, tests, or documentation -- Cost reports or billing artifacts -- Changes to Layer 0, Layer 1, or Layer 2 resolution (user intent always wins) - -## Context - -Economy mode shifts Layer 3 (Task-Aware Auto-Selection) to lower-cost alternatives. It does NOT override persistent config (`defaultModel`, `agentModelOverrides`) or per-agent charter preferences -- those represent explicit user intent and always take priority. - -Use this skill when the user wants to reduce costs across an entire session or permanently, without manually specifying models for each agent. - -## Activation Methods - -| Method | How | -|--------|-----| -| Session phrase | "use economy mode", "save costs", "go cheap", "reduce costs" | -| Persistent config | `"economyMode": true` in `.squad/config.json` | -| CLI flag | `squad --economy` | - -**Deactivation:** "turn off economy mode", "disable economy mode", or remove `economyMode` from `config.json`. - -## Economy Model Selection Table - -When economy mode is **active**, Layer 3 auto-selection uses this table instead of the normal defaults: - -| Task Output | Normal Mode | Economy Mode | -|-------------|-------------|--------------| -| Writing code (implementation, refactoring, bug fixes) | `claude-sonnet-4.5` | `gpt-4.1` or `gpt-5-mini` | -| Writing prompts or agent designs | `claude-sonnet-4.5` | `gpt-4.1` or `gpt-5-mini` | -| Docs, planning, triage, changelogs, mechanical ops | `claude-haiku-4.5` | `gpt-4.1` or `gpt-5-mini` | -| Architecture, code review, security audits | `claude-opus-4.5` | `claude-sonnet-4.5` | -| Scribe / logger / mechanical file ops | `claude-haiku-4.5` | `gpt-4.1` | - -**Prefer `gpt-4.1` over `gpt-5-mini`** when the task involves structured output or agentic tool use. Prefer `gpt-5-mini` for pure text generation tasks where latency matters. - -## AGENT WORKFLOW - -### On Session Start - -1. READ `.squad/config.json` -2. CHECK for `economyMode: true` -- if present, activate economy mode for the session -3. STORE economy mode state in session context - -### On User Phrase Trigger - -**Session-only (no config change):** "use economy mode", "save costs", "go cheap" - -1. SET economy mode active for this session -2. ACKNOWLEDGE: `[x] Economy mode active -- using cost-optimized models this session. (Layer 0 and Layer 2 preferences still apply)` - -**Persistent:** "always use economy mode", "save economy mode" - -1. WRITE `economyMode: true` to `.squad/config.json` (merge, don't overwrite other fields) -2. ACKNOWLEDGE: `[x] Economy mode saved -- cost-optimized models will be used until disabled.` - -### On Every Agent Spawn (Economy Mode Active) - -1. CHECK Layer 0a/0b first (agentModelOverrides, defaultModel) -- if set, use that. Economy mode does NOT override Layer 0. -2. CHECK Layer 1 (session directive for a specific model) -- if set, use that. Economy mode does NOT override explicit session directives. -3. CHECK Layer 2 (charter preference) -- if set, use that. Economy mode does NOT override charter preferences. -4. APPLY economy table at Layer 3 instead of normal table. -5. INCLUDE `?` in spawn acknowledgment: `[TOOL] {Name} ({model} * ? economy) -- {task}` - -### On Deactivation - -**Trigger phrases:** "turn off economy mode", "disable economy mode", "use normal models" - -1. REMOVE `economyMode` from `.squad/config.json` (if it was persisted) -2. CLEAR session economy mode state -3. ACKNOWLEDGE: `[x] Economy mode disabled -- returning to standard model selection.` - -### STOP - -After updating economy mode state and including the `?` indicator in spawn acknowledgments, this skill is done. Do NOT: -- Change Layer 0, Layer 1, or Layer 2 model choices -- Override charter-specified models -- Generate cost reports or comparisons -- Fall back to premium models via economy mode (economy mode never bumps UP) - -## Config Schema - -`.squad/config.json` economy-related fields: - -```json -{ - "version": 1, - "economyMode": true -} -``` - -- `economyMode` -- when `true`, Layer 3 uses the economy table. Optional; absent = economy mode off. -- Combines with `defaultModel` and `agentModelOverrides` -- Layer 0 always wins. - -## Anti-Patterns - -- **Don't override Layer 0 in economy mode.** If the user set `defaultModel: "claude-opus-4.6"`, they want quality. Economy mode only affects Layer 3 auto-selection. -- **Don't silently apply economy mode.** Always acknowledge when activated or deactivated. -- **Don't treat economy mode as permanent by default.** Session phrases activate session-only; only "always" or `config.json` persist it. -- **Don't bump premium tasks down too far.** Architecture and security reviews shift from opus to sonnet in economy mode -- they do NOT go to fast/cheap models. diff --git a/.copilot/skills/error-recovery/SKILL.md b/.copilot/skills/error-recovery/SKILL.md index 7fd7f1a5..751a63b7 100644 --- a/.copilot/skills/error-recovery/SKILL.md +++ b/.copilot/skills/error-recovery/SKILL.md @@ -1,6 +1,6 @@ --- name: "error-recovery" -description: "Standard recovery patterns for all squad agents. When something fails, adapt -- don't just report the failure." +description: "Standard recovery patterns for all agents. When something fails, adapt -- don't just report the failure." domain: "reliability, agent-coordination" confidence: "high" license: MIT @@ -8,7 +8,7 @@ license: MIT # Error Recovery Patterns -Standard recovery patterns for all squad agents. When something fails, **adapt** -- don't just report the failure. +Standard recovery patterns for all agents. When something fails, **adapt** -- don't just report the failure. --- @@ -85,7 +85,7 @@ Standard recovery patterns for all squad agents. When something fails, **adapt** ## Applying These Patterns -Each agent should reference these patterns in their charter's `## Error Recovery` section, tailored to their domain. The charter should list the agent's most common failure modes and map each to the appropriate pattern above. +Each agent should reference these patterns in their charter's `## Error Recovery` section, tailored to their domain. The charter should list the most common failure modes and map each to the appropriate pattern above. **Selection guide:** diff --git a/.copilot/skills/external-comms/SKILL.md b/.copilot/skills/external-comms/SKILL.md deleted file mode 100644 index e1a30947..00000000 --- a/.copilot/skills/external-comms/SKILL.md +++ /dev/null @@ -1,329 +0,0 @@ ---- -name: "external-comms" -description: "PAO workflow for scanning, drafting, and presenting community responses with human review gate" -domain: "community, communication, workflow" -confidence: "low" -source: "manual (RFC #426 -- PAO External Communications)" -tools: - - name: "github-mcp-server-list_issues" - description: "List open issues for scan candidates and lightweight triage" - when: "Use for recent open issue scans before thread-level review" - - name: "github-mcp-server-issue_read" - description: "Read the full issue, comments, and labels before drafting" - when: "Use after selecting a candidate so PAO has complete thread context" - - name: "github-mcp-server-search_issues" - description: "Search for candidate issues or prior squad responses" - when: "Use when filtering by keywords, labels, or duplicate response checks" - - name: "gh CLI" - description: "Fallback for GitHub issue comments and discussions workflows" - when: "Use gh issue list/comment and gh api or gh api graphql when MCP coverage is incomplete" ---- - -## Context - -Phase 1 is **draft-only mode**. - -- PAO scans issues and discussions, drafts responses with the humanizer skill, and presents a review table for human approval. -- **Human review gate is mandatory** -- PAO never posts autonomously. -- Every action is logged to `.squad/comms/audit/`. -- This workflow is triggered manually only ("PAO, check community") -- no automated or Ralph-triggered activation in Phase 1. - -## Patterns - -### 1. Scan - -Find unanswered community items with GitHub MCP tools first, or `gh issue list` / `gh api` as fallback for issues and discussions. - -- Include **open** issues and discussions only. -- Filter for items with **no squad team response**. -- Limit to items created in the last 7 days. -- Exclude items labeled `squad:internal` or `wontfix`. -- Include discussions **and** issues in the same sweep. -- Phase 1 scope is **issues and discussions only** -- do not draft PR replies. - -### Discussion Handling (Phase 1) - -Discussions use the GitHub Discussions API, which differs from issues: - -- **Scan:** `gh api /repos/{owner}/{repo}/discussions --jq '.[] | select(.answer_chosen_at == null)'` to find unanswered discussions -- **Categories:** Filter by Q&A and General categories only (skip Announcements, Show and Tell) -- **Answers vs comments:** In Q&A discussions, PAO drafts an "answer" (not a comment). The human marks it as accepted answer after posting. -- **Phase 1 scope:** Issues and Discussions ONLY. No PR comments. - -### 2. Classify - -Determine the response type before drafting. - -- Welcome (new contributor) -- Troubleshooting (bug/help) -- Feature guidance (feature request/how-to) -- Redirect (wrong repo/scope) -- Acknowledgment (confirmed, no fix) -- Closing (resolved) -- Technical uncertainty (unknown cause) -- Empathetic disagreement (pushback on a decision or design) -- Information request (need more reproduction details or context) - -### Template Selection Guide - -| Signal in Issue/Discussion | -> Response Type | Template | -|---------------------------|-----------------|----------| -| New contributor (0 prior issues) | Welcome | T1 | -| Error message, stack trace, "doesn't work" | Troubleshooting | T2 | -| "How do I...?", "Can Squad...?", "Is there a way to...?" | Feature Guidance | T3 | -| Wrong repo, out of scope for Squad | Redirect | T4 | -| Confirmed bug, no fix available yet | Acknowledgment | T5 | -| Fix shipped, PR merged that resolves issue | Closing | T6 | -| Unclear cause, needs investigation | Technical Uncertainty | T7 | -| Author disagrees with a decision or design | Empathetic Disagreement | T8 | -| Need more reproduction info or context | Information Request | T9 | - -Use exactly one template as the base draft. Replace placeholders with issue-specific details, then apply the humanizer patterns. If the thread spans multiple signals, choose the highest-risk template and capture the nuance in the thread summary. - -### Confidence Classification - -| Confidence | Criteria | Example | -|-----------|----------|---------| -| [GREEN] High | Answer exists in Squad docs or FAQ, similar question answered before, no technical ambiguity | "How do I install Squad?" | -| [YELLOW] Medium | Technical answer is sound but involves judgment calls, OR docs exist but don't perfectly match the question, OR tone is tricky | "Can Squad work with Azure DevOps?" (yes, but setup is nuanced) | -| [RED] Needs Review | Technical uncertainty, policy/roadmap question, potential reputational risk, author is frustrated/angry, question about unreleased features | "When will Squad support Claude?" | - -**Auto-escalation rules:** -- Any mention of competitors -> [RED] -- Any mention of pricing/licensing -> [RED] -- Author has >3 follow-up comments without resolution -> [RED] -- Question references a closed-wontfix issue -> [RED] - -### 3. Draft - -Use the humanizer skill for every draft. - -- Complete **Thread-Read Verification** before writing. -- Read the **full thread**, including all comments, before writing. -- Select the matching template from the **Template Selection Guide** and record the template ID in the review notes. -- Treat templates as reusable drafting assets: keep the structure, replace placeholders, and only improvise when the thread truly requires it. -- Validate the draft against the humanizer anti-patterns. -- Flag long threads (`>10` comments) with `!`. - -### Thread-Read Verification - -Before drafting, PAO MUST verify complete thread coverage: - -1. **Count verification:** Compare API comment count with actually-read comments. If mismatch, abort draft. -2. **Deleted comment check:** Use `gh api` timeline to detect deleted comments. If found, flag as ! in review table. -3. **Thread summary:** Include in every draft: "Thread: {N} comments, last activity {date}, {summary of key points}" -4. **Long thread flag:** If >10 comments, add ! to review table and include condensed thread summary -5. **Evidence line in review table:** Each draft row includes "Read: {N}/{total} comments" column - -### 4. Present - -Show drafts for review in this exact format: - -```text -[MEMO] PAO -- Community Response Drafts ----------------------------------- - -| # | Item | Author | Type | Confidence | Read | Preview | -|---|------|--------|------|------------|------|---------| -| 1 | Issue #N | @user | Type | [GREEN]/[YELLOW]/[RED] | N/N | "First words..." | - -Confidence: [GREEN] High | [YELLOW] Medium | [RED] Needs review - -Full drafts below ? -``` - -Each full draft must begin with the thread summary line: -`Thread: {N} comments, last activity {date}, {summary of key points}` - -### 5. Human Action - -Wait for explicit human direction before anything is posted. - -- `pao approve 1 3` -- approve drafts 1 and 3 -- `pao edit 2` -- edit draft 2 -- `pao skip` -- skip all -- `banana` -- freeze all pending (safe word) - -### Rollback -- Bad Post Recovery - -If a posted response turns out to be wrong, inappropriate, or needs correction: - -1. **Delete the comment:** - - Issues: `gh api -X DELETE /repos/{owner}/{repo}/issues/comments/{comment_id}` - - Discussions: `gh api graphql -f query='mutation { deleteDiscussionComment(input: {id: "{node_id}"}) { comment { id } } }'` -2. **Log the deletion:** Write audit entry with action `delete`, include reason and original content -3. **Draft replacement** (if needed): PAO drafts a corrected response, goes through normal review cycle -4. **Postmortem:** If the error reveals a pattern gap, update humanizer anti-patterns or add a new test case - -**Safe word -- `banana`:** -- Immediately freezes all pending drafts in the review queue -- No new scans or drafts until `pao resume` is issued -- Audit entry logged with halter identity and reason - -### 6. Post - -After approval: - -- Human posts via `gh issue comment` for issues or `gh api` for discussion answers/comments. -- PAO helps by preparing the CLI command. -- Write the audit entry after the posting action. - -### 7. Audit - -Log every action. - -- Location: `.squad/comms/audit/{timestamp}.md` -- Required fields vary by action -- see `.squad/comms/templates/audit-entry.md` Conditional Fields table -- Universal required fields: `timestamp`, `action` -- All other fields are conditional on the action type - -## Examples - -These are reusable templates. Keep the structure, replace placeholders, and adjust only where the thread requires it. - -### Example scan command - -```bash -gh issue list --state open --json number,title,author,labels,comments --limit 20 -``` - -### Example review table - -```text -[MEMO] PAO -- Community Response Drafts ----------------------------------- - -| # | Item | Author | Type | Confidence | Read | Preview | -|---|------|--------|------|------------|------|---------| -| 1 | Issue #426 | @newdev | Welcome | [GREEN] | 1/1 | "Hey @newdev! Welcome to Squad..." | -| 2 | Discussion #18 | @builder | Feature guidance | [YELLOW] | 4/4 | "Great question! Today the CLI..." | -| 3 | Issue #431 ! | @debugger | Technical uncertainty | [RED] | 12/12 | "Interesting find, @debugger..." | - -Confidence: [GREEN] High | [YELLOW] Medium | [RED] Needs review - -Full drafts below ? -``` - -### Example audit entry (post action) - -```markdown ---- -timestamp: "2026-03-16T21:30:00Z" -action: "post" -item_number: 426 -draft_id: 1 -reviewer: "@bradygaster" ---- - -## Context (draft, approve, edit, skip, post, delete actions) -- Thread depth: 3 -- Response type: welcome -- Confidence: [GREEN] -- Long thread flag: false - -## Draft Content (draft, edit, post actions) -Thread: 3 comments, last activity 2026-03-16, reporter hit a preview-build regression after install. - -Hey @newdev! Welcome to Squad ? Thanks for opening this. -We reproduced the issue in preview builds and we're checking the regression point now. -Let us know if you can share the command you ran right before the failure. - -## Post Result (post, delete actions) -https://github.com/bradygaster/squad/issues/426#issuecomment-123456 -``` - -### T1 -- Welcome - -```text -Hey {author}! Welcome to Squad ? Thanks for opening this. -{specific acknowledgment or first answer} -Let us know if you have questions -- happy to help! -``` - -### T2 -- Troubleshooting - -```text -Thanks for the detailed report, {author}! -Here's what we think is happening: {explanation} -{steps or workaround} -Let us know if that helps, or if you're seeing something different. -``` - -### T3 -- Feature Guidance - -```text -Great question! {context on current state} -{guidance or workaround} -We've noted this as a potential improvement -- {tracking info if applicable}. -``` - -### T4 -- Redirect - -```text -Thanks for reaching out! This one is actually better suited for {correct location}. -{brief explanation of why} -Feel free to open it there -- they'll be able to help! -``` - -### T5 -- Acknowledgment - -```text -Good catch, {author}. We've confirmed this is a real issue. -{what we know so far} -We'll update this thread when we have a fix. Thanks for flagging it! -``` - -### T6 -- Closing - -```text -This should be resolved in {version/PR}! ? -{brief summary of what changed} -Thanks for reporting this, {author} -- it made Squad better. -``` - -### T7 -- Technical Uncertainty - -```text -Interesting find, {author}. We're not 100% sure what's causing this yet. -Here's what we've ruled out: {list} -We'd love more context if you have it -- {specific ask}. -We'll dig deeper and update this thread. -``` - -### T8 -- Empathetic Disagreement - -```text -We hear you, {author}. That's a fair concern. - -The current design choice was driven by {reason}. We know it's not ideal for every use case. - -{what alternatives exist or what trade-off was made} - -If you have ideas for how to make this work better for your scenario, we'd love to hear them -- open a discussion or drop your thoughts here! -``` - -### T9 -- Information Request - -```text -Thanks for reporting this, {author}! - -To help us dig into this, could you share: -- {specific ask 1} -- {specific ask 2} -- {specific ask 3, if applicable} - -That context will help us narrow down what's happening. Appreciate it! -``` - -## Anti-Patterns - -- [ ] Posting without human review (NEVER -- this is the cardinal rule) -- [ ] Drafting without reading full thread (context is everything) -- [ ] Ignoring confidence flags ([RED] items need Flight/human review) -- [ ] Scanning closed issues (only open items) -- [ ] Responding to issues labeled `squad:internal` or `wontfix` -- [ ] Skipping audit logging (every action must be recorded) -- [ ] Drafting for issues where a squad member already responded (avoid duplicates) -- [ ] Drafting pull request responses in Phase 1 (issues/discussions only) -- [ ] Treating templates like loose examples instead of reusable drafting assets -- [ ] Asking for more info without specific requests diff --git a/.copilot/skills/gh-auth-isolation/SKILL.md b/.copilot/skills/gh-auth-isolation/SKILL.md index 2416fb77..c5721ba8 100644 --- a/.copilot/skills/gh-auth-isolation/SKILL.md +++ b/.copilot/skills/gh-auth-isolation/SKILL.md @@ -12,9 +12,9 @@ tools: ## Context -Many developers use GitHub through an Enterprise Managed User (EMU) account at work while maintaining a personal GitHub account for open-source contributions. AI agents spawned by Squad inherit the shell's default `gh` authentication -- which is usually the EMU account. This causes failures when agents try to push to personal repos, create PRs on forks, or interact with resources outside the enterprise org. +Many developers use GitHub through an Enterprise Managed User (EMU) account at work while maintaining a personal GitHub account for open-source contributions. Automation processes inherit the shell's default `gh` authentication -- which is usually the EMU account. This causes failures when trying to push to personal repos, create PRs on forks, or interact with resources outside the enterprise org. -This skill teaches agents how to detect the active identity, switch contexts safely, and avoid mixing credentials across operations. +This skill teaches how to detect the active identity, switch contexts safely, and avoid mixing credentials across operations. ## Patterns @@ -139,13 +139,13 @@ git remote set-url origin https://github.com/personaluser/personaluser.github.io ### ? Correct: Agent creates a PR from personal fork to upstream ```powershell -# Fork: personaluser/squad, Upstream: bradygaster/squad -# Agent is on branch contrib/fix-docs in the fork clone +# Fork: personaluser/project, Upstream: maintainer/project +# Working on branch contrib/fix-docs in the fork clone git push origin contrib/fix-docs # Pushes to fork (may need token auth) # Create PR targeting upstream -gh pr create --repo bradygaster/squad --head personaluser:contrib/fix-docs ` +gh pr create --repo maintainer/project --head personaluser:contrib/fix-docs ` --title "docs: fix installation guide" ` --body "Fixes #123" ``` @@ -153,7 +153,7 @@ gh pr create --repo bradygaster/squad --head personaluser:contrib/fix-docs ` ### ? Incorrect: Blindly pushing with wrong account ```bash -# BAD: Agent assumes default gh auth works for personal repos +# BAD: Assuming default gh auth works for personal repos git push origin main # ERROR: Permission denied -- EMU account has no access to personal repo @@ -176,8 +176,8 @@ git push https://personaluser:$token@github.com/personaluser/repo.git main - [ ] **Hardcoding tokens** in scripts, environment variables, or committed files. Use `gh auth token --user` to extract at runtime. - [ ] **Assuming the default `gh` auth works** for all repos. EMU accounts can't access personal repos and vice versa. -- [ ] **Switching `gh auth login`** globally mid-session. This changes the default for ALL processes and can break parallel agents. -- [ ] **Storing personal tokens in `.env`** or `.squad/` files. These get committed by Scribe. Use `gh`'s credential store. +- [ ] **Switching `gh auth login`** globally mid-session. This changes the default for ALL processes and can break parallel operations. +- [ ] **Storing personal tokens in `.env`** or committed config files. Use `gh`'s credential store. - [ ] **Ignoring token cleanup** after inline HTTPS pushes. Always reset the remote URL to avoid persisting tokens. -- [ ] **Using `gh auth switch`** in multi-agent sessions. One agent switching affects all others sharing the shell. +- [ ] **Using `gh auth switch`** in multi-process sessions. One process switching affects all others sharing the shell. - [ ] **Mixing EMU and personal operations** in the same git clone. Use separate clones or explicit remote URLs per operation. diff --git a/.copilot/skills/git-workflow/SKILL.md b/.copilot/skills/git-workflow/SKILL.md index 550162ae..77885b77 100644 --- a/.copilot/skills/git-workflow/SKILL.md +++ b/.copilot/skills/git-workflow/SKILL.md @@ -1,6 +1,6 @@ --- name: "git-workflow" -description: "Squad branching model: dev-first workflow with insiders preview channel" +description: "Dev-first workflow with insiders preview channel" domain: "version-control" confidence: "high" source: "team-decision" @@ -8,7 +8,7 @@ source: "team-decision" ## Context -Squad uses a two-branch model. **All feature work branches from `develop`, never from `main`.** +This project uses a two-branch model. **All feature work branches from `develop`, never from `main`.** | Branch | Purpose | Rules | |--------|---------|-------| @@ -17,11 +17,14 @@ Squad uses a two-branch model. **All feature work branches from `develop`, never ## Branch Naming Convention -Issue branches MUST use: `squad/{issue-number}-{kebab-case-slug}` +Issue branches MUST use: `{type}/{issue-number}-{kebab-case-slug}` + +Types: `feat`, `fix`, `chore`, `docs`, `refactor` Examples: -- `squad/195-fix-version-stamp-bug` -- `squad/42-add-profile-api` +- `fix/195-version-stamp-bug` +- `feat/42-profile-api` +- `chore/468-customizable-install` ## Workflow for Issue Work @@ -29,7 +32,7 @@ Examples: ```bash git checkout develop git pull origin develop - git checkout -b squad/{issue-number}-{slug} + git checkout -b {type}/{issue-number}-{slug} ``` 2. **Mark issue in-progress:** @@ -46,20 +49,20 @@ Examples: 5. **Push and mark ready:** ```bash - git push -u origin squad/{issue-number}-{slug} + git push -u origin {type}/{issue-number}-{slug} gh pr ready ``` 6. **Merge gates -- BOTH must pass before merging:** - - [x] Mickey has approved the PR + - [x] Code review approval - [x] CI checks are green 7. **After merge to develop -- delete the branch immediately:** ```bash git checkout develop git pull origin develop - git branch -d squad/{issue-number}-{slug} - git push origin --delete squad/{issue-number}-{slug} + git branch -d {type}/{issue-number}-{slug} + git push origin --delete {type}/{issue-number}-{slug} ``` ## Parallel Multi-Issue Work (Worktrees) @@ -83,39 +86,39 @@ From the main clone (must be on develop or any branch): git fetch origin develop # Create a worktree per issue -- siblings to the main clone -git worktree add ../squad-195 -b squad/195-fix-stamp-bug origin/develop -git worktree add ../squad-193 -b squad/193-refactor-loader origin/develop +git worktree add ../work-195 -b fix/195-stamp-bug origin/develop +git worktree add ../work-193 -b refactor/193-loader origin/develop ``` -**Naming convention:** `../{repo-name}-{issue-number}` (e.g., `../squad-195`, `../squad-pr-42`). +**Naming convention:** `../{repo-name}-{issue-number}` (e.g., `../dev-setup-195`, `../dev-setup-pr-42`). Each worktree: - Has its own working directory and index -- Is on its own `squad/{issue-number}-{slug}` branch from develop +- Is on its own `{type}/{issue-number}-{slug}` branch from develop - Shares the same `.git` object store (disk-efficient) ### Per-Worktree Agent Workflow -Each agent operates inside its worktree exactly like the single-issue workflow: +Each developer operates inside their worktree exactly like the single-issue workflow: ```bash -cd ../squad-195 +cd ../work-195 # Work normally -- commits, tests, pushes git add -A && git commit -m "fix: stamp bug (#195)" -git push -u origin squad/195-fix-stamp-bug +git push -u origin fix/195-stamp-bug # Create PR targeting develop gh pr create --base develop --title "fix: stamp bug" --body "Closes #195" --draft ``` -All PRs target `develop` independently. Agents never interfere with each other's filesystem. +All PRs target `develop` independently. Multiple worktrees don't interfere with each other's filesystem. ### .squad/ State in Worktrees -The `.squad/` directory exists in each worktree as a copy. This is safe because: +The `.squad/` directory (if present) exists in each worktree as a copy. This is safe because: - `.gitattributes` declares `merge=union` on append-only files (history.md, decisions.md, logs) -- Each agent appends to its own section; union merge reconciles on PR merge to develop +- Each process appends to its own section; union merge reconciles on PR merge to develop - **Rule:** Never rewrite or reorder `.squad/` files in a worktree -- append only ### Cleanup After Merge @@ -124,10 +127,10 @@ After a worktree's PR is merged to develop: ```bash # From the main clone -git worktree remove ../squad-195 +git worktree remove ../work-195 git worktree prune # clean stale metadata -git branch -d squad/195-fix-stamp-bug -git push origin --delete squad/195-fix-stamp-bug +git branch -d fix/195-stamp-bug +git push origin --delete fix/195-stamp-bug ``` If a worktree was deleted manually (rm -rf), `git worktree prune` recovers the state. @@ -136,7 +139,7 @@ If a worktree was deleted manually (rm -rf), `git worktree prune` recovers the s ## Multi-Repo Downstream Scenarios -When work spans multiple repositories (e.g., squad-cli changes need squad-sdk changes, or a user's app depends on squad): +When work spans multiple repositories (e.g., a CLI changes need SDK changes, or a user's app depends on a library): ### Setup @@ -144,12 +147,12 @@ Clone downstream repos as siblings to the main repo: ``` ~/work/ - squad-pr/ # main repo - squad-sdk/ # downstream dependency + main-project/ # main repo + lib-sdk/ # downstream dependency user-app/ # consumer project ``` -Each repo gets its own issue branch following its own naming convention. If the downstream repo also uses Squad conventions, use `squad/{issue-number}-{slug}`. +Each repo gets its own issue branch following its own naming convention. ### Coordinated PRs @@ -158,9 +161,9 @@ Each repo gets its own issue branch following its own naming convention. If the ``` Closes #42 - **Depends on:** squad-sdk PR #17 (squad-sdk changes required for this feature) + **Depends on:** lib-sdk PR #17 (lib-sdk changes required for this feature) ``` -- Merge order: dependencies first (e.g., squad-sdk), then dependents (e.g., squad-cli) +- Merge order: dependencies first (e.g., lib-sdk), then dependents (e.g., main-project) ### Local Linking for Testing @@ -168,15 +171,15 @@ Before pushing, verify cross-repo changes work together: ```bash # Node.js / npm -cd ../squad-sdk && npm link -cd ../squad-pr && npm link squad-sdk +cd ../lib-sdk && npm link +cd ../main-project && npm link lib-sdk # Go # Use replace directive in go.mod: -# replace github.com/org/squad-sdk => ../squad-sdk +# replace github.com/org/lib-sdk => ../lib-sdk # Python -cd ../squad-sdk && uv pip install -e . +cd ../lib-sdk && uv pip install -e . ``` **Important:** Remove local links before committing. `npm link` and `go replace` are dev-only -- CI must use published packages or PR-specific refs. @@ -195,8 +198,8 @@ These compose naturally. You can have: - [ ] Branching from main (always branch from develop) - [ ] PR targeting main directly (always target develop) - [ ] Pushing directly to main or develop (use PRs) -- [ ] Non-conforming branch names (must be squad/{number}-{slug}) -- [ ] Merging without Mickey's approval +- [ ] Non-conforming branch names (must be {type}/{number}-{slug}) +- [ ] Merging without code review approval - [ ] Merging without green CI - [ ] Leaving branches around after merge (delete immediately) - [ ] Deleting main or develop (never) @@ -212,12 +215,11 @@ The `develop` branch requires: ## Merge Gates -### Hard Rule: No Merge Without Mickey Approval -Ralph MUST call `gh pr review {n} --approve` from Mickey BEFORE `gh pr merge`. -Violation history: Sprint 2 (PRs #17-#27), Sprint 3 (PRs #33-#36). -Branch protection on `develop` now enforces this at the GitHub level. +### Hard Rule: No Merge Without Approval +The reviewer MUST call `gh pr review {n} --approve` BEFORE `gh pr merge`. +Branch protection on `develop` enforces this at the GitHub level. ## Promotion Pipeline -- develop -> main: Mickey approves + CI green -> merge, then tag for release +- develop -> main: Code review approval + CI green -> merge, then tag for release - Hotfixes: Branch from develop as `hotfix/{slug}`, PR back to develop, then promote to main diff --git a/.copilot/skills/humanizer/SKILL.md b/.copilot/skills/humanizer/SKILL.md deleted file mode 100644 index b1268807..00000000 --- a/.copilot/skills/humanizer/SKILL.md +++ /dev/null @@ -1,105 +0,0 @@ ---- -name: "humanizer" -description: "Tone enforcement patterns for external-facing community responses" -domain: "communication, tone, community" -confidence: "low" -source: "manual (RFC #426 -- PAO External Communications)" ---- - -## Context - -Use this skill whenever PAO drafts external-facing responses for issues or discussions. - -- Tone must be warm, helpful, and human-sounding -- never robotic or corporate. -- Brady's constraint applies everywhere: **Humanized tone is mandatory**. -- This applies to **all external-facing content** drafted by PAO in Phase 1 issues/discussions workflows. - -## Patterns - -1. **Warm opening** -- Start with acknowledgment ("Thanks for reporting this", "Great question!") -2. **Active voice** -- "We're looking into this" not "This is being investigated" -3. **Second person** -- Address the person directly ("you" not "the user") -4. **Conversational connectors** -- "That said...", "Here's what we found...", "Quick note:" -5. **Specific, not vague** -- "This affects the casting module in v0.8.x" not "We are aware of issues" -6. **Empathy markers** -- "I can see how that would be frustrating", "Good catch!" -7. **Action-oriented closes** -- "Let us know if that helps!" not "Please advise if further assistance is required" -8. **Uncertainty is OK** -- "We're not 100% sure yet, but here's what we think is happening..." is better than false confidence -9. **Profanity filter** -- Never include profanity, slurs, or aggressive language, even when quoting -10. **Baseline comparison** -- Responses should align with tone of 5-10 "gold standard" responses (>80% similarity threshold) -11. **Empathetic disagreement** -- "We hear you. That's a fair concern." before explaining the reasoning -12. **Information request** -- Ask for specific details, not open-ended "can you provide more info?" -13. **No link-dumping** -- Don't just paste URLs. Provide context: "Check out the [getting started guide](url) -- specifically the section on routing" not just a bare link - -## Examples - -### 1. Welcome - -```text -Hey {author}! Welcome to Squad ? Thanks for opening this. -{substantive response} -Let us know if you have questions -- happy to help! -``` - -### 2. Troubleshooting - -```text -Thanks for the detailed report, {author}! -Here's what we think is happening: {explanation} -{steps or workaround} -Let us know if that helps, or if you're seeing something different. -``` - -### 3. Feature guidance - -```text -Great question! {context on current state} -{guidance or workaround} -We've noted this as a potential improvement -- {tracking info if applicable}. -``` - -### 4. Redirect - -```text -Thanks for reaching out! This one is actually better suited for {correct location}. -{brief explanation of why} -Feel free to open it there -- they'll be able to help! -``` - -### 5. Acknowledgment - -```text -Good catch, {author}. We've confirmed this is a real issue. -{what we know so far} -We'll update this thread when we have a fix. Thanks for flagging it! -``` - -### 6. Closing - -```text -This should be resolved in {version/PR}! ? -{brief summary of what changed} -Thanks for reporting this, {author} -- it made Squad better. -``` - -### 7. Technical uncertainty - -```text -Interesting find, {author}. We're not 100% sure what's causing this yet. -Here's what we've ruled out: {list} -We'd love more context if you have it -- {specific ask}. -We'll dig deeper and update this thread. -``` - -## Anti-Patterns - -- [ ] Corporate speak: "We appreciate your patience as we investigate this matter" -- [ ] Marketing hype: "Squad is the BEST way to..." or "This amazing feature..." -- [ ] Passive voice: "It has been determined that..." or "The issue is being tracked" -- [ ] Dismissive: "This works as designed" without empathy -- [ ] Over-promising: "We'll ship this next week" without commitment from the team -- [ ] Empty acknowledgment: "Thanks for your feedback" with no substance -- [ ] Robot signatures: "Best regards, PAO" or "Sincerely, The Squad Team" -- [ ] Excessive emoji: More than 1-2 emoji per response -- [ ] Quoting profanity: Even when the original issue contains it, paraphrase instead -- [ ] Link-dumping: Pasting URLs without context ("See: https://...") -- [ ] Open-ended info requests: "Can you provide more information?" without specifying what information diff --git a/.copilot/skills/model-selection/SKILL.md b/.copilot/skills/model-selection/SKILL.md deleted file mode 100644 index 8f19b7bb..00000000 --- a/.copilot/skills/model-selection/SKILL.md +++ /dev/null @@ -1,117 +0,0 @@ -# Model Selection - -> Determines which LLM model to use for each agent spawn. - -## SCOPE - -[x] THIS SKILL PRODUCES: -- A resolved `model` parameter for every `task` tool call -- Persistent model preferences in `.squad/config.json` -- Spawn acknowledgments that include the resolved model - -[ ] THIS SKILL DOES NOT PRODUCE: -- Code, tests, or documentation -- Model performance benchmarks -- Cost reports or billing artifacts - -## Context - -Squad supports 18+ models across three tiers (premium, standard, fast). The coordinator must select the right model for each agent spawn. Users can set persistent preferences that survive across sessions. - -## 5-Layer Model Resolution Hierarchy - -Resolution is **first-match-wins** -- the highest layer with a value wins. - -| Layer | Name | Source | Persistence | -|-------|------|--------|-------------| -| **0a** | Per-Agent Config | `.squad/config.json` -> `agentModelOverrides.{name}` | Persistent (survives sessions) | -| **0b** | Global Config | `.squad/config.json` -> `defaultModel` | Persistent (survives sessions) | -| **1** | Session Directive | User said "use X" in current session | Session-only | -| **2** | Charter Preference | Agent's `charter.md` -> `## Model` section | Persistent (in charter) | -| **3** | Task-Aware Auto | Code -> sonnet, docs -> haiku, visual -> opus | Computed per-spawn | -| **4** | Default | `claude-haiku-4.5` | Hardcoded fallback | - -**Key principle:** Layer 0 (persistent config) beats everything. If the user said "always use opus" and it was saved to config.json, every agent gets opus regardless of role or task type. This is intentional -- the user explicitly chose quality over cost. - -## AGENT WORKFLOW - -### On Session Start - -1. READ `.squad/config.json` -2. CHECK for `defaultModel` field -- if present, this is the Layer 0 override for all spawns -3. CHECK for `agentModelOverrides` field -- if present, these are per-agent Layer 0a overrides -4. STORE both values in session context for the duration - -### On Every Agent Spawn - -1. CHECK Layer 0a: Is there an `agentModelOverrides.{agentName}` in config.json? -> Use it. -2. CHECK Layer 0b: Is there a `defaultModel` in config.json? -> Use it. -3. CHECK Layer 1: Did the user give a session directive? -> Use it. -4. CHECK Layer 2: Does the agent's charter have a `## Model` section? -> Use it. -5. CHECK Layer 3: Determine task type: - - Code (implementation, tests, refactoring, bug fixes) -> `claude-sonnet-4.6` - - Prompts, agent designs -> `claude-sonnet-4.6` - - Visual/design with image analysis -> `claude-opus-4.6` - - Non-code (docs, planning, triage, changelogs) -> `claude-haiku-4.5` -6. FALLBACK Layer 4: `claude-haiku-4.5` -7. INCLUDE model in spawn acknowledgment: `[TOOL] {Name} ({resolved_model}) -- {task}` - -### When User Sets a Preference - -**Trigger phrases:** "always use X", "use X for everything", "switch to X", "default to X" - -1. VALIDATE the model ID against the catalog (18+ models) -2. WRITE `defaultModel` to `.squad/config.json` (merge, don't overwrite) -3. ACKNOWLEDGE: `[x] Model preference saved: {model} -- all future sessions will use this until changed.` - -**Per-agent trigger:** "use X for {agent}" - -1. VALIDATE model ID -2. WRITE to `agentModelOverrides.{agent}` in `.squad/config.json` -3. ACKNOWLEDGE: `[x] {Agent} will always use {model} -- saved to config.` - -### When User Clears a Preference - -**Trigger phrases:** "switch back to automatic", "clear model preference", "use default models" - -1. REMOVE `defaultModel` from `.squad/config.json` -2. ACKNOWLEDGE: `[x] Model preference cleared -- returning to automatic selection.` - -### STOP - -After resolving the model and including it in the spawn template, this skill is done. Do NOT: -- Generate model comparison reports -- Run benchmarks or speed tests -- Create new config files (only modify existing `.squad/config.json`) -- Change the model after spawn (fallback chains handle runtime failures) - -## Config Schema - -`.squad/config.json` model-related fields: - -```json -{ - "version": 1, - "defaultModel": "claude-opus-4.6", - "agentModelOverrides": { - "fenster": "claude-sonnet-4.6", - "mcmanus": "claude-haiku-4.5" - } -} -``` - -- `defaultModel` -- applies to ALL agents unless overridden by `agentModelOverrides` -- `agentModelOverrides` -- per-agent overrides that take priority over `defaultModel` -- Both fields are optional. When absent, Layers 1-4 apply normally. - -## Fallback Chains - -If a model is unavailable (rate limit, plan restriction), retry within the same tier: - -``` -Premium: claude-opus-4.6 -> claude-opus-4.6-fast -> claude-opus-4.5 -> claude-sonnet-4.6 -Standard: claude-sonnet-4.6 -> gpt-5.4 -> claude-sonnet-4.5 -> gpt-5.3-codex -> claude-sonnet-4 -Fast: claude-haiku-4.5 -> gpt-5.1-codex-mini -> gpt-4.1 -> gpt-5-mini -``` - -**Never fall UP in tier.** A fast task won't land on a premium model via fallback. diff --git a/.copilot/skills/nap/SKILL.md b/.copilot/skills/nap/SKILL.md deleted file mode 100644 index fc58c613..00000000 --- a/.copilot/skills/nap/SKILL.md +++ /dev/null @@ -1,24 +0,0 @@ -# Skill: nap - -> Context hygiene -- compress, prune, archive .squad/ state - -## What It Does - -Reclaims context window budget by compressing agent histories, pruning old logs, -archiving stale decisions, and cleaning orphaned inbox files. - -## When To Use - -- Before heavy fan-out work (many agents will spawn) -- When history.md files exceed 15KB -- When .squad/ total size exceeds 1MB -- After long-running sessions or sprints - -## Invocation - -- CLI: `squad nap` / `squad nap --deep` / `squad nap --dry-run` -- REPL: `/nap` / `/nap --dry-run` / `/nap --deep` - -## Confidence - -medium -- Confirmed by team vote (4-1) and initial implementation diff --git a/.copilot/skills/reskill/SKILL.md b/.copilot/skills/reskill/SKILL.md deleted file mode 100644 index 912a32c2..00000000 --- a/.copilot/skills/reskill/SKILL.md +++ /dev/null @@ -1,92 +0,0 @@ ---- -name: "reskill" -description: "Team-wide charter and history optimization through skill extraction" -domain: "team-optimization" -confidence: "high" -source: "manual -- Brady directive to reduce per-agent context overhead" ---- - -## Context - -When the coordinator hears "team, reskill" (or similar: "optimize context", "slim down charters"), trigger a team-wide optimization pass. The goal: reduce per-agent context consumption by extracting shared patterns from charters and histories into reusable skills. - -This is a periodic maintenance activity. Run whenever charter/history bloat is suspected. - -## Process - -### Step 1: Audit -Read all agent charters and histories. Measure byte sizes. Identify: - -- **Boilerplate** -- sections repeated across ?3 charters with <10% variation (collaboration, model, boundaries template) -- **Shared knowledge** -- domain knowledge duplicated in 2+ charters (incident postmortems, technical patterns) -- **Mature learnings** -- history entries appearing 3+ times across agents that should be promoted to skills - -### Step 2: Extract -For each identified pattern: -1. Create or update a skill at `.squad/skills/{skill-name}/SKILL.md` -2. Follow the skill template format (frontmatter + Context + Patterns + Examples + Anti-Patterns) -3. Set confidence: low (first observation), medium (2+ agents), high (team-wide) - -### Step 3: Trim -**Charters** -- target ?1.5KB per agent: -- Remove Collaboration section entirely (spawn prompt + agent-collaboration skill covers it) -- Remove Voice section (tagline blockquote at top of charter already captures it) -- Trim Model section to single line: `Preferred: {model}` -- Remove "When I'm unsure" boilerplate from Boundaries -- Remove domain knowledge now covered by a skill -- add skill reference comment if helpful -- Keep: Identity, What I Own, unique How I Work patterns, Boundaries (domain list only) - -**Histories** -- target ?8KB per agent: -- Apply history-hygiene skill to any history >12KB -- Promote recurring patterns (3+ occurrences across agents) to skills -- Summarize old entries into `## Core Context` section -- Remove session-specific metadata (dates, branch names, requester names) - -### Step 4: Report -Output a savings table: - -| Agent | Charter Before | Charter After | History Before | History After | Saved | -|-------|---------------|---------------|----------------|---------------|-------| - -Include totals and percentage reduction. - -## Patterns - -### Minimal Charter Template (target format after reskill) - -``` -# {Name} -- {Role} - -> {Tagline -- one sentence capturing voice and philosophy} - -## Identity -- **Name:** {Name} -- **Role:** {Role} -- **Expertise:** {comma-separated list} - -## What I Own -- {bullet list of owned artifacts/domains} - -## How I Work -- {unique patterns and principles -- NOT boilerplate} - -## Boundaries -**I handle:** {domain list} -**I don't handle:** {explicit exclusions} - -## Model -Preferred: {model} -``` - -### Skill Extraction Threshold -- **1 charter** -> leave in charter (unique to that agent) -- **2 charters** -> consider extracting if >500 bytes of overlap -- **3+ charters** -> always extract to a shared skill - -## Anti-Patterns -- Don't delete unique per-agent identity or domain-specific knowledge -- Don't create skills for content only one agent uses -- Don't merge unrelated patterns into a single mega-skill -- Don't remove Model preference line (coordinator needs it for model selection) -- Don't touch `.squad/decisions.md` during reskill -- Don't remove the tagline blockquote -- it's the charter's soul in one line diff --git a/.copilot/skills/session-recovery/SKILL.md b/.copilot/skills/session-recovery/SKILL.md deleted file mode 100644 index 8217e290..00000000 --- a/.copilot/skills/session-recovery/SKILL.md +++ /dev/null @@ -1,155 +0,0 @@ ---- -name: "session-recovery" -description: "Find and resume interrupted Copilot CLI sessions using session_store queries" -domain: "workflow-recovery" -confidence: "high" -source: "earned" -tools: - - name: "sql" - description: "Query session_store database for past session history" - when: "Always -- session_store is the source of truth for session history" ---- - -## Context - -Squad agents run in Copilot CLI sessions that can be interrupted -- terminal crashes, network drops, machine restarts, or accidental window closes. When this happens, in-progress work may be left in a partially-completed state: branches with uncommitted changes, issues marked in-progress with no active agent, or checkpoints that were never finalized. - -Copilot CLI stores session history in a SQLite database called `session_store` (read-only, accessed via the `sql` tool with `database: "session_store"`). This skill teaches agents how to query that store to detect interrupted sessions and resume work. - -## Patterns - -### 1. Find Recent Sessions - -Query the `sessions` table filtered by time window. Include the last checkpoint to understand where the session stopped: - -```sql -SELECT - s.id, - s.summary, - s.cwd, - s.branch, - s.updated_at, - (SELECT title FROM checkpoints - WHERE session_id = s.id - ORDER BY checkpoint_number DESC LIMIT 1) AS last_checkpoint -FROM sessions s -WHERE s.updated_at >= datetime('now', '-24 hours') -ORDER BY s.updated_at DESC; -``` - -### 2. Filter Out Automated Sessions - -Automated agents (monitors, keep-alive, heartbeat) create high-volume sessions that obscure human-initiated work. Exclude them: - -```sql -SELECT s.id, s.summary, s.cwd, s.updated_at, - (SELECT title FROM checkpoints - WHERE session_id = s.id - ORDER BY checkpoint_number DESC LIMIT 1) AS last_checkpoint -FROM sessions s -WHERE s.updated_at >= datetime('now', '-24 hours') - AND s.id NOT IN ( - SELECT DISTINCT t.session_id FROM turns t - WHERE t.turn_index = 0 - AND (LOWER(t.user_message) LIKE '%keep-alive%' - OR LOWER(t.user_message) LIKE '%heartbeat%') - ) -ORDER BY s.updated_at DESC; -``` - -### 3. Search by Topic (FTS5) - -Use the `search_index` FTS5 table for keyword search. Expand queries with synonyms since this is keyword-based, not semantic: - -```sql -SELECT DISTINCT s.id, s.summary, s.cwd, s.updated_at -FROM search_index si -JOIN sessions s ON si.session_id = s.id -WHERE search_index MATCH 'auth OR login OR token OR JWT' - AND s.updated_at >= datetime('now', '-48 hours') -ORDER BY s.updated_at DESC -LIMIT 10; -``` - -### 4. Search by Working Directory - -```sql -SELECT s.id, s.summary, s.updated_at, - (SELECT title FROM checkpoints - WHERE session_id = s.id - ORDER BY checkpoint_number DESC LIMIT 1) AS last_checkpoint -FROM sessions s -WHERE s.cwd LIKE '%my-project%' - AND s.updated_at >= datetime('now', '-48 hours') -ORDER BY s.updated_at DESC; -``` - -### 5. Get Full Session Context Before Resuming - -Before resuming, inspect what the session was doing: - -```sql --- Conversation turns -SELECT turn_index, substr(user_message, 1, 200) AS ask, timestamp -FROM turns WHERE session_id = 'SESSION_ID' ORDER BY turn_index; - --- Checkpoint progress -SELECT checkpoint_number, title, overview -FROM checkpoints WHERE session_id = 'SESSION_ID' ORDER BY checkpoint_number; - --- Files touched -SELECT file_path, tool_name -FROM session_files WHERE session_id = 'SESSION_ID'; - --- Linked PRs/issues/commits -SELECT ref_type, ref_value -FROM session_refs WHERE session_id = 'SESSION_ID'; -``` - -### 6. Detect Orphaned Issue Work - -Find sessions that were working on issues but may not have completed: - -```sql -SELECT DISTINCT s.id, s.branch, s.summary, s.updated_at, - sr.ref_type, sr.ref_value -FROM sessions s -JOIN session_refs sr ON s.id = sr.session_id -WHERE sr.ref_type = 'issue' - AND s.updated_at >= datetime('now', '-48 hours') -ORDER BY s.updated_at DESC; -``` - -Cross-reference with `gh issue list --label "status:in-progress"` to find issues that are marked in-progress but have no active session. - -### 7. Resume a Session - -Once you have the session ID: - -```bash -# Resume directly -copilot --resume SESSION_ID -``` - -## Examples - -**Recovering from a crash during PR creation:** -1. Query recent sessions filtered by branch name -2. Find the session that was working on the PR -3. Check its last checkpoint -- was the code committed? Was the PR created? -4. Resume or manually complete the remaining steps - -**Finding yesterday's work on a feature:** -1. Use FTS5 search with feature keywords -2. Filter to the relevant working directory -3. Review checkpoint progress to see how far the session got -4. Resume if work remains, or start fresh with the context - -## Anti-Patterns - -- [ ] Searching by partial session IDs -- always use full UUIDs -- [ ] Resuming sessions that completed successfully -- they have no pending work -- [ ] Using `MATCH` with special characters without escaping -- wrap paths in double quotes -- [ ] Skipping the automated-session filter -- high-volume automated sessions will flood results -- [ ] Assuming FTS5 is semantic search -- it's keyword-based; always expand queries with synonyms -- [ ] Ignoring checkpoint data -- checkpoints show exactly where the session stopped diff --git a/.copilot/skills/windows-compatibility/SKILL.md b/.copilot/skills/windows-compatibility/SKILL.md index 55285b00..2d115e04 100644 --- a/.copilot/skills/windows-compatibility/SKILL.md +++ b/.copilot/skills/windows-compatibility/SKILL.md @@ -8,7 +8,7 @@ source: "earned (multiple Windows-specific bugs: colons in filenames, git -C fai ## Context -Squad runs on Windows, macOS, and Linux. Several bugs have been traced to platform-specific assumptions: ISO timestamps with colons (illegal on Windows), `git -C` with Windows paths (unreliable), forward-slash paths in Node.js on Windows. +This project runs on Windows, macOS, and Linux. Several bugs have been traced to platform-specific assumptions: ISO timestamps with colons (illegal on Windows), `git -C` with Windows paths (unreliable), forward-slash paths in Node.js on Windows. ## Patterns @@ -39,10 +39,10 @@ const safeTimestamp = () => new Date().toISOString().replace(/:/g, '-').split('. // Git workflow (PowerShell) cd $teamRoot -git add .squad/ +git add . if ($LASTEXITCODE -eq 0) { $msg = @" -docs(ai-team): session log +docs: session log Changes: - Added decisions @@ -57,10 +57,10 @@ Changes: ? **Incorrect:** ```javascript // Colon in filename -const logPath = `.squad/log/${new Date().toISOString()}.md`; // ILLEGAL on Windows +const logPath = `./log/${new Date().toISOString()}.md`; // ILLEGAL on Windows // git -C with Windows path -exec('git -C C:\\src\\squad add .squad/'); // UNRELIABLE +exec('git -C C:\\src\\project add .'); // UNRELIABLE // Inline newlines in commit message exec('git commit -m "First line\nSecond line"'); // FAILS silently in PowerShell diff --git a/.mailmap b/.mailmap index 1a274186..83628f29 100644 --- a/.mailmap +++ b/.mailmap @@ -13,7 +13,7 @@ Copilot <223556219+Copilot@users.noreply.github.com> Earl Tankard, Jr., Ph.D. <45021016+primetimetank21@users.noreply.github.com> Earl Tankard, Jr., Ph.D. <45021016+primetimetank21@users.noreply.github.com> primetimetank21 -# Squad agent fake-emails: these are Earl's local automation (no real GitHub +# Local automation identity mappings: these are Earl's local automation (no real GitHub # accounts). Attribute their commits/co-authorship to Earl. Earl Tankard, Jr., Ph.D. <45021016+primetimetank21@users.noreply.github.com> Jiminy Earl Tankard, Jr., Ph.D. <45021016+primetimetank21@users.noreply.github.com> Jiminy diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 61a1cc4c..25b2ecf3 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -1,7 +1,6 @@ # Architecture: dev-setup -> **Owner:** Mickey (Lead) -- Issue #3 -> **Last updated:** 2026-05-19 (Sprint 11 (formerly Sprint T) refresh -- closes #229) +> **Last updated:** 2026-05-19 --- @@ -23,8 +22,8 @@ Run `bash setup.sh` (Unix) or `powershell -File setup.ps1` (Windows) and walk aw dev-setup/ |---- setup.sh # Entry point -- Unix (Linux / macOS / WSL); thin router |---- setup.ps1 # Entry point -- Windows (PowerShell); thin router -|---- .tool-versions # asdf-style pinned versions (node, nvm, uv, gh, copilot-cli, squad-cli) -|---- .gitattributes # eol=lf for *.sh / *.md / *.yml; eol=crlf for *.ps1 / *.psm1 / *.psd1 (#231) +|---- .tool-versions # asdf-style pinned versions (node, nvm, uv, gh, copilot-cli) +|---- .gitattributes # eol=lf for *.sh / *.md / *.yml; eol=crlf for *.ps1 / *.psm1 / *.psd1 |---- ARCHITECTURE.md # This file |---- CHANGELOG.md # Keep-a-Changelog format |---- CONTRIBUTING.md # Contribution guide @@ -36,7 +35,7 @@ dev-setup/ | | `---- read-tool-version.sh # Same contract for POSIX shells (prints version to stdout) | | | |---- linux/ -| | |---- setup.sh # Core Linux/macOS/WSL installer (Donald) -- runs tools in order +| | |---- setup.sh # Core Linux/macOS/WSL installer -- runs tools in order | | |---- uninstall.sh # Idempotent reverse of the installer | | |---- lib/ | | | `---- log.sh # Shared log_info / log_ok / log_warn / log_error helpers @@ -45,7 +44,6 @@ dev-setup/ | | |---- copilot-cli.sh # Install GitHub Copilot CLI (pin from .tool-versions) | | |---- gh.sh # Install GitHub CLI (pin from .tool-versions) | | |---- nvm.sh # Install nvm + Node (pin from .tool-versions) -| | |---- squad-cli.sh # Install squad-cli (npm; pin from .tool-versions) | | |---- uv.sh # Install uv Python package manager (pin from .tool-versions) | | `---- zsh.sh # Install zsh + set as default shell | | @@ -55,8 +53,8 @@ dev-setup/ | |---- lib/ | | |---- logging.ps1 # Write-Info / Write-Ok / Write-Warn / Write-Err + Assert-LastExit | | `---- path.ps1 # Refresh-SessionPath -- re-reads Machine+User PATH from registry -| `---- tools/ # Per-tool installers (orchestrator + 10 modules; PR #195 split) -| |---- auth.ps1 # GitHub CLI authentication (interactive; moved from top-level in PR #297) +| `---- tools/ # Per-tool installers (orchestrator + 10 modules) +| |---- auth.ps1 # GitHub CLI authentication (interactive) | |---- copilot.ps1 # GitHub Copilot CLI (pin from .tool-versions) | |---- dotfiles.ps1 # Apply config/dotfiles/ on Windows | |---- gh.ps1 # GitHub CLI (pin from .tool-versions) @@ -64,12 +62,11 @@ dev-setup/ | |---- nvm.ps1 # nvm-windows + Node (pin from .tool-versions) | |---- profile.ps1 # PowerShell profile injection (PS 5.1 + PS 7+ paths) | |---- psmux.ps1 # psmux terminal multiplexer (Windows tmux alias) -| |---- squad-cli.ps1 # squad-cli (npm; pin from .tool-versions) | |---- uv.ps1 # uv Python package manager (pin from .tool-versions) | `---- vim.ps1 # Vim editor | |---- config/ -| `---- dotfiles/ # Dotfile templates (Pluto #8, #10, #11) +| `---- dotfiles/ # Dotfile templates | |---- .aliases # Shell aliases (git, dev, utility) | |---- .editorconfig # Editor formatting rules | |---- .gitconfig.template # Git config template @@ -80,8 +77,8 @@ dev-setup/ | `---- README.md # Documents each dotfile and install behaviour | |---- hooks/ # Git hooks; auto-wired via `git config core.hooksPath hooks` -| |---- pre-commit # Branch ancestry + ASCII *.ps1 guard + .squad path allow-list + shellcheck -| |---- prepare-commit-msg # Rewrite auto-merge/revert messages into Conventional Commits form (#212) +| |---- pre-commit # Branch ancestry + ASCII guard + shellcheck +| |---- prepare-commit-msg # Rewrite auto-merge/revert messages into Conventional Commits form | |---- commit-msg # Enforce Conventional Commits format (hard reject on non-conforming) | `---- pre-push # Block direct pushes to main; advisory shellcheck + PSScriptAnalyzer | @@ -103,28 +100,11 @@ dev-setup/ | `---- README.md # Dev container documentation | |---- .github/ -| `---- workflows/ # CI + squad automation (Chip) -- see "CI Workflows" below +| `---- workflows/ # CI workflows | |---- validate.yml # Main CI validation (6 jobs) | |---- e2e-install.yml # E2E smoke test on fresh runners (PR + nightly cron + summary) -| |---- squad-heartbeat.yml # Ralph -- reacts to issue/PR events to keep the loop alive -| |---- squad-history-check.yml # Enforce agent history.md updates on squad:* PRs -| |---- squad-issue-assign.yml # Trigger work when squad:{member} label applied -| |---- squad-label-enforce.yml # Mutual exclusivity for managed label namespaces -| |---- squad-triage.yml # Triage flow when bare `squad` label applied -| `---- sync-squad-labels.yml # Sync label set from .squad/team.md roster +| `---- sprint-end-labels.yml # Sprint label automation | -`---- .squad/ # Squad coordination (most subdirs are not "shipped" via npm; see CONTRIBUTING.md) - |---- agents/ # charter.md + history.md per agent (see Squad Roster) - |---- skills/ # Reusable SKILL.md library (tool-version-pin, pwsh-lastexitcode, ...) - |---- decisions/ # Canonical permanent decision records (committed) - | `---- inbox/ # Per-agent decision drafts (gitignored) - |---- retros/ # Sprint retrospectives - |---- templates/ # loop.md, ceremonies.md, agent + workflow templates - |---- orchestration-log/ # Per-sprint orchestration logs (union-merge) - |---- team.md # Squad roster definition (drives sync-squad-labels.yml) - |---- routing.md # Issue -> agent routing rules - |---- ceremonies.md # Sprint ceremony cadence - `---- decisions.md # Append-only decisions log (union-merge) ``` --- @@ -270,7 +250,7 @@ Reference implementations: `scripts/linux/tools/nvm.sh` and `scripts/windows/too | Safety | `Set-StrictMode -Version Latest` + `$ErrorActionPreference = 'Stop'` | | Idempotency | `Get-Command -ErrorAction SilentlyContinue` before installing | | Logging | Dot-source `scripts/windows/lib/logging.ps1`; call `Write-Info`, `Write-Ok`, `Write-Warn`, `Write-Err` | -| Exit-code discipline | After any external install, call `Assert-LastExit -ToolName ` (use `-AllowedExitCodes` for cases like winget `ALREADY_INSTALLED`); see `.squad/skills/pwsh-lastexitcode/SKILL.md` | +| Exit-code discipline | After any external install, call `Assert-LastExit -ToolName ` (use `-AllowedExitCodes` for cases like winget `ALREADY_INSTALLED`) | | PATH refresh | After an install mutates PATH, dot-source `scripts/windows/lib/path.ps1` and call `Refresh-SessionPath` so `node`, `uv`, `gh`, etc. become callable in the same session | | Version pinning | Read from `.tool-versions` via `Get-ToolVersion` (dot-source `scripts/lib/Read-ToolVersion.ps1`); never hard-code versions | | Install method | Prefer `winget`; fall back to `scoop` or direct download (see `nvm.ps1` for the portable-zip pattern) | @@ -306,7 +286,7 @@ Reference implementations: `scripts/linux/tools/nvm.sh` and `scripts/windows/too run_tool "toolname" ``` -3. **Create a companion GitHub issue** labeled `squad:donald` (if it's a new tool install). +3. **Create a companion GitHub issue** if it's a new tool install. --- @@ -324,17 +304,17 @@ Reference implementations: `scripts/linux/tools/nvm.sh` and `scripts/windows/too The tool scripts in `scripts/linux/tools/` must run in this order (enforced by `scripts/linux/setup.sh`): ``` -zsh -> uv -> nvm -> gh -> auth -> copilot-cli -> squad-cli +zsh -> uv -> nvm -> gh -> auth -> copilot-cli ``` -`copilot-cli` depends on `gh` being installed and (ideally) authenticated. The `auth` script handles interactive GitHub CLI authentication (issue #9). `squad-cli` depends on `nvm` (Node/npm). +`copilot-cli` depends on `gh` being installed and (ideally) authenticated. The `auth` script handles interactive GitHub CLI authentication. ### Windows orchestrator chain The Windows orchestrator `scripts/windows/setup.ps1` is a thin router: it dot-sources two shared libraries first (`lib/logging.ps1` -> `lib/path.ps1`), then dot-sources every per-tool module under `scripts/windows/tools/` so their `Install-*` functions are defined. Dot-source order does **not** drive dependencies -- the authoritative install order is the call sequence inside the `Main` function. The chain is fixed at: ``` -git -> uv -> nvm -> gh -> auth -> vim -> psmux -> copilot -> squad-cli -> dotfiles -> profile -> hooks +git -> uv -> nvm -> gh -> auth -> vim -> psmux -> copilot -> dotfiles -> profile -> hooks ``` Mapped to functions and the `tools/*.ps1` module that defines each: @@ -349,16 +329,14 @@ Mapped to functions and the `tools/*.ps1` module that defines each: | 6 | `Install-Vim` | `tools/vim.ps1` | (Linux: pre-installed / package manager) | | 7 | `Install-Psmux` | `tools/psmux.ps1` | (Linux: tmux already on PATH) | | 8 | `Install-CopilotCli` | `tools/copilot.ps1` | `tools/copilot-cli.sh` | -| 9 | `Install-SquadCli` | `tools/squad-cli.ps1` | `tools/squad-cli.sh` | -| 10 | `Install-Dotfiles` | `tools/dotfiles.ps1` | `config/dotfiles/install.sh` (driven from `tools/zsh.sh`) | -| 11 | `Write-PowerShellProfile`| `tools/profile.ps1` | (Linux: shell-rc work folded into `tools/zsh.sh`) | -| 12 | `Install-GitHook` | inline in `setup.ps1` | `git config core.hooksPath hooks` (same contract) | +| 9 | `Install-Dotfiles` | `tools/dotfiles.ps1` | `config/dotfiles/install.sh` (driven from `tools/zsh.sh`) | +| 10 | `Write-PowerShellProfile`| `tools/profile.ps1` | (Linux: shell-rc work folded into `tools/zsh.sh`) | +| 11 | `Install-GitHook` | inline in `setup.ps1` | `git config core.hooksPath hooks` (same contract) | Cross-platform invariants preserved from the Linux chain above: - `auth` (interactive `gh auth login`) runs after `gh` so the CLI is on PATH when the prompt fires. - `copilot` runs after `auth` so the install can detect an authenticated `gh` session. -- `squad-cli` runs after `nvm` because the install path is `npm i -g @bradygaster/squad-cli` and needs Node on PATH. Windows-only additions vs. the Linux chain: @@ -367,8 +345,6 @@ Windows-only additions vs. the Linux chain: - `dotfiles` + `profile` are Windows-specific finalizers: the Linux side rolls equivalent shell-rc work into `tools/zsh.sh` plus `config/dotfiles/install.sh`, but Windows needs a discrete PowerShell profile injection step (PS 5.1 + PS 7+ profile paths) after the dotfile templates are applied. - `Install-GitHook` is an inline function inside `setup.ps1` (not a separate `tools/*.ps1` module), wired last so `core.hooksPath=hooks` is set only after the working tree is in its final state. -History: the per-tool layout under `scripts/windows/tools/` was introduced in PR #195 (split out from a monolithic `setup.ps1`); `auth.ps1` moved from `scripts/windows/` into `tools/` in PR #297, and the call site in `Main` was updated at the same time. The chain documented above is current as of Sprint 12. - --- ## Idempotency Guarantee @@ -394,9 +370,7 @@ Tool versions are pinned in the repo-root [`.tool-versions`](./.tool-versions) f - `scripts/lib/Read-ToolVersion.ps1` -- exposes `Get-ToolVersion -Name ` (PowerShell) - `scripts/lib/read-tool-version.sh` -- same contract for POSIX shells (prints to stdout) -Currently pinned: `nodejs`, `nvm`, `nvm-windows`, `uv`, `copilot-cli`, `squad-cli`, `gh`. Tool installers (e.g. `scripts/windows/tools/nvm.ps1`, `scripts/linux/tools/uv.sh`) call the library at install time so version bumps are a single-file edit. See `.squad/skills/tool-version-pin/SKILL.md` for the pattern. - -Companion skill: `.squad/skills/pwsh-lastexitcode/SKILL.md` -- the `$LASTEXITCODE = 0` reset pattern required when chaining native commands across pwsh `&` script-call boundaries (CI gating discipline). +Currently pinned: `nodejs`, `nvm`, `nvm-windows`, `uv`, `copilot-cli`, `gh`. Tool installers (e.g. `scripts/windows/tools/nvm.ps1`, `scripts/linux/tools/uv.sh`) call the library at install time so version bumps are a single-file edit. --- @@ -406,25 +380,23 @@ Hooks live in [`hooks/`](./hooks) and are wired automatically by the installers | Hook | Role | |------|------| -| `pre-commit` | Branch-ancestry guard (`squad/*` must descend from `develop`), ASCII-only enforcement for staged `*.ps1`, `.squad/` path allow-list (incl. `decisions/*.md`, `retros/*.md`, and `templates/*.template`), refusal to commit on `develop`/`main`/`master`, shellcheck on staged `*.sh` | -| `prepare-commit-msg` | Rewrites git auto-generated `Merge ...` and `Revert "..."` messages into Conventional Commits form so `commit-msg` accepts them (added in #212) | +| `pre-commit` | Branch-ancestry guard (feature branches must descend from `develop`), ASCII-only enforcement for staged `*.ps1`, refusal to commit on `develop`/`main`/`master`, shellcheck on staged `*.sh` | +| `prepare-commit-msg` | Rewrites git auto-generated `Merge ...` and `Revert "..."` messages into Conventional Commits form so `commit-msg` accepts them | | `commit-msg` | Enforces Conventional Commits format (`type(scope): description`). Hard reject on non-conforming. | | `pre-push` | Blocks direct pushes to `main`; runs shellcheck on changed `*.sh` (advisory) and PSScriptAnalyzer on changed `*.ps1` (advisory) | -The pre-commit allow-list is the canonical source of truth for which paths under `.squad/` may be staged. See `hooks/pre-commit` Check 3 for the full table. - --- ## CI Workflows -All workflows live in [`.github/workflows/`](./.github/workflows). Owned by Chip. +All workflows live in [`.github/workflows/`](./.github/workflows). ### `validate.yml` -- main CI gate (6 jobs) | Job | Runner | Purpose | |-----|--------|---------| | `validate-linux` | `ubuntu-latest` | Run `setup.sh`, assert zsh/uv/nvm/node/gh, idempotency re-run, alias unit + parity tests | -| `validate-macos` | `macos-latest` | Same shape as `validate-linux` + tool-version pin tests (added Sprint 10 (formerly Sprint S)) | +| `validate-macos` | `macos-latest` | Same shape as `validate-linux` + tool-version pin tests | | `lint-shell-scripts` | `ubuntu-latest` | shellcheck across `setup.sh`, `scripts/linux/**`, `config/dotfiles/.aliases` | | `lint-powershell` | `ubuntu-latest` (pwsh) | PSScriptAnalyzer across `setup.ps1` + `scripts/windows/setup.ps1` | | `validate-powershell` | `windows-latest` | `Remove-CustomItem` regression + git-hooks tests under PS 7 | @@ -434,84 +406,9 @@ All workflows live in [`.github/workflows/`](./.github/workflows). Owned by Chip | Job | Runner | Purpose | |-----|--------|---------| -| `e2e-linux` | `ubuntu-latest` | Run `setup.sh` on a fresh runner; assert every tool is reachable from a login shell; `squad --version` regression for `session persistence may fail` warning (#255) | +| `e2e-linux` | `ubuntu-latest` | Run `setup.sh` on a fresh runner; assert every tool is reachable from a login shell | | `e2e-macos` | `macos-latest` | Same shape as `e2e-linux` | | `e2e-windows` | `windows-latest` | Run `setup.ps1`; PowerShell + winget path | -| `summary` | `ubuntu-latest` | Aggregates the three platform results (`needs: [...]`, `if: always()`) and fails the workflow if any platform failed (added in #253) | +| `summary` | `ubuntu-latest` | Aggregates the three platform results (`needs: [...]`, `if: always()`) and fails the workflow if any platform failed | Initially `continue-on-error: true` per platform job; the `summary` job is the single fail-gate. Triggers: `pull_request`, nightly `cron: 0 4 * * *`, and `workflow_dispatch`. - -### Squad automation (Chip + Ralph) - -| Workflow | Trigger | Purpose | -|----------|---------|---------| -| `squad-heartbeat.yml` | `issues` (closed/labeled), `pull_request` (closed), manual | Ralph -- react to completed work / new squad work to keep the loop alive | -| `squad-history-check.yml` | `pull_request` to `develop`/`main` | Enforce `agents/{name}/history.md` updates when a `squad:*` label is present | -| `squad-issue-assign.yml` | `issues` (labeled with `squad:{member}`) | Drop the "Assigned to {Member}" instructional comment | -| `squad-label-enforce.yml` | `issues` (labeled) | Enforce mutual exclusivity for `go:`, `release:`, `type:`, `priority:` namespaces | -| `squad-triage.yml` | `issues` (labeled `squad`) | Lead-agent triage on bare `squad` label | -| `sync-squad-labels.yml` | push to `.squad/team.md`, manual | Sync GitHub labels to match the roster | - ---- - -## Squad Roster - -The squad lives under [`.squad/agents/`](./.squad/agents) -- each agent owns a directory with `charter.md` (identity, boundaries, voice) and `history.md` (append-only work log). - -**Core engineering agents (own code / tests / config):** - -| Agent | Role | Owns | -|-------|------|------| -| Mickey | Lead | Architecture, code review, scope decisions, triage | -| Donald | Linux/macOS engineer | `scripts/linux/`, POSIX tool installers | -| Goofy | Windows engineer | `scripts/windows/`, hooks | -| Chip | Test / CI engineer | `tests/`, `.github/workflows/`, `.devcontainer/` | -| Pluto | Dotfiles & shell config | `config/dotfiles/` | - -**Role-based agents (own process / quality / history):** - -| Agent | Role | Trigger | -|-------|------|---------| -| Doc | Fact-checker | review/verify/fact-check/audit keywords; writes from a dedicated worktree per sprint (see `.squad/decisions/doc-and-jiminy-automation.md`) | -| Jiminy | Conscience / auditor | post-batch audit gate after multi-agent batches (>=3 agents); enforced by `.squad/templates/loop.md` and `.squad/templates/ceremonies.md` | -| Scribe | History & changelog steward | Sprint wrap fold of `history.md` and `CHANGELOG.md` curation | -| Ralph | Heartbeat | Runs as `squad-heartbeat.yml` workflow on issue/PR events; not a human-facing agent | - -Permanent cross-agent decisions live in `.squad/decisions/*.md` (e.g., `doc-and-jiminy-automation.md`, `mickey-architecture-entry-point.md`, `pluto-dotfiles.md`). Drafts land in `.squad/decisions/inbox/` (gitignored) before being promoted. - ---- - -## Team Ownership Map - -| Path | Owner | Issue(s) | -|------|-------|----------| -| `setup.sh` (root) | Mickey | #3 | -| `setup.ps1` (root) | Mickey | #3 | -| `.tool-versions` | Mickey | Sprint 10 | -| `scripts/lib/` | Mickey | Sprint 10 | -| `scripts/linux/setup.sh` | Donald | #1 | -| `scripts/linux/lib/log.sh` | Donald | -- | -| `scripts/linux/uninstall.sh` | Donald | -- | -| `scripts/linux/tools/auth.sh` | Donald | #9 | -| `scripts/linux/tools/zsh.sh` | Donald | #4 | -| `scripts/linux/tools/uv.sh` | Donald | #5 | -| `scripts/linux/tools/nvm.sh` | Donald | #6 | -| `scripts/linux/tools/gh.sh` | Donald | #7 | -| `scripts/linux/tools/copilot-cli.sh` | Donald | #7 | -| `scripts/linux/tools/squad-cli.sh` | Donald | -- | -| `scripts/windows/setup.ps1` | Goofy | #2, #195 | -| `scripts/windows/lib/` | Goofy | #195 | -| `scripts/windows/tools/` | Goofy | #195 | -| `scripts/windows/tools/auth.ps1` | Goofy | #2 | -| `scripts/windows/uninstall.ps1` | Goofy | -- | -| `hooks/pre-commit` | Goofy | #138 | -| `hooks/prepare-commit-msg` | Goofy | #212 | -| `hooks/commit-msg` | Goofy | #138 | -| `hooks/pre-push` | Goofy | #138, #147 | -| `tests/` | Chip | -- | -| `config/dotfiles/` | Pluto | #8, #10, #11 | -| `.devcontainer/` | Chip | -- | -| `.github/workflows/` | Chip | #12, #13, #253 | -| `.squad/agents/` | Each agent owns their own directory | -- | -| `.squad/skills/` | Authoring agent (Mickey reviews) | -- | -| `.squad/decisions/` | Mickey (curator) | -- | diff --git a/CHANGELOG.md b/CHANGELOG.md index 619f65ad..055d1c42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,49 +9,51 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- `gosquad` alias - launches GitHub Copilot CLI with the Squad agent (`copilot --agent squad --yolo`), available on bash/zsh and Windows PowerShell. - ### Changed ### Fixed ### Removed +- Embedded squad infrastructure: removed `.squad/` directory and all internal AI-agent coordination tooling from the repository. Project coordination now handled externally. (PR #475, PR #477) +- Branch naming convention changed from `squad/{issue}-{slug}` to `feat|fix|chore|docs|refactor/{issue}-{slug}` (conventional commit prefixes). +- Removed all `squad-cli` tool references from documentation and examples. + ## [0.9.8] - 2026-05-19 ### Added -- Mandatory hygiene tail template (`.squad/templates/spawn-prompt-hygiene.md`) and `routing.md` section enforcing 6-item discipline at every spawn (#397/#401) +- Mandatory hygiene tail template and routing.md section enforcing 6-item discipline at every spawn (#397/#401) - Two new hygiene skills formalized: `history-md-pre-size-check` and `changelog-fold-completeness` (#398/#399/#402) - Sprint label vocabulary standardized: `sprint:17`, `sprint:18`, `release:shipped-0.9.7` introduced and backfilled across S17 work (#400/#403) - Test G in `tests/test_sprint_end_labels.ps1` -- CRLF regression coverage via function-override shim (#403) -- Sprint 18 decision archive at `.squad/decisions/sprint-18.md` (#408) +- Sprint 18 decision archive (#408) ### Changed - `scripts/sprint-end-labels.sh`: combined `gh issue list` + `gh pr list` queries (issue-list silently excludes PRs); pipe jq through `tr -d '\r'` for Windows CRLF safety (#403) -- `.squad/agents/donald/history.md`, `.squad/agents/ralph/history.md`, `.squad/agents/scribe/history.md` compressed under 15360 B gate per `history-md-pre-size-check` SKILL (#404) +- Agent history files compressed under 15360 B gate per `history-md-pre-size-check` SKILL (#404) ### Fixed - `gh issue list --search` PR-exclusion bug in sprint-end-labels automation -- script now correctly processes both issues and PRs (#403) - Windows jq CRLF idempotency-guard bypass in sprint-end-labels script -- already-labeled items no longer re-labeled on subsequent runs (#403) -- Sprint 18 attribution trail gaps via fixup PRs #406 (Pluto) + #407 (Donald) +- Sprint 18 attribution trail gaps via fixup PRs #406, #407 ## [0.9.7] - 2026-05-17 -- Sprint 17: Hygiene gate restoration + label automation + skill formalization ### Added -- Sprint-end label automation: `scripts/sprint-end-labels.sh` + `.github/workflows/sprint-end-labels.yml`. Applies `release:shipped-X.Y.Z` and removes `release:backlog` across all issues/PRs carrying a given sprint label. Hard-verifies every label op via re-query with 3-retry exponential backoff (1s, 2s, 4s). Dry-run mode (`--dry-run`) for safe rehearsals. Type/area/squad/priority labels are never touched. Covered by `tests/test_sprint_end_labels.ps1` (6 tests, including happy-path and fail-loudly retry scenarios). New skill `.squad/skills/gh-label-verify-retry/SKILL.md` formalizes the write-then-verify-then-retry pattern. (#382) +- Sprint-end label automation: `scripts/sprint-end-labels.sh` + `.github/workflows/sprint-end-labels.yml`. Applies `release:shipped-X.Y.Z` and removes `release:backlog` across all issues/PRs carrying a given sprint label. Hard-verifies every label op via re-query with 3-retry exponential backoff (1s, 2s, 4s). Dry-run mode (`--dry-run`) for safe rehearsals. Type/area/priority labels are never touched. Covered by `tests/test_sprint_end_labels.ps1` (6 tests, including happy-path and fail-loudly retry scenarios). New skill formalizes the write-then-verify-then-retry pattern. (#382) - New skills formalized: `gh-pr-base-develop` (high-conf, --base develop enforcement pattern), `worktree-remove-first` (medium-conf, worktree-remove-before-merge quirk), `gh-label-verify-retry` (high-conf, write-then-verify-then-retry pattern). (#383, #384, #382) -- Per-sprint decisions sub-folders introduced: `sprint-12.md` and `sprint-15.md` added under `.squad/decisions/`; sub-folder policy documented. (#371) +- Per-sprint decisions sub-folders introduced: `sprint-12.md` and `sprint-15.md` added; sub-folder policy documented. (#371) ### Changed -- README refresh: expanded 8-agent roster, updated hooks list, hygiene gates, and skill ecosystem pointer for v0.9.6 state. (#381) +- README refresh: expanded roster, updated hooks list, hygiene gates, and skill ecosystem pointer for v0.9.6 state. (#381) - `decisions.md` restructured to current-sprint-only live file; gate restored from 65737 B to 7228 B. (#371) - `routing.md`: spawn-prompt hygiene section added. (#384) ### Fixed -- `.gitignore` em-dash artifact removed (hand-off slip caught by Jiminy Sprint 17 audit). (#390) +- `.gitignore` em-dash artifact removed. (#390) ### Removed @@ -59,13 +61,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Sprint 16 skill drift watchlist audit at `.squad/decisions/pluto-skill-drift-2026-05-17.md`. (#367) -- New `.copilot/skills/ascii-docs-about-non-ascii/SKILL.md` formalizing the "self-documenting non-ASCII" discipline (medium confidence, 2 observations across Sprint 14 #340 and Sprint 15 #356/#359). (#362) -- New `.copilot/skills/worktree-base-refresh/SKILL.md` formalizing the stale-sprint-branch recovery pattern from Sprint 15 #359 (low confidence, 1 observation). (#364) +- Sprint 16 skill drift watchlist audit documented. (#367) +- New skill formalizing the "self-documenting non-ASCII" discipline (medium confidence). (#362) +- New skill formalizing the stale-sprint-branch recovery pattern (low confidence). (#364) ### Changed -- Decisions ledger archival pass -- 1 stale entry (2025-07-14) moved to .squad/decisions-archive.md. Hard gate (51200 B) not met mid-sprint; follow-up #371 filed for policy review. (#363) +- Decisions ledger archival pass -- 1 stale entry moved to archive. Hard gate (51200 B) not met mid-sprint; follow-up #371 filed for policy review. (#363) - Tag prefix sanity check -- 14/14 tags conform to bare X.Y.Z convention, no drift. (#365) ### Fixed @@ -75,12 +77,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [0.9.5] - 2026-05-17 -- Sprint 15: Legacy non-ASCII sweep + Sprint number normalization ### Added -- Sprint 14 retro at .squad/retros/2026-05-17-sprint-14-retro.md (retroactive Sprint 14 artifact, folded into 0.9.5) -- Doc canonical decision record at .squad/decisions/doc-356-ascii-sweep.md documenting #356 sweep scope, methodology, and conversion mapping table (#359) +- Sprint 14 retro (retroactive Sprint 14 artifact, folded into 0.9.5) +- Canonical decision record documenting sweep scope, methodology, and conversion mapping table (#359) ### Changed - Normalized historical Sprint letter references (Sprint R/S/T) to numbers (Sprint 11/12/13) in CHANGELOG.md historical entries for consistency with current Sprint NN numbering (#355). -- Swept legacy non-ASCII characters (em-dashes, smart quotes, box-drawing) from 33 tracked .md files (.copilot/skills/, ARCHITECTURE.md, tests/README.md, .github/agents/squad.agent.md); ~1250 non-ASCII bytes removed (#356). +- Swept legacy non-ASCII characters (em-dashes, smart quotes, box-drawing) from 33 tracked .md files; ~1250 non-ASCII bytes removed (#356). - history-compression skill: confidence medium -> high (8+ applications in Sprint 14) - per-topic-inbox-routing skill: confidence medium -> high (7+ applications in Sprint 14) @@ -91,13 +93,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [0.9.4] - 2026-05-17 ### Added -- history-compression skill formalized at confidence: medium -- 4-step heuristic (front-matter verbatim / current-sprint verbatim / older to dated bullets / preserve refs), 13 KB target with 2 KB headroom under the 15360 B hard gate (#340) +- history-compression skill formalized at confidence: medium -- 4-step heuristic, 13 KB target with 2 KB headroom under the 15360 B hard gate (#340) - per-topic inbox routing skill formalized at confidence: medium -- routing decision tree, atomic-rm model, dual-model coexistence with chronological journal (#341) ### Changed - README refreshed: pre-commit 6-check description (F1), ascii-sweep.py docs (F2), file-tree hand-converted to ASCII (F3), file-tree updated (F4), pre-commit one-liner expanded (F5) (#342) - Label taxonomy slimmed from 45 to 32 labels (drop 8 GitHub-default duplicates, 4 stale release version labels, 1 lonely status label; rename area:linux/macos/windows -> platform:*) (#347) -- sync-squad-labels.yml: add priority:p3 + platform:* to managed labels, remove dead hasCopilot code (#350) +- Label sync workflow: add priority:p3 + platform:* to managed labels, remove dead hasCopilot code (#350) ### Fixed @@ -106,15 +108,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [0.9.3] - 2026-05-17 -- Sprint 13: Documentation accuracy and ASCII policy hardening ### Added -- squad: skill formalizing the worktree-remove-FIRST PR merge pattern; documents the gh CLI quirk and proven 5-of-5 workaround (#317) -- `.squad/retros/2026-05-17-sprint-13-retro.md`: Sprint 13 retrospective (#339; folded retroactively into 0.9.3 -- PR merged after tag; see `.squad/decisions/changelog-retro-placement.md`) +- Skill formalizing the worktree-remove-FIRST PR merge pattern; documents the gh CLI quirk and proven 5-of-5 workaround (#317) +- Sprint 13 retrospective (#339; folded retroactively into 0.9.3 -- PR merged after tag) ### Changed - docs: ASCII-sweep all repo Markdown files (em-dash, arrows, smart quotes, box-drawing) per repo policy (#322 part A) -- squad: compress 8 over-gate agent history.md files per Scribe HARD GATE (#319) -- squad: fold Sprint 13 Wave 1 hygiene drops into per-topic decisions and re-compress jiminy/history.md back under 15KB gate -- squad: fold Sprint 13 Wave 2 hygiene drops into per-topic decisions and re-compress 4 over-gate agent history.md files +- Compress over-gate agent history.md files per HARD GATE (#319) +- Fold Sprint 13 Wave 1 hygiene drops into per-topic decisions and re-compress history files +- Fold Sprint 13 Wave 2 hygiene drops into per-topic decisions and re-compress 4 over-gate agent history.md files ### Fixed - docs(architecture): correct stale top-level path for auth.ps1; reflects post-PR #297 move to tools/ (#325) @@ -130,13 +132,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - CONTRIBUTING.md `Test Harness Pattern` section: documents the `set -uo` (intentionally NOT `set -euo`) convention for bash tests; failure tally pattern, helper conventions, minimal skeleton (closes #237) ### Changed -- README.md: refreshed to reflect Sprints 8-12 changes (auth.ps1 path move, .tool-versions pinning, expanded squad roster, decisions/retros workflow, numeric sprint naming convention, ARCH/CONTRIB cross-references) (closes #306) +- README.md: refreshed to reflect Sprints 8-12 changes (auth.ps1 path move, .tool-versions pinning, expanded roster, decisions/retros workflow, numeric sprint naming convention, ARCH/CONTRIB cross-references) (closes #306) - ARCHITECTURE.md: documented Windows orchestrator dependency order chain; mirrors the Linux Dependency Order section for parallel install flow visibility (closes #310) - ARCHITECTURE.md: rewrote `Script Conventions` section to point at `scripts/{linux,windows}/lib/` as source of truth; documents `source` / dot-source loading + `Read-ToolVersion.ps1` parser pattern (closes #309) - Sprint naming convention standardized to numeric format: Sprint 8-hotfix, Sprint 9, Sprint 10, Sprint 11; next = Sprint 12. Tier 3 full sweep across 21 files (~170 refs). Retro files renamed with `git mv`. Historical sprint letter references removed in favor of numeric format for consistency. CONTRIBUTING.md "Sprint Naming Convention" section updated with current numeric convention. - `.aliases`: added header marking the file as bash/zsh-only (not POSIX); documents non-POSIX features in use and intended loading pattern (closes #236) -- `.squad/decisions.md`: drained 4 Wave 2 inbox drops (mickey #310, donald #237, goofy #235, jiminy audit); folded staged history modifications (goofy, jiminy); archive gate crossed (57 KB >= 50 KB) but no entries eligible for 7-day cut (oldest live entry 2026-05-14, 3 days old) -- (Sprint 12 Wave 2 fold) -- `.squad/retros/2026-05-17-sprint-12-retro.md`: new Sprint 12 retrospective (3 waves, 10 PRs, 9 issues closed, worktree-isolation + ASCII-scope lessons learned) +- Decision log drained and restructured; archive gate crossed (57 KB >= 50 KB) but no entries eligible for 7-day cut (Sprint 12 Wave 2 fold) +- Sprint 12 retrospective (3 waves, 10 PRs, 9 issues closed, worktree-isolation + ASCII-scope lessons learned) ### Removed - Legacy GitHub labels `priority: high`, `priority: medium`, `priority: low` (with spaces) deleted; canonical taxonomy is now `priority:p0..p3` (closes #254) @@ -147,70 +149,69 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `scripts/windows/tools/auth.ps1` + `scripts/windows/setup.ps1`: applied `$LASTEXITCODE` reset mitigation at 5 sites; eliminates spurious failure detection when callers check exit codes downstream (closes #292) ### Added -- `.squad/skills/pwsh-lastexitcode/SKILL.md`: documents the `$LASTEXITCODE` propagation gotcha across pwsh `&` script-call boundaries; canonical fix is `$global:LASTEXITCODE = 0` after expected-failure commands (closes #288, surfaced by #277) +- PowerShell exit-code discipline skill: documents the `$LASTEXITCODE` propagation gotcha across pwsh `&` script-call boundaries; canonical fix is `$global:LASTEXITCODE = 0` after expected-failure commands (closes #288, surfaced by #277) - CONTRIBUTING.md "PowerShell Exit Code Discipline" section referencing the new skill -- `.squad/decisions/doc-and-jiminy-automation.md`: decision record codifying the post-batch Jiminy audit gate and the Doc subagent worktree pattern (closes #289, #290) -- `.squad/retros/2026-05-17-sprint-11-retro.md`: Sprint 11 retrospective - first sprint exercising the #293 SOPs (Jiminy gates fired clean, Doc worktree not triggered); 6 PRs merged, sequential Goofy pattern validated, Group EE static-source tests added +- Decision record codifying the post-batch audit gate and the subagent worktree pattern (closes #289, #290) +- Sprint 11 retrospective - first sprint exercising the SOPs (gates fired clean, worktree not triggered); 6 PRs merged, sequential pattern validated, Group EE static-source tests added ### Changed -- `scripts/windows/auth.ps1` moved to `scripts/windows/tools/auth.ps1` for consistency with the per-tool layout introduced in #195; all callers updated (closes #230) -- ARCHITECTURE.md: refreshed file trees + agent/skill rosters + hook + CI layout to reflect Sprint 8-hotfix through Sprint 10 changes (`prepare-commit-msg`, per-tool Windows layout, `.tool-versions` pin-driven install, Doc + Jiminy agents, `.squad/decisions/`) (closes #229) -- `hooks/pre-push`: documented advisory-only intent of the PSScriptAnalyzer step with an inline comment block at the top of the PSSA section; clarifies that PSSA findings warn but do not block, explains the three reasons (availability gap, subjective rules, out-of-scope hardening), and flags `|| true` as load-bearing (closes #233) -- `CONTRIBUTING.md` "Why is PSSA advisory in `pre-push`?" subsection under Git Hooks: codifies the advisory model for contributors so the `|| true` in `hooks/pre-push` is not incorrectly "fixed" away (closes #233) -- `.squad/templates/loop.md`, `.squad/templates/ceremonies.md`, and Doc/Jiminy charters: codify post-batch Jiminy audit gate + Doc subagent worktree pattern; eliminates the dual-fold-PR overhead of Sprint 10 (closes #289, #290) -- `CONTRIBUTING.md` "Squad Operational Gates (Coordinator dispatch)" section -- human-facing summary of the Doc worktree + Jiminy auto-dispatch SOPs -- `hooks/pre-commit` Source of Truth allow-list extended to include canonical `.squad/decisions/*.md` files (top-level decisions directory, distinct from the gitignored `inbox/` subdir). Required so permanent decision records like `.squad/decisions/doc-and-jiminy-automation.md` are commit-eligible. +- `scripts/windows/auth.ps1` moved to `scripts/windows/tools/auth.ps1` for consistency with the per-tool layout; all callers updated (closes #230) +- ARCHITECTURE.md: refreshed file trees + rosters + hook + CI layout to reflect Sprint 8-hotfix through Sprint 10 changes (closes #229) +- `hooks/pre-push`: documented advisory-only intent of the PSScriptAnalyzer step with an inline comment block at the top of the PSSA section; clarifies that PSSA findings warn but do not block (closes #233) +- `CONTRIBUTING.md` "Why is PSSA advisory in `pre-push`?" subsection under Git Hooks: codifies the advisory model for contributors (closes #233) +- Templates and charters: codify post-batch audit gate + subagent worktree pattern; eliminates the dual-fold-PR overhead of Sprint 10 (closes #289, #290) +- `CONTRIBUTING.md` operational gates section -- human-facing summary of the worktree + auto-dispatch SOPs +- `hooks/pre-commit` Source of Truth allow-list extended to include canonical decision files (top-level decisions directory, distinct from the gitignored `inbox/` subdir). - Sprint 11 end-of-session cleanup: no straggler branches/worktrees ## [0.9.0] - 2026-05-17 -- Sprint 9 + Sprint 10: Hygiene backlog and tool-version pin sweep ### Added -- `tests/test_nvm_bootstrap.sh` T6-T9: static source checks verifying that squad-cli and copilot-cli scripts read pins from `.tool-versions` and perform version-aware idempotency (closes #255) -- `tests/test_nvm_bootstrap.sh` T10-T11: regression sentinel asserting `@bradygaster/squad-cli` is the installed package and that `squad --version` captures stderr so the "session persistence may fail" warning is surfaced in CI (closes #255) -- `tests/test_windows_setup.ps1` Group DD (DD-1 through DD-5): version-pin validation for Windows squad-cli, copilot, and gh installers (closes #255) -- `tests/test_windows_setup.ps1` Group X -- behavioral tests for pre-commit (ASCII check, rogue .squad/ path) and pre-push (main guard, feature-branch allow, advisory exit-code) hooks (closes #224) -- `tests/test_windows_setup.ps1` Group Z -- coverage for `-Encoding ASCII` enforcement in `profile.ps1` and `uninstall.ps1` (closes #234) -- `tests/test_precommit_hygiene.sh` extended with pre-push section -- 5 bash scenarios covering direct-to-main rejection and advisory exit-code (closes #224) -- `.squad/skills/tool-version-pin/SKILL.md`: documents the bare-idempotency anti-pattern and the canonical version-pin solution -- `.copilot/skills/error-recovery/SKILL.md` -- new generic error-recovery skill -- `.squad/skills/squad-upgrade-hygiene/SKILL.md` -- reusable checklist for auditing future `squad upgrade` runs -- Doc (Fact Checker) joins the squad -- new agent addressing the verifier/validator gap from Sprint 8-hotfix retro. Auto-triggers on `review`/`verify`/`fact-check`/`audit` tasks; produces verification reports with confidence ratings (Verified/Unverified/Contradicted/Needs Investigation). Charter: `.squad/agents/doc/charter.md`. -- `.github/workflows/squad-label-enforce.yml` -- enforces mutual exclusivity for `go:`, `release:`, `type:`, `priority:` label groups -- `.squad/templates/{fact-checker-charter.md, loop.md, squad.agent.md.template}` -- new templates from 0.9.4 -- `hooks/pre-commit` now allows `.squad/templates/*.template` files (squad upgrade ships `squad.agent.md.template`); allow-list extended to permit `.squad/retros/*.md` so session retros can be committed -- `.squad/agents/ralph/charter.md` "Develop Commit Ban" section -- documents that Ralph (and all agents) cannot commit directly to `develop`/`main`/`master`; EOS history entries flow through short-lived branch+PR or Scribe drain process (closes #273) -- CONTRIBUTING.md "Group Letter Assignment" section -- coordinator pre-assigns test group letters to prevent parallel-agent collisions; Sprint 9 example documented (closes #273) -- CONTRIBUTING.md "CHANGELOG Conflict Strategy" section -- documents mechanical resolution for predictable [Unreleased] conflicts when multiple PRs land in one sprint: merge order, unique headers, union entries (closes #273) +- Static source checks verifying that scripts read pins from `.tool-versions` and perform version-aware idempotency (closes #255) +- Regression sentinel asserting correct package is installed and that version output captures stderr (closes #255) +- Version-pin validation for Windows installers (closes #255) +- Behavioral tests for pre-commit (ASCII check) and pre-push (main guard, feature-branch allow, advisory exit-code) hooks (closes #224) +- Coverage for `-Encoding ASCII` enforcement in Windows profile and uninstall scripts (closes #234) +- Bash scenarios covering direct-to-main rejection and advisory exit-code (closes #224) +- Skill documenting the bare-idempotency anti-pattern and the canonical version-pin solution +- New generic error-recovery skill +- Reusable checklist for auditing future upgrade runs +- Fact checker agent joins the project -- addresses the verifier/validator gap. Auto-triggers on `review`/`verify`/`fact-check`/`audit` tasks; produces verification reports with confidence ratings. +- Label enforcement workflow for mutual exclusivity +- New templates from governance upgrade +- `hooks/pre-commit` now allows template files; allow-list extended to permit retro files so session retros can be committed +- "Develop Commit Ban" section -- documents that agents cannot commit directly to `develop`/`main`/`master`; EOS history entries flow through short-lived branch+PR or drain process (closes #273) +- CONTRIBUTING.md "Group Letter Assignment" section -- coordinator pre-assigns test group letters to prevent parallel-agent collisions; example documented (closes #273) +- CONTRIBUTING.md "CHANGELOG Conflict Strategy" section -- documents mechanical resolution for predictable [Unreleased] conflicts (closes #273) - CONTRIBUTING.md "Tool Version Pin Enforcement" section -- documents the version-pin workflow and the npm-package validation step (closes #255) -- `.gitignore` now ignores `*.tgz` tarballs so squad upgrade artifacts cannot accidentally land in commits; Jiminy charter documents the new dispatch SOP +- `.gitignore` now ignores `*.tgz` tarballs so upgrade artifacts cannot accidentally land in commits ### Changed -- `.tool-versions`: added `squad-cli 0.9.4` and `gh 2.92.0` pins; corrected `copilot-cli` from stale `0.0.339` to `1.0.48` (`@github/copilot` npm package) -- `scripts/windows/tools/squad-cli.ps1`, `copilot.ps1`, `gh.ps1`: now dot-source `Read-ToolVersion.ps1` to resolve pinned version at runtime -- `scripts/windows/tools/profile.ps1` and `scripts/windows/uninstall.ps1`: added `-Encoding ASCII` to all `Set-Content` and `Add-Content` calls. Prevents encoding mismatch between PS 5.1 (UTF-16LE BOM default) and PS 7 (UTF-8 BOM default) (closes #234). +- `.tool-versions`: added tool pins and version corrections +- Windows tools now dot-source `Read-ToolVersion.ps1` to resolve pinned version at runtime +- Windows profile and uninstall scripts: added `-Encoding ASCII` to all `Set-Content` and `Add-Content` calls. Prevents encoding mismatch between PS 5.1 (UTF-16LE BOM default) and PS 7 (UTF-8 BOM default) (closes #234). - `.gitattributes` now pins `*.ps1`, `*.psm1`, and `*.psd1` files to explicit CRLF line endings, eliminating platform divergence when `core.autocrlf` is enabled (closes #231). -- `setup.sh` and `scripts/linux/uninstall.sh` now source `scripts/linux/lib/log.sh` instead of defining their own logging helpers. Local `log_*` / `ok` / `info` / `skip` definitions removed; all call sites updated to the canonical `log_ok` / `log_info` / `log_warn` / `log_error` names (closes #223). -- Documentation: README + CONTRIBUTING now document the automatic `core.hooksPath` setup performed by `setup.sh` and `setup.ps1`. Replaced stale "install hooks manually" instruction. Added branch-from-develop validation note per Sprint 8-hotfix retro (closes #228). -- Squad governance upgraded from 0.9.1 to 0.9.4 (dispatch mechanism, `CURRENT_DATETIME` requirement, `name` param in spawn prompts, default models bumped to `claude-sonnet-4.6` / `gpt-5.3-codex`, tier-based agent timeout policy) -- `.github/workflows/squad-heartbeat.yml` removes noisy cron trigger; Ralph now fires on issue events only -- `.github/workflows/squad-triage.yml` and `sync-squad-labels.yml` add `slugify()` for label names (bugfix) -- Dotfile backup strategy: `.bak` files are now timestamped (`.bak.YYYYMMDD-HHMMSS`) on both Linux (`config/dotfiles/install.sh`) and Windows (`scripts/windows/tools/dotfiles.ps1`). Keeps last 5 backups by default (override with `DOTFILE_BACKUP_KEEP` env var); previous versions of dotfiles are no longer lost on re-run (closes #227). +- `setup.sh` and `scripts/linux/uninstall.sh` now source `scripts/linux/lib/log.sh` instead of defining their own logging helpers. Local definitions removed; all call sites updated to the canonical names (closes #223). +- Documentation: README + CONTRIBUTING now document the automatic `core.hooksPath` setup performed by setup scripts. Replaced stale "install hooks manually" instruction. Added branch-from-develop validation note. +- Governance upgraded (dispatch mechanism, CURRENT_DATETIME requirement, name param in spawn prompts, default models bumped, tier-based agent timeout policy) +- Workflows: heartbeat removes noisy cron trigger; label workflows add `slugify()` for label names (bugfix) +- Dotfile backup strategy: `.bak` files are now timestamped (`.bak.YYYYMMDD-HHMMSS`) on both Linux and Windows. Keeps last 5 backups by default (override with `DOTFILE_BACKUP_KEEP` env var) (closes #227). ### Fixed -- `scripts/linux/tools/squad-cli.sh`, `scripts/windows/tools/squad-cli.ps1`: replace bare `command -v squad` idempotency guard with version-aware check; installs pinned version via `npm install -g @bradygaster/squad-cli@`; upgrades silently if installed version drifts from pin (closes #255) -- `scripts/linux/tools/copilot-cli.sh`, `scripts/windows/tools/copilot.ps1`: replace bare binary-exists guard with version-aware check; switch install package from deprecated `@githubnext/github-copilot-cli` to `@github/copilot`; Windows switches from winget (wrong product) to npm for consistency with Linux; pin corrected from stale `0.0.339` (opaque curl-installer version) to `1.0.48` (closes #255) -- `scripts/linux/tools/gh.sh`: Linux now downloads pinned release tarball from GitHub releases instead of `apt-get install -y gh` (latest); macOS logs WARN if brew installs a version other than the pin (brew versioned formulae not available for gh) (closes #255) -- `scripts/windows/tools/gh.ps1`: passes `--version $GhVersion` to winget so runner cache cannot silently use an older gh (closes #255) -- `scripts/linux/uninstall.sh` and `scripts/windows/uninstall.ps1` now run `git config --unset-all core.hooksPath` during uninstall (LOCAL scope, matching setup) so git falls back to per-repo `.git/hooks` defaults instead of pointing at the (now-deleted) dev-setup hooks dir; Windows path resets `$global:LASTEXITCODE = 0` after the unset so the expected non-zero exit when no hookspath is configured no longer fails the uninstall step (closes #271). -- `.github/workflows/e2e-install.yml` -- adds a final `summary` job that fails the workflow if any platform job fails, preventing silent green-dashboard regressions. Per-platform jobs still use `continue-on-error: true` so full matrix telemetry is preserved (closes #253). -- `scripts/linux/tools/squad-cli.sh` -- investigated 'session persistence may fail' warning (#255). Root cause: `@github/copilot-sdk` (transitive dep) attempts node:sqlite session storage on startup; on environments without write access to HOME, it emits this warning. Verified absent in squad-cli 0.9.4 `--version` path. Added regression guard: `e2e-install.yml` now captures `squad --version` output and fails if the warning appears. Static installer tests (`test_nvm_bootstrap.sh` T10-T11) verify correct package name and stderr capture. -- `scripts/windows/tools/*.ps1` -- winget install calls now assert `$LASTEXITCODE` and surface failures to `setup.ps1` (closes #226). 7 install sites previously swallowed non-zero exits silently. -- `.github/workflows/e2e-install.yml` -- bash `-lc` step bodies now use YAML doubled-single-quote escapes for embedded apostrophes; previously, an inner `'session persistence may fail'` could terminate the wrapping single-quoted YAML scalar mid-string. +- Tool install scripts: replace bare binary-exists guard with version-aware check; installs pinned version; upgrades silently if installed version drifts from pin (closes #255) +- Copilot CLI: switch install package from deprecated package to current; Windows switches from winget (wrong product) to npm for consistency with Linux; pin corrected (closes #255) +- Linux gh installer: now downloads pinned release tarball from GitHub releases instead of `apt-get install -y gh` (latest); macOS logs WARN if brew installs a version other than the pin (brew versioned formulae not available for gh) (closes #255) +- Windows gh installer: passes `--version $GhVersion` to winget so runner cache cannot silently use an older gh (closes #255) +- Uninstall scripts now run `git config --unset-all core.hooksPath` during uninstall (LOCAL scope) so git falls back to per-repo `.git/hooks` defaults; Windows path resets `$global:LASTEXITCODE = 0` after the unset (closes #271). +- E2E workflow -- adds a final `summary` job that fails the workflow if any platform job fails, preventing silent green-dashboard regressions. Per-platform jobs still use `continue-on-error: true` so full matrix telemetry is preserved (closes #253). +- Tool CLI warnings investigated; verified absent in version check path. Added regression guard: e2e workflow now captures version output and fails if warning appears. Static installer tests verify correct package name and stderr capture. +- Windows tool installers -- winget install calls now assert `$LASTEXITCODE` and surface failures (closes #226). 7 install sites previously swallowed non-zero exits silently. +- E2E workflow -- bash step bodies now use YAML doubled-single-quote escapes for embedded apostrophes. ## [0.8.0] - 2026-05-16 -- Sprint 8 + Sprint 8-hotfix (formerly Sprint Q): Gap audit refactor and install regression P0s ### Added -- Pre-commit hygiene checks: ASCII-only enforcement on staged `.ps1` files, rogue `.squad/` path validation, staged inbox file detection, and branch ancestry verification for squad branches (closes #240) +- Pre-commit hygiene checks: ASCII-only enforcement on staged `.ps1` files, path validation, staged file detection, and branch ancestry verification for feature branches (closes #240) - `tests/test_precommit_hygiene.sh` -- bash tests for all 4 pre-commit hygiene checks (13 pass/fail cases) - E2E install smoke test workflow `.github/workflows/e2e-install.yml` with 3-OS matrix (Linux, macOS, Windows) -- exercises full setup, tool assertions, idempotency, and uninstall on fresh runners (closes #239) - Triggers: per-PR, nightly cron (04:00 UTC), manual workflow_dispatch @@ -238,28 +239,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `prepare-commit-msg` hook that rewrites git auto-generated merge/revert messages into Conventional Commits form (#212) ### Changed -- Shared logging helpers extracted to `scripts/linux/lib/log.sh` and `scripts/windows/lib/logging.ps1` (closes #186) +- Shared logging helpers extracted to dedicated lib files (closes #186) - Support for `merge` type in commit-msg hook type allowlist (#212) -- `squad-cli` install failure is now a loud error with actionable hints (was silent warning) -- `scripts/linux/tools/nvm.sh` installs pinned Node version from `.tool-versions` (was `--lts`) -- `scripts/linux/tools/nvm.sh` reads nvm version from `.tool-versions` instead of fetching latest -- `scripts/linux/tools/uv.sh` reads uv version from `.tool-versions` instead of fetching latest -- `scripts/linux/tools/copilot-cli.sh` reads copilot-cli version from `.tool-versions` -- `scripts/windows/tools/nvm.ps1` reads nvm version from `.tool-versions` +- Tool install failure is now a loud error with actionable hints (was silent warning) +- Tool installers read versions from `.tool-versions` instead of fetching latest - Made tmux auto-attach opt-in via `TMUX_AUTOSTART=1` env var (was always-on) - Refreshed ARCHITECTURE.md and README.md file trees to match current repo layout - commit-msg no longer needs special-case bypass for merge/revert -- prepare-commit-msg now normalizes them (#212) ### Fixed -- Windows: session PATH now refreshed after every `winget install` so just-installed binaries (nvm, git, gh, vim, copilot, psmux) resolve immediately without restarting the terminal; preserves session-only PATH entries (e.g., GitHub Actions tool-cache, profile-injected paths) (closes #251) -- Windows nvm install switched from winget+nvm-setup.exe to portable nvm-noinstall.zip download (deterministic, no installer race); replaces Wait-ForNvmInstall polling with Install-NvmPortable + Set-NvmEnvironment (#251) -- Pinned Node.js version bumped from 20.11.0 to 22.11.0 in `.tool-versions` to satisfy `squad-cli` engine requirement (`>=22.5.0`); added `nvm alias default` so fresh shells inherit the pinned version; affects Linux, macOS, and Windows setup paths (fixes #252, related #255) +- Windows: session PATH now refreshed after every `winget install` so just-installed binaries resolve immediately without restarting the terminal; preserves session-only PATH entries (closes #251) +- Windows nvm install switched from winget+nvm-setup.exe to portable nvm-noinstall.zip download (deterministic, no installer race); replaces polling with Install-NvmPortable + Set-NvmEnvironment (#251) +- Pinned Node.js version bumped to satisfy engine requirement; added `nvm alias default` so fresh shells inherit the pinned version; affects Linux, macOS, and Windows setup paths (fixes #252, related #255) - E2E install workflow: added Node major version assertion (>=22) to Linux and macOS fresh-shell steps to prevent future regressions (#252) - CI: Added nvm + Node.js validation step to validate-macos job, aligning with validate-linux (closes #225) - Pre-commit hook now refuses commits directly on `develop`, `main`, or `master` with a clear error message directing the user to create a feature branch (closes #249) -- `scripts/windows/tools/nvm.ps1` resolved wrong lib path (one level up instead of two); `Read-ToolVersion.ps1` not found at runtime (closes #221) +- Windows nvm installer resolved wrong lib path; `Read-ToolVersion.ps1` not found at runtime (closes #221) - Added runtime assertion in `nvm.ps1` to catch missing lib directory early -- PS 5.1 compat: psmux install skip-with-warning + profile write diagnostics (PR #198) +- PS 5.1 compat: psmux install skip-with-warning + profile write diagnostics ## [0.7.0] - 2026-04-25 -- Sprint 7: Hooks, psmux, and profile hardening @@ -286,15 +283,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [0.6.0] - 2026-04-18 -- Sprint 6: Tools and CI hardening ### Added -- Vim install via winget on Windows (PR #112) -- Tmux added to system prerequisites (PR #84) -- `va` alias to edit `~/.aliases` in vim (PR #86) -- GitHub issue templates (PR #114) -- Missing aliases added to PowerShell profile (PR #115) -- PS 5.1 validation CI job on Windows runner (PR #116) -- `squad-cli` global install in Windows and Linux setup (PR #118) -- Windows regression tests: PS5 compat, profile idempotency, Copilot CLI install (PR #104) -- Direct-push-to-main override policy documented (PR #117) +- Vim install via winget on Windows +- Tmux added to system prerequisites +- `va` alias to edit `~/.aliases` in vim +- GitHub issue templates +- Missing aliases added to PowerShell profile +- PS 5.1 validation CI job on Windows runner +- Global tool install in Windows and Linux setup +- Windows regression tests: PS5 compat, profile idempotency, Copilot CLI install +- Direct-push-to-main override policy documented ### Fixed - Copilot CLI: remove conflicting `gh` alias before extension install (PR #63) @@ -308,8 +305,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `Test-Path` variable guards for PS 6+ auto-vars in `setup.ps1` (PR #130) ### Changed -- Sprint wrap: ban squash merges in favor of regular merge commits (PR #100) -- Removed log/orchestration-log from git tracking (PR #101) +- Sprint wrap: ban squash merges in favor of regular merge commits +- Removed log/orchestration-log from git tracking ## [0.5.0] - 2026-04-08 -- Sprint 5: Process stabilization @@ -326,11 +323,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [0.4.0] - 2026-04-07 -- Sprint 4: Branch protection and testing ### Added -- Branch protection enabled on `develop` with documented merge gates (PR #47) -- Mickey approval gate enforced and documented in Ralph spec (PR #48) -- Regression test for `Remove-CustomItem` multi-argument behavior (PR #52) -- Test coverage for `create_tmux()` session detection logic (PR #53) -- `uv` replaces `pip` for Python tooling in devcontainer (PR #50) +- Branch protection enabled on `develop` with documented merge gates +- Approval gate enforced and documented +- Regression test for `Remove-CustomItem` multi-argument behavior +- Test coverage for `create_tmux()` session detection logic +- `uv` replaces `pip` for Python tooling in devcontainer ### Fixed - `create_tmux()`: named session check corrected, dead variable removed (PR #39) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3c103893..e893bb39 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ # Contributing to dev-setup -Welcome! This repo is maintained by the **Disney Classic Squad** -- a team of specialized AI agents each owning a slice of the codebase. Human contributors are equally welcome. This guide explains how to work alongside the squad. +Welcome! This guide explains the contribution workflow for dev-setup. --- @@ -17,20 +17,22 @@ The branch protection rule has `enforce_admins` intentionally **disabled**. Why? All work happens on a dedicated branch. **Never commit directly to `develop` or `main`.** ``` -squad/{issue-number}-{kebab-slug} +{type}/{issue-number}-{kebab-slug} ``` +Where `type` is one of: `feat`, `fix`, `chore`, `docs`, `refactor`. + **Examples:** -- `squad/42-add-nvm-install` -- `squad/17-fix-zsh-detection` -- `squad/8-dotfile-editorconfig` +- `feat/42-add-nvm-install` +- `fix/17-fix-zsh-detection` +- `chore/8-dotfile-editorconfig` Base branch is **always `develop`**: ```bash git checkout develop git pull origin develop -git checkout -b squad/{issue-number}-{slug} +git checkout -b {type}/{issue-number}-{slug} ``` --- @@ -42,14 +44,12 @@ git checkout -b squad/{issue-number}-{slug} ```bash git checkout develop git pull origin develop -git checkout -b squad/{issue-number}-{slug} +git checkout -b {type}/{issue-number}-{slug} ``` -**Never fork a squad branch from another squad branch.** Branching from a peer's branch pulls in their unmerged commits, inflating your PR diff and making review harder. If you see commits in your PR that don't belong to your issue, your branch was not forked from `develop`. - -> This rule exists because "branch ancestry bleed" occurred 3 times in Sprint 6. Every time it's violated, PR review quality degrades. +**Never fork a feature branch from another feature branch.** Branching from a peer's branch pulls in their unmerged commits, inflating your PR diff and making review harder. If you see commits in your PR that don't belong to your issue, your branch was not forked from `develop`. -**All squad branches MUST be cut from `develop`, not `main`.** The pre-commit hook validates this: if you commit to a `squad/*` branch that is not an ancestor of `develop`, the hook warns that you may have accidentally forked from `main` or another squad branch. To fix: `git rebase develop` before pushing. +**All feature branches MUST be cut from `develop`, not `main`.** The pre-commit hook validates this: if you commit to a feature branch that is not an ancestor of `develop`, the hook warns that you may have accidentally forked from `main` or another feature branch. To fix: `git rebase develop` before pushing. --- @@ -63,7 +63,7 @@ Before opening a pull request, confirm all of the following: - [ ] CI is green before requesting review - [ ] Commit messages follow conventional commits - [ ] One issue per PR -- [ ] Mickey approval required before merge +- [ ] Approval from a maintainer required before merge --- @@ -94,10 +94,9 @@ Keep the summary under 72 characters. Add a body if the change needs more contex ## Code Review -- **Mickey** is the lead reviewer -- all PRs require Mickey's approval before merge. +- All PRs require approval from a maintainer before merge. - **CI must be green** before requesting review. Do not ask for review on a failing PR. -- Reviewers may request changes or reassign work to a different squad member. -- If Mickey rejects a PR, a *different* agent (not the original author) will be assigned to revise. +- Reviewers may request changes or reassign work to a different contributor. --- @@ -198,13 +197,13 @@ A direct push to `main` is permitted ONLY when ALL of the following conditions a 1. A critical regression or broken state is on `main` that blocks users 2. The fix is small, surgical, and fully understood (not exploratory) 3. `develop` itself is broken or the PR pipeline cannot be expedited -4. The repo owner (Earl Tankard) explicitly authorizes the override in session +4. The repo owner explicitly authorizes the override in session **Required audit trail:** - Commit message must include `[hotfix-override]` annotation -- A squad decision record must be written to `.squad/decisions/inbox/` documenting: what was pushed, why, and who authorized -- The override must be referenced in the next sprint retro +- The override must be documented in decision records or session notes +- The override must be referenced in the next sprint retro if applicable **Reference:** The 2026-04-18 hotfix session (PS 5.x `$MyInvocation.MyCommand.Path` regression) is the canonical example of an authorized override. @@ -212,15 +211,15 @@ A direct push to `main` is permitted ONLY when ALL of the following conditions a --- -## Parallel Agent Work +## Parallel Work ### Why worktree isolation matters -In Sprint 4, two Chip agents ran simultaneously on issues #41 and #43, both sharing the same git working tree. Chip-issue-43 checked out `squad/43` while Chip-issue-41 was mid-commit on a different branch. The result: wrong content landed on the wrong branch, and PR #51 had to be closed and recreated. This is a classic branch-checkout race condition. +In past work, concurrent development on different issues in the same working tree caused branch-checkout race conditions. Wrong content landed on the wrong branch, and PRs had to be closed and recreated. ### How to enable it -Set `SQUAD_WORKTREES=1` before starting any Squad session where parallel work is expected: +Set `SQUAD_WORKTREES=1` before starting work where parallel development is expected: ```bash export SQUAD_WORKTREES=1 @@ -228,7 +227,7 @@ export SQUAD_WORKTREES=1 Or add it permanently to your `.env` / shell profile. The devcontainer sets it by default in `remoteEnv`. -When enabled, the Squad coordinator creates an isolated `git worktree` for each issue before handing control to the agent. Branch checkouts inside one worktree never affect any other. +When enabled, coordinators or automation can create an isolated `git worktree` for each issue. Branch checkouts inside one worktree never affect any other. ### Worktree path convention @@ -254,10 +253,6 @@ git worktree remove /workspaces/dev-setup-56 Or list all active worktrees with `git worktree list`. -### Merging Squad PRs from worktrees - -When merging a Squad PR that was developed in a worktree, follow the worktree-remove-FIRST pattern (remove the worktree and delete the local branch BEFORE `gh pr merge --delete-branch`). See `.squad/skills/worktree-remove-first/SKILL.md` for the five-step sequence and the gh CLI quirk it sidesteps. - --- ## Test Harness Pattern @@ -409,12 +404,8 @@ For the broader rationale around test isolation and second-run safety, see ## Group Letter Assignment (parallel test work) Behavioral tests in `tests/test_windows_setup.ps1` are organized by alphabetic groups -(Group A, B, ..., V, W, X, Y, Z, AA, BB, ...). When 2+ parallel agents may extend this -file in the same sprint, the **coordinator pre-assigns Group letters in each spawn prompt** -to prevent collisions. Sprint 9 (formerly Sprint R) example: Chip #267 picked "Group X" independently while -Goofy #268 also picked "Group X" - required a manual rename to Group Y during rebase. -Going forward, the coordinator's spawn checklist includes Group letter assignment for any -agent that may add tests to this file. +(Group A, B, ..., V, W, X, Y, Z, AA, BB, ...). When 2+ parallel contributors may extend this +file, group letters should be pre-assigned to prevent collisions during rebase. --- @@ -457,17 +448,12 @@ docs that used the letter names. - Next sprint after Sprint 11 = **Sprint 12** (NOT Sprint U). - CHANGELOG release headers MUST include `-- Sprint N: short-name` suffix (matches the 0.1.0-0.7.0 pattern). -- Retro file naming: `.squad/retros/YYYY-MM-DD-sprint-N-retro.md` (numeric). - Retro files renamed to numeric (e.g., `2026-05-16-sprint-8-hotfix-retro.md`). -- Out-of-cadence hotfix sprints (a la Sprint 8-hotfix) get a `-hotfix` suffix in retro filename: - `.squad/retros/YYYY-MM-DD-sprint-N-hotfix-retro.md` -- the version header attribution stays - on the numeric parent (e.g., `Sprint 8 + Sprint 8-hotfix`). +- Out-of-cadence hotfix sprints get a `-hotfix` suffix in retro filename. ### Why this matters -The letter scheme caused real confusion: Sprint 8 silently vanished from CHANGELOG headers -between 0.7.0 and 0.8.0, and the alphabet runs out. Numbers are unbounded and consistent -with the established 1-7 history. +Numbers are unbounded and consistent with established project history. --- @@ -568,58 +554,3 @@ variable). Optionally pair with `2>$null` or `2>&1 | Out-Null` to silence stderr noise. The trailing reset is load-bearing for any script invoked from a workflow `shell: pwsh` step via `& .\path\to\script.ps1` -- without it the GH Actions wrapper fails the step on the next inspection of `$LASTEXITCODE`. - -See `.squad/skills/pwsh-lastexitcode/SKILL.md` for the full pattern, a -detection checklist, and the call-site audit. The discovery PR is #277 -(`fix(uninstall): unset core.hooksPath`); the skill closes #288. - ---- - -## Squad Operational Gates (Coordinator dispatch) - -Two operational SOPs govern Coordinator-side spawn behavior. Both are codified at -three independent surfaces (charter + `.squad/templates/loop.md` + `.squad/templates/ceremonies.md`) -so a single forgotten checkpoint doesn't silently break the SOP. Source decision: -`.squad/decisions/doc-and-jiminy-automation.md` (closes #289, #290). - -### Doc subagent runs in a dedicated worktree (#289) - -Doc (Fact Checker) is a `general-purpose` subagent that inherits the Coordinator's -CWD by default. To prevent his `.squad/agents/doc/history.md` writes from landing -as `M` on `develop` in the primary worktree (Sprint 10 anti-pattern: required PRs -#281 + #283), Doc runs in a dedicated per-sprint worktree. - -**Sprint kickoff (Coordinator, one-time per sprint):** - -```bash -git worktree add ../dev-setup-doc -b squad/doc-history-sprint- -``` - -**Every Doc spawn prompt** MUST begin with an explicit CWD directive pointing at -`..\dev-setup-doc`. Doc commits + pushes after every fact-check. At sprint wrap, -the Coordinator opens ONE fold PR from `squad/doc-history-sprint-` into -`develop`. Target: 1 fold PR per sprint (down from 2 in Sprint 10). - -### Jiminy auto-dispatch after >= 3-agent batches and at session-end (#290) - -The Jiminy dispatch SOP from PR #280 (Coordinator MUST invoke Jiminy after every -3+ agent batch and at session-end) is now enforced at three surfaces: - -1. `.squad/agents/jiminy/charter.md` -> `Triggers` table (canonical). -2. `.squad/templates/loop.md` -> "Squad Operational Gates" (Gate 1 post-batch, Gate 2 session-end). -3. `.squad/templates/ceremonies.md` -> `Sprint Wrap` ceremony, step 1. - -**Trigger condition (Gate 1):** 3 or more agent spawns in a single Coordinator -turn, counted excluding Scribe (which runs silently in background by design). -**Action:** Spawn Jiminy BEFORE returning results to the user. Wait for -`Jiminy clear` or resolve the dirty report. - -**Trigger condition (Gate 2):** user signals session-end OR work queue empties -after a full sprint. **Action:** Jiminy full sweep; BLOCKS session close on dirty -state. Ralph runs after Jiminy for stale-branch cleanup. - -If you (Coordinator or human contributor) ever notice a >= 3-agent batch landed -without a Jiminy run, that is a Sprint Retro action item, not a one-off -self-correction. File a `retro-action` issue. - - diff --git a/README.md b/README.md index 6cc90253..9478c38e 100644 --- a/README.md +++ b/README.md @@ -121,15 +121,7 @@ dev-setup/ | +-- test_remove_custom_item.ps1 | \-- test_windows_setup.ps1 +-- .devcontainer/ -- Dev Container / Codespace configuration -+-- .github/workflows/ -- CI validation and squad automation -\-- .squad/ -- Squad coordination (committed; not installed onto end-user machines) - +-- agents/ -- per-agent charter.md + history.md (9 agents: Mickey, Donald, Goofy, Pluto, Chip, Jiminy, Doc, Scribe, Ralph) - +-- decisions.md -- append-only decision log (<= 50KB gate) - +-- decisions-archive.md -- historical decisions fold here when main >= 50KB - +-- skills/ -- formalized agent skills and patterns (15+ skills) - +-- routing.md -- work routing table + spawn-prompt hygiene rules - +-- retros/ -- sprint retrospectives (committed; pre-commit allow-listed) - \-- ... -- see ARCHITECTURE.md for the full breakdown ++-- .github/workflows/ -- CI validation and automation ``` Root entry points (`setup.sh`, `setup.ps1`) are thin routers -- they detect the OS and delegate to the appropriate script under `scripts/`. They install nothing themselves. @@ -198,12 +190,10 @@ No manual copying needed. After running setup, four hooks are active: Six ordered hygiene checks (fastest-first); the commit is blocked if any check fails: -1. **Branch ancestry** -- `squad/*` (and per-agent `mickey/*`, `goofy/*`, etc.) branches must descend from `develop`. Catches accidental forks-of-forks. -2. **ASCII-only content** on staged `.ps1`, `.md`, and `.sh` files. PS 5.1 on Windows uses CP1252, so non-ASCII bytes (em dashes, smart quotes, curly apostrophes) break string literals; Markdown and shell sources should also stay ASCII-clean for portable `grep`/`sed`/`diff`. If a `.md` file trips this check, run `python scripts/lib/ascii-sweep.py --dry-run` to preview fixes, then drop `--dry-run` to apply (see below). The sweep preserves fenced code blocks verbatim, so any non-ASCII inside ``` ... ``` must be cleaned by hand. Scope extended from `.ps1` only to `.ps1 + .md + .sh` in Sprint 13 (#322B / PR #334). -3. **Rogue path check** under `.squad/` -- only paths in the hook's allow-list (e.g. `.squad/agents/*/charter.md`, `.squad/decisions/*.md`, `.squad/retros/*.md`) may be staged. -4. **Staged inbox guard** -- rejects anything staged under `.squad/decisions/inbox/` (that directory is gitignored; staged content there indicates a `git add -f` accident). -5. **Refuse direct commits on `develop` / `main` / `master`** -- create a feature branch first. -6. **Shellcheck** on staged `.sh` files. Silently skipped if `shellcheck` is not installed. +1. **Branch ancestry** -- feature branches must descend from `develop`. Catches accidental forks-of-forks. +2. **ASCII-only content** on staged `.ps1`, `.md`, and `.sh` files. PS 5.1 on Windows uses CP1252, so non-ASCII bytes (em dashes, smart quotes, curly apostrophes) break string literals; Markdown and shell sources should also stay ASCII-clean for portable `grep`/`sed`/`diff`. If a `.md` file trips this check, run `python scripts/lib/ascii-sweep.py --dry-run` to preview fixes, then drop `--dry-run` to apply (see below). The sweep preserves fenced code blocks verbatim, so any non-ASCII inside ``` ... ``` must be cleaned by hand. +3. **Refuse direct commits on `develop` / `main` / `master`** -- create a feature branch first. +4. **Shellcheck** on staged `.sh` files. Silently skipped if `shellcheck` is not installed. ### `commit-msg` @@ -252,7 +242,6 @@ nvm 0.39.7 nvm-windows 1.2.2 uv 0.4.18 copilot-cli 1.0.48 -squad-cli 0.9.4 gh 2.92.0 ``` @@ -271,7 +260,7 @@ When a sprint wraps, every issue and PR carrying its `sprint:N` label needs the - remove `release:backlog` (if present) - add `release:shipped-X.Y.Z` (if missing) -Type, area, squad, and priority labels are never touched. +Type, area, and priority labels are never touched. This is done by `scripts/sprint-end-labels.sh` and the matching workflow `.github/workflows/sprint-end-labels.yml`. @@ -288,7 +277,7 @@ Remove `--dry-run` to apply changes. **Workflow trigger:** Actions tab -> "Sprint End Labels" -> Run workflow. Inputs: `sprint_label`, `release_label`, `dry_run` (defaults to `true`). -**Verification (HARD REQUIREMENT):** After every label op, the script re-queries the issue and asserts the desired state. On mismatch it retries the read (not the write) with exponential backoff -- 1s, 2s, 4s -- then fails loudly. The CLI's exit code alone is treated as necessary-but-not-sufficient. See `.squad/skills/gh-label-verify-retry/SKILL.md` for the pattern. +**Verification (HARD REQUIREMENT):** After every label op, the script re-queries the issue and asserts the desired state. On mismatch it retries the read (not the write) with exponential backoff -- 1s, 2s, 4s -- then fails loudly. The CLI's exit code alone is treated as necessary-but-not-sufficient. **Idempotent:** Safe to run twice. A second run finds no work to do. @@ -296,32 +285,17 @@ Remove `--dry-run` to apply changes. ## Contributing -This repo is maintained by the **dev-setup squad** -- a team of nine specialized AI agents, each owning a slice of the codebase: - -**Engineering:** Mickey (Lead/Architecture), Donald (Bash/Linux/macOS), Goofy (PowerShell/Windows), Pluto (Dotfiles/Config), Chip (Tests/CI) - -**Process & Hygiene:** Jiminy (Squad Ops Auditor), Doc (Fact-Checker), Scribe (Session Logger), Ralph (Work Queue Monitor) - -Human contributors are welcome too. - -### Squad Resources - -- [ARCHITECTURE.md](./ARCHITECTURE.md) -- full technical overview, OS detection logic, script conventions, Windows dependency order, team ownership map, and a guide for adding a new tool. -- [CONTRIBUTING.md](./CONTRIBUTING.md) -- contributor workflow: branch naming (`squad/{issue}-{slug}` from `develop`), PR checklist, Conventional Commits, test harness pattern, sprint naming convention. -- [CHANGELOG.md](./CHANGELOG.md) -- release history in Keep a Changelog format. Sprints use numeric naming (Sprint 1 through Sprint 16); historical letter-named sprints (Q, R, S, T) appear as `Sprint N (formerly Sprint X)` aliases for grep continuity. -- `.squad/routing.md` -- work routing table (who handles what by task type; spawn-prompt hygiene rules). -- `.squad/skills/` -- formalized agent skills and decision patterns (15+ skills covering architecture, testing, Unix/Windows patterns, and squad operations). -- `.squad/decisions.md` / `.squad/decisions-archive.md` -- team decision log (decisions.md keeps entries under 50KB gate; older entries fold to archive). -- `.squad/agents/` -- per-agent charter (role, scope, trigger rules), history.md (session logs, keeps entries under 15KB gate), and team timeout policy. -- `.squad/retros/` -- sprint retrospectives (v0.9.4+ format). - -### Squad Conventions +See [CONTRIBUTING.md](./CONTRIBUTING.md) for the full contributor workflow: branch naming (`feat|fix|chore|docs|refactor/{issue}-{slug}` from `develop`), PR checklist, Conventional Commits format, test harness pattern. All commits follow **Conventional Commits** format: `type(scope): description`. Valid types: `feat`, `fix`, `docs`, `style`, `refactor`, `test`, `chore`, `ci`, `build`, `perf`, `revert`. The `commit-msg` hook enforces this; `prepare-commit-msg` rewrite auto-converts merge/revert messages to Conventional Commits form. -PRs to `develop` use **squash merges** (feature PRs, sprint work). Release cuts from `develop` to `main` use **regular (non-squash) merges** for clean release lineage. +PRs to `develop` use **regular merge commits** (not squash) for full history preservation. Release cuts from `develop` to `main` also use regular merges. + +### Project Resources -All squad files (charters, histories, decisions, retros) are ASCII-only. Use `python scripts/lib/ascii-sweep.py` to auto-convert em dashes, smart quotes, and other Unicode punctuation to ASCII equivalents. +- [ARCHITECTURE.md](./ARCHITECTURE.md) -- full technical overview, OS detection logic, script conventions, Windows dependency order, and a guide for adding a new tool. +- [CONTRIBUTING.md](./CONTRIBUTING.md) -- contributor workflow, branch naming, PR checklist, test harness pattern. +- [CHANGELOG.md](./CHANGELOG.md) -- release history in Keep a Changelog format. --- diff --git a/config/dotfiles/.aliases b/config/dotfiles/.aliases index 1dc3d337..d2216477 100644 --- a/config/dotfiles/.aliases +++ b/config/dotfiles/.aliases @@ -136,6 +136,7 @@ alias ni='npm install' alias nr='npm run' alias nrd='npm run dev' alias nrt='npm run test' +alias gosquad='copilot --agent squad --yolo' # ── Functions ──────────────────────────────────────────────────────────────── diff --git a/config/dotfiles/.gitconfig.template b/config/dotfiles/.gitconfig.template index b3a6726a..6d68c34a 100644 --- a/config/dotfiles/.gitconfig.template +++ b/config/dotfiles/.gitconfig.template @@ -59,7 +59,7 @@ ui = auto [alias] - # Short-hands used by the whole squad + # Convenient git shortcuts co = checkout br = branch st = status -sb diff --git a/config/dotfiles/README.md b/config/dotfiles/README.md index 920206d5..75e446c1 100644 --- a/config/dotfiles/README.md +++ b/config/dotfiles/README.md @@ -1,8 +1,6 @@ # Dotfile Templates -Managed by **Pluto** (Config Engineer). These templates give every Dev Container -and Codespace a sensible, consistent environment right out of the box -- no -manual config required on day one. +These templates give every Dev Container and Codespace a sensible, consistent environment right out of the box -- no manual config required on day one. --- diff --git a/docs/plans/441-grill-chip-v2.md b/docs/plans/441-grill-chip-v2.md deleted file mode 100644 index 969f9836..00000000 --- a/docs/plans/441-grill-chip-v2.md +++ /dev/null @@ -1,129 +0,0 @@ -# Chip's Re-Grill -- Plan for #441 v2 -**Date:** 2026-05-27 -**Reviewer:** Chip (Tester) -**Verdict:** REVISE -**Author locked out:** Mickey, Goofy - ---- - -## Context - -v1 grill raised 27 issues (5 showstoppers). Mickey authored v2 addressing them. -This re-grill is scoped to: did the 5 showstoppers land? Plus any new holes v2 introduces. - ---- - -## v1 Showstopper Resolution Matrix - -| # | v1 Concern | Status | Evidence | -|---|-----------|--------|----------| -| 1 | Pester `$TestDrive` removed | RESOLVED | Section 3 Decision 2 explicitly drops Pester. "Adding Pester is scope creep for a single fix." No `$TestDrive` anywhere in the v2 test table. GG tests use `Invoke-HostQuery` mock only. | -| 2 | `$PROFILE = $path` read-only in PS7 | PARTIAL | GG tests never assign `$PROFILE` directly -- correct. BUT: existing C-2 (`test_windows_setup.ps1` line 238) and C-3 (line 261) still do `$PROFILE = $c2Profile` / `$PROFILE = $c3Profile`. v2 plan adds GG tests, does not fix C-2/C-3. These still throw in PS7+. The new tests are clean; the existing harness is not. | -| 3 | Legacy orphan cleanup coverage | PARTIAL | GG-4 added. It seeds ONE hardcoded path with the dev-setup block and asserts it is stripped. The algorithm loops over `$legacyPaths = @($ps51Fallback, $ps7Fallback)` -- two paths. GG-4 does not test the case where BOTH legacy paths carry orphaned blocks simultaneously. A loop-break bug after first match would pass GG-4 and silently leave the second orphan. One test is not sufficient coverage here. | -| 4 | Case-insensitive dedup (`Select-Object -Unique`) | RESOLVED | Algorithm changed to `Sort-Object { $_.ToLower() } -Unique`. Script-block key lowers both candidates to the same string; `-Unique` retains one. Returns the original (un-lowercased) path, which is correct for Windows. GG-3 asserts `$profilePaths.Count -eq 1`. This is technically sound. | -| 5 | Multi-line `$PROFILE` output | PARTIAL | GG-6 added. Mock returns `"banner\npath"`. Algorithm uses `Select-Object -First 1` -- this returns the BANNER, not the path. Assertion is only "Return value contains no newline." That assertion PASSES on "banner" (no newline present) even though the wrong value was extracted. Two defects: (a) algorithm direction is wrong (`-First 1` should be `-Last 1` -- `& powershell -Command '$PROFILE'` prints the path last; any banner precedes it); (b) assertion does not verify the return value equals the expected mock path. GG-6 as described would pass silently while the algorithm returns garbage. | - ---- - -## New Holes v2 Introduces - -**NH-1: GG-2 mock mechanism for `Get-Command` not described.** - -The plan says "Mock `Get-Command` returns `$null`" but provides no code showing how. In the -custom `Test-Scenario` harness (no Pester), shadowing the `Get-Command` built-in at the -right scope level is non-trivial. The correct approach -- pass a provably-absent exe name -(e.g., `'powershell-fake-notexist'`) so `Get-Command` naturally returns nothing -- is simple -and reliable, but it is NOT what the plan says. If an implementer tries to redefine -`function Get-Command { return $null }` in the test scope, dot-sourced production code in -the same scope may resolve the mock or the built-in depending on scope ordering, producing -unpredictable behavior. The plan must describe the mechanism explicitly. - -**NH-2: `Invoke-HostQuery` mock ordering is unspecified.** - -v2 mandates `Invoke-HostQuery` in production code (Section 3 Decision 4 -- correct). But the -test plan never shows HOW the test overrides it. In PowerShell, a function defined AFTER a -dot-source overwrites the earlier definition in the same scope. The test must: -1. Dot-source `profile.ps1` (defines the real `Invoke-HostQuery`) -2. Redefine `function Invoke-HostQuery { return $mockPath }` in the same scope AFTER dot-source - -If the mock is placed BEFORE the dot-source, the dot-source overwrites it and the real child -process is called. This subtlety is not documented. Risk: first implementer gets it backwards, -wonders why the mock is not active, and adds a workaround that breaks CI. - -**NH-3: Uninstall inlined resolver has zero test coverage.** - -Section 3 Decision 3 inlines the ~15-line resolver in `uninstall.ps1`. No GG test exercises -the uninstall code path. A copy-paste error in the inlined resolver (e.g., wrong fallback -path, missing `Select-Object -First 1`, wrong case comparison) means uninstall silently -removes the block from the HARDCODED path only, leaving the OneDrive block in place. The -acceptance criterion says "Uninstall removes block from all locations (resolved + legacy)" -- -there is no automated test verifying this. At minimum, one test should call the uninstall -logic (or the inlined resolver function) and assert that an OneDrive-path block is stripped. - -**NH-4 (restatement with specifics): GG-5 algorithm direction bug amplified by weak assertion.** - -This is an extension of showstopper #5. The algorithm bug (`-First 1` vs `-Last 1`) will -cause `Resolve-ProfilePath` to return a corporate banner string whenever a host emits any -console output before printing the path. The weak assertion (`no newline`) means GG-6 is a -false-green test -- it passes while the function is broken. This combination (wrong algorithm -+ wrong assertion) is worse than no test: it provides false confidence. - -Repro for the bug: define `Invoke-HostQuery` to return `"Microsoft Banner`n$mockPath"`. Call -`Resolve-ProfilePath`. Assert return value `eq $mockPath`. The current algorithm fails this -assertion; the current GG-6 assertion does not. - ---- - -## Summary of Remaining Showstoppers - -| # | Severity | Description | -|---|----------|-------------| -| 2 | Medium | C-2 and C-3 harness still use `$PROFILE = $path`; broken in PS7+ | -| 3 | Medium | GG-4 covers one-legacy-path only; dual-orphan loop not tested | -| 5 | High | GG-6 algorithm is directionally wrong (`-First 1`) AND assertion is too weak to catch it | - -Showstoppers 1 and 4 are fully resolved. Three of five remain at medium/high severity. - ---- - -## Required Fixes for v3 - -1. **GG-6 algorithm fix:** Change `Select-Object -First 1` to `Select-Object -Last 1` in - `Resolve-ProfilePath`. The path is the return value of `$PROFILE` and is always the last - line of output; any console banner precedes it. - -2. **GG-6 assertion fix:** Assert `$result -eq $expectedMockPath`, not just "no newline." - -3. **GG-4b: dual-orphan test.** Seed BOTH `$ps51Fallback` and `$ps7Fallback` with the - dev-setup block. Resolve to a different (OneDrive) path. Call `Write-PowerShellProfile`. - Assert neither legacy file contains the BEGIN marker. Assert only the OneDrive path does. - -4. **GG-2: describe mechanism.** Change "Mock `Get-Command` returns `$null`" to use an - absent exe name: `Resolve-ProfilePath -HostExe 'powershell-notexist' -FallbackPath $fb`. - No Get-Command shadowing required or desired. - -5. **Invoke-HostQuery mock ordering note.** Add a code comment in GG-1 showing dot-source - BEFORE mock definition. One line; prevents the most common implementer trap. - -6. **Uninstall test.** Add GG-7: dot-source the inlined uninstall resolver, create a block - at an OneDrive-style mock path, call the strip logic, assert block is gone. - (Or add to Group FF if the group letter scheme assigns uninstall tests there.) - -7. **C-2 / C-3 harness fix (deferred but required before PS7 CI).** These are pre-existing - but v2 does not fix them. Must be addressed before the PR can claim it "runs clean in PS7." - Suggested fix: replace `$PROFILE = $path` with `Invoke-HostQuery` mock pattern matching - what the new GG tests do, so all profile tests run cleanly on both 5.1 and 7+. - This fix is in scope because v2 explicitly acknowledges the PS7 read-only constraint. - ---- - -## Recommendation - -REVISE. Revision owner: Donald. -(Mickey locked out per grill-reviewer constraint. Goofy locked out as original v1 author. -Donald is the appropriate next implementer for targeted algorithm + test plan surgery.) - -The v2 plan is structurally sound -- Invoke-HostQuery seam, case-insensitive dedup, and the -Pester removal are all correct. The remaining work is small: one algorithm direction fix, -two test assertion fixes, two additional test cases, and one uninstall test. This is not a -full redesign. A v3 with just these six items resolves all showstoppers. diff --git a/docs/plans/441-grill-chip-v3.md b/docs/plans/441-grill-chip-v3.md deleted file mode 100644 index d0842399..00000000 --- a/docs/plans/441-grill-chip-v3.md +++ /dev/null @@ -1,234 +0,0 @@ -# Chip's Re-Grill -- Plan for #441 v3 -**Date:** 2026-05-27 -**Reviewer:** Chip (Tester) -**Verdict:** REVISE -**Author locked out:** Goofy, Mickey, Donald - ---- - -## Context - -v2 grill returned REVISE with 3 partial showstoppers (SS-2, SS-3, SS-5) and 4 new holes -(NH-1 through NH-4). Donald revised to v3. This re-grill is scoped to: - (a) did the 3 partials land? - (b) are the new findings from the task brief real? - ---- - -## v2 Partial-Resolution Status - -### SS-5 (GG-6 inverted) -- RESOLVED - -v2 defect: algorithm used `Select-Object -First 1`, returning the banner. Assertion checked -only "no newline" -- passed on wrong value (false-green). - -v3 fix: `-Last 1` with `-NoLogo`; assertion upgraded to `$result -eq $expectedMockPath`. - -Trace through the full pipeline: - Mock returns `"banner`n$mockPath"` - -> `$raw.Trim()` -- no leading/trailing whitespace; internal `\n` preserved: `"banner`n$mockPath"` - -> `-split '\r?\n'` -- produces array: `["banner", "$mockPath"]` - -> `Where-Object { $_ }` -- both non-empty, both kept - -> `Select-Object -Last 1` -- returns `"$mockPath"` (correct) - -> Assertion `$result -eq $mockPath` passes for the right reason - -Edge case: trailing newline in raw output (e.g., `"banner`n$mockPath`n"`): - `$raw.Trim()` strips the trailing `\n` on the full string first -> `"banner`n$mockPath"` - Same result. Handled. - -Edge case: path with trailing space before `\n` (e.g., `"banner`n$mockPath `n"`): - `Trim()` strips trailing space + newline from the whole string -> `"banner`n$mockPath"` - (The space after `$mockPath` is removed because it is the rightmost whitespace on the full - string.) Result is still `$mockPath`. Handled. - -No missed edge case found. SS-5 is RESOLVED. - ---- - -### SS-3 (Legacy cleanup dual orphan, GG-4) -- RESOLVED - -v2 defect: GG-4 seeded only ONE legacy path. A loop-break-after-first bug would pass. - -v3 fix (v3-D3): Section 5 table cell explicitly reads: - Input: "Seed BOTH `$ps51Fallback` AND `$ps7Fallback` with block; mock resolves to - OneDrive path" - Assertion: "Neither legacy file has BEGIN marker; OneDrive file has BEGIN marker" - -The dual assertion is sufficient: if the cleanup loop breaks after the first match, one legacy -file retains the BEGIN marker and the assertion fails. SS-3 is RESOLVED. - -Caveat filed under New Finding 4 below (GG-4 mock ambiguity does not re-open SS-3 but is a -separate coverage gap). - ---- - -### SS-2 (C-2/C-3 PS7+ guard) -- STILL PARTIAL - -v2 defect: existing lines 238 and 261 of `test_windows_setup.ps1` do `$PROFILE = $path`, -read-only in PS7+. v2 plan did not address them. - -v3 decision v3-D4: "Guard C-2 and C-3 with `if ($PSVersionTable.PSVersion.Major -ge 7) { skip }`" -v3 AC item: "C-2 and C-3 guarded against PS7+ `$PROFILE` assignment error (skip with logged reason)" - -Why still partial: - -1. `skip` is not a valid PowerShell command. The Test-Scenario harness (no Pester) has no - native skip mechanism. The plan does not define what `skip` resolves to: early return from - the enclosing block? A `return` before the `$PROFILE = $path` line? A wrapper guard around - the entire setup + Test-Scenario? Without a code snippet, the implementer must invent this. - -2. The `$PROFILE = $path` assignment at line 238 is OUTSIDE the Test-Scenario block (it is - setup code that runs before the block). If a PS7+ guard is placed only inside the - Test-Scenario block, the assignment still fires during setup and throws. The guard must wrap - the setup lines, not just the Test-Scenario body. - -3. "Skip with logged reason" -- what generates the log entry? `Write-Host`? The harness's own - pass/skip counter? The plan does not say. A future PS7-only CI run might see zero output - for C-2 and C-3 with no indication whether they were skipped or never reached. - -Evidence: `tests/test_windows_setup.ps1` lines 235-253 (C-2), 258-276 (C-3). Both show -`$PROFILE = $path` outside the Test-Scenario block. The guard logic must precede those lines. -The plan acknowledges the constraint (v3-D4) but does not specify WHERE or HOW the guard is -inserted into the existing file structure. - -SS-2 remains STILL PARTIAL. The intent is correct; the implementation spec is incomplete. - ---- - -## New Findings - -**NF-1: GG-7 mock cannot set `$LASTEXITCODE` as described -- broken test.** - -The test table says: "Mock `Invoke-HostQuery` sets `$LASTEXITCODE = 1`, returns empty." - -In PowerShell, `$LASTEXITCODE` is scoped. A plain assignment inside a function creates a -LOCAL variable: - - function Invoke-HostQuery { $LASTEXITCODE = 1; return "" } - -When this function returns, the caller's `$LASTEXITCODE` is NOT updated. It retains its -previous value (typically 0 or whatever the last native command set). The production code -immediately after the call does: - - $raw = Invoke-HostQuery -Exe $HostExe - if ($LASTEXITCODE -ne 0) { ... fallback ... } - -With the mock as described, `$LASTEXITCODE` in the caller scope is 0, the condition is false, -and the fallback branch is never taken. GG-7 silently fails to test what it claims. - -To propagate `$LASTEXITCODE` from mock to caller, the mock must use one of: - (a) `$global:LASTEXITCODE = 1` (scope override) - (b) `& cmd /c exit 1` or `& $env:ComSpec /c exit 1` (invoke a real native command) - (c) The production code must be changed so the mock returns a sentinel value that the - algorithm interprets as failure (rather than relying on `$LASTEXITCODE`). - -Option (b) is most portable and least surprising. The plan must specify the mechanism. -As described, GG-7 is a false-green test -- it passes while the fallback branch is untested. -Severity: HIGH (same category as v2 SS-5). - ---- - -**NF-2: GG-2 trace -- OK.** - -`Resolve-ProfilePath 'powershell-notexist' $fb` calls `Get-Command 'powershell-notexist' --EA SilentlyContinue`, which returns `$null`. `-not $null` is `$true`. Function returns -`$FallbackPath`. Assertion `$result -eq $FallbackPath` passes for the right reason. -`Invoke-HostQuery` is never called. This is correct -- GG-2 tests the "host not found" path; -GG-7 is intended to test the "host exits non-zero" path. No issue here. - ---- - -**NF-3: Mock isolation between GG tests is unaddressed.** - -Section 5 header note: "Mock must be defined AFTER dot-sourcing `profile.ps1` or the -dot-source overwrites it." - -This addresses the ordering trap (NH-2 from v2 grill -- noted, partial credit). But it does -not address mock STATE BETWEEN tests. - -If `Test-Scenario` runs its block in the current scope (dot-source pattern `. $block`), then -a function defined inside GG-1's block persists into GG-2, GG-3, etc. Each test must -explicitly redefine its mock. If `Test-Scenario` runs in a child scope (`& $block`), mocks -defined inside do NOT persist -- and then mocks that need to be active during a -`Write-PowerShellProfile` call (which dot-sources production code) may not be visible. - -The plan gives no code example and does not state which scope model `Test-Scenario` uses. -The existing harness at lines 240-251 (C-2) and 263-273 (C-3) show that setup code runs -outside the Test-Scenario block, in the file's top scope. This implies Test-Scenario uses -child or dot-source scope. The GG test implementer needs to know this to avoid: - (a) GG-1 mock leaking into GG-2 (wrong path returned for "fallback" test) - (b) GG-4 mock not visible when Write-PowerShellProfile resolves paths - -Severity: MEDIUM. A note with one code skeleton would close this. - ---- - -**NF-4: GG-4 mock ambiguity -- combined dedup + dual-legacy scenario not explicitly specified.** - -GG-4 description: "mock resolves to OneDrive path" -- does this mean: - (a) Both `powershell` and `pwsh` mock calls return the same OneDrive path (dedup -> 1 entry)? - (b) Only one host mock returns the OneDrive path; the other falls back to a legacy path? - -If (b): the second legacy path is NOT orphaned (it IS the current resolved path for one host) -and should NOT be stripped. Seeding it with a dev-setup block and asserting it is stripped -would test the WRONG behavior. - -If (a): dedup reduces `$profilePaths` to 1 entry; both legacy paths are orphaned; both should -be stripped. This is the intended scenario -- but the description does not say it explicitly. - -The implementer must guess. If they choose interpretation (b), GG-4 would assert that a -non-orphaned legacy path is stripped, which contradicts the algorithm's guard condition. The -test would fail for the wrong reason. Severity: MEDIUM. - ---- - -## Summary Matrix - -| # | Concern | Status | -|---|---------|--------| -| SS-2 | C-2/C-3 PS7+ guard | STILL PARTIAL -- skip mechanism unspecified | -| SS-3 | GG-4 dual-orphan | RESOLVED | -| SS-5 | GG-6 algorithm + assertion | RESOLVED | -| NF-1 | GG-7 $LASTEXITCODE mock broken | NEW HIGH -- false-green test | -| NF-2 | GG-2 absent-exe trace | OK -- confirmed correct | -| NF-3 | Mock isolation between GG tests | NEW MEDIUM -- scope model undocumented | -| NF-4 | GG-4 both-hosts-same-path ambiguity | NEW MEDIUM -- implementer must guess | - ---- - -## Required Fixes for v4 - -1. **GG-7 mock mechanism (HIGH).** Specify `$global:LASTEXITCODE = 1` OR `& cmd /c exit 1` - inside the mock. Alternatively, change the mock to use a sentinel return value and update - the production guard accordingly. Plain `$LASTEXITCODE = 1` inside a function does not - propagate to the caller scope. - -2. **C-2/C-3 guard spec (MEDIUM).** Show WHERE the guard goes in the existing test file - (before the `$PROFILE = $path` setup lines, not only inside the Test-Scenario block). - Define what "skip" means: e.g., a `Write-Host "SKIP C-2: PS7+ $PROFILE read-only"` and - `continue` (or `return` if wrapped in a function). One pseudocode snippet closes this. - -3. **Mock scope note (MEDIUM).** Add one code skeleton to Section 5 showing: - (a) Dot-source profile.ps1 at top of test group - (b) Define mock function immediately after in top scope - (c) Reset or redefine mock before each GG test - Note which scope `Test-Scenario` uses so implementer knows whether mock persists. - -4. **GG-4 mock spec (MEDIUM).** Explicitly state: "Both `Invoke-HostQuery` mock calls return - `$oneDrivePath` so `$profilePaths` deduplicates to 1 entry." Remove ambiguity. - ---- - -## Recommendation - -REVISE. One v2 partial still open (SS-2, skip mechanism underspecified). One new HIGH finding -(NF-1, GG-7 mock broken -- false-green). Two new MEDIUM findings (NF-3, NF-4). - -Suggested reviser: Pluto (Donald and Mickey locked out; Goofy locked out as v1 author; -Donald locked out as v3 author per grill-reviewer constraint). - -If Pluto is unavailable: escalate to Mickey to decide reviser assignment. - -The algorithm itself (v3-D1 through v3-D5) is sound. GG-6 and GG-4 are fixed. The remaining -work is test-plan spec quality: two mechanism descriptions and two ambiguity resolutions. -This is targeted; a v4 pass should close all open items. diff --git a/docs/plans/441-grill-chip-v4.md b/docs/plans/441-grill-chip-v4.md deleted file mode 100644 index 425e6093..00000000 --- a/docs/plans/441-grill-chip-v4.md +++ /dev/null @@ -1,281 +0,0 @@ -# Chip's Grill -- Plan for #441 v4 -**Date:** 2026-05-27 -**Reviewer:** Chip (Tester) -**Verdict:** REVISE -**Plan version reviewed:** v4 (Jiminy revision) -**Author locked out:** Goofy, Mickey, Donald, Jiminy (authored v4 revision) - ---- - -## v3-Concern Regression Check - -### SS-5 (GG-6 inversion) -- RESOLVED - -No regression. GG-6 row in v4 is unchanged from v3: `-Last 1`, `-NoLogo`, assertion -`$result -eq $mockPath`. Trace confirmed correct in v3 grill; v4 does not touch it. - ---- - -### SS-3 (GG-4 dual-orphan) -- RESOLVED - -v4 GG-4 row now explicitly states: "Both Invoke-HostQuery mock calls (for powershell and -pwsh) return the SAME $oneDrivePath; dedup produces 1 entry in $profilePaths; BOTH -$ps51Fallback AND $ps7Fallback files exist in TestDrive seeded with BEGIN marker." - -Both-hosts-same-path is stated. Both legacy files must be stripped. Loop-break-after-first -bug remains catchable. SS-3 remains RESOLVED. - ---- - -### SS-2 (C-2/C-3 PS7+ guard) -- RESOLVED - -My v3 sub-items: -1. "`skip` is not valid PowerShell" -- FIXED: v4 P4 replaces with `if/Write-Host/return`. -2. "$PROFILE = $path assignment is OUTSIDE Test-Scenario block" -- FIXED: v4 P4 moves the - assignment INSIDE the Test-Scenario body. -3. "What generates the skip log entry?" -- FIXED: `Write-Host 'SKIP C-2: PS7+...'` stated. - -All three sub-items addressed. SS-2 RESOLVED per my v3 criteria. - -New concern filed under new findings below (skip appears as silent PASS). That is a v4-new -finding, not a regression of my v3 items. - ---- - -### GG-7 HIGH (NF-1: $LASTEXITCODE mock broken) -- RESOLVED - -v4 P3 fix: mock calls `& $env:ComSpec /c "exit 1"`. - -Mechanism verification: -- `& $env:ComSpec /c "exit 1"` launches cmd.exe, which exits with code 1. -- PowerShell runtime sets `$LASTEXITCODE = 1` at global scope on native-command exit. - This is NOT a local variable -- it is the same global that Resolve-ProfilePath reads - immediately after Invoke-HostQuery returns. -- After the mock function returns, caller's `$LASTEXITCODE` IS 1. Fallback branch fires. - Assertion `$result -eq $FallbackPath` exercises the correct code path. FIXED. - -$env:ComSpec availability: always defined on Windows (set before any user shell -customization; resolves to C:\Windows\System32\cmd.exe). Safe to rely on. - -Idiomatic alternative: `$global:LASTEXITCODE = 1` inside the mock function is lighter -(no child process) and equally correct within the Test-Scenario harness (no Pester Mock -infrastructure). Not a blocking difference; `& $env:ComSpec` is more authentic to -production semantics. Suggest as optional simplification. - -NF-1 HIGH: RESOLVED. - ---- - -### NF-3 MEDIUM (Mock isolation between GG tests) -- RESOLVED - -v4 Section 5 header now states: "redefine mock Invoke-HostQuery at the start of each -individual test (or in a BeforeEach block) so no mock state leaks between tests. -Test-Scenario runs its block in a child scope -- mocks defined in the enclosing (file) -scope are visible inside but are reset between tests by explicit redefinition." - -Scope model is now stated (child scope). The pattern -- define mock in file scope, redefine -before each Test-Scenario call -- is workable: Test-Scenario's `& $block` creates a child -scope that inherits the file-scope function definition; Write-PowerShellProfile called from -within that child scope will look up `Invoke-HostQuery` through the scope chain and find the -mock. [ok] - -Caveat: "or in a BeforeEach block" is misleading -- Test-Scenario has no BeforeEach -mechanism. Filed under new findings (LOW). - -NF-3 MEDIUM: RESOLVED. - ---- - -### NF-4 MEDIUM (GG-4 mock ambiguity) -- RESOLVED - -v4 GG-4 row eliminates ambiguity. Both calls return the same $oneDrivePath; dedup -> 1 -entry; both legacy paths are orphaned. Interpretation (b) is no longer possible. -NF-4 MEDIUM: RESOLVED. - ---- - -## New Findings - -### NF-1 (v4): GG-7 -- Exe spec missing; test may exercise wrong branch (MEDIUM) - -Resolve-ProfilePath has TWO early-exit paths: - (A) Get-Command $HostExe fails -> exe-not-found fallback (no Invoke-HostQuery call) - (B) Invoke-HostQuery runs; $LASTEXITCODE -ne 0 -> exit-code fallback (what GG-7 targets) - -For GG-7 to reach path (B), the exe passed to Resolve-ProfilePath must be present on PATH -so that Get-Command succeeds, allowing execution to reach the Invoke-HostQuery call. - -v4 GG-7 row: "Mock calls `& $env:ComSpec /c 'exit 1'`" -- but DOES NOT specify which exe -name is passed to Resolve-ProfilePath. If the engineer passes 'pwsh' and pwsh is not -installed on the test runner (PS 5.1 only CI), Get-Command returns null; path (A) fires; -Invoke-HostQuery is never called; $LASTEXITCODE is not set by the mock; the test returns -$FallbackPath for the WRONG reason and passes as a false green. - -GG-7 must specify: "pass an exe known to be present on the test runner (e.g., 'powershell' -which is guaranteed on any Windows system, or 'cmd' / 'cmd.exe')." Without this, the test -is environment-dependent. - -Required fix: add to GG-7 Input cell: "Pass $HostExe = 'powershell' (always present on -Windows) so Get-Command succeeds and the LASTEXITCODE path is exercised." - -Severity: MEDIUM. Results in a false green on PS-5.1-only runners. - ---- - -### NF-2 (v4): GG-1/GG-4/GG-5 -- "TestDrive" without Pester; no temp file pattern (MEDIUM) - -GG-4 row: "BOTH $ps51Fallback AND $ps7Fallback files exist in TestDrive seeded with BEGIN -marker." -GG-1: "Block at OneDrive path." -GG-5: "Idempotency (3 runs) -- same file." - -None of these tests can run without writing to disk. The test harness does NOT use Pester; -`$TestDrive` is a Pester-only automatic variable. In this harness, `$TestDrive` is null or -undefined, so any path like `Join-Path $TestDrive '...'` silently resolves to a relative path -from the cwd -- or throws under strict mode. - -The existing C-2/C-3 tests use: - `$c2Profile = Join-Path $PSScriptRoot "temp_profile_c2_$(Get-Random).ps1"` - and clean up with `if (Test-Path $c2Profile) { Remove-Item $c2Profile -Force }` AFTER the - Test-Scenario block. - -GG-1, GG-4, and GG-5 have no equivalent. An engineer reading the plan will either: - (a) Use $TestDrive (undefined -- test fails or writes to bad path), or - (b) Infer the PSScriptRoot + Get-Random pattern from C-2/C-3 (reasonable but undocumented - for GG group), or - (c) Use hard-coded temp paths (not CI-safe). - -Additionally, GG-4 seeds BOTH legacy files. In the existing pattern, legacy paths -($ps51Fallback, $ps7Fallback) are derived from $HOME. The test must either use a fake $HOME -(like FF-6..FF-10 sandbox pattern in the existing suite) or override $ps51Fallback and -$ps7Fallback to point to temp files. The plan does not address this -- writing to real -$HOME paths in a test is destructive. - -Required fix: Section 5 must add a note on the temp file pattern for GG-1/GG-4/GG-5: - - Use Join-Path $PSScriptRoot "temp_gg_$(Get-Random).ps1" for the "mock path." - - Override $ps51Fallback/$ps7Fallback test variables to point to temp files, not real HOME. - - Clean up after each Test-Scenario block with Remove-Item. - -Severity: MEDIUM. Tests that touch $HOME paths in CI are destructive and non-idempotent. -A skilled engineer will notice this gap but the plan should not require such inference. - ---- - -### NF-3 (v4): C-2/C-3 skip-as-pass; PS7+ CI shows green with no logic executed (LOW) - -v4 P4: `if ($PSVersionTable.PSVersion.Major -ge 7) { Write-Host 'SKIP C-2: ...'; return }`. - -`return` inside the Test-Scenario scriptblock causes the block to complete without throwing. -Test-Scenario's pass/fail counter sees no throw -> records PASS. On PS7+ CI, C-2 and C-3 -appear as two green passes in the test tally even though zero lines of their actual logic ran. - -A developer reviewing CI output will see "63 passed" and not notice that C-2/C-3 silently -skipped. Future regressions in those test branches are invisible. - -The Write-Host message provides traceability in the raw log, but the pass count is inflated. - -Mitigation (within vertical slice): the existing harness has a Write-Skip helper (see -history.md "Conditional skip pattern: Get-Command -ErrorAction SilentlyContinue outside test -block, call Write-Skip if found"). If Write-Skip exists, the guard should call Write-Skip -('C-2', 'PS7+ -- $PROFILE conceptually read-only; covered by GG tests') before return. -This correctly increments the skip counter rather than the pass counter. - -Severity: LOW. Does not cause a false green in GG tests. Does cause inflated pass counts and -invisible test gaps on PS7+ runners. - ---- - -### NF-4 (v4): BeforeEach reference in non-Pester harness (LOW) - -Section 5 header: "or in a BeforeEach block." - -BeforeEach is a Pester keyword. The Test-Scenario harness has no BeforeEach. This reference -will confuse engineers unfamiliar with the project test harness. They may attempt to use -BeforeEach and get a "command not found" error or silently skip mock setup. - -Fix: replace "or in a BeforeEach block" with "or in file scope immediately before each -Test-Scenario call." - -Severity: LOW. Terminology error; likely caught at implementation time. - ---- - -### NF-5 (v4): GG-7 suggestion -- $global:LASTEXITCODE is lighter (INFORMATIONAL) - -`& $env:ComSpec /c "exit 1"` is correct but spawns a real child process (adds latency, -depends on cmd.exe being functional). An equivalent approach within the Test-Scenario -harness is: - - function Invoke-HostQuery { $global:LASTEXITCODE = 1; return "" } - -This is idiomatic for non-Pester mocks: explicit global scope qualifier overrides the -variable at global scope, visible to all callers. No child process needed. - -Either approach is correct. Suggesting $global:LASTEXITCODE as an optional simplification -for documentation quality. Not blocking. - ---- - -## Summary Matrix - -| # | Concern | v3 Status | v4 Status | -|---|---------|-----------|-----------| -| SS-5 | GG-6 inversion | RESOLVED | HOLDS -- no regression | -| SS-3 | GG-4 dual-orphan | RESOLVED | HOLDS -- no regression | -| SS-2 | C-2/C-3 PS7+ guard | STILL PARTIAL | RESOLVED (all 3 sub-items) | -| NF-1 | GG-7 LASTEXITCODE mock | HIGH (new in v3) | RESOLVED | -| NF-2 | Mock isolation | MEDIUM (new in v3) | RESOLVED | -| NF-3 | GG-4 mock ambiguity | MEDIUM (new in v3) | RESOLVED | -| NF-1v4 | GG-7 exe spec missing | -- | NEW MEDIUM | -| NF-2v4 | TestDrive without Pester | -- | NEW MEDIUM | -| NF-3v4 | C-2/C-3 skip-as-pass | -- | NEW LOW | -| NF-4v4 | BeforeEach reference | -- | NEW LOW | -| NF-5v4 | LASTEXITCODE alt suggestion | -- | INFORMATIONAL | - ---- - -## Implementation-Ready Verdict - -**Can a competent engineer write Pester (Test-Scenario) code for GG-1..GG-7 today?** - -NO -- not without inferring two non-trivial decisions: - -1. **GG-7:** Which exe to use so the mock is invoked (not the exe-not-found branch). - Without guidance, a PS-5.1-only CI run can produce a false green. - -2. **GG-1/GG-4/GG-5:** How to create and clean up temp files without $TestDrive. Without - the PSScriptRoot+Get-Random pattern (or explicit fake-$HOME sandbox) stated in the plan, - the engineer either writes to real $HOME or produces undefined-variable errors. - -The algorithm and mock mechanism are sound. The two new MEDIUMs are purely test-plan -specification gaps. A targeted v5 pass fixing GG-7 input spec and the temp file pattern -note would close all open items. - ---- - -## Required Fixes for v5 - -1. **GG-7 exe spec (MEDIUM):** Add to GG-7 Input: "Use $HostExe = 'powershell' (always - present on Windows) so Get-Command succeeds and the mock is invoked." - -2. **Temp file pattern (MEDIUM):** Add one-sentence note to Section 5: "GG tests that write - to disk (GG-1, GG-4, GG-5) must use Join-Path $PSScriptRoot 'temp_gg_$(Get-Random).ps1' - for mock paths and override $ps51Fallback/$ps7Fallback to temp paths, not $HOME paths; - clean up with Remove-Item after each Test-Scenario block." - -3. **BeforeEach reference (LOW):** Change "or in a BeforeEach block" to "or in file scope - immediately before each Test-Scenario call." - -4. **Skip-as-pass (LOW):** Change C-2/C-3 guard to call Write-Skip rather than a bare - Write-Host + return, so the skip counter is incremented correctly. - ---- - -## Recommendation - -REVISE. Algorithm is sound; all v3 concerns closed. Two new MEDIUMs (GG-7 exe spec and -TestDrive temp file pattern) prevent implementation-readiness. Both are small, targeted -spec additions -- v5 should be a quick pass. - -Suggested reviser: Jiminy (authored v4; not locked out for v5). Alternatively: any eligible -agent not yet holding a lockout on this plan. diff --git a/docs/plans/441-grill-chip-v5.2.md b/docs/plans/441-grill-chip-v5.2.md deleted file mode 100644 index d9aaf112..00000000 --- a/docs/plans/441-grill-chip-v5.2.md +++ /dev/null @@ -1,202 +0,0 @@ -# Chip's Grill -- Plan #441 v5.2 -**Date:** 2026-05-27 -**Reviewer:** Chip (Tester) -**Verdict:** SHIP -**Plan version reviewed:** v5.2 (Mickey revision -- JN-1/JN-2 patch) -**Author locked out:** Goofy, Mickey, Donald, Jiminy (authored prior revisions) - ---- - -## Verdict: SHIP - -All MEDIUM+ concerns resolved. JN-1 closed cleanly by parameterization. JN-2 improved -(visibility gap closed; skip counter accuracy is a residual LOW). Four carry-forward LOWs -and one new LOW are documented but do not block implementation or introduce silent -destructive writes. - ---- - -## 1. Parameter Override Pattern Assessment (JN-1) - -### Mechanism soundness - -`Write-PowerShellProfile` now declares: - - param( - [string]$Ps51Fallback = [System.IO.Path]::Combine($HOME, 'Documents', 'WindowsPowerShell', ...), - [string]$Ps7Fallback = [System.IO.Path]::Combine($HOME, 'Documents', 'PowerShell', ...) - ) - -These parameters feed: -1. `Resolve-ProfilePath 'powershell' $Ps51Fallback` and `Resolve-ProfilePath 'pwsh' $Ps7Fallback` - (fallback arg when host query fails) -2. `$legacyPaths = @($Ps51Fallback, $Ps7Fallback)` (orphan-strip targets) - -Passing temp paths via `-Ps51Fallback`/`-Ps7Fallback` redirects BOTH the fallback path -returned by `Resolve-ProfilePath` AND the orphan-strip loop targets away from real `$HOME`. -The `$local:` shadowing bug (JN-1) is eliminated. MECHANISM SOUND. [PASS] - -### Per-test verification - -| Test | Both params present? | Temp path pattern stated? | finally cleanup stated? | -|------|---------------------|--------------------------|------------------------| -| GG-1 | YES -- `Write-PowerShellProfile -Ps51Fallback $tempPath51 -Ps7Fallback $tempPath7` | Implicit via Section 5 header | Section 5 header | -| GG-4 | YES -- same pattern | EXPLICIT in row: `Join-Path $env:TEMP "gg-test-441-$(New-Guid)"` | Section 5 header | -| GG-5 | YES -- same pattern | Implicit via Section 5 header | Section 5 header | - -All three disk-writing tests pass both parameters. [PASS] - -### Do tests now DEMONSTRATE the override works? - -YES. With parameters operative, `$legacyPaths` in GG-1/GG-4/GG-5 = temp paths (not real -`$HOME`). The orphan-strip loop and fallback-path branches operate on temp dirs. Tests no -longer silently write to real `$HOME` profile files. [PASS] - ---- - -## 2. GG-2/3/6/7 -- Destructive Write Risk - -**Risk: NO.** Section 5 header explicitly names the disk-writing tests: "GG tests that write -to disk (GG-1, GG-4, GG-5)". By explicit exclusion, GG-2/3/6/7 are not disk-writing tests. - -### Per-test analysis - -**GG-2** -- Input: absent exe. Assertion: `$result -eq $FallbackPath`. - `$result` is a captured return value. `Resolve-ProfilePath` returns a string; it writes - no files. No disk writes. NO DESTRUCTIVE RISK. - -**GG-3** -- Input: mock returns same path, different case. Assertion: `$profilePaths.Count -eq 1`. - `$profilePaths` is a local var inside `Write-PowerShellProfile`; it is not directly - accessible from test scope. The assertion pattern is consistent with the engineer - constructing the dedup array in-line: - `$profilePaths = @('Path', 'PATH') | Sort-Object { $_.ToLower() } -Unique` - and asserting `Count -eq 1`. This tests the dedup expression in isolation -- no call to - `Write-PowerShellProfile`, no disk writes. NO DESTRUCTIVE RISK. - [LOW-CLARITY: row does not explicitly state "test dedup logic in isolation, not via - Write-PowerShellProfile." An engineer calling Write-PowerShellProfile without -Ps51Fallback - / -Ps7Fallback would write to real $HOME fallback paths. The Section 5 "write to disk" - grouping mitigates but does not close this ambiguity. LOW; not blocking.] - -**GG-6** -- Input: mock returns `"banner\n$mockPath"`. Assertion: `$result -eq $mockPath`. - `$result` is a captured return value from `Resolve-ProfilePath`. No disk writes. - NO DESTRUCTIVE RISK. - -**GG-7** -- Input: mock sets `$LASTEXITCODE = 1`. Assertion: `$result -eq $FallbackPath`. - `$result` from `Resolve-ProfilePath`. No disk writes. NO DESTRUCTIVE RISK. - ---- - -## 3. JN-2 Status (NF-3v4 carry-forward) - -v5.2 change: `Write-Host 'SKIP C-2: ...'` -> `Write-Warning '[SKIPPED] C-2: ...'` - -| Sub-concern | Status | Evidence | -|-------------|--------|----------| -| Visible in PS console / CI | RESOLVED | `Write-Warning` writes to warning stream; PS prepends `WARNING:` prefix; always visible | -| "[SKIPPED]" tag grep-able | RESOLVED | Literal `[SKIPPED]` in warning message; grep-able in CI logs | -| Doesn't increment pass count on PS7+ | PARTIAL -- see below | -- | -| D2 (no Pester) preserved | HOLDS | `Write-Warning` has no Pester dependency | - -**Skip counter gap (residual LOW):** `Write-Warning` + `return` does NOT call `Write-Skip` -(the harness function on line 55-61 of `test_windows_setup.ps1` that increments -`$TestsSkipped`). On PS7+, C-2/C-3 exit their Test-Scenario block via `return` with no -assertion result. Whether Test-Scenario counts this as a pass depends on harness -implementation (not shown in plan). The concern originally raised in NF-3v4 -- that skips -appear as passes in CI tallies -- is NOT fully resolved by `Write-Warning`. The visibility -gap IS resolved. Residual: skip counter accuracy. LOW; non-blocking. - -**Summary:** JN-2 = PARTIAL. Visibility: RESOLVED. Skip counter: OPEN (LOW). - ---- - -## 4. Implementation-Ready Check - -### Mock signatures - -`Invoke-HostQuery` mock: defined in file scope, returns a string path. Signature is the -same single-parameter function pattern used across the test group. CLEAR. [PASS] - -### Test invocation pattern (Pester-free, per D2) - -Disk-writing tests (GG-1/4/5): `Write-PowerShellProfile -Ps51Fallback $tempPath51 -Ps7Fallback $tempPath7`. -Parameter names match function signature in Section 4. CLEAR. [PASS] - -Non-disk tests (GG-2/6/7): `$result = Resolve-ProfilePath -HostExe ... -FallbackPath ...`. -Assertion pattern `$result -eq $FallbackPath` is straightforward. CLEAR. [PASS] - -GG-3: Engineer must infer "test dedup in isolation" from assertion syntax. LOW clarity gap -(see Section 2, GG-3 note). - -### Cleanup pattern - -Section 5 header: `finally` block. Explicit. CLEAR. [PASS] - -### LASTEXITCODE reset positioning (F-3 hold) - -Section 5 header: "Before each redefinition, reset `$global:LASTEXITCODE = 0`." -"Before redefinition" = before mock redefinition = before Test-Scenario call. Ordering -preserved. F-3 HOLDS. [PASS] - -**Implementation-ready: YES.** Minor LOW gaps (GG-3 invocation target, GG-1/4 resolved-path -identity) do not block an engineer from writing correct, non-destructive tests. - ---- - -## 5. Carry-Forward LOWs (from v5.1) - -| # | Finding | v5.2 Status | -|---|---------|-------------| -| NF-1 | H1 no encoding assertion in GG-4 | CARRY-FORWARD (unchanged) | -| NF-2 | F-4 middle-of-file case not exercised in GG-4 | CARRY-FORWARD (unchanged) | -| NF-3 / JN-2 | C-2/C-3 skip-as-pass | PARTIAL RESOLUTION (visibility fixed; skip counter open) | -| NF-4 | GG-1 $mockPath identity implicit (resolved path not stated as temp) | CARRY-FORWARD; see NF-4-v5.2 below | - ---- - -## 6. New Finding - -### [LOW] NF-4-v5.2 (NF-4 extended): Resolved-path write target not redirected to temp - -**Citation:** Section 5, GG-1 Input: "Mock returns OneDrive path"; GG-4 Input: "both -`Invoke-HostQuery` mock calls return the SAME `$oneDrivePath`." - -**Root cause:** `-Ps51Fallback`/`-Ps7Fallback` redirect the FALLBACK paths and -`$legacyPaths` (orphan-strip targets) to temp. They do NOT redirect the RESOLVED path -(mock return value). The write loop writes to entries in `$profilePaths`, which are the -paths returned by `Invoke-HostQuery` mock -- i.e., the "OneDrive path" or `$oneDrivePath` -from the mock. If the engineer supplies a real OneDrive path string as the mock return -value, the write loop targets a real profile file. - -**Why it stays LOW:** -1. GG-1 asserts `Test-Path $mockPath` -- this only passes if the write succeeded. On CI - there is no real OneDrive directory; write would fail or `Test-Path` returns false. - Test failure reveals the issue; it is NOT silent destruction. -2. A competent engineer would return a temp path as the mock's "OneDrive path" to make - `Test-Path $mockPath` pass on CI. This is the natural implementation. -3. GG-4 asserts "OneDrive file has BEGIN marker" -- same reasoning applies. - -**Recommendation:** Add one sentence to GG-1 and GG-4 Input cells: "Mock returns a path -within the temp dir (e.g., `Join-Path $tempDir 'OneDrive\Documents\...\profile.ps1'`) so -the write loop targets temp, not real OneDrive." Closes the ambiguity without blocking -implementation. - ---- - -## 7. Summary Matrix - -| # | Concern | v5.1 Status | v5.2 Status | -|---|---------|-------------|-------------| -| JN-1 | $local: prevents test override; GG-1/4/5 write to real $HOME | N/A (introduced by v5) | RESOLVED -- parameterization | -| JN-2 / NF-3 | C-2/C-3 skip-as-pass (Write-Host -> Write-Warning) | OPEN LOW | PARTIAL -- visibility resolved; skip counter open LOW | -| NF-1 | No encoding assertion in GG-4 | LOW | CARRY-FORWARD LOW | -| NF-2 | F-4 middle-of-file regex not exercised | LOW | CARRY-FORWARD LOW | -| NF-4 (extended) | Resolved-path write target not stated as temp in GG-1/4 | LOW (GG-1 only) | CARRY-FORWARD LOW (GG-1 + GG-4) | -| GG-3 clarity | Invocation target ambiguous | N/A | NEW LOW | -| All H-patches | H1-H5, F-4, F-5 regression | ALL HOLD | ALL HOLD | -| F-3 | LASTEXITCODE reset positioning | RESOLVED | HOLDS | - ---- - -**Grilled by:** Chip (Tester) -**Date:** 2026-05-27 -**Session:** 441-grill-v5.2 diff --git a/docs/plans/441-grill-chip-v5.md b/docs/plans/441-grill-chip-v5.md deleted file mode 100644 index f93950ec..00000000 --- a/docs/plans/441-grill-chip-v5.md +++ /dev/null @@ -1,194 +0,0 @@ -# Chip's Grill -- Plan #441 v5.1 -**Date:** 2026-05-27 -**Reviewer:** Chip (Tester) -**Verdict:** SHIP -**Plan version reviewed:** v5.1 (Donald revision -- F-4/F-5 patch) -**Author locked out:** Goofy, Mickey, Donald, Jiminy (authored prior revisions) - ---- - -## C-1 / C-2 / F-3 Status - -### C-1 (GG-7 exe spec) -- RESOLVED - -v5.1 GG-7 Input cell explicitly states: - "$HostExe = 'powershell' (guaranteed present on Windows; 'pwsh' is unsuitable -- - not-installed early-exit would mask the mock invocation and produce a false green on - PS5.1-only runners (v5-H4))" - -Rationale documented inline. Exe spec is unambiguous. C-1 RESOLVED. - ---- - -### C-2 (TestDrive -> real temp path) -- RESOLVED - -Section 5 header: "GG tests that write to disk (GG-1, GG-4, GG-5) create a unique temp dir -via Join-Path $env:TEMP 'gg-test-441-$(New-Guid)', override $ps51Fallback/$ps7Fallback to -paths within it, and clean up in a finally block (v5-H3)." - -GG-4 row: "$ps51Fallback and $ps7Fallback overridden to Join-Path $env:TEMP -'gg-test-441-$(New-Guid)' temp paths (not real $HOME), both seeded with BEGIN marker." - -$TestDrive is gone from all GG rows. finally-block cleanup is documented. Real $HOME is -explicitly called out as NOT used. C-2 RESOLVED. - ---- - -### F-3 ($LASTEXITCODE reset positioning) -- RESOLVED - -Section 5 header: "Before each redefinition, reset $global:LASTEXITCODE = 0 so GG-7's -native-command exit-1 does not contaminate subsequent success-path tests (v5-H2)." - -"Before each redefinition" -- the reset is positioned before the mock function definition, -which is before the Test-Scenario call. This is the correct ordering. GG-7 contamination -into success-path tests is closed. F-3 RESOLVED. - ---- - -## Regression Check (H1-H5, F-4, F-5) - -| Patch | What it did | v5.1 status | -|-------|-------------|-------------| -| H1 | Set-Content missing -Encoding ASCII in orphan-strip | Section 4 snippet shows -Encoding ASCII on the orphan-strip Set-Content line. Consistent with production line 28. HOLDS. | -| H2 | GG-7 LASTEXITCODE contaminates success-path tests | Section 5 reset-before-redefinition closes this. HOLDS. | -| H3 | TestDrive in GG-4 contradicts D2 | Replaced with $env:TEMP + New-Guid + finally pattern. HOLDS. | -| H4 | GG-7 exe unspecified; false green on PS5.1-only runner | 'powershell' with rationale in GG-7 Input. HOLDS. | -| H5 | $ps51Fallback/$ps7Fallback undefined under StrictMode | $local:ps51Fallback and $local:ps7Fallback defined at top of Write-PowerShellProfile. HOLDS. | -| F-4 | Orphan-strip regex: add \r?\n prefix, .+? -> .*? | Section 4 regex now reads "(?s)\r?\n..." and uses .*?. Matches production line 27. HOLDS. | -| F-5 | $local:beginMarker/$local:endMarker undefined | Section 4 shows both defined at top of Write-PowerShellProfile alongside H5 locals. HOLDS. | - ---- - -## New Findings - -### NF-1 (v5): H1 has no encoding assertion in GG-4 (LOW) - -H1 patches Set-Content to use -Encoding ASCII for the orphan-strip write. GG-4 asserts -"Neither legacy file has BEGIN marker" -- it checks content presence but NOT file encoding. - -On PS5.1 the default Set-Content encoding is UTF-16 LE with BOM. If the -Encoding ASCII -parameter were accidentally dropped in production, PS5.1 would silently write UTF-16 and -GG-4 would still pass (the BEGIN marker check reads the file content, which PS5.1 decodes -correctly from UTF-16). - -A byte-level encoding assertion (e.g., reading raw bytes, confirming no BOM preamble 0xFF -0xFE) would catch this regression. However, for a vertical slice test plan this is an -acceptable omission -- the encoding contract is enforced by code review and is observable -at integration time. Flagging as LOW; not blocking for SHIP. - ---- - -### NF-2 (v5): F-4 middle-of-file case not exercised in GG-4 (LOW) - -F-4 adds \r?\n PREFIX to the strip regex so the preceding newline is consumed with the -block (production line 27 behavior). GG-4 seeds both legacy files "with BEGIN marker" but -does not specify that content exists AFTER the block. An engineer implementing GG-4 will -most likely seed files with only the block (block at end of file), in which case TrimEnd() -absorbs the trailing blank line and the \r?\n prefix never fires. - -The case that uniquely exercises the \r?\n prefix is: file has user content, then the -BEGIN..END block, then MORE user content below. If the prefix is absent in that case, an -extra blank line is left above the next section. - -This gap means the F-4 regex fix has no dedicated test exercise for its primary edge case. -Acceptable for vertical slice (the core strip correctness is covered by GG-4); however a -comment in GG-4 Input noting "seed content both above and below the block to exercise -\\r?\\n prefix removal" would close it. LOW; not blocking. - ---- - -### NF-3 (v5): NF-3v4 carry-forward -- C-2/C-3 skip-as-pass (LOW, unchanged) - -v5.1 does not address the skip-as-pass issue noted in my v4 grill. C-2/C-3 guard uses -Write-Host + return, which increments the PASS counter on PS7+ CI. Write-Skip exists in -the harness (tests/test_windows_setup.ps1 line 55-61) and would correctly increment -TestsSkipped. This is a LOW accuracy issue in CI reporting, not a functional gap. -Carry-forward from NF-3v4. Not blocking for SHIP. - ---- - -### NF-4 (v5): GG-1 $mockPath identity implicit, not stated in row (LOW) - -GG-1 Input: "Mock returns OneDrive path." GG-1 Assertion: "Test-Path $mockPath." - -Section 5 header establishes that GG-1 creates a temp dir and $mockPath is within it. -The GG-1 row itself never states that $mockPath is a temp path -- an engineer reading only -the GG-1 row could interpret "OneDrive path" literally and attempt to write to a real -OneDrive directory in CI. - -The Section 5 header guidance is sufficient for a careful implementer. LOW; not blocking. - ---- - -## BeforeEach Reference (NF-4v4) -- RESOLVED - -v5.1 Section 5: "immediately before each Test-Scenario call (not a BeforeEach block -- -Test-Scenario has none)." The misleading Pester terminology is gone. NF-4v4 RESOLVED. - ---- - -## GG-7 Assertion Completeness - -GG-7 asserts $result -eq $FallbackPath (fallback returned). "Warning logged" is in the -Expected column but has no corresponding assertion. This is acceptable: capturing -Write-Warn output requires stream redirection that is disproportionate for this harness. -The fallback-returned assertion proves the correct code path was taken. OK. - ---- - -## Cross-Test Collision Check - -Each disk-writing GG test (GG-1, GG-4, GG-5) uses a New-Guid-seeded directory name. -GUID collision probability is negligible; $env:TEMP is per-user and per-session on CI. -No cross-test path collisions possible. Clean. - ---- - -## Implementation-Ready Verdict - -**YES.** A competent engineer can write all of GG-1..GG-7 from v5.1 without inference gaps -that risk false greens or destructive CI behavior: -- GG-7 exe is specified ('powershell'). -- Temp path pattern is specified (Section 5, $env:TEMP + New-Guid + finally). -- $ps51Fallback/$ps7Fallback overrides are stated (GG-4 row; Section 5 header). -- LASTEXITCODE reset ordering is stated (Section 5 header). -- Mock isolation model is stated (child-scope, file-scope redefinition before each call). -- BeforeEach Pester reference is corrected. - -Four LOWs remain (encoding assertion, middle-of-file regex exercise, skip-as-pass, -GG-1 mockPath identity). None introduce false greens or destructive behavior. All are -acceptable omissions for a vertical slice. - ---- - -## Summary Matrix - -| # | Concern | v4 Status | v5.1 Status | -|---|---------|-----------|-------------| -| C-1 | GG-7 exe spec | NEW MEDIUM | RESOLVED | -| C-2 | TestDrive without Pester | NEW MEDIUM | RESOLVED | -| F-3 | LASTEXITCODE reset position | NEW MEDIUM | RESOLVED | -| H1 | -Encoding ASCII in orphan-strip | NEW HIGH | RESOLVED (patch); NF-1 LOW: no encoding assertion | -| H2 | LASTEXITCODE contamination | NEW MEDIUM | RESOLVED | -| H3 | TestDrive -> temp path | NEW MEDIUM | RESOLVED | -| H4 | GG-7 exe | NEW MEDIUM | RESOLVED (= C-1) | -| H5 | $local: undefined under StrictMode | NEW MEDIUM | RESOLVED | -| F-4 | Regex diverges from production | NEW MEDIUM | RESOLVED (patch); NF-2 LOW: middle-of-file not exercised | -| F-5 | $beginMarker/$endMarker undefined | NEW LOW | RESOLVED | -| NF-3v4 | C-2/C-3 skip-as-pass | LOW | CARRY-FORWARD LOW (= NF-3 here) | -| NF-4v4 | BeforeEach reference | LOW | RESOLVED | -| NF-1 (new) | H1 no encoding assertion | -- | LOW | -| NF-2 (new) | F-4 middle-of-file not tested | -- | LOW | -| NF-4 (new) | GG-1 mockPath identity implicit | -- | LOW | - ---- - -## Verdict: SHIP - -All MEDIUM and higher concerns are resolved. Algorithm is correct. Mock scaffold is -unambiguous. Test harness approach (non-Pester, Test-Scenario, child-scope) is consistent -with existing suite patterns. Four LOWs are documented but do not block implementation. - -**Grilled by:** Chip (Tester) -**Date:** 2026-05-27 -**Session:** 441-grill-v5 diff --git a/docs/plans/441-grill-chip.md b/docs/plans/441-grill-chip.md deleted file mode 100644 index 2fc10401..00000000 --- a/docs/plans/441-grill-chip.md +++ /dev/null @@ -1,334 +0,0 @@ -# Chip's Grill -- Plan for #441 -**Date:** 2026-05-27 -**Verdict:** Revise - ---- - -## Untested Assumptions - -1. **`$TestDrive` is available in the test harness.** Plan Pattern A uses `$TestDrive` in its mock - scaffold. `$TestDrive` is a Pester-specific variable. The existing `test_windows_setup.ps1` - uses a CUSTOM `Test-Scenario`/`Write-Skip` harness -- NOT Pester. `$TestDrive` will be `$null` - at runtime, silently directing writes to the filesystem root or throwing. This is a - showstopper for Pattern A as written. - -2. **`$PROFILE` is assignable in the test host.** The existing Group C tests do - `$PROFILE = $c2Profile` and `$PROFILE = $c3Profile`. This works in PS 5.1 but THROWS in - PS 7+: "Cannot overwrite variable PROFILE because it is read-only or constant." Goofy's GG - tests inherit this harness pattern without acknowledging that it only runs clean on one of - the two target hosts. - -3. **`Invoke-HostQuery` is a real function in production code.** Pattern B requires the - production implementation to expose `Invoke-HostQuery` as an overridable wrapper. The - pseudo-code shows it, but the plan never explicitly mandates it as an implementation - requirement. If the implementer inlines `& $HostExe -NoProfile -Command '$PROFILE'` directly - inside `Resolve-ProfilePath` without the wrapper, Pattern B tests are untestable. - -4. **`Select-Object -Unique` deduplicates Windows paths correctly.** The plan's dedup step uses - `| Select-Object -Unique`. On Windows, paths are case-insensitive but `Select-Object -Unique` - is case-sensitive in PowerShell. `C:\Users\foo\OneDrive\Documents\PowerShell\...` and - `c:\users\foo\onedrive\documents\PowerShell\...` are NOT deduplicated. No test covers this. - -5. **Write-Info output is capturable in the test harness.** GG-9 asserts "Write-Info output - contains the mocked OneDrive path." Write-Info calls Write-Host, which writes to the - information stream (stream 6), not stdout. Capturing it requires `*>&1` or `6>&1` - redirection. The existing harness never does this. GG-9 as described cannot pass or fail -- - it will silently not capture anything. - -6. **The resolved path fits in 260 characters.** OneDrive tenant paths can be long: - `C:\Users\firstname.lastname\OneDrive - Contoso Corp Limited\Documents\WindowsPowerShell\Microsoft.PowerShell_profile.ps1` - is 143 chars just as a base. Usernames + tenant names in corporate environments frequently - exceed 260 chars total. On systems without long-path support (`LongPathsEnabled = 0`), - `New-Item` and `Add-Content` silently fail or throw PathTooLongException. Not in edge cases; - not tested. - -7. **The old hardcoded path and the new resolved path cannot collide in a way that causes double-strip.** - The cleanup pseudo-code checks `$profilePaths -notcontains $legacy` -- a case-sensitive - contains check -- before stripping the legacy file. If the paths match except for case, the - cleanup SKIPS the strip on the file that IS the write target, then the write loop appends a - NEW block on top of the un-stripped existing block. Double-write. Not tested. - -8. **A `pwsh` installed during the same setup run is reachable by `Get-Command`.** If winget - installs PS 7+ earlier in setup.ps1, the new pwsh.exe is not on PATH in the current process - until the terminal restarts (a known Windows PATH refresh problem). `Get-Command pwsh` fails, - fallback triggers, and the block lands at the hardcoded path -- not the OneDrive path. This - is exactly the bug we are fixing, and it silently regresses on first-run installs. Open - Question 3 acknowledges this but the test plan has no coverage. - ---- - -## Missing Edge Cases - -9. **Partial/corrupt block (BEGIN present, END absent).** If setup was killed mid-write, - the profile may contain `# BEGIN dev-setup profile` but no `# END dev-setup profile`. - The strip regex `(?s)\r?\n..BEGIN...*?..END..\r?\n?` does NOT match a partial block - (lazy `.*?` requires END to be present). The next run appends a new complete block on top - of the orphaned BEGIN. After two interrupted runs the file has two BEGIN sentinels and one - END. The idempotency claim breaks. - Repro: `Write-Output "# BEGIN dev-setup profile`n# partial" | Set-Content $testProfile`, - then call `Write-PowerShellProfile`. Verify only one BEGIN exists. - -10. **Profile file exists but is read-only (Group Policy locked or `attrib +R`).** `Add-Content` - throws "Access to the path ... is denied." The catch+continue pattern handles it, but no - test verifies (a) the catch fires, (b) the function continues to the second path rather - than aborting entirely, and (c) `Write-Err` is emitted, not `Write-Ok`. - Repro: `Set-ItemProperty $testProfile -Name IsReadOnly -Value $true` before calling the - function. Assert Write-Ok is NOT in output and the function did not throw. - -11. **Profile directory is a symlink/junction to a disconnected network drive.** `Test-Path` - on a dangling junction returns `$false` on Windows (junction exists, target does not). This - is different from E8 (UNC path offline) -- here `New-Item -Force` is called because - `Test-Path` returned false, and `New-Item` on a junction parent may succeed or fail - depending on driver. Not covered by any edge case entry. - Repro: Create a directory junction `mklink /J "$env:TEMP\fake_docs" "Z:\nonexistent"`; - set the resolved path to point through it; call `Write-PowerShellProfile`; verify error - is caught and reported. - -12. **`$PROFILE` host query returns multi-line output (E6) -- "take first non-empty line" is - not in the pseudo-code.** Goofy lists E6 in the table but the `Resolve-ProfilePath` - pseudo-code only calls `$resolved.Trim()`. Trim() does NOT take the first line of a - multi-line string -- it only strips leading/trailing whitespace. A `$PROFILE` that returns - two lines (e.g., a corporate bootstrap that Write-Hosts a banner before printing the path) - results in a path containing a newline character, which `New-Item` will reject. - Repro: Mock `Invoke-HostQuery` to return `"Banner text`nC:\path\to\profile.ps1"`. - Assert `Resolve-ProfilePath` returns ONLY the path line. - -13. **Long profile path > 260 chars on a system without LongPathsEnabled.** - Repro: Construct a mock `Invoke-HostQuery` response that returns a 270-char path. - Call `Write-PowerShellProfile`. On a system with - `HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem\LongPathsEnabled = 0`, assert - Write-Err is emitted and the function continues to the next path. - -14. **ConstrainedLanguage Mode (CLM) blocks `Add-Content` on the profile file.** E11 notes - "Writing the file may fail if CLM also locks filesystem writes." The plan says "emit - Write-Warn and proceed" but there is no test that verifies this behavior. The existing - test suite has no CLM test at all. - -15. **Legacy orphan on KFM system where BOTH the legacy path and the resolved path exist.** - Scenario: User ran setup with old code (block at `$HOME\Documents\PowerShell\...`) then - also has a real OneDrive Documents profile at the resolved path with unrelated content. - The cleanup should strip only the dev-setup block from the legacy file, leaving the - resolved file's existing content untouched. No test verifies this. - ---- - -## Test Plan Gaps - -16. **No test for legacy orphan cleanup at all.** Section 6 of the plan describes the cleanup - algorithm in detail. GG-1 through GG-10 contain ZERO tests that: - - Create a file at the hardcoded legacy path containing the dev-setup block - - Set the resolved path to something different - - Call `Write-PowerShellProfile` - - Assert the legacy file no longer contains the BEGIN..END block - - Assert the resolved file contains the BEGIN..END block - This is the entire value proposition of the backward-compatibility section. It is - completely untested. - -17. **Idempotency tested only twice (2 runs). Three-run test missing.** Group C-3 tests 2 runs. - The existing comment says "run setup.ps1 twice. Three times." No GG test calls - `Write-PowerShellProfile` three times in succession and asserts the file size and block - count are unchanged. The strip+re-inject pattern creates a subtle risk: run 1 strips - nothing (file empty), injects block; run 2 strips block, injects block; run 3 should - strip block, inject block. If there is any off-by-one in the strip regex (e.g., it strips - one too many leading newlines), the file shrinks by one byte each run and three runs proves - that two runs did not. - -18. **No test that the SECOND profile path gets its own independent idempotency guarantee.** - C-3 only tests one profile path (via `$PROFILE` override). With the new two-path logic, - we need idempotency verified on EACH resolved path independently AND when both paths are - active simultaneously. - -19. **GG-5 (dedup) does not verify write actually occurred.** GG-5 verifies that - `$profilePaths` after dedup has length 1. But it does not verify that the function - WROTE the content to that one file. A bug that deduplicates but then writes nothing - would pass GG-5. - -20. **No test that the function handles `$HOME` being an empty/null string.** If the `HOME` - environment variable is unset (exotic CI, Docker), the fallback construction - `[System.IO.Path]::Combine($HOME, 'Documents', ...)` produces a relative path. No test - covers this; the function would write to a relative path silently. - -21. **GG-S1 static assertion is too narrow.** Checking for literal `$HOME, 'Documents'` does - not catch `"$HOME\Documents"` string interpolation, `$env:USERPROFILE + '\Documents'`, - or `Join-Path $HOME Documents`. A regex-based check for any of these patterns is needed, - or the static test provides false confidence. - -22. **No test for execution policy `Restricted` blocking the child process query.** Risk R2 - notes this. Plan says "-Command (not -File) should work under Restricted." This claim is - unverified by any test. It needs a test that simulates `Get-ExecutionPolicy` returning - Restricted and verifies the child process launch either succeeds or falls back gracefully. - -23. **No test that `Set-Content -NoNewline -Encoding ASCII` during strip does not corrupt a - UTF-8-with-BOM profile.** If the existing profile was saved as UTF-8 BOM, overwriting - with ASCII via Set-Content strips the BOM but also converts the ENTIRE file to ASCII, - potentially corrupting any non-ASCII characters in user content. No test covers this - encoding interaction. - ---- - -## CI Concerns - -24. **OneDrive KFM cannot be simulated on GitHub Actions `windows-latest`.** OneDrive client is - not installed; KFM registry keys are absent; `[Environment]::GetFolderPath('MyDocuments')` - returns the non-redirected default path. Mocked unit tests (Pattern B) can run, but there - is no integration gate that exercises the ACTUAL `& powershell -NoProfile -Command '$PROFILE'` - resolution on a KFM-redirected machine. The fix can ship and silently fail on any KFM - machine without a CI failure. - Recommendation: add a manual integration test checklist item that MUST be run on a real - KFM machine before merge. Document this explicitly in the PR template for issue #441. - -25. **Child process spawning (`& powershell -NoProfile ...`) adds 2-6 seconds per run in CI.** - GitHub Actions runners are slow for process spawning. Each host query spawns a new - PowerShell process. Two hosts = ~4-10 seconds added to the test run. This is probably - acceptable for a setup script but should be measured. If tests mock `Invoke-HostQuery` - correctly, CI tests should NOT spawn real child processes at all -- verify the mock is - active before the child process call, not after. - -26. **PS 5.1 `powershell.exe` is available on `windows-latest` but its behavior differs from - real user machines.** GitHub Actions runners have `powershell.exe` v5.1 but the Documents - folder is NOT redirected via OneDrive. Tests that probe `$PROFILE` resolution via the real - child process will always return the non-KFM path, which passes GG-1 for the wrong reason - (the mock path happens to equal the fallback). Tests MUST use the mocked `Invoke-HostQuery` - and NOT rely on the live child process output to verify correctness. - -27. **The test file now imports `lib/profile-path.ps1` (new file).** If the lib file does not - exist yet (Goofy's implementation is incomplete), every test in Group GG that dot-sources - it will throw at load time, crashing the entire `test_windows_setup.ps1` run and causing - ALL groups (A through G, previously passing) to report as zero tests run. Guard the import. - ---- - -## Manual Smoke Test I'd Actually Run - -Run these steps IN ORDER on a real Windows machine that has OneDrive KFM active. If no KFM -machine is available, simulate it via registry: - -``` -reg add "HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders" ^ - /v "Personal" /t REG_SZ /d "C:\Users\%USERNAME%\OneDrive\Documents" /f -``` - -1. **Verify the bug is reproducible before the fix.** - Open a NEW PowerShell 7 terminal. Run: `$PROFILE` -- note the path (should contain - `OneDrive`). Open Windows Explorer and verify the path the OLD script would write to - (`$env:USERPROFILE\Documents\PowerShell\Microsoft.PowerShell_profile.ps1`) either does - not exist or does not contain the dev-setup block. Run `gs` or `gpl` -- confirm they - are NOT defined. - -2. **Capture baseline file state.** - Note whether `$env:USERPROFILE\Documents\PowerShell\...` and the OneDrive path exist - and their sizes. - -3. **Run setup once with the fix.** - ``` - powershell -ExecutionPolicy Bypass -File setup.ps1 - ``` - Watch for these in the output (required per acceptance criterion): - - "Resolved powershell profile: C:\Users\...\OneDrive\Documents\WindowsPowerShell\..." - - "Resolved pwsh profile: C:\Users\...\OneDrive\Documents\PowerShell\..." - - "Profile written: [OneDrive path] (N bytes)" - - No "Profile written" line pointing to $HOME\Documents (non-OneDrive) - -4. **Verify the correct file was written.** - `Get-Content (& pwsh -NoProfile -Command '$PROFILE')` -- confirm it contains - `# BEGIN dev-setup profile` and `# END dev-setup profile` as distinct lines. - `Get-Content (& powershell -NoProfile -Command '$PROFILE')` -- same check. - -5. **Open a NEW terminal (critical -- must be a fresh session to load the profile).** - Run: `gs`, `gpl`, `ep` -- all must work. Run `Get-Alias gs` -- must resolve to - `Get-GitStatus`. If any alias is missing, the fix failed. - -6. **Run setup a second time (idempotency check).** - Note the file sizes before the second run. Run setup again. Check file sizes are - unchanged. Run `Select-String -Path $PROFILE -Pattern "BEGIN dev-setup profile"` -- - must return exactly ONE match. - -7. **Run setup a third time.** Same size check. Same single-match check. - -8. **Legacy orphan cleanup check (KFM scenario only).** - Manually create the block at the hardcoded path: - ```powershell - $legacy = "$env:USERPROFILE\Documents\PowerShell\Microsoft.PowerShell_profile.ps1" - New-Item -Force -ItemType Directory (Split-Path $legacy) | Out-Null - Add-Content $legacy "# BEGIN dev-setup profile`n# fake`n# END dev-setup profile" - ``` - Run setup again. Check that `$legacy` no longer contains the BEGIN marker. - Check that the OneDrive path still has exactly one BEGIN..END block. - -9. **Uninstall check.** - Run `scripts\windows\uninstall.ps1`. Open a NEW terminal. Run `gs` -- must be - undefined (or return the system default if one existed before). Check BOTH the OneDrive - path and the legacy hardcoded path -- neither should contain the dev-setup block. - -10. **Re-run setup after uninstall (round-trip idempotency).** - Run setup again. Open a new terminal. Confirm aliases work. This catches any state the - uninstall leaves behind that breaks a clean re-install. - ---- - -## If This Goes To Revision - -**Revision owner:** Mickey (NOT Goofy -- original author is excluded per team charter) - -**Required changes (ordered):** - -1. Replace all `$TestDrive` references in Pattern A with an explicit temp directory created - via `$script:GGTempDir = Join-Path $PSScriptRoot "temp_gg_$(Get-Random)"` and cleaned up - in a `finally` block. Do NOT assume Pester is available. - -2. Fix the `$PROFILE` mutability problem. In PS7+, `$PROFILE` cannot be assigned directly. - The test harness must stop using `$PROFILE = $path`. Instead, use the `Invoke-HostQuery` - mock pattern exclusively (Pattern B). Remove Pattern A from the plan entirely or mark it - PS5.1-only with a runtime version guard. - -3. Mandate that `Invoke-HostQuery` is a named, overridable function in the production - implementation of `Resolve-ProfilePath`. Add this as an explicit implementation - requirement (not just a test pattern). Without this, the tests cannot mock the child - process. - -4. Fix `Select-Object -Unique` to use case-insensitive dedup on Windows paths: - `$profilePaths | Sort-Object { $_.ToLower() } -Unique` - or use a HashSet with `StringComparer.OrdinalIgnoreCase`. Add test GG-5b covering - paths that differ only in case. - -5. Add the multi-line output guard to `Resolve-ProfilePath` pseudo-code. After `Trim()`, - add: `$resolved = ($resolved -split '\r?\n' | Where-Object { $_ -ne '' } | Select-Object -First 1)`. - Add test for this (GG-8b: Invoke-HostQuery returns banner + path on two lines). - -6. Add legacy orphan cleanup tests. At minimum: - - GG-11: legacy path exists with dev-setup block, resolved path differs -- after run, - legacy block is gone, resolved path has the block. - - GG-12: legacy path and resolved path are the same -- no double-strip. - - GG-13: legacy path exists with ONLY user content (no sentinel) -- run does not touch it. - -7. Add three-run idempotency test (GG-14): call `Write-PowerShellProfile` three times in - a loop on a temp file; assert block count == 1 and file size is stable after runs 2 and 3. - -8. Add read-only file test (GG-15): create temp profile, set IsReadOnly = $true, call - function, assert Write-Err was emitted (redirect stream 3) and function did not throw. - -9. Add partial-block test (GG-16): create temp profile with only the BEGIN marker and no - END, call function twice, assert exactly one BEGIN and one END exist after both calls. - -10. Document OneDrive KFM integration test as a MANUAL GATE in the PR checklist. Add a - `[MANUAL GATE -- KFM machine required]` section to the PR template for issue #441. - CI cannot substitute for this. - ---- - -## Final Verdict - -The architecture in Goofy's plan is sound -- asking the host for its own `$PROFILE` is the -correct fix and the edge case analysis is thorough. However, the test plan has multiple -showstopper gaps that would let the fix ship with no actual test coverage of its core -value: the legacy orphan cleanup is completely untested, the `$TestDrive` reference -breaks the harness on the existing non-Pester scaffold, and the `$PROFILE` direct -assignment pattern fails silently in PS7+ (the very host this fix targets). A plan with -this many untested paths is not approvable as written. - -The revision required is primarily TEST PLAN surgery, not algorithm surgery. Goofy's -algorithm is defensible -- the tests just do not prove it. The case-insensitive dedup bug -and the multi-line Trim() gap are also real code defects that must be corrected before -implementation. Mickey must own the revision to enforce architectural consistency across -the test harness, since the PS7+ `$PROFILE` mutability issue cuts across the entire Group -C pattern and a redesign affects all existing profile tests, not just Group GG. diff --git a/docs/plans/441-grill-doc-v3.md b/docs/plans/441-grill-doc-v3.md deleted file mode 100644 index 9fca7bfd..00000000 --- a/docs/plans/441-grill-doc-v3.md +++ /dev/null @@ -1,174 +0,0 @@ -# Doc's Fact-Check -- Plan #441 v3 (NEW CLAIMS ONLY) - -**Date:** 2026-05-27 -**Verdict:** REVISE-DEFER -**Session:** 441-grill-doc-v3 - ---- - -## V3-Specific Claims Verified - -### 1. Banner Output with -NonInteractive and -NoLogo - -**CLAIM:** `powershell -NoProfile -NonInteractive -Command '$PROFILE'` emits a single-line string -- no copyright banner on stdout. Verified locally: `-NoProfile` allows $PROFILE query without loading profile. - -**VERDICT:** [VERIFIED] -- **Test Result:** Ran `powershell -NoProfile -NonInteractive -NoLogo -Command '$PROFILE'` and received single-line output containing only the resolved path (C:\Users\Earl Tankard\Documents\WindowsPowerShell\Microsoft.PowerShell_profile.ps1). -- **Banner behavior:** Both tests (with and without -NoLogo) returned only the path. When `-NonInteractive` is present, no banner appears on stdout in either case. -- **Source:** Direct testing on Windows PowerShell 5.1 and PowerShell 7.6 in dev environment. -- **Implication:** v3-D1 decision is sound. `-NoLogo` provides explicit suppression; `-Last 1` is an additional defense-in-depth measure. Donald's local verification confirmed. - ---- - -### 2. -NoLogo Flag Validity (PS 5.1 and PS 7) - -**CLAIM:** `-NoLogo` is a valid flag for `powershell.exe` (PS 5.1) AND for `pwsh.exe` (PS 7). - -**VERDICT:** [VERIFIED] -- **PS 5.1:** Flag confirmed in `powershell -?` help output. -- **PS 7:** Flag confirmed in `pwsh -?` help output. -- **Source:** Microsoft Learn -- about_powershell_exe (PS 5.1) and about_pwsh (PS 7). Both explicitly list `-NoLogo` in their parameter syntax. -- **Citation:** https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_powershell_exe and https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_pwsh -- **Implication:** v3-D1 is sound. Both hosts support the flag; algorithm can use it uniformly. - ---- - -### 3. & $Exe Exit Code Behavior (No Exception, $LASTEXITCODE Set) - -**CLAIM:** `& $Exe` "exits non-zero without throwing; `try/catch` does not fire." - -**VERDICT:** [VERIFIED] -- **Test Result:** Invocation operator `&` did NOT throw an exception even when the invoked process exited with non-zero code (tested with `cmd.exe /c "exit 1"`). -- **Try/Catch:** Exception was NOT caught. `$caught` remained `$false`. -- **$LASTEXITCODE:** Correctly set to the process exit code (e.g., 1, 7, 8). -- **Source:** Direct testing; consistent with PowerShell semantics where `&` operator returns exit code via `$LASTEXITCODE`, not exceptions. -- **Implication:** v3-D2 decision is sound. Checking `$LASTEXITCODE -ne 0` is the correct pattern, not exception handling. - ---- - -### 4. $PROFILE Read-Only in PS7+ - -**CLAIM:** "Both tests assign `$PROFILE = $path`, read-only in PS7+. Full refactor deferred; behavior these tests cover is superseded by this PR." - -**VERDICT:** [FALSE / REVISE-DEFER] -- **Test Result:** Direct assignment to `$PROFILE` in PowerShell 7.6 SUCCEEDED without error: - ```powershell - $PROFILE = "C:\test\path" # No error thrown; assignment succeeded - ``` -- **Microsoft Documentation:** https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_automatic_variables states: "Conceptually, most of these variables are considered to be read-only. Even though they _can_ be written to, for backward compatibility they _should not_ be written to." -- **Reality vs Claim:** `$PROFILE` is NOT technically read-only; it CAN be assigned. It is *conceptually* read-only (should not be written to), but the engine does not block assignment. -- **Pester Context Note:** The plan mentions guarding C-2 and C-3 tests with a version check because they assign `$PROFILE = $path`. If these tests are failing on PS7+, the cause is not a read-only restriction. It may be a Pester/TestDrive-specific behavior or a different error. The plan correctly defers the full refactor and uses a guard (`if ($PSVersionTable.PSVersion.Major -ge 7) { skip }`), which is the right mitigation. -- **Implication:** The v3 decision to guard the tests (not refactor) is CORRECT. The characterization of $PROFILE as "read-only in PS7+" is INACCURATE but does not block the plan, since the guard prevents the problematic assignment anyway. -- **Action:** Document in v3 decision notes that $PROFILE is conceptually (not technically) read-only; the guard works regardless. - ---- - -### 5. Sort-Object -Unique Dedup Behavior - -**CLAIM:** "`Sort-Object { $_.ToLower() } -Unique` ... `-Unique` keys on the script block output". - -**VERDICT:** [VERIFIED] -- **Test Input:** @("apple", "APPLE", "Apple", "banana", "BANANA") -- **Test Result:** Output was @("apple", "banana") -- 2 items. -- **Dedup Mechanism:** The `-Unique` parameter applied deduplication BASED ON the script block output (`{ $_.ToLower() }`). Items with identical `.ToLower()` values were treated as duplicates and only the first was retained. -- **Source:** Direct testing; consistent with Sort-Object documentation (https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/sort-object). -- **Implication:** v3-D5 decision is sound. The algorithm's case-insensitive deduplication (line 110: `Sort-Object { $_.ToLower() } -Unique`) is verified to work as intended. - ---- - -### 6. 2>$null Effect on $LASTEXITCODE - -**CLAIM:** "`2>$null` on `& $Exe` -- does this affect `$LASTEXITCODE` propagation?" (Donald's v3 reads `$LASTEXITCODE` after a redirected call.) - -**VERDICT:** [VERIFIED - 2>$null DOES NOT AFFECT LASTEXITCODE] -- **Test A (without redirect):** `& cmd.exe /c "exit 8"` -> $LASTEXITCODE = 8 [ok] -- **Test B (with 2>$null):** `& cmd.exe /c "exit 7" 2>$null` -> $LASTEXITCODE = 7 [ok] -- **Implication:** Redirecting stderr with `2>$null` does NOT clear or affect the propagation of `$LASTEXITCODE`. v3 algorithm's check on line 92 (`if ($LASTEXITCODE -ne 0)` after `Invoke-HostQuery -Exe $HostExe`) is safe and correct. -- **Source:** Direct testing in PowerShell 7.6. - ---- - -### 7. Regex Rejection of UNC Paths - -**CLAIM:** Regex `'^[A-Za-z]:\\'` rejects malformed/UNC paths like `\\server\share\...` and validates that paths match Windows drive-letter format. - -**VERDICT:** [VERIFIED - CORRECT BY DESIGN] -- **Test Results:** - - `C:\Users\...\profile.ps1` -> MATCHES [ok] - - `E:\Data\...` -> MATCHES [ok] - - `\\server\share\...` -> DOES NOT MATCH (rejected) [ok] - - Variable paths, relative paths -> DOES NOT MATCH (rejected) [ok] -- **Plan Scope Check:** Section 2 explicitly lists "UNC paths" as OUT of scope: "UNC paths, long paths > 260 chars, Unicode usernames, partial/corrupt blocks -- file issue if reported." -- **Implication:** The rejection of UNC paths is INTENTIONAL and CORRECT per the plan's scope. If a user's profile resolves to a UNC path (network drive), they will fall back to the hardcoded path. This is documented as a known limitation and acceptable (users can file an issue for enhancement). -- **Source:** Direct regex testing on Windows paths; plan Section 2 scope definition. - ---- - -## Summary of v3 Deltas - -| Claim | Status | Impact | Citation | -|-------|--------|--------|----------| -| Banner + -NoLogo | VERIFIED | v3-D1 sound | Direct test | -| -NoLogo for both PS 5.1 and 7 | VERIFIED | v3-D1 sound | MS Learn + test | -| & operator non-throwing | VERIFIED | v3-D2 sound | Direct test | -| $PROFILE read-only in PS7+ | FALSE | Cosmetic (guard works anyway) | MS Learn about_automatic_variables | -| Sort-Object -Unique dedup key | VERIFIED | v3-D5 sound | Direct test | -| 2>$null doesn't affect LASTEXITCODE | VERIFIED | v3 algorithm safe | Direct test | -| Regex rejects UNC paths | VERIFIED | Correct by design (out-of-scope) | Direct test + plan scope | - ---- - -## Critical Issues - -**None.** All v3-critical claims either verified or mitigated by existing guards. - ---- - -## Cosmetic Issues - -**$PROFILE "read-only" characterization (Claim 4):** -- **Issue:** Plan states "$PROFILE ... read-only in PS7+" but testing shows it CAN be assigned. -- **Reality:** Microsoft docs: "$PROFILE ... Conceptually, most of these variables are considered to be read-only. Even though they _can_ be written to, for backward compatibility they _should not_ be written to." -- **Plan Impact:** NONE. The v3 mitigation (guard with version check, skip test in PS7+) works regardless of whether $PROFILE is technically or conceptually read-only. -- **Recommendation:** Update plan Section 3 D4 narrative to clarify: "$PROFILE is conceptually read-only; PS7+ guarded tests avoid assignment to prevent intentional misuse." - ---- - -## Counter-Hypotheses (v3-Specific) - -### H1: Does -NoLogo actually suppress the banner, or does -NonInteractive do all the work? -**Tested:** Both `-NonInteractive` alone and `-NonInteractive -NoLogo` produced identical output (just the path, no banner). Conclusion: `-NonInteractive` already suppresses the banner in this context. `-NoLogo` is redundant but provides explicit, documented intent. No contradiction; defense-in-depth. - -### H2: What if a child process (pwsh or powershell) fails to launch or is missing? -**Plan mitigation:** Get-Command check before invocation (line 86: `if (-not (Get-Command $HostExe -EA SilentlyContinue))`). Falls back safely. Verified in plan Section 4. - -### H3: What if the regex rejects a valid Windows path format? -**Tested:** Regex accepts single-letter drive paths (C:, E:, etc.). Rejects UNC, relative, and variable paths. Behavior is intentional per plan scope. No issue. - ---- - -## Verification Summary - -- **Date Verified:** 2026-05-27 (Session: 441-grill-doc-v3) -- **Test Environment:** Windows 11, PowerShell 7.6, PowerShell 5.1 -- **Method:** Direct testing + Microsoft Learn documentation review -- **All v3-critical claims:** Verified or properly mitigated -- **Process integrity:** No contradictions with prior v1 grill (v1 claims remain valid) - ---- - -## Recommendation - -**PROCEED with notation on v3-D4:** - -Update the plan's narrative for Section 3 D4 to read: - -> Decision: Guard C-2 and C-3 with `if ($PSVersionTable.PSVersion.Major -ge 7) { skip }`. The `$PROFILE` automatic variable is conceptually read-only in PowerShell (per Microsoft Learn); assigning to it is unsupported and may cause issues in test contexts. Full refactor deferred; behavior these tests cover is superseded by this PR. - -**Verdict:** PROCEED - ---- - -**Fact-Checked by:** Doc (Fact Checker) -**Date:** 2026-05-27 -**Session:** 441-grill-doc-v3 (worktree-441 read-only) diff --git a/docs/plans/441-grill-doc.md b/docs/plans/441-grill-doc.md deleted file mode 100644 index 3d68e09a..00000000 --- a/docs/plans/441-grill-doc.md +++ /dev/null @@ -1,178 +0,0 @@ -# Doc's Fact-Check -- Plan for #441 -**Date:** 2026-05-27 -**Verdict:** Proceed - ---- - -## Claims Verified - -### 1. $PROFILE.CurrentUserAllHosts Behavior -**[Verified]** -- `$PROFILE.CurrentUserAllHosts` exists in both PS 5.1 and PS 7+ and returns the path to the Current User, All Hosts profile. -- **Source:** Microsoft Learn: PowerShell Profiles (https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_profiles) -- **Evidence:** Documentation explicitly lists `$PROFILE.CurrentUserAllHosts` as a valid profile variable in both versions. - -### 2. OneDrive Known Folder Move (KFM) Redirection -**[Verified]** -- KFM does redirect Documents folder to `C:\Users\\OneDrive\Documents` (personal) or tenant-specific paths (business). -- **Source:** Microsoft Learn: OneDrive Redirect Known Folders (https://learn.microsoft.com/en-us/onedrive/redirect-known-folders) -- **Evidence:** Microsoft confirms KFM "redirects and move[s] known folders to OneDrive" including Documents. Environment variable `$env:OneDrive` points to the OneDrive root. -- **Note:** Plan correctly states this breaks the hardcoded `$HOME\Documents` assumption. - -### 3. $HOME Resolution on Windows -**[Verified]** -- `$HOME` on Windows equals `$env:USERPROFILE` (typically `C:\Users\`). -- **Source:** PowerShell automatic variable behavior + web search results -- **Evidence:** $HOME is set by PowerShell to the value of `$env:USERPROFILE` on Windows. Confirmed via testing in session. - -### 4. PS 5.1 vs PS 7+ Default Profile Paths -**[Verified]** -- Paths listed in plan are correct: -- PS 5.1 (powershell.exe): `C:\Users\\Documents\WindowsPowerShell\Microsoft.PowerShell_profile.ps1` -- PS 7+ (pwsh.exe): `C:\Users\\Documents\PowerShell\Microsoft.PowerShell_profile.ps1` -- **Source:** Microsoft Learn: PowerShell Profiles; web search results -- **Evidence:** Documentation and search results confirm both paths are the default "Current User, Current Host" profiles for their respective versions. - -### 5. pwsh -NoProfile -Command '$PROFILE' Behavior -**[Verified]** -- Command returns the RESOLVED profile path without executing the profile. -- **Source:** Testing + web search confirmation -- **Evidence:** Tested locally: `pwsh -NoProfile -NonInteractive -Command '$PROFILE'` returns full resolved path (C:\Users\Earl Tankard\Documents\PowerShell\Microsoft.PowerShell_profile.ps1). The `-NoProfile` flag prevents loading the profile but does NOT prevent resolution of the `$PROFILE` automatic variable. -- **Counter-verified:** `-NoProfile` does not skip resolution; PowerShell always sets `$PROFILE` when the engine starts, regardless of profile loading. - -### 6. Constrained Language Mode (CLM) -**[Verified]** -- CLM is a real Windows security feature enforced by Device Guard / AppLocker / policy. -- **Source:** Microsoft Learn: PowerShell Language Modes (https://learn.microsoft.com/en-us/powershell/scripting/learn/deep-dives/understanding-language-modes) -- **Evidence:** CLM exists as a security mechanism that restricts .NET object creation, reflection, COM, and certain cmdlet operations. -- **Plan claim validation:** Plan correctly states that launching a NEW process with `-NoProfile` escapes CLM of parent (new process, new language mode determination). Correct. - -### 7. Registry Keys for Shell Folder Redirection -**[Verified]** -- Registry keys and their roles are accurately described: -- **`HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders`** -- Contains fully expanded paths (read by Windows, not user-edited) -- **`HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\User Shell Folders`** -- Contains environment-variable-based paths (source of truth for edits) -- **Source:** Windows registry documentation + web search -- **Evidence:** Confirmed via search: Windows reads `User Shell Folders`, expands variables, populates `Shell Folders`. Plan does not edit Shell Folders directly (correct practice). - -### 8. PowerShell Profile URL Reference -**[Verified]** -- Issue #441 URL exists and is accessible. -- **Source:** Issue tracker (https://github.com/primetimetank21/dev-setup/issues/441) -- **Evidence:** Referenced issue is real; plan URL is well-formed. - -### 9. $PSVersionTable.PSVersion and Major Property -**[Verified]** -- Correct method for version detection. -- **Source:** Microsoft .NET Framework API documentation + web search -- **Evidence:** `$PSVersionTable.PSVersion.Major` correctly returns the major version number (e.g., 5 or 7) and is the standard pattern for version checks in PowerShell. - -### 10. CSIDL and GetFolderPath API -**[Verified]** -- Plan's mention of PowerShell using `[Environment]::GetFolderPath('MyDocuments')` (PS 5.1) or SHGetKnownFolderPath for {Personal} KNOWNFOLDERID (PS 7+) is correct technical foundation. -- **Source:** Microsoft .NET API documentation -- **Evidence:** .NET's `GetFolderPath` and Win32 `SHGetKnownFolderPath` both honor OS folder redirects and OneDrive KFM. - ---- - -## Counter-Hypotheses - -### H1: Does -NoProfile actually skip $PROFILE resolution? -**Tested:** No. `-NoProfile` prevents LOADING the profile but does not prevent RESOLUTION of the `$PROFILE` variable. -- **Evidence:** Confirmed via `pwsh -NoProfile -NonInteractive -Command '$PROFILE'` returned full path. -- **Implication:** Plan is sound; the resolver will work correctly. - -### H2: Does $PROFILE differ between PowerShell hosts (ISE, VSCode, etc.)? -**Confirmed:** Yes, different hosts have different CurrentHost profiles (e.g., Microsoft.PowerShell_profile.ps1 vs Microsoft.VSCode_profile.ps1), but plan correctly targets CurrentUserCurrentHost, not AllHosts. -- **Implication:** Plan decision to use `$PROFILE` (CurrentUserCurrentHost) instead of `$PROFILE.CurrentUserAllHosts` is justified; host-specific aliases belong in host-specific profiles. - -### H3: What if a user has manually set $PROFILE in a bootstrap script before our resolver runs? -**Analysis:** Plan's resolver queries the CHILD PROCESS via `& pwsh -NoProfile ...`, which starts a fresh engine with no parent bootstrap interference. The child process determines its own `$PROFILE` based on its environment and installed shell-folder settings. -- **Implication:** Correct approach; child process query gives the final answer. - -### H4: Does the fallback path ever get written if the host exists? -**Plan design:** Only if host query returns empty or throws an exception. If host exists and returns a path, the resolved path is used. -- **Implication:** Fallback is only a safety net; primary flow uses the host's authority. - ---- - -## Consistency Check - -### Cross-Check with .squad/decisions.md -- **No contradictions identified.** -- Plan does not violate any team policy (commit trailer, squash-merge directive, etc.). -- Plan follows existing lib/ pattern for shared helpers (approved pattern in codebase). -- No conflict with Sprint 16 or 17 directives. - -### Consistency Within Plan -- **Edge case coverage:** Plan lists 20 edge cases (E1-E20) with mitigations. All treated consistently: - - Resolved path preferred where available - - Fallback used only when resolver unavailable or fails - - Cleanup handles both old (hardcoded) and new (resolved) paths - - Deduplication prevents duplicate writes -- **Test plan alignment:** Test cases (GG-1 through GG-10 + GG-S1) directly map to algorithm decisions and edge cases. -- **Files-touched alignment:** All changed files support the algorithm (profile.ps1, uninstall.ps1, lib/profile-path.ps1, tests/test_windows_setup.ps1). - ---- - -## Load-Bearing Assumptions: Validated - -| Assumption | Validated? | Evidence | Risk | -|------------|------------|----------|------| -| `$PROFILE` is the source of truth for PS startup | YES | Microsoft Learn, plan reasoning sound | Minimal -- $PROFILE is always set by PS engine | -| OneDrive KFM causes `$HOME\Documents` mismatch | YES | Microsoft Learn confirms KFM redirects Documents | Minimal -- this is the problem plan solves | -| `-NoProfile` allows $PROFILE query without loading profile | YES | Tested; confirmed in PowerShell | Minimal -- tested behavior | -| Resolved path can be written to even if file doesn't exist | YES | Plan uses `New-Item -Force -ItemType Directory` | Minimal -- standard PowerShell pattern | -| Both PS 5.1 and PS 7+ set `$PROFILE` on startup | YES | Microsoft Learn; plan logic assumes this | Minimal -- core PowerShell behavior | -| Deduplication is safe (no loss of information) | YES | Plan explicitly handles duplicate resolution | Minimal -- explicit dedup check in plan | - ---- - -## Known Unknowns (from plan Section 9) - -Plan explicitly lists 6 known unknowns: -1. **`$PROFILE` vs `CurrentUserAllHosts` decision** -- Correctly deferred to Mickey/Earl. Plan recommends CurrentUserHost; awaiting architectural decision. -2. **PS 7 not on PATH** -- Plan acknowledges; secondary probe of known install paths may be needed (deferred as threshold unknown). -3. **Race condition on first PS 7 install** -- Plan acknowledges; needs confirmation of install order relative to PATH refresh (Issue #251 pattern). -4. **UNC path connectivity check** -- Plan acknowledges; not yet added. -5. **CLM and child process launch** -- Plan acknowledges; likely works but not verified in CLM environment. -6. **uninstall.ps1 lib dependency** -- Correctly identified; Option B (extract to lib/) preferred; Option A (inline) available as fallback. - -**Assessment:** All unknowns are explicitly listed, not hidden. Plan is transparent about these and proposes mitigation or deferral. Acceptable for "Draft" status. - ---- - -## Risks: Assessed - -Plan Section 10 lists 8 risks (R1-R8) with likelihood/impact/mitigation: -- **R1 (child process overhead):** Medium likelihood, low impact. Acceptable. -- **R2 (execution policy):** Low likelihood. `-Command` works even under Restricted; mitigation sound. -- **R3 (legacy cleanup collision):** Very low likelihood (unique sentinel). Acceptable. -- **R4 (case-insensitive dedup):** Low likelihood. Plan recommends case-insensitive compare; good catch. -- **R5 (lib dependency in uninstall):** Medium likelihood, medium impact. Fallback to Option A available. -- **R6 (PS preview variants):** Low likelihood, low impact. Known limitation; acceptable. -- **R7 (KFM policy applied post-install):** Low likelihood. Design covers via union of resolved + fallback paths. -- **R8 (Unicode path in logs):** Low likelihood. Write-Info handles Unicode; ASCII guard applies to file content only. - -**Assessment:** All risks are realistic and mitigations are sound. No show-stoppers. - ---- - -## Recommendation - -**PROCEED** - -### Rationale - -1. **Technical claims validated:** All 10 key technical claims (Windows/PowerShell behavior) are verified against Microsoft documentation and local testing. -2. **Algorithm sound:** The resolver design correctly uses the PowerShell host as the authority for profile paths, bypassing all hardcoded assumptions. -3. **Edge cases handled:** 20 edge cases documented with mitigations; test plan covers critical paths. -4. **Known unknowns transparent:** Plan explicitly lists 6 unknowns and proposes deferral or secondary investigation (none are blockers). -5. **Risks identified and mitigated:** 8 risks assessed; none are show-stoppers. -6. **No contradictions:** Plan does not conflict with team decisions or codebase conventions. -7. **Backward compatibility:** Cleanup strategy handles existing wrong installations. - -### Pre-Implementation Gate (Optional) - -Before starting implementation, recommend: -1. **Confirm architectural decision:** CurrentUserHost vs CurrentUserAllHosts (awaiting Mickey/Earl input per plan Section 9.1). -2. **Verify install order:** Confirm PS 7 install happens before `Write-PowerShellProfile` call (Issue #251 context). -3. **CLM environment test:** If available, verify child process launch (`& pwsh -NoProfile ...`) works under CLM constraint. - -These are not blockers; implementation can proceed in parallel with confirmation. - ---- - -**Fact-Checked by:** Doc (Fact Checker) -**Date:** 2026-05-27 -**Session:** 441-grill-doc (worktree-441 read-only) diff --git a/docs/plans/441-grill-donald-v4.md b/docs/plans/441-grill-donald-v4.md deleted file mode 100644 index fad724f0..00000000 --- a/docs/plans/441-grill-donald-v4.md +++ /dev/null @@ -1,228 +0,0 @@ -# Grill Report: #441 -- profile.ps1 writes to wrong path on OneDrive/KFM systems - -**Griller:** Donald (Implementation Lead -- authored v3; uniquely owns gap context) -**Plan reviewed:** docs/plans/441-profile-path.md (v4, author: Jiminy) -**Date:** 2026-05-27 -**Session:** 441-grill-v4 -**Verdict:** REVISE - ---- - -## Verdict - -REVISE. No single finding is a full ship-blocker, but P1's patch introduced a concrete -encoding bug (HIGH) that would silently corrupt legacy profile encoding on PS5.1 if -shipped as written. One MEDIUM finding (TestDrive contradiction with D2) forces the -implementer to guess. Two other MEDIUMs need notes to close. Algorithm is otherwise sound -and P1-P7 regression patches land correctly. - ---- - -## Regression Check (P1-P7) - -| # | Griller | v3 Finding | v4 Status | -|---|---------|-----------|-----------| -| P1 | Pluto | foreach body empty stub | RESOLVED -- inline strip regex + Set-Content + Write-Info filled in | -| P2 | Pluto | function boundary ambiguous | RESOLVED -- Write-PowerShellProfile wrapper explicit with comment explaining dot-source safety | -| P3 | Chip | GG-7 $LASTEXITCODE mock broken (function-local assignment) | RESOLVED -- `& $env:ComSpec /c "exit 1"` inside mock sets $LASTEXITCODE globally via native-command semantics; Doc-grill verified this pattern directly | -| P4 | Chip | C-2/C-3 skip unspecified + $PROFILE assignment outside Test-Scenario | RESOLVED -- `skip` replaced with if/Write-Host/return; `$PROFILE = $path` moved inside Test-Scenario body per v3-D4 | -| P5 | Chip | Mock isolation scope model undocumented | RESOLVED (with note) -- "child scope" model stated, redefinition before each test specified; no code skeleton but mechanism is unambiguous given confirmed Test-Scenario implementation (`& $Test`) | -| P6 | Chip | GG-4 both-hosts-same-path ambiguous | RESOLVED -- both mock calls explicitly return same $oneDrivePath; dedup to 1 entry stated; both legacy paths explicitly called orphaned | -| P7 | Doc | $PROFILE "read-only" characterization inaccurate | RESOLVED -- updated to "conceptually (not technically) read-only per MS Learn" | - -Note on P3 verification: `& $env:ComSpec /c "exit 1"` inside a PS function DOES set -`$global:LASTEXITCODE = 1`. PowerShell's $LASTEXITCODE is set at global scope by any -native-command invocation, regardless of the calling function's local scope. Doc confirmed -this with `cmd.exe /c "exit 7" 2>$null` -> $LASTEXITCODE = 7 (not reset by redirection). -The GG-7 mock mechanism is correct. P3 is fully resolved. - ---- - -## New Findings - -### [HIGH] F-1: Missing `-Encoding ASCII` in orphan-strip Set-Content (P1 introduced) - -**Citation:** Section 4, foreach body, line: `Set-Content $legacy $stripped.TrimEnd() -NoNewline` - -**Reference:** production profile.ps1 line 28: `Set-Content $profilePath $raw -NoNewline -Encoding ASCII` - -The P1 patch fills the empty stub with: -```powershell -Set-Content $legacy $stripped.TrimEnd() -NoNewline -``` - -Production line 28 uses `-Encoding ASCII` for all profile writes. The orphan-strip code -omits it. Default encoding for Set-Content: -- PS 5.1: UTF-16 LE with BOM -- PS 7+: UTF-8 without BOM - -Legacy orphan files were written by the old production code using ASCII. Rewriting without -`-Encoding ASCII` silently re-encodes the file. On PS5.1 this produces UTF-16 LE output -(PS5.1 can load UTF-16 LE profiles, so it does not break loading), but it: -1. Is inconsistent with the encoding contract established by production code -2. Is inconsistent with production line 28 -- an implementer sees two Set-Content calls - in the same function with different encoding behavior, which signals an undocumented - split that will generate future confusion -3. Could interact unexpectedly with downstream tooling that probes the profile as ASCII - -This is a net-new production bug introduced by Jiminy's P1 fix. The fix is one word: -add `-Encoding ASCII` to the orphan-strip Set-Content call. - ---- - -### [MEDIUM] F-2: "TestDrive" in GG-4 row contradicts Section 3 Decision 2 - -**Citation:** Section 5, GG-4 row (Input column): "BOTH `$ps51Fallback` AND `$ps7Fallback` -files exist in TestDrive seeded with BEGIN marker" - -**Reference:** Section 3, Decision 2: "Use the existing Test-Scenario harness with the -`Invoke-HostQuery` mock pattern. `$PROFILE` is read-only in PS 7+ and `$TestDrive` is -Pester-specific. Adding Pester is scope creep." - -"TestDrive" unambiguously refers to Pester's `TestDrive:` PSDrive. Section 3 D2 -explicitly rejects Pester and cites `$TestDrive` as Pester-specific. The GG-4 row then -references the same term in a test input description. - -An implementer reading the plan literally must choose one of: - (a) Use Pester TestDrive -- contradicts D2 - (b) Treat "TestDrive" as loose shorthand for "test temporary real-path" -- requires a guess - (c) Be confused and pause for clarification - -The intended interpretation is almost certainly (b): seed $ps51Fallback and $ps7Fallback -as real temporary paths (probably under $env:TEMP or a test subdir), create the files -there, run the function, assert the files are stripped. But the plan must say that instead -of "TestDrive." One sentence in the GG-4 input column closes this. - ---- - -### [MEDIUM] F-3: $LASTEXITCODE stale contamination from GG-7 into success-path tests - -**Citation:** Section 5, GG-7 row; Section 5, GG-1 through GG-6 mocks (implied) - -GG-7's mock calls `& $env:ComSpec /c "exit 1"`, which sets `$global:LASTEXITCODE = 1`. -Success-path mocks (GG-1 through GG-6) return paths via a function body that does NOT -call any native command. PowerShell does not reset $LASTEXITCODE to 0 between function -calls or test runs. If GG-7 runs before any success-path test, $LASTEXITCODE = 1 persists -into that test. Production code checks `if ($LASTEXITCODE -ne 0)` immediately after -`Invoke-HostQuery` returns -- a stale 1 causes the fallback branch to fire and the test -fails for the wrong reason. - -The plan specifies test redefinition-before-each-test for mock isolation, but says nothing -about $LASTEXITCODE state between tests. Two viable fixes: - - (a) Success-path mocks call `& $env:ComSpec /c "exit 0"` before returning, resetting - $LASTEXITCODE to 0 explicitly. Portable and self-contained. - (b) Plan states GG-7 must run last and documents the ordering requirement explicitly. - -Neither is currently stated. As written, a test runner that executes GG-7 first (e.g., -running only GG-7 during development, then full suite) leaves subsequent success tests -contaminated. - ---- - -### [MEDIUM] F-4: Orphan-strip regex diverges from production regex without rationale - -**Citation:** Section 4, strip regex: -`(?s)$([regex]::Escape($beginMarker)).+?$([regex]::Escape($endMarker))\r?\n?` - -**Reference:** production profile.ps1 line 27: -`(?s)\r?\n$([regex]::Escape($beginMarker)).*?$([regex]::Escape($endMarker))\r?\n?` - -Two differences introduced without explanation: - -1. Production has `\r?\n` PREFIX before the BEGIN marker (strips the preceding newline - together with the block). Plan's orphan-strip regex omits this prefix. Effect: when - block is in the MIDDLE of a file, plan's regex leaves an extra blank line that production - does not. `TrimEnd()` compensates only when the block is at the END of the file. - -2. Production uses `.*?` (zero-or-more); plan uses `.+?` (one-or-more). Edge case: a - BEGIN marker immediately followed by END marker (empty block) is handled by production - but not by the plan's regex. Trivial in practice but is a divergence. - -Note: the plan's regex is actually SUPERIOR for one edge case (block at START of file) -because the production regex requires `\r?\n` before BEGIN and would fail to match when -BEGIN is on line 1. However, introducing a different regex for the same conceptual -operation (stripping a BEGIN..END block) without documenting the divergence is a -maintainability trap. The implementer reading both the plan and production code will see -two regex patterns and not know which is authoritative. - -The plan should either adopt the production regex (with a note that TrimEnd() handles the -trailing newline) or document why the new regex differs. - ---- - -### [LOW] F-5: $beginMarker, $endMarker, $ps51Fallback, $ps7Fallback undefined in Section 4 snippet - -**Citation:** Section 4, `Write-PowerShellProfile` function body - -The Section 4 pseudocode references `$beginMarker`, `$endMarker`, `$ps51Fallback`, and -`$ps7Fallback` without defining them in the snippet. A comment says -"# Write to each resolved path (existing strip+re-inject logic)" -- indicating Section 4 -is a PARTIAL view of the function and definitions exist elsewhere in production. An -experienced implementer will infer these from production lines 12-19. However, for an -implementer treating the plan as a self-contained spec, the undefined variables create -ambiguity about exact values. A one-line comment citing "production lines 12-19 for -variable definitions" would close this cleanly. - ---- - -## Implementation-Readiness Verdict - -A competent engineer can implement the core algorithm from v4; P1-P7 patches are -substantively correct and the function boundary is now unambiguous. However, following -the plan LITERALLY produces two concrete bugs: (1) the orphan-strip Set-Content silently -changes file encoding on PS5.1 (F-1, HIGH, one-word fix), and (2) success-path GG tests -contaminated by GG-7's $LASTEXITCODE if run out of order (F-3, MEDIUM). The TestDrive -contradiction (F-2) forces an implementer guess. Three of four issues are trivial to -resolve with targeted wording changes; F-4 requires a sentence of rationale. Overall: -NOT ready to hand to an implementer today without the F-1 encoding fix at minimum. - ---- - -## Required Fixes for v5 - -1. **F-1 [HIGH].** Section 4 orphan-strip Set-Content: add `-Encoding ASCII` to match - production line 28. One word. - -2. **F-2 [MEDIUM].** Section 5 GG-4 Input column: replace "TestDrive" with an explicit - description of the real-path seeding mechanism (e.g., "$ps51Fallback and $ps7Fallback - set to real temp-directory paths; files created before calling Write-PowerShellProfile"). - -3. **F-3 [MEDIUM].** Section 5 header or GG-7 row: add one of -- (a) success-path mocks - call `& $env:ComSpec /c "exit 0"` to reset $LASTEXITCODE before returning, or (b) an - explicit note that GG-7 must run after GG-1 through GG-6 and state why. - -4. **F-4 [MEDIUM].** Section 4 strip regex: add a comment or footnote explaining the - intentional divergence from production line 27 (dropped `\r?\n` prefix, `.+?` vs `.*?`), - or adopt the production regex and note that `TrimEnd()` handles the trailing blank. - -5. **F-5 [LOW].** Section 4: add a comment directing the implementer to production lines - 12-19 for `$beginMarker`, `$endMarker`, `$ps51Fallback`, `$ps7Fallback` definitions. - ---- - -## What v4 Got Right - -- **P1 loop body**: Pluto's BLOCKING finding is filled. The regex is functional and the - `$isOrphan` condition is correct (`.Count -eq 0` is exact and readable). -- **P2 function boundary**: Comment explaining dot-source safety is clear and directly - addresses Pluto's ambiguity concern. Zero chance of misreading the scope now. -- **P3 GG-7 mock mechanism**: `& $env:ComSpec /c "exit 1"` is the right answer to Chip's - NF-1. Not a global assignment hack; uses native-command semantics, which is - self-documenting. -- **P4 C-2/C-3 guard**: Moving `$PROFILE = $path` INSIDE the Test-Scenario body is - the correct fix -- Chip called out that the assignment was outside and the guard inside - was therefore ineffective. Jiminy caught this precisely. -- **P6 GG-4 dual-path spec**: Both mock calls returning same $oneDrivePath is now - explicitly stated, ending the (a) vs (b) ambiguity Chip raised. -- **P7 $PROFILE wording**: Doc's correction accepted cleanly. "Conceptually not - technically" is accurate and closes the false claim without disturbing the guard logic. -- **Test-Scenario scope model**: Plan correctly identifies `& $Test` child-scope model - (confirmed in tests/test_windows_setup.ps1 lines 37-53). "Mocks in enclosing scope are - visible inside" is accurate for `& $block` execution. - ---- - -**Grilled by:** Donald (Implementation Lead) -**Date:** 2026-05-27 -**Session:** 441-grill-v4 diff --git a/docs/plans/441-grill-donald.md b/docs/plans/441-grill-donald.md deleted file mode 100644 index 50e7cfe7..00000000 --- a/docs/plans/441-grill-donald.md +++ /dev/null @@ -1,247 +0,0 @@ -# Grill Report: #441 -- profile.ps1 writes to wrong path on OneDrive/KFM systems - -**Griller:** Donald (Shell Developer -- shell/algorithm correctness angle) -**Plan reviewed:** docs/plans/441-profile-path.md (v2, author: Mickey) -**Date:** 2026-05-27 -**Verdict:** REVISE - -**Author locked out:** Mickey (v2 author), Goofy (v1 author) -**Eligible next reviser (if REVISE):** Pluto -- dominant holes are PS 5.1 behavior, Windows -path validation, and algorithm correctness; all squarely in Pluto's Config Engineer lane. - ---- - -## Angle: Shell / Algorithm Correctness - ---- - -### Findings - -**1. GG-6 cannot pass -- internal contradiction between Section 4 and Section 5** - -Section 4 algorithm (plan lines ~84-85): - -``` -$resolved = (Invoke-HostQuery -Exe $HostExe).Trim() -$resolved = ($resolved -split '\r?\n' | Where-Object { $_ } | Select-Object -First 1) -``` - -Section 5, GG-6: - -| GG-6 | Multi-line output | Mock returns "banner\npath" | Only path extracted | Return value contains no newline | - -`Select-Object -First 1` returns the FIRST non-empty line. When the mock returns -`"banner\npath"`, the first line is `"banner"`, not the path. The algorithm would -return the wrong line. GG-6's "Only path extracted" assertion would FAIL against the -code in Section 4. - -The plan acknowledges that banner output may exist but provides no filtering logic to -distinguish a banner line from a path line. The algorithm implicitly relies on the child -process emitting ONLY the path -- but GG-6 explicitly tests the contrary case with no -corresponding algorithm change to handle it. Section 4 and Section 5 cannot both be -correct as written. - -**Fix required:** Either (a) change the algorithm to take the LAST non-empty line -(profile paths come after any banner on stdout), or (b) add a path-shape validation -(e.g., line matches `^[A-Za-z]:\\`) to find the real path in multi-line output, and -update GG-6 accordingly. - ---- - -**2. `$LASTEXITCODE` unchecked -- silent fallback on broken PS 5.1 install** - -Section 4 `Resolve-ProfilePath` uses a `try/catch` to detect failures: - -``` -try { - $resolved = (Invoke-HostQuery -Exe $HostExe).Trim() - ... -} catch { - Write-Warn "Query failed for $HostExe -- fallback: $FallbackPath" - return $FallbackPath -} -``` - -In PowerShell, `& $Exe ...` (calling an external executable) does NOT throw on a -non-zero exit code. It sets `$LASTEXITCODE` and returns. If `powershell.exe -Command -'$PROFILE'` exits non-zero (e.g., execution policy blocks the `-Command` invocation, -or the PS 5.1 install is partially corrupt), the try block continues executing with -empty/null stdout. The `if ([string]::IsNullOrEmpty($resolved))` check silently returns -`$FallbackPath` with NO warning to the user -- the catch never fires. - -This is the exact silent failure pattern my charter prohibits: a broken PS 5.1 install -falls back to the old hardcoded path with no diagnostic, and the issue appears fixed -to the user (old aliases load from hardcoded path) while the real path is never written. - -`$LASTEXITCODE` must be checked immediately after `& $Exe -Command '$PROFILE'`: - -```powershell -$raw = & $Exe -NoProfile -NonInteractive -Command '$PROFILE' 2>$null -if ($LASTEXITCODE -ne 0) { - Write-Warn "$Exe exited $LASTEXITCODE -- fallback: $FallbackPath" - return $FallbackPath -} -``` - ---- - -**3. No path-shape validation on resolved output** - -After `Select-Object -First 1`, the returned string is accepted as a profile path -without any validation. The plan does not check whether the string: -- starts with a drive letter (`C:\`, `D:\`, etc.) -- contains the expected profile filename (`Microsoft.PowerShell_profile.ps1`) -- is a plausible Windows filesystem path at all - -If the child process emits any unexpected content on stdout (error message, partial -stack trace, PSReadLine prompt artifact, BOM), that string is passed to -`Split-Path $resolved` and subsequently to `New-Item -ItemType Directory`. On a badly -formed string, `Split-Path` may return `$null` or an empty string, and -`New-Item -ItemType Directory -Path ""` under `Set-StrictMode -Version Latest` will -throw -- but the error message will point at `New-Item`, not at the broken query. - -A one-liner guard: -```powershell -if ($resolved -notmatch '^[A-Za-z]:\\') { return $FallbackPath } -``` -would catch the majority of garbage values and give a meaningful fallback. - ---- - -**4. Trim-then-split order is defensible only if no stdout precedes the path** - -The `.Trim()` is applied to the ENTIRE multi-line output BEFORE splitting. This means: -- Leading/trailing whitespace/newlines on the full blob are stripped. -- But if ANY non-empty content appears on a line BEFORE the path value, `.Trim()` does - NOT help -- that content survives the split and `Select-Object -First 1` takes it. - -As noted in Finding 1, GG-6 tests exactly this case. The order is not the bug; the -missing path-shape filter is. However the comment "(handles PS5.1 banner output)" -in the plan (or the GG-6 row title "Multi-line output") implies the plan's author -believed this order would handle the banner case. It does not. - -This is not a standalone REVISE trigger -- it is covered by Finding 1 -- but the -reviewer should be aware the Trim-then-split ordering provides no protection against -a leading banner line. - ---- - -**5. `~15 lines` estimate for inlining is off by roughly 2x** - -Section 3, Decision 3: -> "The resolver is ~15 lines -- inlining is acceptable." - -Counting Section 4's two functions as written: -- `Invoke-HostQuery`: 4 lines -- `Resolve-ProfilePath`: ~18 lines (param block, guard, try/catch with 3 branches) -- `$profilePaths` construction + Sort-Object: 4 lines -- `$legacyPaths` + foreach loop stub: 5 lines - -Total: ~31 lines before the `$ps51Fallback`/`$ps7Fallback` variable definitions are -added. The "~15 lines" claim is roughly half the actual count. At 30+ lines, the -inlining decision is less clear-cut. If Findings 2 and 3 above add `$LASTEXITCODE` -checks and path-shape guards, the count grows further toward 40 lines. - -The decision in Section 3 should re-evaluate Option B (lib file) at the corrected -line count, or accept the higher inline count explicitly. - ---- - -**6. `Sort-Object { $_.ToLower() } -Unique` -- works, but lacks citation** - -`Sort-Object -Unique` with a script block compares adjacent sorted items by their -expression output, not by the original object. Two paths differing only by case will -have identical `.ToLower()` values and will be deduplicated correctly. This does work -in both PS 5.1 and PS 7. - -However, the plan asserts this behavior without a PS documentation reference or a -note that GG-3 is intended to confirm it empirically. Since the PowerShell docs for -`Sort-Object -Unique` with script blocks are sparse and the behavior can surprise -contributors, a brief inline comment or a note pointing to GG-3 as the confirmation -test would be appropriate. This is a minor documentation gap, not a blocking hole. - ---- - -**7. `Invoke-HostQuery` quoting -- CORRECT (no hole)** - -``` -& $Exe -NoProfile -NonInteractive -Command '$PROFILE' 2>$null -``` - -Single-quoted `'$PROFILE'` in the parent shell passes the literal string `$PROFILE` -to the child. The child PS process evaluates it and emits the profile path. This is -correct for both `powershell.exe` (5.1) and `pwsh.exe` (7+). Double-quoted -`"$PROFILE"` would expand in the PARENT shell (producing the PARENT'S profile path) -before the child sees it -- so the single-quoting here is intentional and correct. - ---- - -**8. `Get-Command $HostExe` -- PATH-order concern is low risk** - -`Get-Command 'powershell'` resolves the first `powershell.exe` on PATH. On most -Windows systems this is the only PS 5.1 binary and the path concern is theoretical. -The check is used only to gate whether to attempt the query; the same PATH ordering -that `Get-Command` uses is also used by `& $Exe`. So the two calls are consistent. -Not a blocking hole. - ---- - -### What v2 Got Right - -v2 improves substantially over v1: - -- **Testability seam via `Invoke-HostQuery`:** mandating a wrapper function that tests - can mock is the correct architecture. v1 had no seam and was untestable. - -- **Case-insensitive dedup:** v1 had no dedup at all. The `Sort-Object { $_.ToLower() } - -Unique` approach is correct (Finding 6 above confirms it works). - -- **Legacy cleanup design:** probing legacy paths and stripping orphaned blocks is the - right migration strategy. The isLegacy check using `.ToLower()` comparison is correct. - -- **Fallback pattern:** `$ps51Fallback` / `$ps7Fallback` as explicit named variables - rather than inline literals is cleaner than v1. - -- **Scope discipline:** OUT items in Section 2 are well-chosen. CLM, UNC, LongPaths, - and Unicode usernames are correctly deferred with a "file issue if reported" policy. - -- **`2>$null` on the child call:** redirecting stderr from the query child prevents - PS 5.1 startup warnings from polluting the captured output. Correct. - -- **Decision 4 (uninstall lib dependency):** correct diagnosis. `uninstall.ps1` must - be self-contained. Option A (inline) is the right answer; the line count estimate - is wrong, but the decision itself is sound. - ---- - -### Verdict - -REVISE. Two findings are blocking: - -**Finding 1** is a plan-internal contradiction: GG-6 asserts behavior that the -Section 4 algorithm cannot deliver. A griller cannot approve a plan where a named -test is structurally guaranteed to fail against the named algorithm. This must be -resolved before implementation. - -**Finding 2** is a shell correctness issue: unguarded `$LASTEXITCODE` on an external -process call is an anti-pattern in any shell script. It creates a silent failure path -on broken PS 5.1 installs -- the exact scenario the fix is designed to serve. - -Finding 3 (no path validation) and Finding 5 (line count) are secondary but should -be addressed in the same revision pass to avoid a second REVISE cycle. - -Findings 4, 6, 7, 8 are non-blocking and may be handled as implementation notes. - ---- - -## If Revision Needed - -**Revision owner:** Pluto -**Sections requiring change:** -- Section 4 algorithm: fix `Select-Object -First 1` vs. GG-6 contradiction - (add path-shape filter OR change to last-non-empty-line selection) -- Section 4 algorithm: add `$LASTEXITCODE` check after `& $Exe` call -- Section 4 algorithm: add path-shape guard before accepting resolved string -- Section 3 Decision 3: restate the line-count estimate (~31 lines, not ~15) -**Re-grill required after revision:** Yes -- Section 4 is substantively changed. -Scope the re-grill to Section 4 and Section 5 (GG tests); other sections are stable. diff --git a/docs/plans/441-grill-jiminy-v5.2.md b/docs/plans/441-grill-jiminy-v5.2.md deleted file mode 100644 index 392f1863..00000000 --- a/docs/plans/441-grill-jiminy-v5.2.md +++ /dev/null @@ -1,240 +0,0 @@ -# Grill Report: #441 -- v5.2 Verification (JN-1 / JN-2 patch by Mickey) - -**Griller:** Jiminy (Quality Auditor) -**Plan reviewed:** docs/plans/441-profile-path.md (v5.2, author: Mickey) -**Date:** 2026-05-27 -**Session:** 441-grill-v5.2 -**Verdict:** SHIP - ---- - -## Verdict Summary - -SHIP. JN-1 is fully resolved: `Write-PowerShellProfile` is parameterized; defaults match -production lines 17-18 exactly; no `$local:` shadowing of the fallback values; GG-1/GG-4/GG-5 -all invoke with explicit `-Ps51Fallback`/`-Ps7Fallback` temp paths; v5.2-D1 states the contract. -JN-2 is resolved for C-2; C-3 is implied but not spelled out (LOW gap, non-blocking). Two new -LOW findings documented below. No blocking issues. - ---- - -## JN-1 Verification - -### [JN-1-1] param() block contains -Ps51Fallback AND -Ps7Fallback with defaults - -- [x] PASS - -Section 4 `Write-PowerShellProfile`: - -``` -param( - [string]$Ps51Fallback = [System.IO.Path]::Combine($HOME, 'Documents', 'WindowsPowerShell', 'Microsoft.PowerShell_profile.ps1'), - [string]$Ps7Fallback = [System.IO.Path]::Combine($HOME, 'Documents', 'PowerShell', 'Microsoft.PowerShell_profile.ps1') -) -``` - -Both parameters present with defaults. PASS. - ---- - -### [JN-1-2] Defaults exactly match production lines 17-18 - -- [x] PASS - -Production `profile.ps1`: - -``` -line 17: [System.IO.Path]::Combine($HOME, 'Documents', 'WindowsPowerShell', 'Microsoft.PowerShell_profile.ps1') # PS 5.1 -line 18: [System.IO.Path]::Combine($HOME, 'Documents', 'PowerShell', 'Microsoft.PowerShell_profile.ps1') # PS 7+ -``` - -`$Ps51Fallback` default = line 17 verbatim. `$Ps7Fallback` default = line 18 verbatim. -Exact match confirmed. PASS. - ---- - -### [JN-1-3] NO $local:ps51Fallback / $local:ps7Fallback redefinition inside function body - -- [x] PASS - -Section 4 function body has ONLY: - -``` -$local:beginMarker = '# BEGIN dev-setup profile' -$local:endMarker = '# END dev-setup profile' -``` - -No `$local:ps51Fallback` or `$local:ps7Fallback` anywhere in the body. -The v5/H5 `$local:` definitions are gone; params replace them entirely. -No shadow bug. PASS. - ---- - -### [JN-1-4] GG-1, GG-4, GG-5 show explicit named-parameter invocation - -- [x] PASS - -| Test | Invocation in Section 5 | -|------|------------------------| -| GG-1 | `Write-PowerShellProfile -Ps51Fallback $tempPath51 -Ps7Fallback $tempPath7` | -| GG-4 | `Write-PowerShellProfile -Ps51Fallback $tempPath51 -Ps7Fallback $tempPath7` | -| GG-5 | `Write-PowerShellProfile -Ps51Fallback $tempPath51 -Ps7Fallback $tempPath7` 3x | - -All three disk-writing tests redirect to temp paths via named parameters. PASS. - ---- - -### [JN-1-5] Section 3 v5.2-D1 explains the parameter contract - -- [x] PASS - -Section 3 v5.2-D1 (verbatim): - -> `Write-PowerShellProfile` accepts optional `-Ps51Fallback`/`-Ps7Fallback` parameters. -> Defaults equal production lines 17-18. Production: call with no arguments (defaults apply; -> zero callsite changes). Tests: pass explicit temp-dir paths to redirect all disk writes -> away from real `$HOME` profile files. `$beginMarker`/`$endMarker` require no test override -> and remain `$local:` constants inside the function. - -Contract is stated. PASS. - ---- - -### [JN-1-6] v5/H5 supersession documented - -- [x] PASS (with minor cosmetic gap -- see NF-J3) - -The v5.2 changelog table JN-1 row documents the patch. Section 4 algorithm shows only the -parameterized form; `$local:ps51Fallback`/`$local:ps7Fallback` are absent from the current -algorithm. The Section 4 comment `# v5.2-JN-1: optional params allow test override; defaults -mirror production lines 17-18` confirms intent. - -Minor: the v5 changelog H5 entry still reads "Two `$local:` definitions added" without an -explicit "superseded by v5.2/JN-1" annotation. Cosmetic only -- see NF-J3 below. - -Functional supersession is complete. PASS. - ---- - -## JN-2 Verification - -### [JN-2-1] Write-Warning '[SKIPPED] ...' used in BOTH C-2 and C-3 - -- [ ] PARTIAL (C-2 explicit; C-3 inferred only) - -v3-D4 (updated for v5.2) explicitly shows: - -``` -if ($PSVersionTable.PSVersion.Major -ge 7) { - Write-Warning '[SKIPPED] C-2: PS7+ -- $PROFILE conceptually read-only; covered by GG tests' - return -} -``` - -C-3 is mentioned by name in "Guard C-2 and C-3 with proper skip" and in Section 7 AC#6 -("C-2 and C-3 guarded against PS7+ assignment error"), but no explicit -`Write-Warning '[SKIPPED] C-3: ...'` example appears in the plan. Implementer must infer C-3 -from C-2 pattern. Low risk (pattern established); flagged as NF-J4 below. - ---- - -### [JN-2-2] Message includes [SKIPPED] prefix so it is grep-able - -- [x] PASS - -v3-D4 explicit text: `'[SKIPPED] C-2: PS7+ -- $PROFILE conceptually read-only; covered by GG tests'` - -`[SKIPPED]` prefix present. PASS. - ---- - -### [JN-2-3] D2 (no Pester) preserved - -- [x] PASS - -Section 3 D2 unchanged: "Use the existing `Test-Scenario` harness with the `Invoke-HostQuery` -mock pattern. `$PROFILE` is read-only in PS 7+ and `$TestDrive` is Pester-specific. Adding -Pester is scope creep." No Pester dependency introduced. PASS. - ---- - -## Holistic Check - -### StrictMode satisfied - -PASS. Parameters declared in `param()` are auto-defined on function entry regardless of -whether the caller passes arguments. Defaults initialize `$Ps51Fallback` and `$Ps7Fallback` -on every call. `$local:beginMarker`/`$local:endMarker` are defined immediately in the body. -No variable referenced before assignment under `Set-StrictMode -Version Latest`. PASS. - -### No new shadow / scope bugs introduced - -PASS. The only `$local:` bindings remaining in `Write-PowerShellProfile` are `$beginMarker` -and `$endMarker`. These are explicitly NOT expected to be test-overridable (v5.2-D1 states -so). No other function-local shadows observed. The outer functions (`Invoke-HostQuery`, -`Resolve-ProfilePath`) are unchanged. PASS. - -### Vertical slice intact - -PASS. 7 GG tests (GG-1..GG-7), no extras. 9+1 decisions (D1-D4, v3-D1..v3-D5, v5.2-D1). -No new architecture layers, no new configuration knobs. Section 3 v5.2-D1 is the only -addition -- scoped correctly to document the JN-1 fix contract. PASS. - -### Production callsite impact - -PASS. `Write-PowerShellProfile` is called with no arguments in production (Section 4 comment -confirms zero callsite changes). Defaults mirror lines 17-18. Behavior on KFM/OneDrive systems -is unchanged. PASS. - ---- - -## New Findings - -### [LOW] NF-J3: H5 entry in v5 changelog now historically misleading - -**Citation:** docs/plans/441-profile-path.md, v5 Changes table, H5 row: -"Two `$local:` definitions added at top of `Write-PowerShellProfile` in Section 4" - -**Issue:** This entry has no "superseded by v5.2/JN-1" annotation. A reader scanning the -changelog table top-to-bottom (v5 before v5.2) could believe `$local:ps51Fallback`/ -`$local:ps7Fallback` are still in the function body. The current Section 4 algorithm is -correct (they are absent); the risk is reader confusion only. - -**Impact:** Documentation cosmetic. No implementation risk. -**Recommendation:** Add "(superseded by v5.2/JN-1)" note to H5 entry in a future pass. -**Blocking:** NO. - ---- - -### [LOW] NF-J4: C-3 skip not explicitly specified - -**Citation:** Section 3 v3-D4; v5.2 changelog JN-2 row; Section 7 AC#6. - -**Issue:** v3-D4 and the JN-2 changelog entry only show the C-2 `Write-Warning` example -explicitly. C-3 appears in "Guard C-2 and C-3" prose and in AC#6 but has no corresponding -`Write-Warning '[SKIPPED] C-3: ...'` example in the plan. - -**Risk:** Implementer could ship C-3 without a `[SKIPPED]`-prefixed warning, or omit the -PS7+ guard entirely for C-3. AC#6 provides a backstop but is an acceptance criterion, not -a specification. - -**Recommendation:** Add one line to v3-D4: `Write-Warning '[SKIPPED] C-3: ...'` mirroring -the C-2 example. -**Blocking:** NO. - ---- - -## Convergence Table (v5.2 scope) - -| Finding | Sev | Status in v5.2 | -|---------|-----|----------------| -| JN-1: $local: shadow makes test override inoperable | MEDIUM | RESOLVED | -| JN-2: Write-Host skip increments pass not skip | LOW | RESOLVED (C-2); C-3 inferred (NF-J4) | -| NF-J3: H5 entry no supersession note | LOW | NEW (cosmetic) | -| NF-J4: C-3 skip not explicit | LOW | NEW (non-blocking) | - ---- - -**Grilled by:** Jiminy (Quality Auditor) -**Date:** 2026-05-27 -**Session:** 441-grill-v5.2 diff --git a/docs/plans/441-grill-jiminy-v5.md b/docs/plans/441-grill-jiminy-v5.md deleted file mode 100644 index 231de7d5..00000000 --- a/docs/plans/441-grill-jiminy-v5.md +++ /dev/null @@ -1,272 +0,0 @@ -# Grill Report: #441 -- profile.ps1 writes to wrong path on OneDrive/KFM systems - -**Griller:** Jiminy (Quality Auditor -- process/quality gate, final grill before ship) -**Plan reviewed:** docs/plans/441-profile-path.md (v5.1, author: Donald) -**Date:** 2026-05-27 -**Session:** 441-grill-v5 -**Verdict:** REVISE - -**Author locked out for this grill:** Donald (v5/v5.1), Jiminy (v4); eligible revisor = Chip or Pluto -**Grill scope:** Vertical-slice integrity, plan-to-implementation contract, internal consistency, - convergence of all v4 findings (Donald F-1..F-5, Chip C-1/C-2/NF-3v4/NF-4v4, - Pluto A-1). Final gate before handoff to implementer. - ---- - -## Verdict Summary - -REVISE. One new MEDIUM finding (JN-1) introduced by Donald's v5 revision: the v5 H5 fix adds -`$local:` bindings inside `Write-PowerShellProfile` that cannot be overridden from test scope, -directly contradicting the v5 H3 test override language. GG-1/GG-4/GG-5 disk-writing tests as -specified will silently target REAL `$HOME` profile paths instead of temp paths. This is a -destructive write risk in CI and on dev machines. All other v4 findings are resolved. One LOW -(NF-3v4 Write-Skip) remains open but is non-blocking. - ---- - -## Task 1: Vertical Slice Integrity - -### Scope creep check (v1 -> v5.1) - -| Version | Author | IN-scope additions | New tests beyond GG-1..GG-7 | New arch layers/knobs | -|---------|--------|-------------------|-----------------------------|-----------------------| -| v1 | Goofy | Original scope | None | None | -| v2 | Mickey | Fallback + dedup + legacy cleanup + test seam | GG-1..GG-6 (6 tests) | Invoke-HostQuery wrapper | -| v3 | Donald | GG-7 + $LASTEXITCODE + path-shape guard + -NoLogo/-Last1 | GG-7 added (7 total) | Resolve-ProfilePath split out | -| v4 | Jiminy | Precision patches only (P1-P7) | No new tests | No new layers | -| v5 | Donald | Precision patches only (H1-H5) | No new tests | No new layers | -| v5.1 | Donald | Precision patches only (F-4/F-5) | No new tests | No new layers | - -PASS. No scope expansion from v4 onward. All 7 GG tests (GG-1..GG-7) are present; no extras. -No new options, parameters, configuration knobs, or architecture layers added in any revision. - -### Word count - -v5.1 raw word count: ~1839 (counted from plan file). Task brief estimated ~1196 -- brief was -counting prose only, not the changelog tables. Full file including 3 changelog tables (v4, v5, -v5.1) accounts for the delta. Growth is justified: changelog tables are required process -artifacts, not scope additions. PASS. - ---- - -## Task 2: Plan-to-Implementation Contract - -### Can the implementer write Write-PowerShellProfile from Section 4 alone? - -PARTIAL. The function is nearly fully specified: -- Invoke-HostQuery: fully defined (param, body, 2>$null) -- Resolve-ProfilePath: fully defined (guard, try/catch, LASTEXITCODE check, path-shape filter, - trim, fallback) -- Write-PowerShellProfile local vars: defined ($local:ps51Fallback, $local:ps7Fallback, - $local:beginMarker, $local:endMarker) -- Orphan-strip loop: fully defined (isOrphan check, Get-Content, regex, Set-Content with - -Encoding ASCII, Write-Info) -- Write loop: STUB ("# Write to each resolved path (existing strip+re-inject logic)") - -The write-loop stub was accepted as Pluto A-2 LOW (impl note; implementer reads production -lines 262-309). Still present in v5.1. Accepted deferral; a COMPETENT engineer with production -file access can implement it. NOT a new hole. - -Verdict: YES with the write-loop caveat (known accepted gap). - -### Can the implementer write GG-1..GG-7? - -BLOCKED by JN-1 (see Task 3 / new findings). GG-1/GG-4/GG-5 as specified direct the -implementer to "override $ps51Fallback/$ps7Fallback to temp paths" but the function defines -them as $local: -- the override is impossible from test scope. The implementer following both -sections literally will write tests that destructively target real $HOME paths. - -GG-2, GG-3, GG-6, GG-7: fully specified and implementable without additional context. PASS. - -### Can all 9 decisions (Section 3) be made without ambiguity? - -YES. (Note: the task brief counted 11; the plan contains 9: D1-D4 + v3-D1..v3-D5. No count -discrepancy in the actual plan text -- all 9 are unambiguous.) - ---- - -## Task 3: Internal Consistency - -### Section 3 decisions vs Section 4 algorithm - -| Decision | Algorithm cite | Consistent? | -|----------|---------------|-------------| -| D1: $PROFILE (CurrentUserCurrentHost) | Invoke-HostQuery calls 'powershell'/'pwsh' | YES | -| D2: Test-Scenario harness | Section 5 uses Test-Scenario | YES | -| D3: Inline in uninstall.ps1 | Section 4 comment "(and inlined in uninstall.ps1)" | YES | -| D4: Mandate Invoke-HostQuery | function defined in Section 4 | YES | -| v3-D1: -Last 1 / -NoLogo | Section 4: Select-Object -Last 1 | YES | -| v3-D2: $LASTEXITCODE check | Section 4: if ($LASTEXITCODE -ne 0) | YES | -| v3-D3: GG-4 dual-orphan | GG-4 row: both hosts return same $oneDrivePath | YES | -| v3-D4: C-2/C-3 guarded | Section 3 text: if/Write-Host/return; Section 7 AC#6 | YES | -| v3-D5: Sort-Object dedup | Section 4: Sort-Object { $_.ToLower() } -Unique | YES | - -All 9 decisions consistent with algorithm. PASS. - -### Section 5 tests vs Section 4 algorithm -- branch coverage - -| Algorithm branch | Covered by | Status | -|-----------------|------------|--------| -| Get-Command fails -> fallback | GG-2 | YES | -| $LASTEXITCODE -ne 0 -> fallback + warn | GG-7 | YES | -| IsNullOrEmpty -> fallback | (subset of GG-7 via empty return) | IMPLICIT | -| -notmatch '^[A-Za-z]:\\' -> fallback | No dedicated test | GAP (accepted LOW -- Pluto A-3) | -| Normal resolved path -> write | GG-1 | YES | -| Case-insensitive dedup | GG-3 | YES | -| Orphan strip (both paths) | GG-4 | YES (but blocked by JN-1) | -| Idempotency | GG-5 | YES (but blocked by JN-1) | -| Multi-line output | GG-6 | YES | - -Branch coverage is complete except for path-shape validation negative case (accepted gap from -prior grills). PASS with known LOW gap. - -### v5/v5.1 changelog accuracy - -| Entry | Claim | Plan text | Match? | -|-------|-------|-----------|--------| -| H1 | Set-Content -Encoding ASCII added | Section 4 orphan Set-Content has -Encoding ASCII | YES | -| H2 | $global:LASTEXITCODE = 0 reset before each test | Section 5 header states it | YES | -| H3 | TestDrive replaced with Join-Path $env:TEMP New-Guid | Section 5 header and GG-4 row | YES | -| H4 | GG-7: $HostExe = 'powershell' specified | GG-7 row | YES | -| H5 | $local:ps51Fallback/$ps7Fallback defined in Write-PowerShellProfile | Section 4 | YES | -| F-4 | Regex: \r?\n prefix + .*? | Section 4 regex matches production line 27 | YES | -| F-5 | $local:beginMarker/$endMarker defined | Section 4 | YES | - -Changelog is accurate. However: the BeforeEach removal (Chip NF-4v4 fix) is present in -Section 5 text ("not a BeforeEach block -- Test-Scenario has none") but is NOT listed as a -named H-entry in the v5 changelog. Minor documentation gap in changelog; the fix is present. - -### Orphaned version references - -None found. "v3-D1" etc. are intentional decision-naming conventions, not stale citations. - ---- - -## Task 4: Convergence Table - -All findings from v4 grill cycle (Donald F-1..F-5, Chip C-1/C-2/NF-3v4/NF-4v4/NF-5v4, -Pluto A-1). - -| Finding | Griller | Sev | Resolution | Status | -|---------|---------|-----|------------|--------| -| F-1: Set-Content missing -Encoding ASCII | Donald | HIGH | v5 H1: added -Encoding ASCII | RESOLVED | -| F-2: TestDrive contradicts D2 | Donald | MEDIUM | v5 H3: replaced with $env:TEMP New-Guid | RESOLVED | -| F-3: $LASTEXITCODE stale contamination | Donald | MEDIUM | v5 H2: reset $global:LASTEXITCODE = 0 | RESOLVED | -| F-4: Regex diverges from production | Donald | MEDIUM | v5.1: \r?\n prefix + .*? aligned to prod | RESOLVED | -| F-5: $beginMarker/$endMarker undefined | Donald | LOW | v5.1: $local:beginMarker/endMarker added | RESOLVED | -| C-1 (NF-1v4): GG-7 exe unspecified | Chip | MEDIUM | v5 H4: $HostExe = 'powershell' stated | RESOLVED | -| C-2 (NF-2v4): TestDrive / temp file pattern | Chip | MEDIUM | v5 H3: $env:TEMP New-Guid + cleanup | RESOLVED | -| NF-3v4: C-2/C-3 skip-as-pass (Write-Skip) | Chip | LOW | NOT addressed; still uses Write-Host | OPEN (LOW) | -| NF-4v4: BeforeEach reference | Chip | LOW | Fixed in text (not in changelog) | RESOLVED | -| NF-5v4: $global:LASTEXITCODE alt suggestion | Chip | INFO | N/A (informational; $env:ComSpec kept) | N/A | -| A-1: $ps51Fallback/$ps7Fallback undefined | Pluto | MEDIUM | v5 H5: $local: defs added | RESOLVED* | - -*A-1 resolution (v5 H5) introduces JN-1 (see below). - -CONVERGENCE COMPLETE for all HIGH + MEDIUM items. One LOW (NF-3v4) open, non-blocking. - ---- - -## New Findings - -### [MEDIUM] JN-1: H5 $local: definitions make Section 5 test override inoperable - -**Citation:** Section 4, Write-PowerShellProfile body (v5 H5): -``` -$local:ps51Fallback = [System.IO.Path]::Combine($HOME, 'Documents', 'WindowsPowerShell', ...) -$local:ps7Fallback = [System.IO.Path]::Combine($HOME, 'Documents', 'PowerShell', ...) -``` - -**Contradicts:** Section 5 header (v5 H3): -"GG tests that write to disk (GG-1, GG-4, GG-5) create a unique temp dir via -`Join-Path $env:TEMP "gg-test-441-$(New-Guid)"`, override `$ps51Fallback`/`$ps7Fallback` -to paths within it, and clean up in a `finally` block" - -**Root cause:** In PowerShell, assigning a variable inside a function (with or without the -`$local:` qualifier) creates a function-local binding that shadows any variable of the same -name in the calling scope. When the test sets `$ps51Fallback = $tempPath` before calling -`Write-PowerShellProfile`, the function immediately overwrites it with its own `$local:` -definition pointing to the real `$HOME` path. The test override has NO effect. - -**Impact:** GG-1, GG-4, and GG-5 as specified will: -1. Construct `$legacyPaths = @($ps51Fallback, $ps7Fallback)` using REAL $HOME paths -2. The orphan-strip loop (if legacy files exist) will TARGET REAL PRODUCTION PROFILE FILES -3. The write loop will WRITE TO REAL $HOME PROFILE PATHS in CI or on the developer's machine -4. The test passes but the side-effects are destructive and non-idempotent in the real env - -This is a plan-internal contradiction introduced when H5 (Pluto A-1 fix) and H3 (Chip C-2 -fix) were authored in the same v5 revision without a consistency check between them. - -**Severity:** MEDIUM. Results in destructive test writes to real profile files. Not a false -green per se -- the test exercises real code -- but it violates the "no real $HOME writes in -test" safety requirement that H3 was added to establish. - -**Fix (one of three options; reviser chooses):** - -Option A (PREFERRED -- aligns with Pluto's architecture intent): -Add fallback-path parameters with defaults to Write-PowerShellProfile: -```powershell -function Write-PowerShellProfile { - param( - [string]$PS51FallbackOverride = [System.IO.Path]::Combine($HOME, 'Documents', 'WindowsPowerShell', 'Microsoft.PowerShell_profile.ps1'), - [string]$PS7FallbackOverride = [System.IO.Path]::Combine($HOME, 'Documents', 'PowerShell', 'Microsoft.PowerShell_profile.ps1') - ) - $local:ps51Fallback = $PS51FallbackOverride - $local:ps7Fallback = $PS7FallbackOverride -``` -Tests call: `Write-PowerShellProfile -PS51FallbackOverride $tempPath1 -PS7FallbackOverride $tempPath2` -Production callers: `Write-PowerShellProfile` (defaults apply, no change to callsites). - -Option B: Define fallback paths at file scope (outside all functions). Tests set them via -`$script:ps51Fallback = $tempPath` before each call. Function reads from scope chain. Less -explicit than Option A; pollutes file scope. - -Option C: Test creates temp copies of the REAL profile files, runs Write-PowerShellProfile, -inspects real files, restores from copies. Brittle and cannot run in CI safely. - -Option A is recommended: adds two optional parameters, zero impact on production callers, and -closes the override gap cleanly without file-scope pollution. - ---- - -### [LOW] JN-2: NF-3v4 (Write-Skip) remains open - -**Citation:** Section 3 v3-D4: "Write-Host 'SKIP C-2: PS7+ -- $PROFILE conceptually read-only'" -**Issue:** Chip NF-3v4 requested Write-Skip to increment skip counter (not pass counter). -**Impact:** On PS7+ CI, C-2/C-3 appear as green passes; skip count understated. -**Status:** Not addressed in v5 or v5.1. LOW -- does not affect GG tests. Non-blocking. -**Recommendation:** Address in the same revision pass that fixes JN-1 (one-word change). - ---- - -## Required Fixes for v6 - -1. **JN-1 [MEDIUM]:** Choose Option A (parameters) or Option B (file scope) and update - Section 4 to reflect. Update Section 5 to show how the test passes the temp paths. - Option A recommended: add two optional params to Write-PowerShellProfile with default - $HOME-derived values; tests pass temp paths explicitly. - -2. **JN-2 [LOW]:** Section 3 v3-D4: replace `Write-Host 'SKIP C-2: ...'` with - `Write-Skip 'C-2' 'PS7+ -- $PROFILE conceptually read-only; covered by GG tests'` - (or equivalent skip-counter call per harness convention). - ---- - -## What v5.1 Got Right - -- **F-1 through F-5:** All Donald's findings resolved. Encoding, regex, markers, LASTEXITCODE - contamination -- all closed. The orphan-strip code now matches production line by line. -- **H4 (GG-7 exe spec):** `$HostExe = 'powershell'` is the correct answer; the rationale for - rejecting 'pwsh' (not-installed early-exit masks mock invocation) is documented. -- **H3 intent:** The INTENT of overriding fallback paths to temp dirs is correct and necessary. - The gap is purely in the mechanism (local var shadowing), not the safety strategy. -- **BeforeEach removal:** "not a BeforeEach block -- Test-Scenario has none" is present and - correct. NF-4v4 is substantively closed. -- **v5.1 regex:** Matches production line 27 exactly. F-4 resolved cleanly. -- **Vertical slice discipline:** 6 revisions; scope held. Zero drift. Earl's constraint honored. - ---- - -**Grilled by:** Jiminy (Quality Auditor) -**Date:** 2026-05-27 -**Session:** 441-grill-v5 diff --git a/docs/plans/441-grill-jiminy.md b/docs/plans/441-grill-jiminy.md deleted file mode 100644 index f8b6c21d..00000000 --- a/docs/plans/441-grill-jiminy.md +++ /dev/null @@ -1,157 +0,0 @@ -# Grill Report: #441 -- profile.ps1 writes to wrong path on OneDrive/KFM systems - -**Griller:** Jiminy (Hygiene & Governance Reviewer) -**Plan reviewed:** docs/plans/441-profile-path.md (v2 -- vertical slice revision) -**Date:** 2026-05-27 -**Verdict:** REVISE - ---- - -## Angle: AC Alignment + Hole Closure + Hygiene - -### AC Coverage Matrix - -| Acceptance Criterion | Plan Section | Status | -|---|---|---| -| AC#1: Aliases appear in both PS 5.1 and PS 7+ on stock Windows | Section 2 (Decision: Scope, IN item 1) + Section 4 (Algorithm) | IMPLICIT - not listed in Section 7 ACs | -| AC#2: Aliases appear on OneDrive/KFM systems | Section 7, top bullet | COVERED | -| AC#3: On PS 7+-only box, no PS 5.1 write attempted | Section 2 (IN item 2 fallback logic) | IMPLICIT - not listed in Section 7 ACs | -| AC#4: Diagnostic log shows resolved path | Section 7, 4th bullet | COVERED | -| AC#5: uninstall.ps1 mirrors resolution logic | Section 7, 3rd bullet | COVERED | -| AC#6: Test mocks $PROFILE and verifies target | Section 7, 5th bullet | COVERED | - -**Finding:** ACs #1 and #3 are implicitly handled but NOT explicitly listed in Section 7. Plan Section 7 redefines scope to migration scenarios (re-running setup on old orphaned block). This is TIGHTER THAN the issue ACs. Mismatch. - -### v1 Hole Closure Matrix (Mickey's 10 holes) - -| # | Hole from Mickey | v2 Status | Evidence / Citation | -|---|---|---|---| -| 1 | $PROFILE vs CurrentUserAllHosts contradiction | CLOSED BUT MISMATCHED | Section 3.1 DECIDES: use $PROFILE (CurrentUserCurrentHost). Issue spec says CurrentUserAllHosts. v2 picked the OTHER choice. Decision is explicit; rationale cites Doc's H2 feedback. But contradicts issue proposed fix. | -| 2 | Cold-start fallback writes never-sourced path (pre-provision vs skip) | ADDRESSED | Section 2 Scope: "Fallback to hardcoded path when host absent". Section 4 returns fallback when Get-Command fails. Idempotency handled via check-before-strip logic. Not a semantic decision issue, treated as acceptable. | -| 3 | Idempotency legacy cleanup runs every time | ADDRESSED | Section 4 Algorithm checks `Select-String -Path $legacy -Pattern $beginMarker -Quiet` BEFORE stripping. Only strips if sentinel exists. Section 6 Migration strategy confirms probing + conditional strip. | -| 4 | Deduplication case-sensitive on Windows | CLOSED | Section 4 Algorithm: `$profilePaths \| Sort-Object { $_.ToLower() } -Unique`. Explicitly normalizes to lowercase. | -| 5 | Invoke-HostQuery wrapper not in production code | CLOSED | Section 3.4 Decision mandates wrapper in production code. Section 4 Algorithm shows `function Invoke-HostQuery` call inside `Resolve-ProfilePath`. | -| 6 | Uninstall lib dependency breaking change | CLOSED | Section 3.3 Decision: "Inline the resolver in uninstall.ps1 (Option A)". Removes lib dependency. | -| 7 | GG-6 logs "not found" but doesn't verify fallback USED | ADDRESSED | Section 5 Test Plan GG-6: "Mock returns ... Only path extracted | Return value contains no newline". Verifies return value, not log. Assertion checks return type, not usage. Incomplete. | -| 8 | GG-S1 static test fragile (string literal matching) | NOT ADDRESSED | Section 5 mentions GG tests but no GG-S1 listed. Section 1 mentions "STATIC TEST" in risk R4 but no mitigation shown. Still fragile. | -| 9 | No test for Unicode username / non-ASCII path | NOT ADDRESSED | Section 2 Scope lists "Unicode usernames -- if reported, file issue" as OUT-OF-SCOPE. This is explicit deferral, not a hole. Status: N/A (vertical slice). | -| 10 | Missing AC check: diagnostic log shows resolved path (not hardcoded) | PARTIALLY ADDRESSED | Section 7 AC lists "Diagnostic log shows resolved path, not hardcoded path" but no explicit test case in Section 5 that verifies this. AC listed, test not shown. | - -**Findings:** -- 6 holes CLOSED (1, 4, 5, 6 fully; 2, 3 functionally) -- 2 holes NOT ADDRESSED in v2 (8, 10) -- 1 hole DEFERRED as out-of-scope (9 -- N/A per vertical slice) -- 1 hole MISMATCHED: Mickey's hole #1 about choosing CurrentUserCurrentHost vs CurrentUserAllHosts -- v2 DECIDED but picked the OPPOSITE of what issue spec says - -### v1 Hole Closure Matrix (Chip's 27 issues) - -| # | Issue from Chip | v2 Status | Evidence / Note | -|---|---|---|---| -| 1 | $TestDrive reference in test harness | NOT ADDRESSED | Section 5 test cases do not mention $TestDrive. v2 uses existing Test-Scenario harness "with the Invoke-HostQuery mock pattern" -- suggests mock-based pattern. But does not explicitly rewrite the test harness. Chip's underlying issue: Pattern A fails on non-Pester harness. Status: IMPLICIT (test harness redesign needed, not shown). | -| 2 | $PROFILE assignable in test host (PS 7+ blocks it) | NOT ADDRESSED | Section 5 does not mention PS version guard or how to handle PS 7+ $PROFILE immutability. Section 3.2 says "Use existing Test-Scenario harness with Invoke-HostQuery mock pattern" but doesn't resolve the $PROFILE assignment problem in PS 7+ test environment. | -| 3 | Invoke-HostQuery mandated in production | CLOSED | Section 3.4 explicitly mandates it. Section 4 shows it in algorithm. | -| 4 | Select-Object -Unique case-sensitive | CLOSED | Section 4: `Sort-Object { $_.ToLower() } -Unique`. | -| 5 | Write-Info output capturable in harness | NOT ADDRESSED | GG test cases do not specify stream capture (6>&1 or *>&1). Section 5 does not address how Write-Info output is redirected for test assertion. | -| 6 | Resolved path > 260 chars on non-long-path system | N/A | Section 2 Scope OUT: "Long paths > 260 chars -- if reported, file issue". Deferred by Earl's vertical-slice directive. | -| 7 | Case-sensitive contains check legacy cleanup collision | PARTIALLY ADDRESSED | Section 4 uses case-insensitive comparison: `-notcontains` still exists but paired with explicit check: `$legacy.ToLower() -eq $legacy.ToLower()`. Actually looking closer, the pseudocode says: `$profilePaths \| Where-Object { $_.ToLower() -eq $legacy.ToLower() }`. ADDRESSED. | -| 8 | pwsh installed during setup not on PATH | N/A | Section 2 Scope OUT: "pwsh not on PATH after same-session install -- existing #251 pattern applies". Deferred as known issue. | -| 9 | Partial/corrupt block (BEGIN without END) | N/A | Section 2 Scope OUT: "Partial/corrupt blocks (interrupted writes) -- if reported, file issue". Deferred by vertical slice. | -| 10 | Profile file read-only (Group Policy) | N/A | Not mentioned in Section 2 Scope. This is an edge case not explicitly marked as out-of-scope. Chip's issue: no test coverage. Status: OPEN. | -| 11 | Profile directory symlink to disconnected network | N/A | Not mentioned. Out-of-scope by omission. | -| 12 | Multi-line output (banner + path) | ADDRESSED | Section 4 Algorithm: `($resolved -split '\r?\n' \| Where-Object { $_ } \| Select-Object -First 1)`. Explicitly handles multi-line by taking first non-empty line. | -| 13 | Long path > 260 chars | N/A | Section 2 Scope OUT: "Long paths > 260 chars". | -| 14 | ConstrainedLanguage Mode (CLM) blocks write | N/A | Section 2 Scope OUT: "Constrained Language Mode (CLM) -- if reported, file issue". | -| 15 | Legacy orphan + resolved path both exist | NOT ADDRESSED | Section 5 Test Plan lists GG-1 through GG-6 but none explicitly test: legacy path with block + resolved path with different content after run. Section 6 Migration strategy describes the logic but no test case verification shown. | -| 16 | NO TEST FOR LEGACY CLEANUP AT ALL | OPEN | Section 5 lists 6 test cases (GG-1 through GG-6). None of these test the legacy cleanup scenario from Section 6. GG-4 is described in Section 5 table but the actual test case logic is not provided. This is a CRITICAL GAP. | -| 17 | Idempotency tested only twice | OPEN | Section 5 GG-5 says "Idempotency (3 runs)" in the description but the table lists it as: "Exactly one block | $profilePaths.Count -eq 1". Assertion checks array count, not file stability across 3 runs. Test as described does not verify stable file size across runs 1, 2, 3. | -| 18 | Second profile path independent idempotency | OPEN | No test case listed that verifies idempotency on BOTH resolved paths simultaneously. | -| 19 | GG-5 dedup doesn't verify write occurred | OPEN | Section 5 GG-5 checks array count but does not show assertion that written file contains the block. | -| 20 | $HOME empty/null | N/A | Out-of-scope by omission. Not tested, not explicitly deferred. Status: OPEN. | -| 21 | GG-S1 static assertion too narrow | OPEN | Not mentioned in v2 plan. Out-of-scope by omission. | -| 22 | Execution policy Restricted blocks child process | OPEN | Not mentioned in Section 5 test cases. Not explicitly deferred in Section 2 Scope. | -| 23 | UTF-8-with-BOM encoding interaction | OPEN | Not mentioned. Out-of-scope by omission. | -| 24 | CI: OneDrive KFM cannot be simulated on GitHub Actions | N/A | Section 8 Known Limitations does not list manual KFM testing requirement. Chip's rec: add manual gate. Status: OPEN. | -| 25 | CI: process spawning perf (2-6 seconds per run) | N/A | Not mentioned. Out-of-scope by omission. Known limitation candidate but not documented. | -| 26 | CI: PS 5.1 behavior differs from KFM systems | N/A | Not mentioned. Known limitation candidate but not documented. | -| 27 | Test file import guard (lib/profile-path.ps1 may not exist yet) | OPEN | Section 5 mentions "Use Invoke-HostQuery mock pattern" but doesn't show test harness safeguards. If lib import fails at load time, all GG tests crash. | - -**Findings:** -- 3 holes CLOSED (3, 4, 12) -- 7 holes N/A by vertical slice (6, 8, 9, 13, 14; #24 implicit) -- 17 holes OPEN or PARTIALLY ADDRESSED (1, 2, 5, 7, 10, 11, 15, 16, 17, 18, 19, 20, 21, 22, 23, 25, 26, 27) -- Critical gap: **LEGACY CLEANUP UNTESTED** (hole #16 repeated from Chip's verdict) - ---- - -## Hygiene Findings - -1. **AC#1 and AC#3 missing from Section 7 acceptance criteria list.** Issue #441 has 6 ACs. v2 plan Section 7 lists 5 bullets, but two original ACs are implicit only: - - AC#1 ("Aliases load on stock Windows with both PS 5.1 and 7+") is mentioned in Section 2 but NOT in Section 7 AC list - - AC#3 ("Only PS 7+, no PS 5.1 write") is mentioned in Section 2 but NOT in Section 7 AC list - This creates ambiguity: are these in-scope for acceptance testing or not? - -2. **$PROFILE vs CurrentUserAllHosts mismatch.** Issue #441 proposed fix explicitly says `$PROFILE.CurrentUserAllHosts`. v2 Section 3.1 DECIDES to use `$PROFILE` (CurrentUserCurrentHost). Rationale cites "Doc's H2 confirms different hosts have different CurrentHost profiles". This is a valid decision (host-specific aliases belong in host-specific profiles), BUT it contradicts the issue spec. This decision should be called out in an EXPLICIT statement like "Decision: Changed from issue spec's CurrentUserAllHosts to CurrentUserCurrentHost because..." and flagged for Earl's review. - -3. **GG-S1 static test mentioned in risks but no mitigation shown.** Section 1 risks list R4 (fragile string literal matching). Section 5 Test Plan lists GG-1 through GG-6 but no GG-S1. Where is the static test? This is mentioned as a risk but the mitigation is not shown in the test list. - -4. **Legacy cleanup test completely absent.** Chip's hole #16 is critical: "No test for legacy orphan cleanup at all." Section 6 describes the migration strategy in detail. Section 5 Test Plan lists 6 test cases (GG-1 through GG-6) but NONE explicitly test the legacy cleanup scenario. GG-4 is listed as "Legacy cleanup | Mock returns OneDrive path; create block at hardcoded path | Hardcoded path stripped" but the actual test logic is not shown in the table. This is either (a) incomplete test design, or (b) the test plan table is a summary and full test harness exists elsewhere. If (b), cite the location. If (a), add explicit test cases. - -5. **Diagnostic log coverage unclear.** Issue AC#4 requires "Diagnostic log lines show the resolved path (not the constructed one)". Section 7 AC lists this. Section 5 Test Plan: GG-3 is "Case-insensitive dedup" (not log verification). No test case explicitly verifies that resolved path is logged and hardcoded path is NOT logged. This is an acceptance criterion but not tested in the shown test plan. - -6. **Test harness compatibility not addressed.** Chip's holes #1, #2, #5: Section 3.2 says "Use existing Test-Scenario harness with Invoke-HostQuery mock pattern" but Section 5 does not show: - - How $TestDrive is replaced (if used at all) - - How $PROFILE mutability in PS 7+ is handled - - How Write-Info output is captured for assertion - These are implementation details that belong in Section 5 test pseudocode. As written, test cases are ambiguous on execution environment. - -7. **Word count (936 total) is genuinely short for this scope.** v2 plan cuts scope to vertical-slice, which is valid. But: - - Section 3 "Decisions Made" has 4 bullet points labeled as decisions but 3 are basically restatements of v1 questions (#1: which $PROFILE flavor, #2: Pester vs existing harness, #3: inline vs lib). These read as "we decided what Chip/Mickey asked us to decide" rather than NEW judgment calls. Decision #4 (Invoke-HostQuery wrapper) is new. - - Section 5 Test Plan has 6 test cases described in a compact table. The table shows ID, Name, Input, Expected, Assertion but OMITS test pseudocode or execution details. GG-4 (legacy cleanup) is listed but the implementation logic is not shown. - - Section 8 Known Limitations lists 5 items but Known Limitations are usually doc'd to users, not buried in a plan. Are these in Section 1 of profile.ps1 module docs? Not stated. - -8. **Section 6 Migration Story is one paragraph.** Bullet list describes: install does 4 things (resolve, probe, conditional strip, write). But the paragraph does NOT explicitly say: "If a user upgrades from old version of dev-setup (with orphaned block at hardcoded path), running install on new version will automatically migrate them by stripping the old block and writing to the correct resolved path." This is the actual migration story the user cares about, and it's IMPLIED but not STATED. - -9. **Decision #1 ($PROFILE flavor) is mischaracterized as "decided" when Mickey's hole shows it's OPEN.** Section 3.1 says "Doc's H2 confirms..." but Chip's hole #1 explicitly flags this as a BLOCKING discrepancy requiring Earl's ruling. v2 decided without getting Earl's sign-off. This is a governance issue: design decisions that contradict issue spec should be escalated, not embedded in pseudo-code. - -10. **No evidence that v2 is actually a revision of v1 vs a rewrite.** The plan does not cite which holes from Mickey/Chip it's addressing. Reading it cold, it looks like Goofy rewrote it from scratch. Best practice: v2 should have a "Revision Notes" section at the top saying "Addressing Mickey's holes #1,4,5,6 and Chip's holes #3,4,7,12. Deferring #9 as out-of-scope (vertical slice). OPEN: #8,10 (test harness). Escalating #1 to Earl for $PROFILE decision." - ---- - -## Self-Consistency Check (Section 2 vs Section 4 vs Section 5) - -**Section 2 IN list vs Section 4 Algorithm:** -- Section 2, IN item 1: "Query each host for its $PROFILE" [ok] Section 4 shows `Invoke-HostQuery -Exe $HostExe` -- Section 2, IN item 2: "Fallback to hardcoded path when host absent" [ok] Section 4 shows `if (-not (Get-Command $HostExe...` with fallback return -- Section 2, IN item 3: "Case-insensitive deduplication" [ok] Section 4 shows `Sort-Object { $_.ToLower() } -Unique` -- Section 2, IN item 4: "Legacy cleanup of orphaned blocks at old hardcoded paths" [ok] Section 4 shows cleanup loop -- Section 2, IN item 5: "Test coverage via mocked Invoke-HostQuery" [ok] Section 5 GG tests - -**Section 4 Algorithm vs Section 5 Tests:** -- Section 4 has pseudo-code; Section 5 lists 6 GG tests -- Mapping: GG-1 (resolved path used), GG-2 (fallback when host absent), GG-3 (case-insensitive dedup), GG-4 (legacy cleanup), GG-5 (idempotency), GG-6 (multi-line output) -- GAP: Section 4 Algorithm has lines showing `| Where-Object { $_ }` for empty-line filtering. Section 5 GG-6 says "Only path extracted | Return value contains no newline". This matches Section 4 line `($resolved -split '\r?\n' | Where-Object { $_ } | Select-Object -First 1)` [ok] -- CRITICAL GAP: Section 4 legacy cleanup loop is 4 lines of pseudocode. Section 5 GG-4 is listed as a test but the table row does NOT show the test pseudocode or assertion details. Is GG-4 actually implemented? Unclear. - -**Verdict on self-consistency: PARTIAL.** The three sections agree on WHAT ships (algorithm matches test cases), but Section 5 test descriptions are too terse to verify the tests will actually prove the algorithm works, especially for legacy cleanup. - ---- - -## Verdict - -**REVISE.** - -The plan addresses most of Mickey's holes (6/10 closed, 3 N/A by vertical slice) and many of Chip's holes (3 closed, 7 N/A by scope, but 17 remain open or partially addressed). However, three blocking issues prevent approval: - -1. **AC#1 and AC#3 are missing from Section 7 acceptance criteria.** v2 redefines scope tighter than issue #441 spec. AC list must either include all 6 original ACs or explicitly state which are deferred and why. - -2. **$PROFILE vs CurrentUserAllHosts decision was made without escalating to Earl.** Chip flagged this as a blocking contradiction between issue spec (AllHosts) and Goofy's plan (CurrentHost). v2 decided to go with CurrentHost but called it "Doc's feedback" rather than "decision overriding issue spec requiring Earl's sign-off." - -3. **Legacy cleanup is completely untested.** Section 6 describes it, Section 4 pseudo-codes it, but Section 5 does not show actual test pseudocode or assertions for the GG-4 legacy cleanup case. This is the entire value proposition of the backward-compatibility story. Chip called this "critical gap" in hole #16. - -**Revision owner:** Mickey (NOT the original author) per lockout rule. Scope of revisions: -- Section 7: Add ACs #1 and #3 OR explicitly defer them with rationale -- Section 3.1: Escalate $PROFILE decision to Earl; show his ruling in plan (e.g., "Per Earl 2026-05-27: use CurrentUserCurrentHost because...") before finalizing decision -- Section 5: Add explicit test pseudocode for GG-4 (legacy cleanup). Show test harness details for PS 7+ $PROFILE mutability, Write-Info capture, multi-test idempotency. - -**Re-grill required after revision:** Yes. The $PROFILE decision and legacy cleanup tests are structural changes. Full re-grill (Mickey + Chip angles) required after revision. If Mickey and Chip confirm the revised plan still addresses their original holes, re-grill can be scoped to AC coverage + test harness sections only. - diff --git a/docs/plans/441-grill-mickey.md b/docs/plans/441-grill-mickey.md deleted file mode 100644 index 598ed48e..00000000 --- a/docs/plans/441-grill-mickey.md +++ /dev/null @@ -1,85 +0,0 @@ -# Mickey's Grill -- Plan for #441 -**Date:** 2026-05-27 -**Verdict:** Revise - -## Holes Found - -1. **$PROFILE vs $PROFILE.CurrentUserAllHosts -- plan contradicts issue.** - Issue #441 explicitly specifies `$PROFILE.CurrentUserAllHosts` in both the proposed fix and acceptance criteria. Goofy's plan uses `$PROFILE` (CurrentUserCurrentHost). This is flagged as an "open question" but treated as decided in the pseudo-code. This is a BLOCKING discrepancy -- the stakeholder (Earl) must confirm which file to target before implementation. Do not start coding until this is resolved. - -2. **Cold-start scenario underdefined -- fallback writes to a never-sourced path.** - E3/E4 say "fallback used for PS 7+ path (write is benign no-op if dir absent)" -- but that's wrong. The code CREATES the directory and writes the file. On a brand-new machine with only PS 5.1 and no pwsh, the script writes to `$HOME\Documents\PowerShell\...` which will NEVER be sourced (PS 7+ isn't installed). That's benign clutter, not a no-op. Explicit decision needed: should we skip the write entirely when host is absent, or write anyway as "pre-provisioning"? The plan glosses over this. - -3. **Idempotency of legacy cleanup -- runs every time, not once.** - The cleanup pass probes legacy paths on EVERY install run. If `$HOME\Documents\WindowsPowerShell\...` exists and differs from the resolved path, we strip it every time. That's wasteful I/O and creates log noise on every run. Better: write a one-time marker file (e.g., `.dev-setup-migrated`) or check if the legacy file actually contains the sentinel before logging "Removing stale dev-setup block". The pseudo-code DOES check `Select-String`, but the log line "Removing stale dev-setup block" will print every run if the file still exists (empty or with other content). Clarify intent. - -4. **Deduplication is case-sensitive -- Windows paths are case-insensitive.** - `$profilePaths | Select-Object -Unique` uses default equality, which is case-sensitive for strings. If pwsh returns `C:\Users\Earl\OneDrive\Documents\PowerShell\...` and powershell returns `c:\users\earl\onedrive\documents\WindowsPowerShell\...`, deduplication fails to collapse them when it should. Risk R4 notes this but offers no concrete fix. Add `-Unique` with a custom comparer or normalize to lowercase before deduplication. - -5. **Invoke-HostQuery wrapper not mentioned in production code section.** - Section 5 (Test Plan) proposes Pattern B using an `Invoke-HostQuery` wrapper for testability. But Section 3 (pseudo-code) directly calls `& $HostExe -NoProfile ...`. If we want Pattern B, the PRODUCTION code must use the wrapper, not just tests. The plan doesn't commit to refactoring `Resolve-ProfilePath` to use `Invoke-HostQuery`. Clarify: is Pattern B adopted for production, or is it test-only mocking? - -6. **Uninstall lib dependency is a breaking change -- needs migration strategy.** - Open Question #6 flags this but doesn't resolve it. Currently `uninstall.ps1` is standalone (no dot-source dependencies). Adding `lib/profile-path.ps1` as a dependency means uninstall breaks if the user deletes the repo after install. The plan recommends Option B (shared lib) but doesn't address the portability regression. Either: (a) inline the resolver in uninstall.ps1 (Option A), or (b) embed the resolver logic directly in the profile block itself so uninstall doesn't need external deps. - -7. **Test case GG-6 logs "not found" but doesn't verify fallback is USED.** - GG-6 says "Verify Write-Info called with 'not found' substring". That proves the log line fired, not that the fallback path was actually used for the write. Add an assertion that the written file path equals the fallback. - -8. **Static test GG-S1 is fragile -- pattern match on string literal.** - Searching for `$HOME, 'Documents'` to prevent regression is clever but brittle. A refactor that changes quote style (`$HOME, "Documents"`) or whitespace would bypass the guard. Consider a more robust approach: parse the AST and verify no `[System.IO.Path]::Combine` call with literal 'Documents' outside the designated fallback variable. - -9. **No test for E17 (non-ASCII username / Unicode path).** - Edge case E17 is documented but no corresponding test case in Group GG. Add GG-11: mock `Invoke-HostQuery` to return a path with non-ASCII chars (e.g., `C:\Users\Muller\...` with umlaut) and verify write succeeds without mojibake. - -10. **Missing acceptance criterion check: diagnostic log shows resolved path.** - Issue #441 acceptance criterion 4 requires "Diagnostic log lines show the resolved path (not the constructed one)". The plan mentions this in Section 3, and GG-9 covers it -- but only for the OneDrive case. Add a test that verifies the hardcoded fallback is NOT logged when resolution succeeds (i.e., no leakage of `$HOME\Documents\...` in output when resolved path differs). - -## Strong Points - -- **Root cause analysis is thorough.** Scenarios A-E cover the real-world configurations that break the current code. OneDrive KFM is the primary driver, but the plan correctly identifies registry redirects, symlinks, and `$HOME` overrides. - -- **Edge case table is comprehensive.** E1-E20 is a solid enumeration. The plan anticipates Constrained Language Mode, UNC paths, trailing CRLF, and WSL interop -- these are non-obvious. - -- **Backward compat strategy is sound.** Probing both legacy paths AND resolved paths on install/uninstall handles the migration case where old installs wrote to the wrong location. - -- **Files Touched section is explicit.** The plan clearly lists which files change and why. No hidden changes. - -- **Group GG naming is correct.** Tests go A-Z, then AA, DD, EE, FF -- GG is the next slot. (Note: BB/CC appear to be skipped in the existing file, but GG follows FF correctly.) - -- **Deduplicate-before-write is necessary.** If both hosts resolve to the same file (rare but possible), double-injecting the block would corrupt the profile. Plan handles this. - -## Decisions That Need Explicit Capture - -1. **$PROFILE vs $PROFILE.CurrentUserAllHosts** -- Which one? Issue says AllHosts; plan says CurrentHost. Get Earl's ruling before implementation. - -2. **Skip write when host is absent, or pre-provision?** -- If pwsh is not installed, should we write to the PS 7+ profile path anyway (pre-provisioning for when pwsh is installed later) or skip entirely? Current code writes; plan preserves that behavior but calls it a "no-op", which it isn't. - -3. **Inline resolver in uninstall.ps1 (Option A) or shared lib (Option B)?** -- Plan recommends B but doesn't close on it. If B, document that uninstall requires the lib to be present. - -4. **Case-insensitive deduplication on Windows** -- Commit to normalizing paths to lowercase before `Select-Object -Unique`. - -5. **Invoke-HostQuery wrapper in production code** -- If we want Pattern B testability, the wrapper must exist in `profile-path.ps1`, not just tests. - -## If This Goes To Revision - -**Revision owner:** Donald (Shell Dev) -**Required changes:** - -1. Resolve the $PROFILE vs CurrentUserAllHosts question with Earl and update pseudo-code accordingly. -2. Add explicit decision on skip-vs-pre-provision for absent hosts; update E3/E4 descriptions. -3. Fix case-insensitive deduplication -- add `-ToLower()` or use `[System.IO.Path]::GetFullPath()` normalization. -4. Commit to Option A (inline resolver in uninstall.ps1) OR Option B (shared lib) -- close the open question. -5. If Pattern B for testing, refactor pseudo-code to use `Invoke-HostQuery` in production `Resolve-ProfilePath`. -6. Add test GG-11 for Unicode path handling. -7. Add test GG-12 verifying fallback path does NOT appear in logs when resolution succeeds. -8. Clarify idempotency of legacy-cleanup pass (log only if sentinel actually removed, not on every run). - -## Final Verdict - -This plan demonstrates solid research and covers the problem space well. The edge-case enumeration, backward-compat strategy, and test scaffolding are all strong. However, there are two blocking issues that prevent approval: - -First, the plan contradicts the issue on a fundamental design choice ($PROFILE vs CurrentUserAllHosts) and treats it as an open question while simultaneously embedding the opposite choice in the pseudo-code. This must be resolved with Earl before anyone writes code. - -Second, the plan has several implicit assumptions masquerading as decisions: case-sensitive deduplication, pre-provisioning for absent hosts, and the uninstall lib dependency. These need explicit team sign-off. - -**Verdict: REVISE.** Donald should take over, resolve the open questions with Earl, and tighten the implementation spec. Once the CurrentUserAllHosts question is answered and the deduplication/idempotency issues are addressed, this plan will be ready for implementation. diff --git a/docs/plans/441-grill-pluto-v4.md b/docs/plans/441-grill-pluto-v4.md deleted file mode 100644 index 36826a4c..00000000 --- a/docs/plans/441-grill-pluto-v4.md +++ /dev/null @@ -1,289 +0,0 @@ -# Grill Report: #441 -- profile.ps1 writes to wrong path on OneDrive/KFM systems - -**Griller:** Pluto (Platform Architect -- architecture/algorithm correctness) -**Plan reviewed:** docs/plans/441-profile-path.md (v4, Jiminy revision) -**Date:** 2026-05-27 -**Verdict:** SHIP - -**Grilling against:** docs/plans/441-grill-pluto.md (own v3 grill) -**Cross-reference:** docs/plans/441-grill-chip-v3.md - ---- - -## v3 Blocking Concern Regression Check - -### P1 / Finding 5 (empty foreach loop stub) -- RESOLVED - -v3 defect: the legacy cleanup loop body contained only the placeholder comment -`# Strip block from orphaned legacy file`. GG-4 asserted both legacy files -stripped, but Section 4 had no stripping code -- a plan-internal contradiction. - -v4 fix (Jiminy P1): loop body now contains explicit, executable implementation: - - $content = Get-Content $legacy -Raw - $stripped = $content -replace "(?s)$([regex]::Escape($beginMarker)).+?$([regex]::Escape($endMarker))\r?\n?", '' - Set-Content $legacy $stripped.TrimEnd() -NoNewline - Write-Info "Stripped orphaned block from legacy path: $legacy" - -Real code, not a stub. GG-4 assertion is directly implementable from the plan. -Status: RESOLVED. - ---- - -### P2 / Finding 6 (scope ambiguity -- top-level vs. function) -- RESOLVED - -v3 defect: Section 4 pseudocode showed $profilePaths and the foreach loop as bare -top-level statements. If placed at file scope, dot-sourcing profile.ps1 runs the -resolution immediately -- before any test mock is defined. All GG tests would call -the real Invoke-HostQuery rather than the mock. GG-1 through GG-7 would silently -test the wrong thing. - -v4 fix (Jiminy P2): Section 4 wraps the entire algorithm in -`function Write-PowerShellProfile { }`. An explicit comment block states: - - # ALL path-resolution and write logic lives inside this function. - # Dot-sourcing profile.ps1 only defines the three functions above; it does NOT - # execute resolution or writes. Tests can define mock Invoke-HostQuery AFTER - # dot-sourcing and BEFORE calling Write-PowerShellProfile. - -The comment is unambiguous. Dot-source safety guarantee is explicit and architecturally -correct. Production profile.ps1 line 11 confirms this is the pre-existing pattern. -Status: RESOLVED. - ---- - -## New Architectural Findings - -### [MEDIUM] A-1: $ps51Fallback and $ps7Fallback referenced without definition in scope - -plan:Section 4, Write-PowerShellProfile body: - - $profilePaths = @( - (Resolve-ProfilePath 'powershell' $ps51Fallback), - (Resolve-ProfilePath 'pwsh' $ps7Fallback) - ) | Sort-Object { $_.ToLower() } -Unique - - $legacyPaths = @($ps51Fallback, $ps7Fallback) - -Neither $ps51Fallback nor $ps7Fallback is: - (a) a parameter of Write-PowerShellProfile - (b) defined as a local variable in the function body shown in Section 4 - (c) defined at file scope anywhere in Section 4 - -Production profile.ps1 defines equivalent hardcoded paths INSIDE Write-PowerShellProfile -at lines 17-19 (they are the $profilePaths array itself). v4 needs two separate fallback -constants. Under Set-StrictMode -Version Latest (active, production line 6), referencing -an undefined variable throws VariableIsUndefined immediately at runtime. - -The plan must specify WHERE these constants are defined. Three valid options: - (A) Define at top of Write-PowerShellProfile as local constants -- consistent with - the production pattern (lines 12-19), no file-scope pollution. - (B) Add as parameters with default values. - (C) Define at file scope before the three function definitions. - -Option A is architecturally consistent with the existing file. This is not a -show-stopper -- any StrictMode-aware implementer will catch it -- but the plan has -a gap that directly causes a runtime error if missed. - -Post-#441 deferral: NO. Must be addressed before coding starts. -Severity: MEDIUM. - ---- - -### [LOW] A-2: Write loop shown as comment stub -- variable alignment unconfirmed - -plan:Section 4, end of Write-PowerShellProfile: - - # Write to each resolved path (existing strip+re-inject logic) - -This comment defers to production code at profile.ps1 lines 262-309, which runs -`foreach ($profilePath in $profilePaths)`. The new algorithm also names the resolved -array $profilePaths. Names align; the existing writer loop should operate correctly -on the dynamically resolved array. However, the plan does not explicitly confirm this. - -Interaction check (performed here as review): - - Legacy cleanup fires only for paths NOT in $profilePaths ($isOrphan = true). - - Writer loop fires only for paths IN $profilePaths. - - Mutual exclusivity is structurally enforced by the $isOrphan guard. - - No double-strip path exists; no conflict with production lines 22-29. - -One sentence confirming the variable-name alignment and the mutual-exclusivity -guarantee would close this. As-is, an implementer must infer the integration. - -Post-#441 deferral: YES. Implementation note only. -Severity: LOW. - ---- - -### [LOW] A-3: Regex divergence between legacy cleanup and writer-loop strip - -Legacy cleanup (Section 4): - $content -replace "(?s)$([regex]::Escape($beginMarker)).+?$([regex]::Escape($endMarker))\r?\n?", '' - -Production writer-loop strip (profile.ps1 line 27): - $raw -replace "(?s)\r?\n$([regex]::Escape($beginMarker)).*?$([regex]::Escape($endMarker))\r?\n?", '' - -Two differences: - -1. Production includes `\r?\n` prefix before $beginMarker; legacy cleanup omits it. - Effect: production consumes the newline that precedes BEGIN, leaving no blank line - at the strip site. Legacy cleanup does not -- a blank line may remain if other - content exists after the END marker in the legacy file (e.g., user's own aliases - appended below the dev-setup block). - -2. Production uses `.*?` (zero or more chars); legacy cleanup uses `.+?` (one or more). - Practical effect: negligible for normal blocks. A hypothetical corrupted empty block - (BEGIN followed immediately by END) is stripped by production but not by legacy cleanup. - -The missing `\r?\n` prefix is the more material gap. Legacy profiles often have -user content below the dev-setup block; the orphaned blank line is cosmetically wrong. -TrimEnd() in the legacy cleanup Set-Content call partially mitigates this only when -the block is at the end of the file. - -Post-#441 deferral: YES. Functional result is correct in all non-edge cases. -Severity: LOW. - ---- - -### [LOW] A-4: Empty $profilePaths guard absent -- contingent on A-1 - -plan:Section 4 algorithm uses Resolve-ProfilePath which always returns $FallbackPath -as its last resort. If $ps51Fallback and $ps7Fallback are non-empty (A-1 resolved), -$profilePaths will always contain at least one entry. No empty-array guard is needed -once A-1 is addressed. - -If A-1 is NOT resolved (undefined variables, StrictMode throw), the function aborts -before reaching the Sort-Object line. This finding is subsumed by A-1. - -Post-#441 deferral: YES. Contingent on A-1. -Severity: LOW. - ---- - -## Composability and Failure-Mode Analysis - -### Idempotency - -GG-5 (3 runs, one block) covers this. Algorithm decomposition: - 1. Resolve paths: deterministic given same environment (same hosts installed). - 2. Legacy cleanup: idempotent -- Test-Path + Select-String guards; no file touched - unless it exists AND contains the marker. - 3. Writer loop: strips then injects -- existing production pattern (lines 22-29). - Strip-then-inject is idempotent by construction. - -Structural guarantee: on repeated runs against a correctly-written profile, the strip -finds the existing block, removes it, and re-injects. Net result: one block. Correct. - -### Both Hosts Fail - -Both Resolve-ProfilePath calls fall back to $ps51Fallback and $ps7Fallback respectively. -$profilePaths = @($ps51Fallback, $ps7Fallback) | Sort-Object -Unique (two distinct paths; -WindowsPowerShell vs. PowerShell subdir -- they are never equal under standard PS). -Legacy cleanup: neither path is orphaned (both are in $profilePaths, $isOrphan = false). -Writer loop: writes to both paths. Behavior identical to current production. -Correct. - -### Null / Empty Paths (contingent on A-1) - -Resolve-ProfilePath always returns $FallbackPath as last resort. If $ps51Fallback / -$ps7Fallback are properly defined (A-1 resolved), no null/empty path can reach the -writer loop. No additional guard is architecturally needed. - -### Mid-Run Throw - -Under ErrorActionPreference = Stop (production line 7), a Set-Content failure propagates -immediately. Partial-write window: legacy orphan may have been stripped but resolved path -not yet written, leaving the user with no dev-setup block anywhere. Re-running setup -(idempotent) recovers. The window is narrow and self-healing. Acceptable for #441 scope. - -### Interaction with Production Lines 250-307 Writer Logic - -The existing writer loop (lines 262-309) iterates over $profilePaths. In v4, -$profilePaths is dynamically resolved rather than hardcoded -- but the loop code is -unchanged. Variable name matches. Legacy cleanup and writer-loop strip are mutually -exclusive by the $isOrphan guard. No conflict, no duplication. - ---- - -## API Surface - -function Write-PowerShellProfile: - Parameters: none (fire-and-forget side-effectful function -- consistent with - production pattern and correct for this use case) - Return value: none documented (void) - Side effects: file writes to resolved PS profile paths; log output via - Write-Info / Write-Warn / Write-Err - Implicit dependencies (undocumented in plan): - - $ps51Fallback, $ps7Fallback (see A-1 -- must be defined before use) - - Write-Info, Write-Warn, Write-Err (dot-sourced from logging.ps1, production line 9) - - Invoke-HostQuery (file-scope mockability seam -- correct architectural choice) - -The zero-parameter API is appropriate for the #441 vertical slice. A future refactor -requiring path injection can add parameters without breaking callers. No change needed. - ---- - -## Drift Analysis: v3 -> v4 - -Jiminy's patches are surgical and architecturally non-regressive. - - P1 (foreach body): fills the stub with self-contained inline code. Consistent with - Option A identified in Pluto's v3 grill as most compatible with D3 self-containment. - Uses the same Set-Content -NoNewline -TrimEnd() pattern as the writer loop. - Minor regex inconsistency noted in A-3 -- not introduced by Jiminy; the v3 plan - silently implied the same gap, and v4 does not make it worse. - - P2 (function wrapper): adds the Write-PowerShellProfile boundary and explicit dot-source - safety comment. Consistent with production file structure. No new top-level code - introduced. No global-state leakage. - - P3-P7 (Chip's items): test-plan changes outside Pluto's lens. - -Error handling style: Invoke-HostQuery and Resolve-ProfilePath continue to use -$LASTEXITCODE for external-process failures and try/catch for PS exceptions. Consistent -between both functions. No style mixing introduced by v4. - ---- - -## Summary Matrix - -| # | Concern | v3 Status | v4 Status | -|---|---------|-----------|-----------| -| P1 | foreach stub empty | BLOCKING | RESOLVED | -| P2 | scope ambiguity | BLOCKING | RESOLVED | -| A-1 | $ps51Fallback/$ps7Fallback undefined in fn | new | MEDIUM -- plan fix before code | -| A-2 | write-loop comment stub, alignment unconfirmed | new | LOW -- impl note | -| A-3 | regex divergence legacy cleanup vs writer loop | new | LOW -- impl note | -| A-4 | empty $profilePaths guard | new | LOW -- contingent on A-1 | - ---- - -## Verdict - -SHIP. - -Both v3 blocking concerns are genuinely resolved. The explicit function-boundary wrapper -with dot-source safety comment closes the scope ambiguity hole completely. The foreach -body contains real, executable strip code that directly satisfies GG-4's dual-orphan -assertion. - -Finding A-1 ($ps51Fallback/$ps7Fallback undefined) is the only concern of meaningful -severity. It is a plan documentation gap, not an algorithmic error. The natural -resolution -- define both constants as local variables at the top of Write-PowerShellProfile -(matching production lines 12-19) -- requires one or two lines of implementation code. -A StrictMode-aware implementer will encounter this immediately. The plan should carry an -implementation note, but it does not invalidate the architecture. - -The algorithm is architecturally sound: - - One clear entry point (Write-PowerShellProfile) - - Dot-source safe (no top-level execution) - - Idempotent (strip-then-inject pattern) - - Correct dual-host failure fallback - - Mutually exclusive cleanup paths (no double-strip) - - Clean mockability seam (Invoke-HostQuery at file scope) - - No global-state leakage - - No interaction conflict with existing writer loop - -SHIP with implementation note: define $ps51Fallback and $ps7Fallback at the top of -Write-PowerShellProfile before the Resolve-ProfilePath calls (consistent with production -lines 17-19). diff --git a/docs/plans/441-grill-pluto-v5.2.md b/docs/plans/441-grill-pluto-v5.2.md deleted file mode 100644 index fff3b48e..00000000 --- a/docs/plans/441-grill-pluto-v5.2.md +++ /dev/null @@ -1,213 +0,0 @@ -# Grill Report: #441 -- profile.ps1 writes to wrong path on OneDrive/KFM systems - -**Griller:** Pluto (Platform Architect -- architecture/algorithm correctness) -**Plan reviewed:** docs/plans/441-profile-path.md (v5.2, Mickey revision) -**Date:** 2026-05-27 -**Session:** 441-grill-v5.2 -**Prior grills:** 441-grill-pluto-v4.md (v4), 441-grill-pluto-v5.md (v5.1 -- I shipped SHIP; missed JN-1) - ---- - -## Verdict - -**SHIP.** - -JN-1 is fully resolved. Parameter design is sound, backward-compatible, and scope-leak free. -No HIGH or MEDIUM findings. Two INFO-level observations, neither blocking. - -I shipped SHIP on v5.1 and missed JN-1 -- the $local: shadow that made test overrides inoperable. -Mickey's param solution is cleaner than Option A in Jiminy's JN-1 writeup: no intermediate -$local: reassignment layer, params are used directly throughout the function body. I missed the -plan-internal contradiction between H5 and H3. I am not re-litigating that; I am verifying the -fix is correct. - ---- - -## 1. Parameter Design Assessment - -### 1a. Names and types - -| Param | Type | Name valid? | PS convention? | -|-----------------|----------|-------------|----------------| -| `-Ps51Fallback` | `[string]` | YES | ACCEPTABLE (see PV-2) | -| `-Ps7Fallback` | `[string]` | YES | ACCEPTABLE (see PV-2) | - -Both are valid PowerShell parameter names. `[string]` is the correct type for file path -arguments that will be consumed by `Test-Path`, `Get-Content`, `Set-Content`, and -`Select-String` -- all of which accept `[string]` natively. - -### 1b. Defaults: constants vs evaluated expressions - -Both defaults use `[System.IO.Path]::Combine($HOME, ...)` -- the same expression form as -production lines 17-18. These are evaluated expressions, not compile-time constants. In -PowerShell, parameter default expressions are evaluated at invocation time in the calling -scope. `$HOME` is an automatic variable set by PowerShell from the OS environment; it is -always defined, even under `Set-StrictMode -Version Latest`. No scenario in the plan scope -(Section 2 OUT list excludes UNC/CLM/long-path edge cases) can produce a null `$HOME`. - -Production lines 17-18 use the SAME expression form inside the function body -- the defaults -are not more or less safe than the production literals were. Match confirmed. PASS. - -### 1c. Parameter validation - -No `[ValidateNotNullOrEmpty()]` or `[ValidateScript()]` applied. Assessment: - -- `[ValidateNotNullOrEmpty()]` would reject an explicit empty-string call. The production - caller never passes empty strings; tests pass valid temp paths. Adding it is defensive but - not necessary for correctness. The plan explicitly defers input-sanitation edge cases - (Section 2 OUT). Not scope creep to add; not a defect to omit. PASS as-is. -- `[ValidateScript()]` for path format would duplicate the `'^[A-Za-z]:\\'` check that - already lives inside `Resolve-ProfilePath`. Redundancy without benefit. PASS as-is. - -### 1d. Markers: $beginMarker / $endMarker - -`$local:beginMarker` and `$local:endMarker` remain `$local:` constants inside the function -body (v5.1-F5). These require no test override -- tests control where files are written, not -what markers they contain. There is no calling-scope variable named `$beginMarker` or -`$endMarker` in the test harness. No shadowing conflict in either direction. Under -`Set-StrictMode -Version Latest`, declaring `$local:beginMarker = '...'` and then reading -`$beginMarker` (without qualifier) resolves correctly to the local binding -- StrictMode -only fires on reads of unassigned variables, not on unqualified reads of locally-assigned -ones. PASS. - ---- - -## 2. Function Signature Stability - -### 2a. Production callsite impact - -The new params are optional with defaults. Any existing caller that invokes -`Write-PowerShellProfile` with no arguments continues to receive the same default path -values as production lines 17-18. Zero callsite changes required. v5.2-D1 confirms this. -PASS. - -### 2b. Single entry point / global state - -- Function has one entry point. No top-level execution on dot-source (comment preserved - in Section 4 algorithm). PASS. -- No `$global:` or `$env:` mutations introduced in production code. `$global:LASTEXITCODE` - reset is test-setup only (Section 5 header). PASS. -- No new file-scope variables. All declarations are parameter-bound or `$local:` inside the - function. PASS. - ---- - -## 3. Scope Leak Regression - -Full variable audit of `Write-PowerShellProfile` under `Set-StrictMode -Version Latest`: - -| Variable | How bound | Defined before read? | Safe? | -|-------------------|-----------------------------------|----------------------|-------| -| `$Ps51Fallback` | param (default evaluated at call) | YES -- param | PASS | -| `$Ps7Fallback` | param (default evaluated at call) | YES -- param | PASS | -| `$beginMarker` | `$local:` def at top of body | YES | PASS | -| `$endMarker` | `$local:` def at top of body | YES | PASS | -| `$profilePaths` | assigned from pipeline result | YES | PASS | -| `$legacyPaths` | assigned as array literal | YES | PASS | -| `$legacy` | foreach loop variable | YES (by foreach) | PASS | -| `$isOrphan` | assigned top of each iteration | YES | PASS | -| `$content` | assigned inside if-guard | YES (guarded) | PASS | -| `$stripped` | assigned inside if-guard | YES (guarded) | PASS | - -`Resolve-ProfilePath` variables (`$HostExe`, `$FallbackPath`, `$raw`, `$resolved`) are -unchanged from v5.1 -- all verified in my prior grill. No regressions. - -**No variable is referenced without prior assignment. StrictMode hazard is fully closed. No -new global-var mutations introduced.** - ---- - -## 4. Idempotency / Failure Mode Check - -| Scenario | Outcome | Status | -|----------|---------|--------| -| Both hosts absent | Both `Resolve-ProfilePath` calls return `$Ps51Fallback`/`$Ps7Fallback` (param defaults = production paths). Neither is orphaned. Writer loop writes to both. Behavior identical to current production. | PASS | -| One host absent | That host's `Resolve-ProfilePath` returns its param fallback. Dedup may or may not collapse to 1 path. Write loop handles both entries correctly. | PASS | -| Both hosts fail ($LASTEXITCODE != 0) | Same as both-absent: both fall back to param defaults. | PASS | -| Empty profilePaths | Impossible: `Resolve-ProfilePath` always returns `$FallbackPath`; params are non-empty strings (enforced by defaults). | PASS | -| Regex no-match in orphan-strip | Replace returns `$content` unchanged; `TrimEnd()` is no-op; `Select-String -Quiet` guard prevents reaching replace for block-less files. | PASS | -| Re-callable (idempotency) | Strip-then-inject pattern unchanged; GG-5 (3-run test) exercises this via temp paths. | PASS | -| Mid-run Set-Content throw | Under `$ErrorActionPreference = Stop` (production line 7), propagates immediately. Re-run is safe recovery. Pre-existing risk, unchanged by v5.2. | PASS | -| Test temp-path isolation | GG-1/GG-4/GG-5 pass explicit temp paths via `-Ps51Fallback`/`-Ps7Fallback`. Real `$HOME` paths are never touched during test execution. JN-1 fix confirmed effective. | PASS | - ---- - -## 5. New Findings - -### [INFO] PV-2: `Ps51`/`Ps7` casing is non-idiomatic for PS acronyms - -**Citation:** Section 4 param block. - -PowerShell style conventions (PascalCase) treat acronyms of 3+ chars as PascalCase -(`Xml`, `Http`) and 2-char acronyms as all-caps (`PS`, `IO`). Idiomatic names would be -`-PS51Fallback`/`-PS7Fallback`. Mickey used `Ps51`/`Ps7` (lowercase `s`). This is -functionally harmless -- PowerShell parameter binding is case-insensitive; `-ps51fallback` -and `-PS51FALLBACK` both bind correctly. Internal consistency across plan/decision/test rows -is maintained. No implementer confusion risk. - -**Impact on ship decision:** NONE. -**Severity:** INFO. Cosmetic only. Do not revise for this alone. - -### [INFO] PV-3: GG-5 mock setup unspecified - -**Citation:** Section 5, GG-5 row. - -GG-5 specifies "Write-PowerShellProfile -Ps51Fallback $tempPath51 -Ps7Fallback $tempPath7 -3x same temp file" but does not specify what `Invoke-HostQuery` mock should return. For the -write to target `$tempPath51`/`$tempPath7`, the mock must either (a) return those paths, or -(b) return invalid/empty output so `Resolve-ProfilePath` falls back to the params. Both -scenarios produce the same result: write lands in the temp files, idempotency holds. A -competent implementer infers this without ambiguity. Pre-existing accepted gap (Pluto A-2 -pattern). Not a new hole. - -**Impact on ship decision:** NONE. -**Severity:** INFO. No revision needed. - ---- - -## 6. Prior Findings: Full Status Matrix (v5.2) - -| # | Griller | Sev | v5.1 Status | v5.2 Status | -|---|---------|-----|-------------|-------------| -| P1 | Pluto | BLOCKING | RESOLVED | HOLDS | -| P2 | Pluto | BLOCKING | RESOLVED | HOLDS | -| A-1 | Pluto | MEDIUM | RESOLVED (H5) | HOLDS -- but H5 introduced JN-1 | -| A-2 | Pluto | LOW | Deferred | Still deferred (write-loop stub); no regression | -| A-3 | Pluto | LOW | RESOLVED (F-4) | HOLDS | -| A-4 | Pluto | LOW | RESOLVED (subsumed) | HOLDS | -| H1-H5 | Donald | HIGH/MED | RESOLVED | HOLDS | -| F-4/F-5 | Donald | MED/LOW | RESOLVED | HOLDS | -| C-1/C-2 | Chip | MEDIUM | RESOLVED | HOLDS | -| NF-3v4 | Chip | LOW | OPEN (PV-1) | RESOLVED (JN-2: Write-Warning applied) | -| NF-4v4 | Chip | LOW | RESOLVED | HOLDS | -| JN-1 | Jiminy | MEDIUM | OPEN (introduced by H5) | **RESOLVED (v5.2 params)** | -| JN-2 | Jiminy | LOW | OPEN | **RESOLVED (Write-Warning applied)** | -| PV-1 | Pluto | LOW | OPEN (carry-over) | RESOLVED via JN-2 | -| PV-2 | Pluto | INFO | N/A | NEW (see above; non-blocking) | -| PV-3 | Pluto | INFO | N/A | NEW (see above; non-blocking) | - -All BLOCKING, HIGH, and MEDIUM items resolved. No open items above INFO severity. - ---- - -## Architectural Readiness Verdict - -v5.2 is architecturally ship-ready: - -- **JN-1 fix correct:** Parameterizing fallback paths is the right pattern. Parameters are - bound before any function body code executes -- StrictMode-safe, no shadow risk, no scope - pollution. Cleaner than Jiminy's Option A (no intermediate $local: reassignment needed). -- **Backward-compatible:** Zero-arg production call unchanged; defaults guarantee same - behavior as lines 17-18. -- **Scope-leak free:** All 10 function-body variables confirmed defined before read. -- **Test isolation correct:** Real $HOME files unreachable during test execution. -- **Idempotency preserved:** Strip-then-inject pattern unchanged; all failure-mode paths - handled. - -**SHIP.** - ---- - -**Grilled by:** Pluto (Platform Architect) -**Date:** 2026-05-27 -**Session:** 441-grill-v5.2 diff --git a/docs/plans/441-grill-pluto-v5.md b/docs/plans/441-grill-pluto-v5.md deleted file mode 100644 index 7ae3147b..00000000 --- a/docs/plans/441-grill-pluto-v5.md +++ /dev/null @@ -1,174 +0,0 @@ -# Grill Report: #441 -- profile.ps1 writes to wrong path on OneDrive/KFM systems - -**Griller:** Pluto (Platform Architect -- architecture/algorithm correctness) -**Plan reviewed:** docs/plans/441-profile-path.md (v5.1, Donald revision) -**Date:** 2026-05-27 -**Session:** 441-grill-v5 -**Prior grills:** 441-grill-pluto-v4.md (v4), 441-grill-donald-v4.md (v4), 441-grill-chip-v4.md (v4) - ---- - -## Verdict - -**SHIP.** - -A-1 is fully resolved. All seven patches (H1-H5, F-4, F-5) land correctly with no -regressions. No new findings of blocking or HIGH severity. Two pre-acknowledged LOWs -remain open and are acceptable for ship. - ---- - -## Task 1: A-1 Status -- RESOLVED - -**Claim:** `$local:ps51Fallback` and `$local:ps7Fallback` defined at top of -`Write-PowerShellProfile` before first use (v5-H5). `$local:beginMarker` and -`$local:endMarker` similarly defined (v5.1-F-5). - -**Verification against plan Section 4:** - -``` -$local:ps51Fallback = [System.IO.Path]::Combine($HOME, 'Documents', 'WindowsPowerShell', 'Microsoft.PowerShell_profile.ps1') -$local:ps7Fallback = [System.IO.Path]::Combine($HOME, 'Documents', 'PowerShell', 'Microsoft.PowerShell_profile.ps1') -$local:beginMarker = '# BEGIN dev-setup profile' -$local:endMarker = '# END dev-setup profile' -``` - -Checks: - -1. **Defined at TOP of function:** YES. Four `$local:` declarations are the first four - lines of the `Write-PowerShellProfile` body, before any call to `Resolve-ProfilePath` - or any array construction. Order of execution: define -> use. Correct. - -2. **Before first use:** YES. - - `$ps51Fallback` first used on the next line (`Resolve-ProfilePath 'powershell' $ps51Fallback`). Defined above it. PASS. - - `$ps7Fallback` first used two lines later. PASS. - - `$beginMarker` first used in Select-String pattern check inside the foreach. PASS. - - `$endMarker` first used inside the regex replace. PASS. - -3. **Values match production:** - - `$ps51Fallback`: matches production line 17 exactly (`WindowsPowerShell\Microsoft.PowerShell_profile.ps1`). PASS. - - `$ps7Fallback`: matches production line 18 exactly (`PowerShell\Microsoft.PowerShell_profile.ps1`). PASS. - - `$beginMarker`: matches production line 12 exactly (`# BEGIN dev-setup profile`). PASS. - - `$endMarker`: matches production line 13 exactly (`# END dev-setup profile`). PASS. - -4. **Other variables under StrictMode:** - All remaining variables in the algorithm body are assigned before read: - - `$profilePaths` -- assigned from pipeline result. PASS. - - `$legacyPaths` -- assigned as array literal. PASS. - - `$legacy` -- loop variable (assigned by foreach). PASS. - - `$isOrphan` -- assigned at top of each loop iteration. PASS. - - `$content` -- assigned inside the if-block guard; only read after assignment. PASS. - - `$stripped` -- assigned inside the if-block guard; only read after assignment. PASS. - - `$raw`, `$resolved` in `Resolve-ProfilePath` -- assigned in try block before read. PASS. - - No undefined variable references remain in the algorithm. StrictMode hazard fully closed. - -**A-1: RESOLVED.** - ---- - -## Task 2: Regression Check (H1-H5, F-4, F-5) - -| Patch | Finding | What Was Changed | Regression Check | -|-------|---------|------------------|-----------------| -| H1 | Donald F-1 (HIGH) -- missing `-Encoding ASCII` on orphan-strip `Set-Content` | Added `-Encoding ASCII` to the orphan-strip `Set-Content` call | Plan line 162: `Set-Content $legacy $stripped.TrimEnd() -NoNewline -Encoding ASCII`. Matches production line 28 pattern. No inconsistency between the two `Set-Content` calls in the function. PASS. | -| H2 | Donald F-3 (MEDIUM) -- stale `$LASTEXITCODE` from GG-7 contaminates success-path tests | Section 5 header: `$global:LASTEXITCODE = 0` reset before each mock redefinition | Test-plan only; no production code altered. Production `Resolve-ProfilePath` still reads `$LASTEXITCODE` correctly after `Invoke-HostQuery`. No mutation of `$global:LASTEXITCODE` introduced in production path. PASS. | -| H3 | Donald F-2 + Chip C-2 (MEDIUM) -- `TestDrive` in GG-4 contradicts D2 (Pester rejected) | Replaced `TestDrive` with `Join-Path $env:TEMP "gg-test-441-$(New-Guid)"` temp-path language | Test-plan only. No real `$HOME` paths touched. `New-Guid` generates unique names; cleanup via `finally` block documented. Section 3 D2 (no Pester) not violated. PASS. | -| H4 | Chip C-1 (MEDIUM) -- GG-7 exe unspecified; false green on PS5.1-only runner | GG-7 row specifies `$HostExe = 'powershell'` with rationale | `powershell` is guaranteed present on all Windows systems. Explanation that `'pwsh'` would mask the not-installed early-exit branch (path A) is explicit and correct. Path B (LASTEXITCODE != 0) is now unambiguously the target. PASS. | -| H5 | Pluto A-1 (MEDIUM) -- `$ps51Fallback`/`$ps7Fallback` undefined under StrictMode | Two `$local:` definitions added at top of `Write-PowerShellProfile` | See Task 1 above. RESOLVED. | -| F-4 | Donald F-4 (MEDIUM) -- orphan-strip regex diverges from production without rationale | Added `\r?\n` prefix and changed `.+?` to `.*?`; note cites production line 27 | Plan line 161 regex: `(?s)\r?\n$([regex]::Escape($beginMarker)).*?$([regex]::Escape($endMarker))\r?\n?`. Matches production line 27 exactly. Both differences from v4 eliminated. The start-of-file edge case (v4 A-3) no longer exists because both regexes are now identical; the pre-existing `TrimEnd()` is retained. PASS. | -| F-5 | Donald F-5 (LOW) -- `$beginMarker`/`$endMarker` undefined in Section 4 snippet | `$local:beginMarker`/`$local:endMarker` defined in `Write-PowerShellProfile` | Verified in Task 1. Values match production lines 12-13. PASS. | - -**Scope / mutation audit for all patches:** -- No new file-scope variables introduced. All new declarations are `$local:` inside a function. PASS. -- No new `$env:` mutations in production code. `$env:TEMP` reference is test-plan only (Section 5). PASS. -- No `$global:` mutations in production code. `$global:LASTEXITCODE = 0` is test-setup only (Section 5 header). PASS. -- Function still has single entry point (`Write-PowerShellProfile`). PASS. -- Dot-source safety preserved (explicit comment in function, no top-level execution). PASS. -- Idempotency preserved (strip-then-inject pattern unchanged). PASS. - ---- - -## Failure-Mode Spot-Check (post-patch) - -| Scenario | Outcome | -|----------|---------| -| Both hosts absent | Both `Resolve-ProfilePath` calls return `$ps51Fallback` / `$ps7Fallback` respectively. `$profilePaths` = both fallbacks. Legacy cleanup: neither is orphaned (`$isOrphan = false`). Writer loop writes to both. Identical to current production. CORRECT. | -| Regex no-match in orphan-strip | `$content -replace ...` returns `$content` unchanged. `TrimEnd()` is a no-op on a block-less file. Conditional guard (`Select-String -Quiet`) prevents reaching the replace anyway. CORRECT. | -| Empty block (BEGIN immediately followed by END) | F-4 changed `.+?` to `.*?`; now matches zero-content blocks. CORRECT. | -| Block at start of file (no preceding newline) | F-4 regex includes `\r?\n` prefix -- same as production. A file where the block is on line 1 will not match the strip regex. This is a pre-existing production limitation (applies equally to the writer-loop strip). OUT OF SCOPE (#441). | -| `$profilePaths` empty | Impossible: `Resolve-ProfilePath` always returns `$FallbackPath` as last resort; fallbacks are non-empty `$local:` constants (A-1 resolved). CORRECT. | -| Mid-run `Set-Content` throw | Under `$ErrorActionPreference = Stop` (production line 7), propagates immediately. Orphan may be stripped but resolved path not yet written. Re-running is idempotent recovery. Pre-existing acceptable risk; unchanged by v5.1. CORRECT. | - ---- - -## Prior Concerns: Full Status Matrix - -| # | Griller | Severity | v4 Status | v5.1 Status | -|---|---------|----------|-----------|-------------| -| P1 | Pluto | BLOCKING | RESOLVED | HOLDS | -| P2 | Pluto | BLOCKING | RESOLVED | HOLDS | -| A-1 | Pluto | MEDIUM | Open (plan fix before code) | RESOLVED (H5 + F-5) | -| A-2 | Pluto | LOW | Deferred | Still deferred -- write-loop stub comment unchanged; variable alignment ($profilePaths) confirmed correct; no regression | -| A-3 | Pluto | LOW | Deferred | RESOLVED by F-4 (regex now identical to production) | -| A-4 | Pluto | LOW | Contingent on A-1 | RESOLVED (subsumed by A-1 resolution) | -| F-1 | Donald | HIGH | Open | RESOLVED (H1) | -| F-2 | Donald | MEDIUM | Open | RESOLVED (H3) | -| F-3 | Donald | MEDIUM | Open | RESOLVED (H2) | -| F-4 | Donald | MEDIUM | Open | RESOLVED (F-4 patch) | -| F-5 | Donald | LOW | Open | RESOLVED (F-5 patch + H5) | -| C-1 | Chip | MEDIUM | Open | RESOLVED (H4) | -| C-2 | Chip | MEDIUM | Open | RESOLVED (H3) | -| NF-3v4 | Chip | LOW | Open | Still open (see below) | -| NF-4v4 | Chip | LOW | Open | RESOLVED (BeforeEach language removed from Section 5) | - ---- - -## New Findings - -### [LOW] PV-1: C-2/C-3 skip-as-pass unresolved (Chip NF-3v4 carry-over) - -**Citation:** Section 3 v3-D4 and Section 7 acceptance criterion C-2/C-3. - -Chip's NF-3v4 (LOW) noted that the `if (...PSVersion.Major -ge 7) { Write-Host 'SKIP...'; return }` -guard causes `Test-Scenario` to record a PASS (not a skip) on PS7+ runners, inflating the pass -count and hiding coverage gaps. The plan retains `Write-Host + return` and does not call any -`Write-Skip` helper. - -**Assessment:** The plan is internally consistent -- it uses the mechanism that v3-D4 specified. -Whether `Write-Skip` exists in the test harness is an implementation detail outside the plan's -scope. The guard correctly prevents the destructive `$PROFILE = $path` assignment from running -on PS7+. The `Write-Host` log provides audit trail. The inflated pass count is a cosmetic -reporting concern; it does not produce false green on the NEW GG tests that cover this PR's -actual behavior. - -**Impact on ship decision:** NONE. This is a pre-existing LOW from the v4 cycle, carried forward -unchanged. It does not affect architectural correctness, algorithm safety, or GG-1 through GG-7 -reliability. The implementer note from Chip remains valid if `Write-Skip` is available. - -**Post-ship deferral:** YES. -**Severity:** LOW (carry-over; no severity escalation). - ---- - -## Architectural Readiness Verdict - -The v5.1 algorithm is architecturally ship-ready: - -- **Single entry point:** `Write-PowerShellProfile` only. No top-level execution on dot-source. -- **StrictMode-safe:** All variables defined before use. `$local:` declarations prevent scope leaks. -- **Idempotent:** Strip-then-inject in writer loop; orphan-strip guarded by `$isOrphan`, `Test-Path`, and `Select-String`; GG-5 (3-run test) covers this empirically. -- **Both-hosts-fail handled:** Falls back to hardcoded paths; behavior identical to current production. -- **Legacy cleanup correct:** `$isOrphan` mutual exclusivity ensures no path is both orphan-stripped and writer-loop-written. -- **Encoding consistent:** Both `Set-Content` calls in the function use `-Encoding ASCII`. Matches production. -- **No global-state leakage:** No new file-scope variables. No production `$global:` or `$env:` mutations. -- **Regex parity:** F-4 closes the orphan-strip/writer-loop divergence. Both regexes now identical to production line 27. -- **Test plan implementable:** All seven GG tests have unambiguous input, mock, and assertion specs. Temp file pattern explicit. `$HostExe` spec explicit in GG-7. - -**SHIP.** - ---- - -**Grilled by:** Pluto (Platform Architect) -**Date:** 2026-05-27 -**Session:** 441-grill-v5 diff --git a/docs/plans/441-grill-pluto.md b/docs/plans/441-grill-pluto.md deleted file mode 100644 index ad6bd6a0..00000000 --- a/docs/plans/441-grill-pluto.md +++ /dev/null @@ -1,351 +0,0 @@ -# Grill Report: #441 -- profile.ps1 writes to wrong path on OneDrive/KFM systems - -**Griller:** Pluto (Config Engineer -- architecture/algorithm correctness angle) -**Plan reviewed:** docs/plans/441-profile-path.md (v3, author: Donald) -**Date:** 2026-05-27 -**Verdict:** REVISE - -**Author locked out:** Goofy (v1), Mickey (v2), Donald (v3) -**Eligible next reviser (if REVISE):** Chip - ---- - -## Angle: Architecture & Algorithm Correctness - ---- - -### Findings - -**1. Drive-letter regex `'^[A-Za-z]:\\'` -- correct and scope-consistent (no hole)** - -plan:Section 4, line 98: `if ($resolved -notmatch '^[A-Za-z]:\\') { return $FallbackPath }` - -The regex is correct: -- Accepts both uppercase and lowercase drive letters. [Environment]::GetFolderPath returns - uppercase on standard Windows, but lowercase is legal on case-insensitive NTFS. The - inclusive range is the right defensive posture. -- Rejects UNC paths (`\\server\share\...`). UNC is explicitly OUT per Section 2. Rejecting - a UNC-returning host and falling back to the hardcoded path is consistent with the scope - decision. No contradiction. -- Rejects error message strings, PSReadLine artifacts, partial stack traces, and other - garbage that could slip through `Select-Object -Last 1`. - -Minor gap (non-blocking): individual split-result lines are NOT trimmed before the regex -check. `$raw.Trim()` strips the outer blob; `Where-Object { $_ }` drops empty lines; but -each surviving line may still carry a trailing space or trailing `\r` if the host emits -unexpected whitespace. The regex `'^[A-Za-z]:\\'` checks the PREFIX only -- it passes a -path like `C:\Users\Earl\..._profile.ps1 ` (trailing space). Downstream `Split-Path` and -`New-Item` accept such strings on most PS versions, but strict-mode + symlinked directories -can misbehave. A one-liner `$resolved = $resolved.Trim()` after `Select-Object -Last 1` -would close this gap. - -Verdict on item: not blocking; suggest add-trim as implementation note. - ---- - -**2. `-NoLogo` on powershell.exe 5.1 and pwsh.exe -- valid, non-issue** - -plan:Section 3 v3-D1, Section 4 Invoke-HostQuery: `& $Exe -NoProfile -NonInteractive -NoLogo -Command '$PROFILE' 2>$null` - -`-NoLogo` has been a documented parameter of `powershell.exe` since Windows PowerShell 2.0 -(suppresses the copyright banner). It is equally valid on `pwsh.exe` (PS 7+). No version -caveat applies. - -If `-NoLogo` were unsupported on a given binary (hypothetical), `powershell.exe` would -write the "Unknown parameter" message to stderr and exit non-zero. `2>$null` suppresses -the stderr noise; the non-zero `$LASTEXITCODE` -- now checked in v3 -- triggers the warning -and fallback. Defense-in-depth is intact even for the hypothetical. - -Verdict on item: clean. No hole. - ---- - -**3. `$LASTEXITCODE` after `Invoke-HostQuery` with `2>$null` -- no interference, check is correct** - -plan:Section 4 Resolve-ProfilePath: `$raw = Invoke-HostQuery -Exe $HostExe` then `if ($LASTEXITCODE -ne 0)` - -`$LASTEXITCODE` is set by the exit code of the child process at the moment it exits. Stream -redirection (`2>$null`) suppresses output bytes -- it has no effect on the exit-code -integer. The check is valid. - -`Invoke-HostQuery` is a PS function, not an external command. Calling it does NOT reset -`$LASTEXITCODE`. Inside the function, `& $Exe ...` is the only statement, so `$LASTEXITCODE` -after the function returns equals the exit code of that one external call. The variable is -global-scoped in PS and is not reset by function return. The pattern is correct. - -One implementation note: if a previous external command in the same script left a non-zero -`$LASTEXITCODE` and `Get-Command $HostExe` somehow throws BEFORE `Invoke-HostQuery` is -reached, the stale `$LASTEXITCODE` could theoretically be read. But the guard is `if (-not -(Get-Command $HostExe -EA SilentlyContinue))`, which is a PS cmdlet -- it does not touch -`$LASTEXITCODE`. And the check fires only AFTER `Invoke-HostQuery` returns, not before. So -no stale-value leak path exists in the algorithm as written. - -Verdict on item: clean. No hole. - ---- - -**4. `try/catch` + `$LASTEXITCODE` ordering under `$ErrorActionPreference = 'Stop'` -- correct** - -plan:Section 4 Resolve-ProfilePath, production profile.ps1 line 7: `$ErrorActionPreference = 'Stop'` - -Under `$ErrorActionPreference = 'Stop'`, PS cmdlet non-terminating errors become -terminating (caught by `catch`). External process calls via `& $Exe` are NOT subject to -this promotion -- they set `$LASTEXITCODE` and return silently regardless of exit code. -The two error-handling paths are orthogonal: - -- `catch` fires only for PS-native exceptions (e.g., `CommandNotFoundException` if `& $Exe` - somehow resolves to a nonexistent command after the `Get-Command` gate, or if - `Write-Info`/`Write-Warn` throw due to a logging failure). -- `$LASTEXITCODE -ne 0` fires only for successful-launch-but-failed-exit of the external - process. - -The plan question "does try/catch catch it before $LASTEXITCODE fires?" applies only if -the external process itself throws in the PARENT scope -- which `& $Exe` cannot do (it -either launches the process, or fails to launch and throws `CommandNotFoundException`, -which is a PS exception handled by `catch`). If `$PROFILE` inside the CHILD process is -somehow inaccessible, the child exits (possibly non-zero) and the parent's `$LASTEXITCODE` -check handles it. The catch block is never involved in child-process-level errors. - -Verdict on item: clean. Algorithm ordering is correct and complete. - ---- - -**5. Legacy cleanup loop body is an empty stub -- BLOCKING** - -plan:Section 4, lines 113-118: - -``` -foreach ($legacy in $legacyPaths) { - $isLegacy = -not ($profilePaths | Where-Object { $_.ToLower() -eq $legacy.ToLower() }) - if ($isLegacy -and (Test-Path $legacy) -and (Select-String -Path $legacy -Pattern $beginMarker -Quiet)) { - # Strip block from orphaned legacy file - } -} -``` - -The loop body is a single comment. No stripping code exists. The plan does not name a -function to call, does not inline the regex, and does not reference the strip logic from -production `uninstall.ps1` (`Remove-DevSetupProfileBlock`). The comment reads as a -placeholder that was never filled in. - -This is a plan-internal contradiction: -- Section 5 GG-4 asserts "Both legacy paths stripped" and checks that neither legacy file - has the BEGIN marker after the run. -- Section 4 contains no stripping code. GG-4 would fail against the algorithm as written. - -The fix is one of three options: - (A) Inline the strip regex (same pattern as profile.ps1 lines 26-29 or uninstall.ps1 - lines 93-94) directly in the loop body. - (B) Extract a shared `Remove-ProfileBlock` helper, call it here and in uninstall.ps1. - (C) Import or dot-source the strip function -- but this conflicts with the self-contained - goal stated in D3. - -Option A is consistent with the self-containment rationale and keeps the cognitive footprint -local. But the plan must state which option is chosen and show the implementation, not a -comment. A stub comment is not an algorithm. - -Note: `Remove-DevSetupProfileBlock` in uninstall.ps1 does the right thing but lives only -in that file. profile.ps1 (production) has inline strip logic at lines 23-29 but only for -the "already-written" path, not for orphaned-legacy stripping. The v3 plan adds a new -stripping context that currently has no implementation. - -Verdict on item: BLOCKING. GG-4 directly contradicts Section 4. - ---- - -**6. Ambiguity: is the Section 4 algorithm top-level or inside Write-PowerShellProfile? -- BLOCKING** - -plan:Section 4 comment: `# In profile.ps1 (and inlined in uninstall.ps1):` -plan:Section 5 header: "Invoke-HostQuery mock defined AFTER dot-sourcing profile.ps1." - -Section 4 shows `$profilePaths = @(Resolve-ProfilePath ...) | Sort-Object ...` and the -foreach legacy loop as bare top-level statements -- not wrapped in a function. If this code -is actually placed at the top level of profile.ps1 (outside any function), it executes the -moment the script is dot-sourced. The test mock-after-dot-source pattern in Section 5 then -breaks: - -1. Test dot-sources profile.ps1. -2. Top-level code runs immediately -- calls `Resolve-ProfilePath` -> calls `Invoke-HostQuery` - (the real one, not the mock, because mock isn't defined yet). -3. Test defines mock `Invoke-HostQuery`. Too late -- the resolution already ran. - -GG-1 through GG-7 all depend on the mock intercepting calls to `Invoke-HostQuery`. If the -algorithm runs at dot-source time, every GG test silently tests the wrong thing (real host -queries, not mocked ones), and failures appear only on machines missing the hosts. - -The current production profile.ps1 wraps all logic inside `Write-PowerShellProfile`. The -v3 plan must explicitly state that the algorithm remains inside that function (or an -equivalent named function). The Section 4 pseudocode block is ambiguous on this point -- -it omits the enclosing function boundary. - -Section 5 note ("Mock must be defined AFTER dot-sourcing profile.ps1 or the dot-source -overwrites it") is correct for the function-scoped case but says nothing to prevent a -future implementer from reading Section 4 literally and placing the code at top level. - -The plan must add a function-boundary wrapper in Section 4, making the scope explicit: - -```powershell -function Write-PowerShellProfile { - # ... Invoke-HostQuery and Resolve-ProfilePath defined here, or at file scope above - $profilePaths = @( - (Resolve-ProfilePath 'powershell' $ps51Fallback), - (Resolve-ProfilePath 'pwsh' $ps7Fallback) - ) | Sort-Object { $_.ToLower() } -Unique - ... -} -``` - -Without this clarification, Section 4 and Section 5 are potentially inconsistent depending -on where the implementer places the code. - -Verdict on item: BLOCKING. Scoping ambiguity invalidates the test design in Section 5. - ---- - -**7. GG-2 "absent exe name" -- correct approach, minor fragility (non-blocking)** - -plan:Section 5 GG-2: input `'powershell-notexist'` - -`Get-Command 'powershell-notexist' -EA SilentlyContinue` returns nothing (no match on any -reasonable dev machine). The exe-not-found guard fires and the fallback is returned. - -This tests the right thing: that the guard logic works without needing to shadow -`Get-Command` (which would be a risky built-in mock). The approach is cleaner than v2. - -The only fragility: if some future PATH entry installs a binary literally named -`powershell-notexist.exe`, the test fails for the wrong reason. Using a GUID-based name -(`powershell-absent-{hex}`) is mathematically guaranteed absent. This is a low-probability -concern on any real dev machine; not a blocking hole. Consider as an implementation note. - -Verdict on item: non-blocking. Acceptable for practical purposes. - ---- - -**8. D3 re-evaluation gap: Option B analysis was requested but not performed -- non-blocking process concern** - -Donald's revision instructions (441-grill-donald.md, If Revision Needed section): -> "Section 3 Decision 3: restate the line-count estimate (~31 lines, not ~15)" -> "re-evaluate Option B (lib file) at the corrected line count" - -v3 D3: "Resolver is ~30 lines inlined -- acceptable for self-containment." - -v3 restated the count (31->30, roughly) but skipped the Option B analysis Donald requested. -The phrase "acceptable for self-containment" is a restatement of the original conclusion -from v1/v2, not a freshly-performed comparison. - -Additionally, the ~30 line claim is still low. Counting Section 4 as written: -- `Invoke-HostQuery` function: ~5 lines (header + param + body + closing brace) -- `Resolve-ProfilePath` function: ~22 lines (header, param block, Get-Command guard, try - block with 4 exit paths, catch block, closing brace) -- `$profilePaths = @(...) | Sort-Object`: ~4 lines -- `$legacyPaths = @(...)`: 1 line -- `foreach ($legacy in $legacyPaths) { ... }`: ~7 lines (with the currently-empty body) -Total: ~39 lines BEFORE the actual strip logic in the loop body (Finding 5). - -At 40+ lines in uninstall.ps1, the Option B question is material. The plan should document -the Option B evaluation, even if the conclusion remains Option A. - -Verdict on item: non-blocking for this re-grill, but the missing analysis is a process gap -that weakens confidence in D3. If Finding 5 is fixed by adding inline strip logic, the -line count grows further and Option B deserves a real answer. - ---- - -**9. Dedup + legacy cleanup interaction for shared-path scenario -- handled correctly** - -plan:Section 4 algorithm, Section 3 v3-D5 - -Scenario: both hosts resolve to the same OneDrive path (dedup leaves one entry in -`$profilePaths`). Legacy cleanup iterates `$legacyPaths = @($ps51Fallback, $ps7Fallback)`. -For each: `$isLegacy = -not ($profilePaths | Where-Object { $_.ToLower() -eq $legacy.ToLower() })`. - -Since both `$ps51Fallback` (`$HOME\Documents\WindowsPowerShell\...`) and `$ps7Fallback` -(`$HOME\Documents\PowerShell\...`) are different from the OneDrive-resolved path, both -evaluate `$isLegacy = true`. If both legacy files exist with the BEGIN marker, both get -stripped. The single resolved OneDrive path receives the block write. This is correct. - -Practical note: PS5.1 `$PROFILE` always contains `WindowsPowerShell` in the subdir and -PS7 `$PROFILE` always contains `PowerShell`. Their values cannot be identical under -standard PS behavior. The shared-path dedup scenario is only possible with non-standard -profile configuration. The algorithm handles it correctly regardless; no special case needed. - -Verdict on item: clean. No hole. - ---- - -**10. Power-of-9 decisions cargo-cult check -- one weak entry (D3)** - -D1 (Select-Object -Last 1 + -NoLogo): directly addresses Donald's Finding 1. Rationale -is concrete ("banner appears before path on stdout; -Last 1 selects path"). Not cargo-cult. - -D2 ($LASTEXITCODE check): directly addresses Donald's Finding 2. Rationale is correct PS -semantics. Not cargo-cult. - -D3 (inline ~30 lines): the re-evaluation step was skipped (see Finding 8). The conclusion -is the same as v1/v2 restated with a corrected number, not freshly derived. Weak. - -D4 (GG-4 dual-orphan expansion): concrete rationale ("one-path test cannot detect a -loop-break bug after the first match"). Not cargo-cult. - -D5 (Sort-Object -Unique confirmed): explicitly states mechanism ("keys on script block -output; equal .ToLower() values deduplicated") and points to GG-3 as empirical confirmation. -Not cargo-cult. - -Only D3 reads as "we restated the prior conclusion with the corrected number." All other -decisions show real reasoning. D3 is a process gap (see Finding 8), not an algorithm error. - ---- - -### What v3 Got Right - -- **GG-6 contradiction fixed:** changing to `Select-Object -Last 1` correctly resolves - Donald's Finding 1. A banner prefix no longer produces the wrong line. -- **$LASTEXITCODE guard added:** v3-D2 adds the check Donald required, closes the silent- - fallback-on-broken-PS5.1 hole. GG-7 provides the corresponding test coverage. -- **Regex path validation added:** `'^[A-Za-z]:\\'` guard prevents garbage strings from - reaching `Split-Path`/`New-Item`. Consistent with Section 2 UNC OUT decision. -- **Single-quoting on `'$PROFILE'`:** correct in both PS5.1 and PS7. Parent shell does not - expand the variable; child shell evaluates it. This was correct in prior versions and - remains correct. -- **D3 line-count honesty:** accepting ~30 lines (even if slightly low) over v2's "~15 lines" - is a direct improvement. The inline-for-self-containment rationale is sound. -- **D4 dual-orphan test (GG-4):** expanding to seed both legacy paths simultaneously is the - right test design. Catches loop-break-after-first-match bugs that a one-path test misses. - ---- - -### Verdict - -REVISE. Two findings are blocking. - -**Finding 5** is a plan-internal contradiction of the same severity as Donald's Finding 1 -against v2: GG-4 asserts both legacy paths are stripped, but Section 4 contains no -stripping code -- only a comment placeholder. An implementer reading the plan cannot -determine what to write in the loop body. Any implementation that fills the stub will be -unreviewed. - -**Finding 6** is an architecture ambiguity that threatens the entire test design: if -Section 4's top-level-looking code is placed at file scope in profile.ps1, dot-source -runs it before any test mock is defined, and every GG test silently calls the real host -query instead of the mock. The plan must show the function-boundary wrapper in Section 4 -to make the scope unambiguous. - -Findings 1 (minor trim gap), 7 (GG-2 fragility), 8 (D3 analysis gap) are non-blocking and -may be handled as implementation notes during the revision pass. - ---- - -## If Revision Needed - -**Revision owner:** Chip -**Sections requiring change:** -- Section 4 algorithm: add explicit function-boundary wrapper (e.g., inside - `Write-PowerShellProfile`) so top-level vs. function scope is unambiguous -- Section 4 algorithm: fill the empty loop body -- name the strip function or show the - inline strip code; do not leave a comment as implementation -- Section 3 D3: document the Option B (lib file) comparison at ~40 lines; accept or - reject with reasoning, not just a number change -- Section 5 GG-4: confirm the assertion still holds once the loop body is filled (it - should; the assertion is correct, the code is not) -**Re-grill required after revision:** Yes -- Section 4 is substantively changed. -Scope to Section 4 algorithm and Section 5 GG-4; other sections are stable. diff --git a/docs/plans/441-profile-path.md b/docs/plans/441-profile-path.md index e05546df..5402bc4b 100644 --- a/docs/plans/441-profile-path.md +++ b/docs/plans/441-profile-path.md @@ -1,38 +1,38 @@ # Fix Plan: #441 -- profile.ps1 writes to wrong path on OneDrive/KFM systems -**Author:** Goofy (v1), Mickey (v2), Donald (v3), Jiminy (v4 -- quality audit revision), Donald (v5 -- hole-patch revision), Donald (v5.1 -- F-4/F-5 patch), Mickey (v5.2 -- JN-1/JN-2 patch) +**Author:** Contributor **Date:** 2026-05-27 **Issue:** https://github.com/primetimetank21/dev-setup/issues/441 -**Branch:** squad/441-profile-path-fix -**Status:** Ready for re-grill (v5.2) +**Branch:** fix/441-profile-path +**Status:** v5.2 --- -## v5 Changes (Donald revision) +## v5 Changes (revision 5) -| # | Hole | Griller | Sev | Patch | +| # | Hole | Reviewer | Sev | Patch | |---|------|---------|-----|-------| -| H1 | `Set-Content` in foreach body missing `-Encoding ASCII` | Donald F-1 | HIGH | Added `-Encoding ASCII` to orphan-strip `Set-Content` (matches production line 28) | -| H2 | GG-7 exit-1 leaves stale `$LASTEXITCODE`; contaminates success-path tests | Donald F-3 | MEDIUM | Section 5 header: each test resets `$global:LASTEXITCODE = 0` before mock redefinition | -| H3 | `TestDrive` in GG-4 contradicts Section 3 D2 (Pester rejected as scope creep) | Donald F-2 + Chip C-2 | MEDIUM | Replaced `TestDrive` with `Join-Path $env:TEMP "gg-test-441-$(New-Guid)"` temp-path language in GG-4; temp-dir cleanup sentence added to Section 5 header | -| H4 | GG-7 exe unspecified; false green on PS5.1-only runner | Chip C-1 | MEDIUM | GG-7 row: `$HostExe = 'powershell'` (guaranteed on Windows); note that `'pwsh'` would mask the not-installed early-exit | -| H5 | `$ps51Fallback`/`$ps7Fallback` undefined inside `Write-PowerShellProfile` under `Set-StrictMode -Version Latest` | Pluto A-1 | MEDIUM | Two `$local:` definitions added at top of `Write-PowerShellProfile` in Section 4 (mirror production lines 17-19) | +| H1 | `Set-Content` in foreach body missing `-Encoding ASCII` | F-1 | HIGH | Added `-Encoding ASCII` to orphan-strip `Set-Content` (matches production line 28) | +| H2 | GG-7 exit-1 leaves stale `$LASTEXITCODE`; contaminates success-path tests | F-3 | MEDIUM | Section 5 header: each test resets `$global:LASTEXITCODE = 0` before mock redefinition | +| H3 | `TestDrive` in GG-4 contradicts Section 3 D2 (Pester rejected as scope creep) | F-2 + C-2 | MEDIUM | Replaced `TestDrive` with `Join-Path $env:TEMP "gg-test-441-$(New-Guid)"` temp-path language in GG-4; temp-dir cleanup sentence added to Section 5 header | +| H4 | GG-7 exe unspecified; false green on PS5.1-only runner | C-1 | MEDIUM | GG-7 row: `$HostExe = 'powershell'` (guaranteed on Windows); note that `'pwsh'` would mask the not-installed early-exit | +| H5 | `$ps51Fallback`/`$ps7Fallback` undefined inside `Write-PowerShellProfile` under `Set-StrictMode -Version Latest` | A-1 | MEDIUM | Two `$local:` definitions added at top of `Write-PowerShellProfile` in Section 4 (mirror production lines 17-19) | -## v5.2 Changes (Mickey revision) +## v5.2 Changes (revision 5.2) -| # | Hole | Griller | Sev | Patch | +| # | Hole | Reviewer | Sev | Patch | |---|------|---------|-----|-------| -| JN-1 | `$local:ps51Fallback`/`$local:ps7Fallback` inside `Write-PowerShellProfile` shadow calling scope; test-scope assignment inoperable; GG-1/GG-4/GG-5 would write to real `$HOME` paths | Jiminy JN-1 | MEDIUM | Parameterized `Write-PowerShellProfile` with `-Ps51Fallback`/`-Ps7Fallback` (defaults = production lines 17-18); tests pass temp paths as named parameters; Section 3 v5.2-D1 added; Section 5 GG-1/GG-4/GG-5 updated | -| JN-2 | C-2/C-3 PS7+ skip uses `Write-Host`; increments pass counter instead of skip counter on PS7+ CI | Jiminy JN-2 / Chip NF-3v4 | LOW | `Write-Host 'SKIP C-2: ...'` -> `Write-Warning '[SKIPPED] C-2: ...'` in Section 3 v3-D4; warning stream is visually distinct in PS output; no Pester dependency (D2 preserved) | +| JN-1 | `$local:ps51Fallback`/`$local:ps7Fallback` inside `Write-PowerShellProfile` shadow calling scope; test-scope assignment inoperable; GG-1/GG-4/GG-5 would write to real `$HOME` paths | JN-1 | MEDIUM | Parameterized `Write-PowerShellProfile` with `-Ps51Fallback`/`-Ps7Fallback` (defaults = production lines 17-18); tests pass temp paths as named parameters; Section 3 v5.2-D1 added; Section 5 GG-1/GG-4/GG-5 updated | +| JN-2 | C-2/C-3 PS7+ skip uses `Write-Host`; increments pass counter instead of skip counter on PS7+ CI | JN-2 / NF-3v4 | LOW | `Write-Host 'SKIP C-2: ...'` -> `Write-Warning '[SKIPPED] C-2: ...'` in Section 3 v3-D4; warning stream is visually distinct in PS output; no Pester dependency (D2 preserved) | -## v5.1 Changes (Donald patch) +## v5.1 Changes (revision 5.1) - F-4: Orphan-strip regex matches production line 27 (`\r?\n` prefix added; `.+?` -> `.*?`) - F-5: `$local:beginMarker`/`$local:endMarker` defined in `Write-PowerShellProfile` (mirrors production lines 12-13) --- -## v4 Changes (Jiminy revision) +## v4 Changes (revision 4) | # | Griller | Sev | Patch | |---|---------|-----|-------| diff --git a/docs/plans/451-pwsh-parity-gaps.md b/docs/plans/451-pwsh-parity-gaps.md index 81fa6647..4863fa36 100644 --- a/docs/plans/451-pwsh-parity-gaps.md +++ b/docs/plans/451-pwsh-parity-gaps.md @@ -2,9 +2,9 @@ **Date:** 2026-05-28T02:56:01-04:00 **Revised:** 2026-05-27T23:47:00-04:00 -**Author:** Chip (Tester) +**Author:** Contributor **Issue:** #451 -**Status:** v3 -- Post-Grill Revision (Round 2) +**Status:** v3 --- diff --git a/docs/plans/468-customizable-install.md b/docs/plans/468-customizable-install.md index c2233c76..4718638f 100644 --- a/docs/plans/468-customizable-install.md +++ b/docs/plans/468-customizable-install.md @@ -1,13 +1,13 @@ # Plan: #468 Customizable Install (Pick-and-Choose Tools) **Date:** 2026-05-30 -**Author:** Pluto -- v4 (full rewrite); Donald -- v5 (polish pass); Pluto -- v6 (final polish); Jiminy -- v7 (fixture provenance); Pluto -- v8 (coherence reconciliation); Mickey -- v9 (semantic fix); Doc -- v10 (factual corrections); Mickey -- v11 (2x2 npm-absent matrix); Doc -- v12 (Windows example syntax fix); Mickey -- v13 (Windows flag syntax fix); Goofy -- v14 (PowerShell quoting nits) +**Author:** Contributor (v14) **Issue:** #468 **Status:** Ready for review --- -## v14 Changelog (Goofy -- PowerShell quoting nits, 2026-05-30) +## v14 Changelog (revision 14, 2026-05-30) > **Source:** Doc-2 v13 fact-check findings. Two pre-existing quoting-style nits from v5 > survived all previous passes: one bare token and one double-quoted value in Windows prose diff --git a/scripts/changelog-fold.ps1 b/scripts/changelog-fold.ps1 index 56845a5a..3459250d 100644 --- a/scripts/changelog-fold.ps1 +++ b/scripts/changelog-fold.ps1 @@ -1,6 +1,5 @@ # scripts/changelog-fold.ps1 -- CHANGELOG fold automation (Issue #415) # -# Owner: Donald # Closes: #415 (Windows mirror of scripts/changelog-fold.sh) # # Enumerates all PRs merged and issues closed since the last release tag, diff --git a/scripts/linux/setup.sh b/scripts/linux/setup.sh index 7a39a435..660300b0 100755 --- a/scripts/linux/setup.sh +++ b/scripts/linux/setup.sh @@ -2,7 +2,6 @@ # scripts/linux/setup.sh -- Core Linux/macOS/WSL installer # # Called by: setup.sh (root entry point) -# Owner: Donald (#1, #4-#7, #9) # # This script installs system prerequisites and runs individual tool installers # from scripts/linux/tools/. Each tool script is idempotent -- safe to re-run. diff --git a/scripts/linux/tools/auth.sh b/scripts/linux/tools/auth.sh index 270cae30..f3b1f9a1 100644 --- a/scripts/linux/tools/auth.sh +++ b/scripts/linux/tools/auth.sh @@ -1,9 +1,8 @@ #!/usr/bin/env bash -# scripts/linux/tools/auth.sh — GitHub authentication check and prompt +# scripts/linux/tools/auth.sh -- GitHub authentication check and prompt # # Called by: scripts/linux/setup.sh (after gh is installed) -# Owner: Donald (#13) -# Idempotent: yes — exits 0 immediately if already authenticated +# Idempotent: yes -- exits 0 immediately if already authenticated set -euo pipefail @@ -12,7 +11,7 @@ set -euo pipefail # Require gh CLI if ! command -v gh &>/dev/null; then - log_warn "gh CLI not found — skipping auth check (run gh.sh first)" + log_warn "gh CLI not found -- skipping auth check (run gh.sh first)" exit 0 fi diff --git a/scripts/linux/tools/copilot-cli.sh b/scripts/linux/tools/copilot-cli.sh index 0e729eaa..7d299a35 100755 --- a/scripts/linux/tools/copilot-cli.sh +++ b/scripts/linux/tools/copilot-cli.sh @@ -2,7 +2,6 @@ # scripts/linux/tools/copilot-cli.sh -- Install GitHub Copilot CLI at pinned version # # Called by: scripts/linux/setup.sh -# Owner: Donald / Goofy (#255) # Idempotent: yes -- version-aware; upgrades if installed version != pinned version. # # Install mechanism: npm install -g @github/copilot@ diff --git a/scripts/linux/tools/gh.sh b/scripts/linux/tools/gh.sh index 51e2586a..158aacc8 100755 --- a/scripts/linux/tools/gh.sh +++ b/scripts/linux/tools/gh.sh @@ -2,7 +2,6 @@ # scripts/linux/tools/gh.sh -- Install GitHub CLI (gh) at pinned version # # Called by: scripts/linux/setup.sh -# Owner: Donald / Goofy (#255) # Idempotent: yes -- version-aware; upgrades if installed version != pinned version. # # Linux: downloads the pinned release tarball from GitHub releases (reliable diff --git a/scripts/linux/tools/nvm.sh b/scripts/linux/tools/nvm.sh index a807b638..aa18c032 100755 --- a/scripts/linux/tools/nvm.sh +++ b/scripts/linux/tools/nvm.sh @@ -2,7 +2,6 @@ # scripts/linux/tools/nvm.sh -- Install nvm (Node Version Manager) + pinned Node # # Called by: scripts/linux/setup.sh -# Owner: Goofy (#2) / Donald (#4) # Idempotent: yes -- checks if pinned Node version is already installed set -euo pipefail diff --git a/scripts/linux/tools/uv.sh b/scripts/linux/tools/uv.sh index 37f504bf..8f380bda 100755 --- a/scripts/linux/tools/uv.sh +++ b/scripts/linux/tools/uv.sh @@ -1,9 +1,8 @@ #!/usr/bin/env bash -# scripts/linux/tools/uv.sh — Install uv (Python package manager) +# scripts/linux/tools/uv.sh -- Install uv (Python package manager) # # Called by: scripts/linux/setup.sh -# Owner: Donald (#5) -# Idempotent: yes — checks if uv is already installed before acting +# Idempotent: yes -- checks if uv is already installed before acting set -euo pipefail @@ -20,7 +19,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" UV_VERSION="$(sh "${SCRIPT_DIR}/../../lib/read-tool-version.sh" uv)" curl -LsSf "https://astral.sh/uv/${UV_VERSION}/install.sh" | sh -# uv installs to ~/.local/bin — ensure it's on PATH in current session +# uv installs to ~/.local/bin -- ensure it's on PATH in current session export PATH="$HOME/.local/bin:$PATH" log_ok "uv installed: $(uv --version)" diff --git a/scripts/linux/tools/zsh.sh b/scripts/linux/tools/zsh.sh index c382c48f..ce47c6b1 100755 --- a/scripts/linux/tools/zsh.sh +++ b/scripts/linux/tools/zsh.sh @@ -2,7 +2,6 @@ # scripts/linux/tools/zsh.sh -- Install and configure zsh # # Called by: scripts/linux/setup.sh -# Owner: Donald (#7) # Idempotent: yes -- checks if zsh is already installed before acting set -euo pipefail diff --git a/scripts/sprint-end-labels.sh b/scripts/sprint-end-labels.sh index 76d650f7..b4237ef0 100644 --- a/scripts/sprint-end-labels.sh +++ b/scripts/sprint-end-labels.sh @@ -1,7 +1,6 @@ #!/usr/bin/env bash # scripts/sprint-end-labels.sh -- Sprint-end label automation (Issue #382) # -# Owner: Donald # Closes: #382 # # Applies sprint-end label transitions to all issues and PRs carrying a given diff --git a/scripts/windows/setup.ps1 b/scripts/windows/setup.ps1 index 57698db2..6a16797e 100644 --- a/scripts/windows/setup.ps1 +++ b/scripts/windows/setup.ps1 @@ -1,7 +1,6 @@ # scripts/windows/setup.ps1 - Core Windows installer orchestrator # # Called by: setup.ps1 (root entry point) -# Owner: Goofy (#2) # # Orchestrates developer tool installation on Windows by delegating to per-tool scripts. # Each tool installer is idempotent - safe to run multiple times. diff --git a/scripts/windows/tools/auth.ps1 b/scripts/windows/tools/auth.ps1 index 3316ebc0..2622f081 100644 --- a/scripts/windows/tools/auth.ps1 +++ b/scripts/windows/tools/auth.ps1 @@ -1,7 +1,6 @@ # scripts/windows/tools/auth.ps1 - GitHub authentication check and prompt # # Called by: scripts/windows/setup.ps1 (after gh CLI is installed) -# Owner: Goofy (#2) # Idempotent: yes - exits cleanly if already authenticated # # Mirrors scripts/linux/tools/auth.sh behavior: diff --git a/scripts/windows/tools/copilot.ps1 b/scripts/windows/tools/copilot.ps1 index 3b6f30c6..e2d6d443 100644 --- a/scripts/windows/tools/copilot.ps1 +++ b/scripts/windows/tools/copilot.ps1 @@ -1,6 +1,5 @@ # scripts/windows/tools/copilot.ps1 - GitHub Copilot CLI installer # -# Owner: Goofy (#2, #255) # Installs GitHub Copilot CLI at pinned version from .tool-versions via npm. # Version-aware: upgrades if installed version != pinned version. # Package: @github/copilot (modern, active). Do NOT use @githubnext/github-copilot-cli (deprecated). diff --git a/scripts/windows/tools/dotfiles.ps1 b/scripts/windows/tools/dotfiles.ps1 index 61970708..7fc464f5 100644 --- a/scripts/windows/tools/dotfiles.ps1 +++ b/scripts/windows/tools/dotfiles.ps1 @@ -1,6 +1,5 @@ # scripts/windows/tools/dotfiles.ps1 - Dotfile installer for Windows # -# Owner: Pluto (Config Engineer) # Copies dotfiles to %USERPROFILE% with timestamped .bak backup on change. # No symlinks -- plain copy for maximum compatibility (no admin/developer mode). # diff --git a/scripts/windows/tools/gh.ps1 b/scripts/windows/tools/gh.ps1 index df16ef1e..aa701c20 100644 --- a/scripts/windows/tools/gh.ps1 +++ b/scripts/windows/tools/gh.ps1 @@ -1,6 +1,5 @@ # scripts/windows/tools/gh.ps1 - GitHub CLI installer # -# Owner: Goofy (#2, #255) # Installs GitHub CLI (gh) at pinned version from .tool-versions. # Version-aware: upgrades if installed version != pinned version. diff --git a/scripts/windows/tools/git.ps1 b/scripts/windows/tools/git.ps1 index 3588e237..8981754e 100644 --- a/scripts/windows/tools/git.ps1 +++ b/scripts/windows/tools/git.ps1 @@ -1,6 +1,5 @@ # scripts/windows/tools/git.ps1 - Git for Windows installer # -# Owner: Goofy (#2) # Installs Git for Windows (includes Git Bash - MinGW bash) Set-StrictMode -Version Latest diff --git a/scripts/windows/tools/nvm.ps1 b/scripts/windows/tools/nvm.ps1 index c758a2f4..e7cc15fe 100644 --- a/scripts/windows/tools/nvm.ps1 +++ b/scripts/windows/tools/nvm.ps1 @@ -1,6 +1,5 @@ # scripts/windows/tools/nvm.ps1 - nvm-windows + Node.js installer # -# Owner: Goofy (#2) # Installs nvm-windows (Node Version Manager for Windows), then auto-installs # the pinned Node.js version from .tool-versions so node/npm are usable in # the same setup session. diff --git a/scripts/windows/tools/profile.ps1 b/scripts/windows/tools/profile.ps1 index 341f4b22..c4b9c3b9 100644 --- a/scripts/windows/tools/profile.ps1 +++ b/scripts/windows/tools/profile.ps1 @@ -1,6 +1,5 @@ # scripts/windows/tools/profile.ps1 - PowerShell profile writer # -# Owner: Goofy (#2) # Writes dev-setup shortcuts to PowerShell profile (both PS 5.1 and PS 7+) Set-StrictMode -Version Latest @@ -226,6 +225,9 @@ Set-Alias -Name nrt -Value Invoke-NpmRunTest -Force -Scope Global function Invoke-Python { python $args } # python shorthand Set-Alias -Name py -Value Invoke-Python -Force -Scope Global +function Invoke-CopilotSquad { copilot --agent squad --yolo $args } # run Copilot with Squad agent +Set-Alias -Name gosquad -Value Invoke-CopilotSquad -Force -Scope Global + Set-Alias -Name c -Value Clear-Host -Force -Scope Global # clear the screen # -- Utility -------------------------------------------------------------------- diff --git a/scripts/windows/tools/psmux.ps1 b/scripts/windows/tools/psmux.ps1 index 494b5433..4ba87256 100644 --- a/scripts/windows/tools/psmux.ps1 +++ b/scripts/windows/tools/psmux.ps1 @@ -1,6 +1,5 @@ # scripts/windows/tools/psmux.ps1 - psmux (tmux for Windows) installer # -# Owner: Goofy (#2) # Installs psmux - terminal multiplexer for Windows PowerShell Set-StrictMode -Version Latest diff --git a/scripts/windows/tools/uv.ps1 b/scripts/windows/tools/uv.ps1 index 9e11dea1..14f29886 100644 --- a/scripts/windows/tools/uv.ps1 +++ b/scripts/windows/tools/uv.ps1 @@ -1,6 +1,5 @@ # scripts/windows/tools/uv.ps1 - uv (Python package manager) installer # -# Owner: Goofy (#2) # Installs uv via official install script Set-StrictMode -Version Latest diff --git a/scripts/windows/tools/vim.ps1 b/scripts/windows/tools/vim.ps1 index 31fce2c6..c5ed7082 100644 --- a/scripts/windows/tools/vim.ps1 +++ b/scripts/windows/tools/vim.ps1 @@ -1,6 +1,5 @@ # scripts/windows/tools/vim.ps1 - Vim text editor installer # -# Owner: Goofy (#2) # Installs vim with PATH registration (winget doesn't reliably add it to PATH) Set-StrictMode -Version Latest diff --git a/tests/test_alias_parity.sh b/tests/test_alias_parity.sh index 78ca0dcc..12071760 100644 --- a/tests/test_alias_parity.sh +++ b/tests/test_alias_parity.sh @@ -102,7 +102,7 @@ extract_windows_aliases() { # Internal functions: Write-Info, Write-Ok, Write-Warn, Write-Err, Write-PowerShellProfile grep -E '^\s*function\s+[A-Za-z]' "$file" \ | sed -E 's/^\s*function\s+([A-Za-z_][A-Za-z0-9_-]*).*/\1/' \ - | grep -vE '^(Write-Info|Write-Ok|Write-Warn|Write-Err|Write-PowerShellProfile|Remove-CustomItem|Set-FileTimestamp|Get-GitStatus|Invoke-GitCommit|Get-GitBranch|Add-GitFiles|Get-GitLogPretty|Get-GitLog|Invoke-GitFetch|Invoke-GitFetchPrune|Invoke-GitStash|Get-GitStashList|Add-GitAllFiles|Invoke-GitCommitMessage|New-GitBranch|Invoke-GitCheckout|Get-GitDiff|Get-GitDiffStaged|Invoke-GitStashPop|Invoke-GitPush|Invoke-GitPushForce|Invoke-GitPull|Invoke-GitRebase|Invoke-GitRebaseInteractive|Invoke-GitRestore|Invoke-GitRestoreStaged|New-GhPR|Get-GhPRList|Get-GhPRView|Get-GhIssueList|Get-GhIssueView|Invoke-UvRun|Invoke-UvSync|Invoke-NpmInstall|Invoke-NpmRun|Invoke-NpmRunDev|Invoke-NpmRunTest|Invoke-Python|Get-MyIp|Invoke-PingBing|Edit-Profile|Invoke-PsmuxList|Invoke-PsmuxKillServer|Invoke-PsmuxNewSession|Invoke-PsmuxAttach|Invoke-ShutdownNow|Invoke-TimedShutdown|Invoke-CancelTimedShutdown)$' + | grep -vE '^(Write-Info|Write-Ok|Write-Warn|Write-Err|Write-PowerShellProfile|Remove-CustomItem|Set-FileTimestamp|Get-GitStatus|Invoke-GitCommit|Get-GitBranch|Add-GitFiles|Get-GitLogPretty|Get-GitLog|Invoke-GitFetch|Invoke-GitFetchPrune|Invoke-GitStash|Get-GitStashList|Add-GitAllFiles|Invoke-GitCommitMessage|New-GitBranch|Invoke-GitCheckout|Get-GitDiff|Get-GitDiffStaged|Invoke-GitStashPop|Invoke-GitPush|Invoke-GitPushForce|Invoke-GitPull|Invoke-GitRebase|Invoke-GitRebaseInteractive|Invoke-GitRestore|Invoke-GitRestoreStaged|New-GhPR|Get-GhPRList|Get-GhPRView|Get-GhIssueList|Get-GhIssueView|Invoke-UvRun|Invoke-UvSync|Invoke-NpmInstall|Invoke-NpmRun|Invoke-NpmRunDev|Invoke-NpmRunTest|Invoke-Python|Get-MyIp|Invoke-PingBing|Edit-Profile|Invoke-PsmuxList|Invoke-PsmuxKillServer|Invoke-PsmuxNewSession|Invoke-PsmuxAttach|Invoke-ShutdownNow|Invoke-TimedShutdown|Invoke-CancelTimedShutdown|Invoke-CopilotSquad)$' } # -- Compare -------------------------------------------------------------------