diff --git a/.github/workflows/squad-upgrade-matrix.yml b/.github/workflows/squad-upgrade-matrix.yml new file mode 100644 index 000000000..71597196f --- /dev/null +++ b/.github/workflows/squad-upgrade-matrix.yml @@ -0,0 +1,92 @@ +name: Squad Upgrade Matrix + +# Old published Squad releases × OS × project state shape, upgraded with the CLI packed from this +# tree. See test/squad-20-upgrade-matrix.test.ts and docs/src/content/docs/scenarios/upgrading.md +# ("Tested upgrade paths"). Producers come from the registry npm is configured for on the runner +# (set NPM_CONFIG_REGISTRY to point elsewhere) and from GitHub Release standalone bundles; a producer +# that cannot be obtained is skipped loudly and recorded in the results artifact — never silently +# passed. Known product defects are pinned in test-fixtures/spec/suite-v0.2/upgrade-matrix.json. + +on: + pull_request: + branches: [dev, main, 'squad2-*'] + types: [opened, synchronize, reopened] + workflow_dispatch: + inputs: + producers: + description: 'Comma-separated producer versions to run (default: all declared in upgrade-matrix.json)' + required: false + default: '' + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + upgrade-matrix: + name: upgrade-matrix (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + timeout-minutes: 45 + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest] + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7 + - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 #v6 + with: + node-version: 22 + cache: 'npm' + - name: Install dependencies + shell: bash + run: | + success=false + for i in 1 2 3; do + if npm install; then success=true; break; fi + echo "Retry $i/3 — npm install failed, retrying in 5s..."; sleep 5 + done + [ "$success" = true ] || { echo "::error::npm install failed after 3 attempts"; exit 1; } + - name: Build (no version bump) + shell: bash + run: npm run build + env: + SKIP_BUILD_BUMP: '1' + - name: Registry in use for old producers + shell: bash + run: | + echo "NPM_CONFIG_REGISTRY=${NPM_CONFIG_REGISTRY:-}" + echo "npm config get registry → $(npm config get registry)" + - name: Run upgrade matrix + shell: bash + run: npx vitest run test/squad-20-upgrade-matrix.test.ts --reporter=default + env: + SQUAD_UPGRADE_MATRIX: '1' + SQUAD_UPGRADE_MATRIX_PRODUCERS: ${{ github.event.inputs.producers }} + SQUAD_UPGRADE_MATRIX_OUT: ${{ github.workspace }}/upgrade-matrix-results/results-${{ runner.os }}.json + # The tree under test: the PR head (a PR checkout is a merge ref), or the dispatched/pushed commit. + SQUAD_UPGRADE_MATRIX_TARGET_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + # Read-only; lets release-asset downloads by id use the authenticated rate limit. + GITHUB_TOKEN: ${{ github.token }} + - name: Upload results + if: always() + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 #v7 + with: + name: upgrade-matrix-results-${{ runner.os }} + path: upgrade-matrix-results/ + if-no-files-found: warn + retention-days: 30 + + docs-in-sync: + name: upgrade-matrix docs in sync + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7 + - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 #v6 + with: + node-version: 22 + - name: Rendered "Tested upgrade paths" section matches upgrade-matrix.json + run: node scripts/render-upgrade-matrix.mjs --check diff --git a/cspell.json b/cspell.json index b9a9bb092..c1835b6b4 100644 --- a/cspell.json +++ b/cspell.json @@ -234,6 +234,8 @@ "worktree", "worktrees", "xarray", + "xfail", + "xpass", "ZUAER" ], "dictionaries": [ diff --git a/docs/src/content/docs/get-started/migration.md b/docs/src/content/docs/get-started/migration.md index 9564f4872..5147f7797 100644 --- a/docs/src/content/docs/get-started/migration.md +++ b/docs/src/content/docs/get-started/migration.md @@ -44,6 +44,7 @@ squad upgrade - **Multi-host projections:** Pass `--client claude,kiro,opencode,cursor,codex,antigravity` (or `--client all`) to `squad init` or `squad upgrade` to project your team across all 7 supported hosts; use `uninstallClients()` from `@bradygaster/squad-sdk/client` for byte-preserving rollback. - **SDK imports:** `@bradygaster/squad-sdk/client` is the canonical subpath for client and host-adapter APIs (`@bradygaster/squad-sdk/clients` is retained as a deprecated alias). Experimental backend APIs live under `@bradygaster/squad-sdk/backend`. - **Full details:** See [Upgrading to Squad 2.0](../scenarios/upgrading.md#upgrading-to-squad-20) and [Using Squad across clients](../scenarios/using-squad-across-clients.md). +- **Which old versions are verified:** [Tested upgrade paths](../scenarios/upgrading.md#tested-upgrade-paths) lists every published release (0.9.1 through 1.1.0, pinned npm tarball or GitHub Release bundle) that the CI matrix upgrades on Windows and Linux under each project state shape, with per-row results, every not-applicable cell explained, and the known findings. There is no supported downgrade — see [There is no supported downgrade](../scenarios/upgrading.md#there-is-no-supported-downgrade). --- diff --git a/docs/src/content/docs/scenarios/upgrading.md b/docs/src/content/docs/scenarios/upgrading.md index a74c667e3..465f91b0a 100644 --- a/docs/src/content/docs/scenarios/upgrading.md +++ b/docs/src/content/docs/scenarios/upgrading.md @@ -113,6 +113,18 @@ uninstallClients(['claude', 'cursor', 'codex'], { Then remove the corresponding slug(s) from `clients` in `.squad/config.json`. +This is **projection uninstall only**. It does not change the installed CLI, your `.squad/` team state, or the Squad version your repository is on, and it is **not** a way to return to an older Squad. + +### There is no supported downgrade + +Never run an older Squad CLI (`npx @bradygaster/squad-cli@`, an old standalone bundle, or a stale global install) against a repository that has been upgraded: + +- Old CLIs have no newer-version guard; a real `squad upgrade` from them rewrites the coordinator, skills, workflows and templates back to the old version. +- 0.9.1's `squad upgrade --dry-run` is **not trustworthy**: it performs the downgrade instead of previewing it. Later 0.13.x–1.1.0 CLIs honor `--dry-run` but still preview overwriting Squad-owned files. +- If an old CLI has already touched an upgraded repository, re-run `squad upgrade` with the current CLI to repair the Squad-owned files. Team state (`team.md`, `decisions.md`, agents' charters and histories) is not changed by either direction. + +The verified behavior of every old release is recorded under [Tested upgrade paths](#tested-upgrade-paths) below. + ### Removed in 2.0 Squad 2.0 removes some 1.x features outright; the single record of what was removed is the **BREAKING** row in [What's New → Breaking & Behavior Changes (v2.0.0)](../whatsnew.md#breaking--behavior-changes-v200). There is no migration tool for removed features. Squad now resolves a team only from the project `.squad/` (current directory and parents): outside a project, `squad status`, `squad doctor`, `squad cast` and `squad cost` exit with `No squad in this directory. Run "squad init" first.` @@ -125,6 +137,252 @@ Squad does not support downgrading in place. Older CLIs have no "newer workspace --- + + + + +## Tested upgrade paths + +Every row below is a pinned, published artifact of an older Squad release driven through `squad init` under a given project state shape, then **upgraded** with the CLI packed from the tree under test (`npm pack` of `@bradygaster/squad-sdk` + `@bradygaster/squad-cli`) by `test/squad-20-upgrade-matrix.test.ts`. A row **passes** only when every non-xfail assertion passed; a row that carries a pinned finding is reported as **pass WITH known … failure (F)**, never as a bare pass; **fail** names the failing checks; **not applicable** cells are each explained individually below (none means "not run"). Machine-readable source: `test-fixtures/spec/suite-v0.2/upgrade-matrix.json` (protocol `upgrade-matrix/v1`). + +### Three different things + +| Term | Meaning | +|------|---------| +| **upgrade** | `squad upgrade` with the current CLI: refreshes Squad-owned files, migrates casting/state, preserves user state. The only supported direction. | +| **projection uninstall** | `uninstallClients()` from `@bradygaster/squad-sdk/client`: removes generated host projections and restores shared files byte-exact. Does not change the installed CLI, `.squad/` state, or the repository's Squad version. | +| **unsupported downgrade** | Running an older Squad CLI against an upgraded repository. Not supported; the `downgrade-probe` check only records what each old CLI does so users are warned (0.9.1 `--dry-run` mutates — F3). | + +> ⚠️ **Never run an older Squad CLI against an upgraded repository.** There is no supported downgrade. The `downgrade-probe` rows record what each old CLI does: 0.9.1 `squad upgrade --dry-run` is **not trustworthy** — it rewrites the coordinator, skills, workflows and templates in place (F3); 0.13.1–1.1.0 preview overwriting them. If an old CLI has already touched an upgraded repository, re-run `squad upgrade` with the current CLI to repair the Squad-owned files; your `.squad/` team state is not changed by either direction. `uninstallClients()` removes host projections only — it is **not** a way back to an older Squad. + +### Runs + +All recorded runs executed against **one tree**, commit `9da01a2e41ba15794d31b0b8251ddaa3abd34ba8`, with harness hash `bca27c39dc2dcbff…` (sha256 over the suite, renderer, workflow, fs hook and this fixture minus `runs`; the integrity test refuses a recorded run whose harness differs from the committed one, so folding results may change nothing but the runs data). + +| Platform | Arch | Node | Target | Tree (targetSha) | Trigger | Recorded | Outcome | +|----------|------|------|--------|------------------|---------|----------|---------| +| Linux (`linux`) | x64 | v22.23.3 | v1.2.0-preview.1 | `9da01a2e41ba` | [workflow_dispatch](https://github.com/bradygaster/squad/actions/runs/39903943542) | 2026-10-23 | 35 rows passed all non-xfail assertions; 4 of them carry a known expected failure (F3: Squad 0.9.1 squad upgrade --dry-run is not dry); 0 rows failed; 7 rows are structurally inapplicable (listed below). | +| Windows (`win32`) | x64 | v22.23.4 | v1.2.0-preview.1 | `9da01a2e41ba` | [workflow_dispatch](https://github.com/bradygaster/squad/actions/runs/39903943542) | 2026-10-23 | 35 rows passed all non-xfail assertions; 4 of them carry a known expected failure (F3: Squad 0.9.1 squad upgrade --dry-run is not dry); 0 rows failed; 7 rows are structurally inapplicable (listed below). | + +### Producers (pinned, immutable provenance) + +| Version | Provenance | Artifact pins | Why it is in the matrix | `--state-backend` | +|---------|------------|---------------|-------------------------|-------------------| +| **0.9.1** | npm | `@bradygaster/squad-cli@0.9.1`
sha1 `a5ee3dbfc44b7457eba56476f708ad6fdc2e09ca`
`sha512-TcZqRbJk3XBg4N8Y5/5OWJeA52ARAMuzk4MGtB0bfdr87e7iJRV6lJEw8+OIYz9IQ+1JxN/Q30dZDALMOcdi2w==`
`@bradygaster/squad-sdk@0.9.1`
sha1 `4735aba9f125de415160cd979ea3f1467903f16e`
`sha512-1KqZ2N6Lt/B4uYlU/Su9JjA1s/7ng0OAEb9CZrkRk19YSPi6tg7K5QWdTKl5gY/gp8zPAJqfjVffOwrZwGlzmw==` | Oldest 0.x line that still installs on Node 22 (engines >=22.5); pinned with its own @bradygaster/squad-sdk@0.9.1 (its `>=0.9.0` range would otherwise float to 1.0.1). Scaffolds `.squad/` with an EMPTY `casting/` directory and skills under `.copilot/skills/`; predates --state-backend. | no | +| **0.13.1** | npm | `@bradygaster/squad-cli@0.13.1`
sha1 `c8f6427bab4cae989934574315eabda98495fb37`
`sha512-ogfx2nhyx6fqMyqlaI5tDnOzUbRdh+a6Uh/rMQ0aNJwAXnvQp4N37Xp/eOTyB5CxcB7E34NBtAfCxyXbWSiS8w==`
`@bradygaster/squad-sdk@0.13.1`
sha1 `90e98450971e921ca2072673b325fcc8126a2fb6`
`sha512-1Jt6c9kahJq1JCcE77j/50D79YcXVG6Rt1genuLJ3O9J2SKTZKemlbvHgftkc5LgGH22DkWqHh5zLNEQ/p+ukA==` | Last 0.x stable; first line with --state-backend (local\|orphan\|two-layer) and user-profile squads (writes teamRoot when a profile squad exists). | yes | +| **1.0.0** | npm | `@bradygaster/squad-cli@1.0.0`
sha1 `e4e776ef938cbe8c65d8d2c729f2249a48bca638`
`sha512-mnZXnA+GAtp4KSLf7a4Zw0BpeOaHFAxYV5oxAy58ancZ9C2LguUGbcTxWIG0djM96XWET8K9fcncif3Vm6Z9RQ==`
`@bradygaster/squad-sdk@1.0.0`
sha1 `4abb8e450e79827b865a7846af6589789d079cab`
`sha512-9f4NthJaQDq6ifXU3f6VfsZz2bbWs2u57fHhXKtDgDMSvZ3Xq8FPjMvddxhRvseWqaSlkYcWsag3M/lzTtKaJA==` | First 1.x stable; last version published to npm (dist-tag latest). | yes | +| **1.0.1** | GitHub Release | [bradygaster/squad v1.0.1](https://github.com/bradygaster/squad/releases/tag/v1.0.1) — assets by immutable id:
`squad-darwin-arm64.tar.gz` id `610330560` sha256 `47a8d44d0a9a338ae2c2b6192cb761187ebd71edbd1171a4800cee11cb001b43`
`squad-darwin-x64.tar.gz` id `610330564` sha256 `b88db7e8949878ba2cdf756758651cd50a0e9b5fc83f4c759bd7438d596b4951`
`squad-linux-arm64.tar.gz` id `610330558` sha256 `9d8105c903eeac4d1762e43432f54827a6c4676f4aa42cfdf34bf645e497e756`
`squad-linux-x64.tar.gz` id `610330551` sha256 `283a1893be9ddad11056dcc8bd0673eff6b48a789bbf523b3f7ec31994d27ac7`
`squad-win32-arm64.zip` id `610330674` sha256 `586c4e2d3d708f4d3f08b2c8ffb965d9312d6586d0830a1b8a817b27c3c14bd0`
`squad-win32-x64.zip` id `610330559` sha256 `f38a8f85e60982dba340187b761488f01de1aec68ea50c1b85725783d32df2fe` | Patch release whose CLI never reached npm (release run 37222633918: SDK publish failed, CLI publish skipped); only the standalone bundles exist. | yes | +| **1.1.0** | GitHub Release | [bradygaster/squad v1.1.0](https://github.com/bradygaster/squad/releases/tag/v1.1.0) — assets by immutable id:
`squad-darwin-arm64.tar.gz` id `659607638` sha256 `057c8d2296e1c95cd77933bae55475409a2dce88aa5b3443cc6ca6cfcd545631`
`squad-darwin-x64.tar.gz` id `659607644` sha256 `4e4e8574f6a339f5537c82eb586d66ecf2ee3005e86b058b834b965be33a43cd`
`squad-linux-arm64.tar.gz` id `659607639` sha256 `992e9dbfe677e3c772446960051329aaed1578d5e4dbcb3350827dbb755e8b00`
`squad-linux-x64.tar.gz` id `659607641` sha256 `2de647a3feaef334051eb1b3084acba37a511818aad029a16abfe32fadb41aff`
`squad-win32-arm64.zip` id `659607808` sha256 `3c922c37863b255546403cffb2aa1c7dff2a347be87ca582759c149aec7bb1da`
`squad-win32-x64.zip` id `659607643` sha256 `571d734f7ae718fefefdfa9864f9f82a3d1fb1546f01ec8bf8866fa7e1829c57` | Latest 1.x release; npm CLI publish failed in release run 39442663504 ("Could not verify @bradygaster/squad-cli@1.1.0 on npm dist-tag 'latest' after 20 attempts"), so the standalone bundle is the only published artifact. | yes | +| **HEAD** | local HEAD (pack) | npm pack of packages/squad-sdk and packages/squad-cli from the tree under test (targetSha) | Same-version idempotency: the CLI packed from this tree scaffolds and then upgrades itself. | yes | + +npm tarballs are fetched by exact version through the registry npm is configured for and verified against the recorded sha1 + sha512 before use; GitHub Release bundles are downloaded by immutable asset id and verified against the recorded sha256. A verification mismatch **fails** the producer's rows (it is never a skip); only an unreachable registry/release yields a skip, and that skip carries the reason. + +### State shapes + +| Shape | Setup | +|-------|-------| +| `default` | `squad init` with an empty profile directory, plus a custom agent (`agents/probe/`), an appended decision, a hand-written `.mcp.json` server, `AGENTS.md`, `CLAUDE.md` and a `.gitignore` rule. | +| `profile` | As `default`, but the platform config dir is pre-seeded with the 1.x user-profile layout (a roster directory and a `.squad/` under `/squad/`); the whole tree must stay sha256-identical and the project must not be linked to it. | +| `legacy-casting` | As `default`, then `.squad/casting/` is replaced by a hand-written `registry.json` (`{universe, agents:[{name, role, status}]}`) with no history — the shape an agent-edited 1.x registry has. | +| `orphan` | `squad init --state-backend orphan` (producers that have the flag); the `squad-state` branch and `stateBackend` must survive. | +| `two-layer` | `squad init --state-backend two-layer` (producers that have the flag); the `squad-state` branch and `stateBackend` must survive. | +| `ai-team` | As `default`, then `.squad/` is renamed to the legacy `.ai-team/` and the upgrade runs with `--migrate-directory`. | +| `upstream` | HEAD only: a second squad is declared via `squad upstream add --name org`; `upstream.json` must survive the upgrade byte-exact. | + +### Results (version × shape × OS) + +| Producer | Provenance | Shape | Linux | Windows | +|----------|------------|-------|---|---| +| **0.9.1** | npm | `default` | ✅⚠️ pass WITH known destructive downgrade-probe failure (F3) | ✅⚠️ pass WITH known destructive downgrade-probe failure (F3) | +| **0.9.1** | npm | `profile` | ✅⚠️ pass WITH known destructive downgrade-probe failure (F3) | ✅⚠️ pass WITH known destructive downgrade-probe failure (F3) | +| **0.9.1** | npm | `legacy-casting` | ✅⚠️ pass WITH known destructive downgrade-probe failure (F3) | ✅⚠️ pass WITH known destructive downgrade-probe failure (F3) | +| **0.9.1** | npm | `orphan` | ⏭️ not applicable — `@bradygaster/squad-cli@0.9.1` has no `--state-backend` flag (first shipped in 0.13.x), so an orphan-backend project cannot be produced by it. | ⏭️ not applicable — `@bradygaster/squad-cli@0.9.1` has no `--state-backend` flag (first shipped in 0.13.x), so an orphan-backend project cannot be produced by it. | +| **0.9.1** | npm | `two-layer` | ⏭️ not applicable — `@bradygaster/squad-cli@0.9.1` has no `--state-backend` flag (first shipped in 0.13.x), so a two-layer-backend project cannot be produced by it. | ⏭️ not applicable — `@bradygaster/squad-cli@0.9.1` has no `--state-backend` flag (first shipped in 0.13.x), so a two-layer-backend project cannot be produced by it. | +| **0.9.1** | npm | `ai-team` | ✅⚠️ pass WITH known destructive downgrade-probe failure (F3) | ✅⚠️ pass WITH known destructive downgrade-probe failure (F3) | +| **0.9.1** | npm | `upstream` | ⏭️ not applicable — Upstream inheritance (`.squad/upstream.json`, `squad upstream add`) is a Squad 2.0 feature; 0.9.1 cannot declare an upstream, so there is nothing for the upgrade to preserve. | ⏭️ not applicable — Upstream inheritance (`.squad/upstream.json`, `squad upstream add`) is a Squad 2.0 feature; 0.9.1 cannot declare an upstream, so there is nothing for the upgrade to preserve. | +| **0.13.1** | npm | `default` | ✅ pass | ✅ pass | +| **0.13.1** | npm | `profile` | ✅ pass | ✅ pass | +| **0.13.1** | npm | `legacy-casting` | ✅ pass | ✅ pass | +| **0.13.1** | npm | `orphan` | ✅ pass | ✅ pass | +| **0.13.1** | npm | `two-layer` | ✅ pass | ✅ pass | +| **0.13.1** | npm | `ai-team` | ✅ pass | ✅ pass | +| **0.13.1** | npm | `upstream` | ⏭️ not applicable — Upstream inheritance (`.squad/upstream.json`, `squad upstream add`) is a Squad 2.0 feature; 0.13.1 cannot declare an upstream, so there is nothing for the upgrade to preserve. | ⏭️ not applicable — Upstream inheritance (`.squad/upstream.json`, `squad upstream add`) is a Squad 2.0 feature; 0.13.1 cannot declare an upstream, so there is nothing for the upgrade to preserve. | +| **1.0.0** | npm | `default` | ✅ pass | ✅ pass | +| **1.0.0** | npm | `profile` | ✅ pass | ✅ pass | +| **1.0.0** | npm | `legacy-casting` | ✅ pass | ✅ pass | +| **1.0.0** | npm | `orphan` | ✅ pass | ✅ pass | +| **1.0.0** | npm | `two-layer` | ✅ pass | ✅ pass | +| **1.0.0** | npm | `ai-team` | ✅ pass | ✅ pass | +| **1.0.0** | npm | `upstream` | ⏭️ not applicable — Upstream inheritance (`.squad/upstream.json`, `squad upstream add`) is a Squad 2.0 feature; 1.0.0 cannot declare an upstream, so there is nothing for the upgrade to preserve. | ⏭️ not applicable — Upstream inheritance (`.squad/upstream.json`, `squad upstream add`) is a Squad 2.0 feature; 1.0.0 cannot declare an upstream, so there is nothing for the upgrade to preserve. | +| **1.0.1** | GitHub Release | `default` | ✅ pass | ✅ pass | +| **1.0.1** | GitHub Release | `profile` | ✅ pass | ✅ pass | +| **1.0.1** | GitHub Release | `legacy-casting` | ✅ pass | ✅ pass | +| **1.0.1** | GitHub Release | `orphan` | ✅ pass | ✅ pass | +| **1.0.1** | GitHub Release | `two-layer` | ✅ pass | ✅ pass | +| **1.0.1** | GitHub Release | `ai-team` | ✅ pass | ✅ pass | +| **1.0.1** | GitHub Release | `upstream` | ⏭️ not applicable — Upstream inheritance (`.squad/upstream.json`, `squad upstream add`) is a Squad 2.0 feature; 1.0.1 cannot declare an upstream, so there is nothing for the upgrade to preserve. | ⏭️ not applicable — Upstream inheritance (`.squad/upstream.json`, `squad upstream add`) is a Squad 2.0 feature; 1.0.1 cannot declare an upstream, so there is nothing for the upgrade to preserve. | +| **1.1.0** | GitHub Release | `default` | ✅ pass | ✅ pass | +| **1.1.0** | GitHub Release | `profile` | ✅ pass | ✅ pass | +| **1.1.0** | GitHub Release | `legacy-casting` | ✅ pass | ✅ pass | +| **1.1.0** | GitHub Release | `orphan` | ✅ pass | ✅ pass | +| **1.1.0** | GitHub Release | `two-layer` | ✅ pass | ✅ pass | +| **1.1.0** | GitHub Release | `ai-team` | ✅ pass | ✅ pass | +| **1.1.0** | GitHub Release | `upstream` | ⏭️ not applicable — Upstream inheritance (`.squad/upstream.json`, `squad upstream add`) is a Squad 2.0 feature; 1.1.0 cannot declare an upstream, so there is nothing for the upgrade to preserve. | ⏭️ not applicable — Upstream inheritance (`.squad/upstream.json`, `squad upstream add`) is a Squad 2.0 feature; 1.1.0 cannot declare an upstream, so there is nothing for the upgrade to preserve. | +| **HEAD** | local HEAD (pack) | `default` | ✅ pass | ✅ pass | +| **HEAD** | local HEAD (pack) | `profile` | ✅ pass | ✅ pass | +| **HEAD** | local HEAD (pack) | `legacy-casting` | ✅ pass | ✅ pass | +| **HEAD** | local HEAD (pack) | `orphan` | ✅ pass | ✅ pass | +| **HEAD** | local HEAD (pack) | `two-layer` | ✅ pass | ✅ pass | +| **HEAD** | local HEAD (pack) | `ai-team` | ✅ pass | ✅ pass | +| **HEAD** | local HEAD (pack) | `upstream` | ✅ pass | ✅ pass | + +### Not-applicable cells, each explained + +These 7 cells are structurally impossible to exercise — the old producer cannot create that project shape — so they are **not** "not run"; the suite asserts the recorded skipped rows are exactly this list. + +| Producer | Shape | Why it cannot apply | +|----------|-------|---------------------| +| **0.9.1** | `orphan` | `@bradygaster/squad-cli@0.9.1` has no `--state-backend` flag (first shipped in 0.13.x), so an orphan-backend project cannot be produced by it. | +| **0.9.1** | `two-layer` | `@bradygaster/squad-cli@0.9.1` has no `--state-backend` flag (first shipped in 0.13.x), so a two-layer-backend project cannot be produced by it. | +| **0.9.1** | `upstream` | Upstream inheritance (`.squad/upstream.json`, `squad upstream add`) is a Squad 2.0 feature; 0.9.1 cannot declare an upstream, so there is nothing for the upgrade to preserve. | +| **0.13.1** | `upstream` | Upstream inheritance (`.squad/upstream.json`, `squad upstream add`) is a Squad 2.0 feature; 0.13.1 cannot declare an upstream, so there is nothing for the upgrade to preserve. | +| **1.0.0** | `upstream` | Upstream inheritance (`.squad/upstream.json`, `squad upstream add`) is a Squad 2.0 feature; 1.0.0 cannot declare an upstream, so there is nothing for the upgrade to preserve. | +| **1.0.1** | `upstream` | Upstream inheritance (`.squad/upstream.json`, `squad upstream add`) is a Squad 2.0 feature; 1.0.1 cannot declare an upstream, so there is nothing for the upgrade to preserve. | +| **1.1.0** | `upstream` | Upstream inheritance (`.squad/upstream.json`, `squad upstream add`) is a Squad 2.0 feature; 1.1.0 cannot declare an upstream, so there is nothing for the upgrade to preserve. | + +### Expected failures (pinned) + +Each pin names an open finding; the suite reports the check as `xfail` and **fails the moment it starts passing** (`xpass`), so a pin cannot outlive its bug. + +| Check | Producers | Shapes | Platforms | Finding | +|-------|-----------|--------|-----------|---------| +| `downgrade-probe` | 0.9.1 | all | all | [F3](#f3) | + +### Checks + +| Check | Meaning | +|-------|---------| +| `producer-artifact-verified` | The pinned artifact that was executed matched its recorded hashes (npm: sha1 shasum + sha512 integrity of both tarballs; GitHub Release: sha256 of the asset downloaded by id). A mismatch FAILS every row of that producer — it is never a skip; only an unreachable registry/release skips, with the reason. | +| `scenario-setup` | The matrix could set up the shape (e.g. configure the `upstream` squad). Only recorded when setup fails: a row can never be `fail` without a failing check (LD-2). | +| `producer-init` | The OLD CLI (the pinned producer artifact) scaffolds the project non-interactively (exit 0, `.squad/team.md` present). | +| `upgrade-plain` | `squad upgrade` (plus `--migrate-directory` for `ai-team`) with the NEW packed CLI exits 0. | +| `coordinator-is-new-template` | `.github/agents/squad.agent.md` carries the new version stamp (``, `Squad (vX)`). | +| `user-state-preserved` | Full manifest compare (path + sha256 of every file outside `.git/`) before/after the plain upgrade: every seeded user file — active and retired agent folders, nested unknown files, filenames with spaces and Unicode, appended `decisions.md`/`team.md`/`routing.md`, a user skill — is byte-identical; every changed path matches a declared Squad-owned pattern (`squadOwned`); the custom `config.json` key is kept. For `orphan`/`two-layer` the mutable state (`decisions.md`, `agents/*/history.md`) is seeded as git blobs on `squad-state` and every seeded blob OID must be unchanged and no branch path deleted. For `ai-team` the legacy directory is mapped onto `.squad/` first so the migration itself is not counted. | +| `casting-pair-valid` | `readCastingRegistryPair` from the packed SDK reads a numbered registry + history; for `legacy-casting` the hand-written original is archived and `probe` is in the repaired registry. | +| `state-backend-preserved` | `config.json` `stateBackend` is unchanged; for `orphan`/`two-layer` the `squad-state` branch still exists. | +| `ai-team-migrated` | `.ai-team/` is gone and `.squad/team.md` exists (only for the `ai-team` shape). | +| `team-root-absent` | `config.json` has no `teamRoot` after the upgrade (Squad 2.0 never links a project to a profile directory). | +| `profile-untouched` | The seeded 1.x `/squad/` profile tree (roster directory + `.squad/`) is sha256-identical after EVERY phase (upgrade, second upgrade, downgrade probe, projection, doctor, uninstall — the offending phase is named) AND the fs access logger loaded into every Squad 2.0 process (`NODE_OPTIONS=--require test/fixtures/upgrade-matrix/fs-access-log.cjs`) recorded zero reads, probes or writes under it (only for `profile`). | +| `idempotent-second-upgrade` | A second plain `squad upgrade` changes nothing: sha256 of every file outside `.git/`, every git ref OID (`for-each-ref`, incl. the `squad-state` branch and any notes refs), `git status --porcelain` and the `squad-state` commit count are identical. | +| `downgrade-probe` | UNSUPPORTED PATH, recorded not endorsed: the OLD CLI's `upgrade --dry-run` against a copy of the upgraded project must modify nothing; its exit code and message are recorded as a note. Squad has no supported downgrade. | +| `clients-projected` | `squad upgrade --client all --force` exits 0, `config.json` `clients` lists all 7 hosts, and every projection PARSES with its host's format — JSON (`.mcp.json`, `.kiro/agents/squad.json`, `.kiro/settings/mcp.json`, `opencode.json`, `.cursor/mcp.json`), TOML (`.codex/agents/squad.toml`, `.codex/config.toml`), YAML frontmatter (`.github/agents/squad.agent.md`, `.claude/agents/squad.md`, `.kiro/steering/squad.md`, `.opencode/agent/squad.md`, `.cursor/agents/squad.md`, `.cursor/rules/squad.mdc`, `.agents/agents/squad.md`) — with a `squad_state` MCP entry where the host has one; mirrors the per-host assertions in `test/clients.test.ts`. | +| `user-config-preserved` | After projection the user's `.mcp.json` server, `AGENTS.md`/`CLAUDE.md` paragraphs and `.gitignore` rule are intact (for `upstream`: `upstream.json` is byte-identical). | +| `doctor` | `squad doctor` exits 0 with no ❌ line. | +| `projection-uninstall-byte-exact` | `uninstallClients(CLIENT_IDS)` from the packed SDK, judged against the tree right after the plain upgrade (before any projection): `AGENTS.md`, `CLAUDE.md`, `GEMINI.md`, `.gitignore` byte-identical to that baseline (absent stays absent); all 8 projection files removed (agents, Kiro steering, Cursor rules, Kiro agent JSON); `.mcp.json` equal to the baseline minus the `squad_state` entry Squad installed, with the user's server intact; Kiro/Cursor MCP JSON, `opencode.json` and `.codex/config.toml` created by the projection carry no `squad_state`; seeded `.squad/` files and `squad-state` blobs untouched. This is projection uninstall — NOT a package or repository rollback and NOT a downgrade. | + +Paths a plain `squad upgrade` may create or rewrite (the only changes `user-state-preserved` accepts): + +- `^\.github/agents/squad\.agent\.md(\.local-backup)?$` +- `^\.github/copilot-instructions\.md$` +- `^\.github/skills/` +- `^\.copilot/skills/` +- `^\.github/workflows/(squad-[\w-]+|sync-squad-labels)\.yml(\.local-backup)?$` +- `^\.squad/\.first-run$` +- `^\.squad/config\.json$` +- `^\.squad/casting/` +- `^\.squad/templates/` +- `^\.squad/casting-(history|policy|registry)\.json$` +- `^\.squad/agents/(rai|Rai|fact-checker|ralph|scribe)/` +- `^\.squad/(rai|fact-checker|identity|memory)/` +- `^\.squad/(ceremonies|ralph-instructions)\.md$` +- `^\.gitignore$` +- `^\.gitattributes$` +- `^\.mcp\.json$` +- `^\.copilot/mcp-config\.json$` +- `^\.vscode/` + +### Findings + +Real upgrade defects observed by the suite. An open finding is pinned as an expected failure until fixed (the fix lands in the layer named, not in this test-only layer); a resolved finding keeps its repro here so the matrix row that used to carry it documents the regression guard. + +#### F1 — Stale `teamRoot` written by 0.13.x–1.x `init` survives `squad upgrade`; the upgraded project resolves in remote mode against the removed profile squad + +**Severity:** high · **Owner:** C — user-profile squad removal (`runUpgrade` / `resolveSquadPaths`) · **Status:** ✅ resolved + +Repro: + +1. Seed a 1.x profile tree under the platform config dir: the user-profile roster directory and `.squad/` under `/squad/` (APPDATA on Windows, XDG_CONFIG_HOME on Linux). +1. `squad init` with @bradygaster/squad-cli 0.13.1 or 1.0.0 (npm) or the 1.0.1 / 1.1.0 standalone bundle: it prints its profile-detected notice and writes `.squad/config.json` → `{ "version": 1, "teamRoot": "\\squad" }`. +1. Upgrade with the 2.0 CLI packed from this tree: `squad upgrade` (exit 0, no warning about teamRoot). +1. `cat .squad/config.json` still contains `teamRoot: /squad`; `squad doctor` reports `Mode: remote` and ❌ `routing.md`, `casting/registry.json`, `decisions.md` not found — it is reading `/squad/.squad/`, not the project. + +Observed: `team-root-absent` fails (`teamRoot=/squad`) and `doctor` fails for the `profile` shape on every 0.13.1+ producer. The profile tree itself stays sha256-identical (`profile-untouched` passes), so this is a resolution defect, not a write: Squad 2.0 keeps honouring a link to a directory it no longer supports. Expected: `squad upgrade` drops (or refuses with guidance) a `teamRoot` that points at the platform config dir, and the project resolves in local mode. + +Resolved: Fixed on squad2-c @ 21ecca77 (PR #2279): `squad upgrade` removes a `teamRoot` that resolves to the platform config dir / `~/.squad` / `SQUAD_HOME` (`isRetiredProfileTeamRoot()` in `@bradygaster/squad-sdk/resolution`) and prints `Removed stale teamRoot pointing to ; project state is now local`; `--dry-run` previews it; `resolveSquadPaths` treats such a teamRoot as local mode. Dual-root targets are untouched. Verified by this matrix: `team-root-absent` and `doctor` now pass for every 0.13.1+ producer × `profile` on both OSes (the former pins produced `xpass` before removal). + +#### F2 — `squad upgrade` never recognizes its own stamped coordinator: every run reports "local customizations" and overwrites `squad.agent.md.local-backup` + +**Severity:** medium · **Owner:** CLI core — `packages/squad-cli/src/cli/core/upgrade.ts` `writeAgentTemplate` · **Status:** ✅ resolved + +Repro: + +1. `squad init` (any producer, including the 2.0 CLI itself), then `squad upgrade` once — a genuine pre-upgrade coordinator is saved to `.github/agents/squad.agent.md.local-backup`. +1. Run `squad upgrade` again with nothing changed. +1. Output again says `⚠️ squad.agent.md has local customizations — backed up to squad.agent.md.local-backup`; the backup is overwritten with the pristine current coordinator, so the user's real pre-upgrade copy is gone. The only file whose sha256 changes between run 1 and run 2 is `.github/agents/squad.agent.md.local-backup`. + +Observed: `idempotent-second-upgrade` fails with exactly `.github/agents/squad.agent.md.local-backup` for every old producer × shape × platform. On a project initialized by the 2.0 CLI itself the warning is printed on every run too (`Already up to date` + `⚠️ … local customizations`), but the overwritten backup is byte-identical, so the HEAD rows are not pinned. Cause: `writeAgentTemplate` strips `` before comparing, but `stampVersion` writes ``, `- **Version:** X` and `Squad vX`, so a stamped coordinator never equals the template. Expected: a second `squad upgrade` is a byte-for-byte no-op, prints no customization warning, and never touches an existing `.local-backup`. + +Resolved: Fixed on squad2-c @ 21ecca77 (PR #2279): `stripAgentStamps()` (`@bradygaster/squad-cli/core/upgrade`) normalizes ``, the `- **Version:**` token, `` `Squad vX` ``, `@bradygaster/squad-cli@X` and the legacy `` before the customization compare, and an identical `.local-backup` is not rewritten. Verified by this matrix: `idempotent-second-upgrade` now passes for every producer × shape on both OSes. + +#### F3 — Squad 0.9.1 `squad upgrade --dry-run` is not dry: pointed at a 2.0 project it silently downgrades the coordinator, skills, workflows and templates in place + +**Severity:** info (historical producer defect — not fixable in 2.0; recorded for the downgrade story) · **Owner:** none — behavior of the published 0.9.1 CLI · **Status:** 🔴 open + +Repro: + +1. Scaffold with 0.9.1 (`npm install @bradygaster/squad-cli@0.9.1`), upgrade with the 2.0 CLI, copy the project. +1. In the copy run the 0.9.1 CLI: `squad upgrade --dry-run`. +1. It exits 0, prints the normal upgrade summary (`Upgrade complete: v1.2.0-preview.1 → v0.9.1 … Preserves user state: …`) and rewrites ~44 files with the pinned 0.9.1 SDK (~83 when 0.9.1's `>=0.9.0` SDK range floats to 1.0.1): `.github/agents/squad.agent.md` (back to 0.9.1), `.copilot/skills/*`, `.github/workflows/squad-*.yml`, `.squad/templates/*`, plus stray `.squad/charter.md`, `.squad/roster.md`, `.squad/history.md` … at the state root. + +Observed: `downgrade-probe` fails for every 0.9.1 row because the dry-run modified files (destructive: it IS the downgrade) and printed `Upgrade complete: v1.2.0-preview.1 → v0.9.1`; 0.13.1, 1.0.0, 1.0.1 and 1.1.0 honor `--dry-run` (`Run without --dry-run to apply.`, nothing modified) but their preview lists `overwrite .github/workflows/squad-*.yml` etc. — none of the old CLIs has a newer-version guard, so running any of them for real against a 2.0 project downgrades the Squad-owned files. Guidance for users: never run an older CLI against an upgraded repository; re-run `squad upgrade` with the 2.0 CLI to repair a downgraded coordinator (team state is not touched by the downgrade). + +Pinned checks: `downgrade-probe` (0.9.1 × all shapes × all platforms). + +#### F4 — First `squad upgrade` of a 0.9.x project (empty `.squad/casting/`) does not converge: the coordinator's team-capabilities block is only rendered by the second upgrade + +**Severity:** medium · **Owner:** CLI core — `packages/squad-cli/src/cli/core/upgrade.ts` (coordinator write + `syncTeamCapabilities` run before the casting pair is scaffolded) · **Status:** ✅ resolved + +Repro: + +1. `npx @bradygaster/squad-cli@0.9.1 init` with `@bradygaster/squad-sdk@0.9.1` pinned (the faithful 0.9.1 layout): `.squad/casting/` exists but contains no `registry.json`/`history.json`. +1. Run the 2.0 CLI `squad upgrade`: it prints `⚠️ Could not refresh squad.agent.md team capabilities: Cannot read a consistent casting registry/history pair: both files are required`, then creates the casting pair later in the same run. +1. Run `squad upgrade` again: `✓ upgraded squad.agent.md` — the coordinator gains the 17-line `` block that run 1 could not render. Nothing else changes. + +Observed: `idempotent-second-upgrade` fails with exactly `.github/agents/squad.agent.md` for 0.9.1 × `default`/`profile`/`ai-team` on both OSes. 0.9.1 × `legacy-casting` is unaffected because the hand-written registry is repaired in the preflight, so the pair exists before the coordinator is written. The project is correct after the second run; the defect is ordering — the capabilities sync should run after the casting pair is ensured (or the pair should be ensured in the preflight). + +Resolved: Fixed on squad2-c @ 50afa91a (PR #2279): `runUpgrade` runs `preflightCastingPair()` (ensureCastingDefaults) before `writeAgentTemplate` in both the already-current and the normal path, so a 0.9.x project with an empty `.squad/casting/` gets the `` block on the first upgrade and the second upgrade is byte-identical (C test: `test/upgrade-profile-teamroot.test.ts` › "F4: a 0.9.x project (empty casting/) upgrades in ONE pass"). Verified by this matrix: with the pin still in place, `idempotent-second-upgrade` produced `xpass` on exactly 0.9.1 × default/profile/ai-team; the pin is removed. + +### Unsupported downgrade — what each old CLI does to an upgraded repository + +Recorded by the `downgrade-probe` check (dry-run on a copy; the only assertion is "modified nothing"). This is **not** a supported path — see the warning at the top of this section. + +| Platform | Producer | Shapes | Observed | +|----------|----------|--------|----------| +| `linux` | 0.9.1 | all 4 shapes | exit 0, last lines "✓ refreshed .squad/templates/ \| Upgrade complete: v1.2.0-preview.1 → v0.9.1 \| Preserves user state: team.md, decisions/, agents/*/history.md"; MODIFIED 45 files (20 coordinator/templates/workflows, e.g. .copilot/skills/agent-collaboration/SKILL.md, .copilot/skills/cross-squad/SKILL.md, .copilot/skills/git-workflow/SKILL.md …) | +| `linux` | 0.13.1 | all 6 shapes | exit 0, last lines "overwrite ../.github/workflows/squad-triage.yml \| overwrite ../.github/workflows/sync-squad-labels.yml \| Run without --dry-run to apply."; modified nothing | +| `linux` | 1.0.0 | all 6 shapes | exit 0, last lines "overwrite ../.github/workflows/squad-triage.yml \| overwrite ../.github/workflows/sync-squad-labels.yml \| Run without --dry-run to apply."; modified nothing | +| `linux` | 1.0.1 | all 6 shapes | exit 0, last lines "overwrite ../.github/workflows/squad-triage.yml \| overwrite ../.github/workflows/sync-squad-labels.yml \| Run without --dry-run to apply."; modified nothing | +| `linux` | 1.1.0 | all 6 shapes | exit 0, last lines "overwrite ../.github/workflows/squad-triage.yml \| overwrite ../.github/workflows/sync-squad-labels.yml \| Run without --dry-run to apply."; modified nothing | +| `win32` | 0.9.1 | all 4 shapes | exit 0, last lines "✓ refreshed .squad/templates/ \| Upgrade complete: v1.2.0-preview.1 → v0.9.1 \| Preserves user state: team.md, decisions/, agents/*/history.md"; MODIFIED 44 files (20 coordinator/templates/workflows, e.g. .copilot/skills/agent-collaboration/SKILL.md, .copilot/skills/cross-squad/SKILL.md, .copilot/skills/git-workflow/SKILL.md …) | +| `win32` | 0.13.1 | all 6 shapes | exit 0, last lines "overwrite ../.github/workflows/squad-triage.yml \| overwrite ../.github/workflows/sync-squad-labels.yml \| Run without --dry-run to apply."; modified nothing | +| `win32` | 1.0.0 | all 6 shapes | exit 0, last lines "overwrite ../.github/workflows/squad-triage.yml \| overwrite ../.github/workflows/sync-squad-labels.yml \| Run without --dry-run to apply."; modified nothing | +| `win32` | 1.0.1 | all 6 shapes | exit 0, last lines "overwrite ../.github/workflows/squad-triage.yml \| overwrite ../.github/workflows/sync-squad-labels.yml \| Run without --dry-run to apply."; modified nothing | +| `win32` | 1.1.0 | all 6 shapes | exit 0, last lines "overwrite ../.github/workflows/squad-triage.yml \| overwrite ../.github/workflows/sync-squad-labels.yml \| Run without --dry-run to apply."; modified nothing | + + + +--- + ## Migrations Some upgrades require additive changes to your team state directory — like creating a new subdirectory that didn't exist in older versions. diff --git a/scripts/render-upgrade-matrix.mjs b/scripts/render-upgrade-matrix.mjs new file mode 100644 index 000000000..d1eb0e267 --- /dev/null +++ b/scripts/render-upgrade-matrix.mjs @@ -0,0 +1,338 @@ +#!/usr/bin/env node +/** + * Render the "Tested upgrade paths" section of + * docs/src/content/docs/scenarios/upgrading.md from + * test-fixtures/spec/suite-v0.2/upgrade-matrix.json, and fold suite results into that JSON. + * + * The JSON is the source of truth (runs are produced by test/squad-20-upgrade-matrix.test.ts + * with SQUAD_UPGRADE_MATRIX=1; findings/expectedFailures are hand-maintained). + * + * node scripts/render-upgrade-matrix.mjs # rewrite the docs section + * node scripts/render-upgrade-matrix.mjs --check # exit 1 if the docs section is stale + * node scripts/render-upgrade-matrix.mjs --merge results.json # replace the run for that platform, then render + * node scripts/render-upgrade-matrix.mjs --summary # per-platform counts (for reports) + */ +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +export const MATRIX_PATH = path.join(ROOT, 'test-fixtures', 'spec', 'suite-v0.2', 'upgrade-matrix.json'); +export const PAGE_PATH = path.join(ROOT, 'docs', 'src', 'content', 'docs', 'scenarios', 'upgrading.md'); +export const START = ''; +export const END = ''; + +const PLATFORM_LABEL = { win32: 'Windows', linux: 'Linux', darwin: 'macOS' }; +const STATUS_ICON = { pass: '✅', fail: '🔴', skipped: '⏭️' }; + +const esc = (s) => String(s ?? '').replace(/\|/g, '\\|').replace(/\r?\n/g, ' '); + +export function platformsOf(matrix) { + return [...new Set(matrix.runs.map((r) => r.platform))].sort(); +} + +function resultFor(run, producer, shape) { + return run?.results.find((r) => r.producer === producer && r.shape === shape) ?? null; +} + +/** Finding ids pinned on this result (recorded by the suite as notes `": F"`). */ +export function pinnedFindings(result) { + const ids = new Set(); + for (const n of result.notes ?? []) { + const m = /^[\w-]+: (F\d+)$/.exec(n); + if (m) ids.add(m[1]); + } + for (const c of result.checks) if (c.status === 'xfail' && /^F\d+$/.test(c.detail ?? '')) ids.add(c.detail); + return [...ids].sort(); +} + +/** Short provenance label for a row: npm / GitHub Release / local HEAD. */ +export function provenanceLabel(source) { + return source === 'npm' ? 'npm' : source === 'github-release' ? 'GitHub Release' : 'local HEAD (pack)'; +} + +/** + * One cell. Vocabulary is deliberate: a row "passes" only when every non-xfail assertion passed; + * a row with pinned findings is "pass WITH known … failure (F)", never a bare pass. + */ +export function cell(result, matrix) { + if (!result) return '—'; + if (result.status === 'skipped') return `⏭️ not applicable — ${esc(result.reason)}`; + const known = pinnedFindings(result); + const fails = result.checks.filter((c) => c.status === 'fail' || c.status === 'xpass').map((c) => `${c.id}${c.detail ? `: ${c.detail}` : ''}`); + if (result.status === 'fail') return `🔴 fail — ${esc(fails.join('; ') || result.reason)}`; + if (known.length) { + const labels = known.map((id) => { + const checks = [...new Set(result.checks.filter((c) => c.status === 'xfail').map((c) => c.id))]; + const f = (matrix?.findings ?? []).find((x) => x.id === id); + const destructive = f && /destructive|mutat|rewrit/i.test(`${f.title} ${f.observed}`) ? 'destructive ' : ''; + return `${destructive}${checks.join('/')} failure (${id})`; + }); + return `✅⚠️ pass WITH known ${labels.join(' and ')}`; + } + return '✅ pass'; +} + +export function summarize(matrix) { + const out = {}; + for (const run of matrix.runs) { + const s = { + passedAllNonXfail: 0, withKnownFailures: 0, failed: 0, inapplicable: 0, knownFindings: new Set(), + target: run.target, targetSha: run.targetSha, harnessHash: run.harnessHash, event: run.event, runUrl: run.runUrl, recordedAt: run.recordedAt, node: run.node, arch: run.arch, + }; + for (const r of run.results) { + if (r.status === 'pass') { + s.passedAllNonXfail++; + const k = pinnedFindings(r); + if (k.length) { s.withKnownFailures++; k.forEach((id) => s.knownFindings.add(id)); } + } else if (r.status === 'fail') s.failed++; + else s.inapplicable++; + } + s.knownFindings = [...s.knownFindings].sort(); + out[run.platform] = s; + } + return out; +} + +/** The one sentence a run summary must use (never "N pass / 0 fail" without the qualifier). */ +export function summarySentence(s, matrix) { + const findingLabel = (id) => { + const f = (matrix?.findings ?? []).find((x) => x.id === id); + return f ? `${id}: ${f.title.split(':')[0].replace(/`/g, '')}` : id; + }; + const known = s.withKnownFailures + ? `; ${s.withKnownFailures} of them carry a known expected failure (${s.knownFindings.map(findingLabel).join('; ')})` + : ''; + return `${s.passedAllNonXfail} rows passed all non-xfail assertions${known}; ${s.failed} rows failed; ${s.inapplicable} rows are structurally inapplicable (listed below).`; +} + +function artifactCell(p) { + if (p.source === 'npm') { + const reg = p.artifacts.registry; + return Object.entries(reg).map(([n, a]) => `\`${n}@${a.version}\`
sha1 \`${a.shasum}\`
\`${a.integrity}\``).join('
'); + } + if (p.source === 'github-release') { + const rel = p.artifacts.release; + return `[${rel.repository} ${rel.tag}](https://github.com/${rel.repository}/releases/tag/${rel.tag}) — assets by immutable id:
` + + Object.entries(rel.assets).map(([n, a]) => `\`${n}\` id \`${a.id}\` sha256 \`${a.sha256}\``).join('
'); + } + return esc(p.artifacts.pack); +} + +export function renderSection(matrix) { + const platforms = platformsOf(matrix); + const sum = summarize(matrix); + const lines = []; + lines.push(START); + lines.push(''); + lines.push(''); + lines.push(''); + lines.push('## Tested upgrade paths'); + lines.push(''); + lines.push(`Every row below is a pinned, published artifact of an older Squad release driven through \`squad init\` under a given project state shape, then **upgraded** with the CLI packed from the tree under test (\`npm pack\` of \`@bradygaster/squad-sdk\` + \`@bradygaster/squad-cli\`) by \`test/squad-20-upgrade-matrix.test.ts\`. A row **passes** only when every non-xfail assertion passed; a row that carries a pinned finding is reported as **pass WITH known … failure (F)**, never as a bare pass; **fail** names the failing checks; **not applicable** cells are each explained individually below (none means "not run"). Machine-readable source: \`${path.relative(ROOT, MATRIX_PATH).replace(/\\/g, '/')}\` (protocol \`${matrix.protocol}\`).`); + lines.push(''); + lines.push('### Three different things'); + lines.push(''); + lines.push('| Term | Meaning |'); + lines.push('|------|---------|'); + for (const [k, v] of Object.entries(matrix.concepts)) lines.push(`| **${k}** | ${esc(v)} |`); + lines.push(''); + lines.push('> ⚠️ **Never run an older Squad CLI against an upgraded repository.** There is no supported downgrade. The `downgrade-probe` rows record what each old CLI does: 0.9.1 `squad upgrade --dry-run` is **not trustworthy** — it rewrites the coordinator, skills, workflows and templates in place (F3); 0.13.1–1.1.0 preview overwriting them. If an old CLI has already touched an upgraded repository, re-run `squad upgrade` with the current CLI to repair the Squad-owned files; your `.squad/` team state is not changed by either direction. `uninstallClients()` removes host projections only — it is **not** a way back to an older Squad.'); + lines.push(''); + lines.push('### Runs'); + lines.push(''); + if (matrix.runs.length === 0) { + lines.push('_No runs recorded yet. The CI workflow `squad-upgrade-matrix.yml` records a run per OS at the tree under test; fold both artifacts with `node scripts/render-upgrade-matrix.mjs --merge `._'); + lines.push(''); + } else { + const shas = [...new Set(matrix.runs.map((r) => r.targetSha))]; + const hashes = [...new Set(matrix.runs.map((r) => r.harnessHash))]; + lines.push(`All recorded runs executed against **one tree**, commit \`${shas.join('`, `')}\`, with harness hash \`${hashes.map((h) => h.slice(0, 16)).join('`, `')}…\` (sha256 over the suite, renderer, workflow, fs hook and this fixture minus \`runs\`; the integrity test refuses a recorded run whose harness differs from the committed one, so folding results may change nothing but the runs data).`); + lines.push(''); + lines.push('| Platform | Arch | Node | Target | Tree (targetSha) | Trigger | Recorded | Outcome |'); + lines.push('|----------|------|------|--------|------------------|---------|----------|---------|'); + for (const run of matrix.runs) { + const s = sum[run.platform]; + const trigger = run.runUrl ? `[${run.event}](${run.runUrl})` : run.event; + lines.push(`| ${PLATFORM_LABEL[run.platform] ?? run.platform} (\`${run.platform}\`) | ${run.arch} | ${run.node} | v${run.target} | \`${run.targetSha.slice(0, 12)}\` | ${trigger} | ${run.recordedAt.slice(0, 10)} | ${esc(summarySentence(s, matrix))} |`); + } + lines.push(''); + } + lines.push('### Producers (pinned, immutable provenance)'); + lines.push(''); + lines.push('| Version | Provenance | Artifact pins | Why it is in the matrix | `--state-backend` |'); + lines.push('|---------|------------|---------------|-------------------------|-------------------|'); + for (const p of matrix.producers) { + lines.push(`| **${p.version}** | ${provenanceLabel(p.source)} | ${artifactCell(p)} | ${esc(p.why)} | ${p.supports.stateBackend ? 'yes' : 'no'} |`); + } + lines.push(''); + lines.push('npm tarballs are fetched by exact version through the registry npm is configured for and verified against the recorded sha1 + sha512 before use; GitHub Release bundles are downloaded by immutable asset id and verified against the recorded sha256. A verification mismatch **fails** the producer\'s rows (it is never a skip); only an unreachable registry/release yields a skip, and that skip carries the reason.'); + lines.push(''); + lines.push('### State shapes'); + lines.push(''); + lines.push('| Shape | Setup |'); + lines.push('|-------|-------|'); + for (const [id, d] of Object.entries(matrix.shapes)) lines.push(`| \`${id}\` | ${esc(d)} |`); + lines.push(''); + lines.push('### Results (version × shape × OS)'); + lines.push(''); + const header = platforms.length ? platforms.map((p) => PLATFORM_LABEL[p] ?? p) : ['(no runs yet)']; + lines.push(`| Producer | Provenance | Shape | ${header.join(' | ')} |`); + lines.push(`|----------|------------|-------|${header.map(() => '---').join('|')}|`); + for (const p of matrix.producers) { + for (const shape of Object.keys(matrix.shapes)) { + const cells = platforms.length + ? platforms.map((pl) => cell(resultFor(matrix.runs.find((r) => r.platform === pl), p.version, shape), matrix)) + : [inapplicableFor(matrix, p.version, shape) ? '⏭️ not applicable (see below)' : '—']; + lines.push(`| **${p.version}** | ${provenanceLabel(p.source)} | \`${shape}\` | ${cells.join(' | ')} |`); + } + } + lines.push(''); + lines.push('### Not-applicable cells, each explained'); + lines.push(''); + lines.push(`These ${matrix.inapplicable.length} cells are structurally impossible to exercise — the old producer cannot create that project shape — so they are **not** "not run"; the suite asserts the recorded skipped rows are exactly this list.`); + lines.push(''); + lines.push('| Producer | Shape | Why it cannot apply |'); + lines.push('|----------|-------|---------------------|'); + for (const x of matrix.inapplicable) lines.push(`| **${x.producer}** | \`${x.shape}\` | ${esc(x.reason)} |`); + lines.push(''); + if (matrix.expectedFailures.length) { + lines.push('### Expected failures (pinned)'); + lines.push(''); + lines.push('Each pin names an open finding; the suite reports the check as `xfail` and **fails the moment it starts passing** (`xpass`), so a pin cannot outlive its bug.'); + lines.push(''); + lines.push('| Check | Producers | Shapes | Platforms | Finding |'); + lines.push('|-------|-----------|--------|-----------|---------|'); + for (const x of matrix.expectedFailures) { + lines.push(`| \`${x.check}\` | ${x.producers === '*' ? 'all' : x.producers.join(', ')} | ${x.shapes === '*' ? 'all' : x.shapes.map((s) => `\`${s}\``).join(', ')} | ${x.platforms === '*' ? 'all' : x.platforms.join(', ')} | [${x.finding}](#${x.finding.toLowerCase()}) |`); + } + lines.push(''); + } + lines.push('### Checks'); + lines.push(''); + lines.push('| Check | Meaning |'); + lines.push('|-------|---------|'); + for (const [id, d] of Object.entries(matrix.checks)) lines.push(`| \`${id}\` | ${esc(d)} |`); + lines.push(''); + lines.push('Paths a plain `squad upgrade` may create or rewrite (the only changes `user-state-preserved` accepts):'); + lines.push(''); + for (const re of matrix.squadOwned) lines.push(`- \`${re}\``); + lines.push(''); + if (matrix.findings.length) { + lines.push('### Findings'); + lines.push(''); + lines.push('Real upgrade defects observed by the suite. An open finding is pinned as an expected failure until fixed (the fix lands in the layer named, not in this test-only layer); a resolved finding keeps its repro here so the matrix row that used to carry it documents the regression guard.'); + lines.push(''); + for (const f of matrix.findings) { + lines.push(`#### ${f.id} — ${f.title}`); + lines.push(''); + lines.push(`**Severity:** ${f.severity} · **Owner:** ${f.layer} · **Status:** ${f.resolved ? '✅ resolved' : '🔴 open'}`); + lines.push(''); + lines.push('Repro:'); + lines.push(''); + for (const step of f.repro) lines.push(`1. ${step}`); + lines.push(''); + lines.push(`Observed: ${f.observed}`); + lines.push(''); + if (f.resolved) { + lines.push(`Resolved: ${f.resolved}`); + lines.push(''); + } + const pins = matrix.expectedFailures.filter((x) => x.finding === f.id); + if (pins.length) { + lines.push(`Pinned checks: ${pins.map((x) => `\`${x.check}\` (${x.producers === '*' ? 'all producers' : x.producers.join(', ')} × ${x.shapes === '*' ? 'all shapes' : x.shapes.join(', ')} × ${x.platforms === '*' ? 'all platforms' : x.platforms.join(', ')})`).join('; ')}.`); + lines.push(''); + } + } + } + const downgrade = []; + for (const run of matrix.runs) { + for (const p of matrix.producers) { + const withNote = run.results.filter((r) => r.producer === p.version && (r.notes ?? []).some((n) => /upgrade --dry-run on upgraded project/.test(n))); + const pick = withNote.find((r) => r.shape === 'default') ?? withNote[0]; + if (!pick) continue; + const note = pick.notes.find((n) => /upgrade --dry-run on upgraded project/.test(n)); + const sameEverywhere = withNote.every((r) => r.notes.find((n) => /upgrade --dry-run on upgraded project/.test(n)) === note); + downgrade.push({ platform: run.platform, producer: p.version, shapes: sameEverywhere ? `all ${withNote.length} shapes` : `\`${pick.shape}\``, note }); + } + } + if (downgrade.length) { + lines.push('### Unsupported downgrade — what each old CLI does to an upgraded repository'); + lines.push(''); + lines.push('Recorded by the `downgrade-probe` check (dry-run on a copy; the only assertion is "modified nothing"). This is **not** a supported path — see the warning at the top of this section.'); + lines.push(''); + lines.push('| Platform | Producer | Shapes | Observed |'); + lines.push('|----------|----------|--------|----------|'); + for (const n of downgrade) lines.push(`| \`${n.platform}\` | ${n.producer} | ${n.shapes} | ${esc(n.note.replace(/^old [\w.]+ upgrade --dry-run on upgraded project: /, ''))} |`); + lines.push(''); + } + lines.push(END); + return lines.join('\n') + '\n'; +} + +function inapplicableFor(matrix, producer, shape) { + return matrix.inapplicable.find((x) => x.producer === producer && x.shape === shape); +} + +export function extractSection(page) { + const a = page.indexOf(START); + const b = page.indexOf(END); + if (a === -1 || b === -1 || b < a) return null; + return page.slice(a, b + END.length) + '\n'; +} + +export function splice(page, section) { + const existing = extractSection(page); + if (existing) { + const at = page.indexOf(existing); + return page.slice(0, at) + section + page.slice(at + existing.length); + } + // First render: insert before "## Migrations". + const anchor = page.indexOf('\n## Migrations'); + if (anchor === -1) return page.trimEnd() + '\n\n---\n\n' + section; + return page.slice(0, anchor) + '\n' + section + '\n---\n' + page.slice(anchor); +} + +export function mergeRun(matrix, run) { + if (!run || typeof run.platform !== 'string' || !Array.isArray(run.results)) throw new Error('results file must contain {platform, results[]}'); + if (!/^[0-9a-f]{40}$/.test(run.targetSha ?? '') || !/^[0-9a-f]{64}$/.test(run.harnessHash ?? '')) throw new Error('results file must carry targetSha (40 hex) and harnessHash (64 hex) — produced by the suite, not hand-written'); + const known = new Set(matrix.producers.map((p) => p.version)); + for (const r of run.results) if (!known.has(r.producer)) throw new Error(`results reference unknown producer ${r.producer}`); + const others = matrix.runs.filter((r) => r.platform !== run.platform); + const foreign = others.find((r) => r.targetSha !== run.targetSha); + if (foreign) throw new Error(`run for ${run.platform} targets ${run.targetSha} but the recorded ${foreign.platform} run targets ${foreign.targetSha}; all recorded runs must come from one tree — re-record every platform`); + matrix.runs = [...others, run].sort((a, b) => a.platform.localeCompare(b.platform)); + return matrix; +} + +const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url); +if (isMain) { + const matrix = JSON.parse(fs.readFileSync(MATRIX_PATH, 'utf8')); + const args = process.argv.slice(2); + const mergeIdx = args.indexOf('--merge'); + if (mergeIdx !== -1) { + const file = args[mergeIdx + 1]; + if (!file) { console.error('--merge requires a results file'); process.exit(2); } + mergeRun(matrix, JSON.parse(fs.readFileSync(file, 'utf8'))); + fs.writeFileSync(MATRIX_PATH, JSON.stringify(matrix, null, 2) + '\n'); + console.log(`merged ${file} into ${path.relative(ROOT, MATRIX_PATH)}`); + } + if (args.includes('--summary')) { + console.log(JSON.stringify(summarize(matrix), null, 2)); + } else if (args.includes('--check')) { + const expected = renderSection(matrix).replace(/\r\n/g, '\n'); + const page = (fs.existsSync(PAGE_PATH) ? fs.readFileSync(PAGE_PATH, 'utf8') : '').replace(/\r\n/g, '\n'); + const actual = extractSection(page); + if (expected !== actual) { + console.error(`${path.relative(ROOT, PAGE_PATH)} upgrade-matrix section is stale; run: node scripts/render-upgrade-matrix.mjs`); + process.exit(1); + } + console.log('upgrading.md upgrade-matrix section is up to date'); + } else { + const page = fs.readFileSync(PAGE_PATH, 'utf8'); + const eol = page.includes('\r\n') ? '\r\n' : '\n'; + const next = splice(page.replace(/\r\n/g, '\n'), renderSection(matrix)).replace(/\n/g, eol); + fs.writeFileSync(PAGE_PATH, next); + console.log(`wrote ${path.relative(ROOT, PAGE_PATH)}`); + } +} diff --git a/test-fixtures/spec/suite-v0.2/upgrade-matrix.json b/test-fixtures/spec/suite-v0.2/upgrade-matrix.json new file mode 100644 index 000000000..21fe25b72 --- /dev/null +++ b/test-fixtures/spec/suite-v0.2/upgrade-matrix.json @@ -0,0 +1,6022 @@ +{ + "protocol": "upgrade-matrix/v1", + "description": "Record of real old-release → Squad 2.0 upgrade runs. Produced by test/squad-20-upgrade-matrix.test.ts (SQUAD_UPGRADE_MATRIX=1) in CI at the recorded targetSha, merged with `node scripts/render-upgrade-matrix.mjs --merge `, rendered into docs/src/content/docs/scenarios/upgrading.md. Producers are pinned, immutable artifacts: npm tarballs by exact version + sha1 shasum + sha512 integrity (fetched through the registry npm is configured for; integrity mismatch fails closed), or GitHub Release assets by release tag + asset id + sha256 (downloaded by asset id; mismatch fails closed). Each recorded run carries the harnessHash of the suite/renderer/workflow/fixture it ran with; the integrity test refuses runs whose harness differs from the committed one, so a fold commit may change only the runs data.", + "concepts": { + "upgrade": "`squad upgrade` with the current CLI: refreshes Squad-owned files, migrates casting/state, preserves user state. The only supported direction.", + "projection uninstall": "`uninstallClients()` from `@bradygaster/squad-sdk/client`: removes generated host projections and restores shared files byte-exact. Does not change the installed CLI, `.squad/` state, or the repository's Squad version.", + "unsupported downgrade": "Running an older Squad CLI against an upgraded repository. Not supported; the `downgrade-probe` check only records what each old CLI does so users are warned (0.9.1 `--dry-run` mutates — F3)." + }, + "producers": [ + { + "version": "0.9.1", + "source": "npm", + "why": "Oldest 0.x line that still installs on Node 22 (engines >=22.5); pinned with its own @bradygaster/squad-sdk@0.9.1 (its `>=0.9.0` range would otherwise float to 1.0.1). Scaffolds `.squad/` with an EMPTY `casting/` directory and skills under `.copilot/skills/`; predates --state-backend.", + "supports": { + "stateBackend": false, + "dryRun": true + }, + "artifacts": { + "registry": { + "@bradygaster/squad-cli": { + "version": "0.9.1", + "shasum": "a5ee3dbfc44b7457eba56476f708ad6fdc2e09ca", + "integrity": "sha512-TcZqRbJk3XBg4N8Y5/5OWJeA52ARAMuzk4MGtB0bfdr87e7iJRV6lJEw8+OIYz9IQ+1JxN/Q30dZDALMOcdi2w==" + }, + "@bradygaster/squad-sdk": { + "version": "0.9.1", + "shasum": "4735aba9f125de415160cd979ea3f1467903f16e", + "integrity": "sha512-1KqZ2N6Lt/B4uYlU/Su9JjA1s/7ng0OAEb9CZrkRk19YSPi6tg7K5QWdTKl5gY/gp8zPAJqfjVffOwrZwGlzmw==" + } + } + } + }, + { + "version": "0.13.1", + "source": "npm", + "why": "Last 0.x stable; first line with --state-backend (local|orphan|two-layer) and user-profile squads (writes teamRoot when a profile squad exists).", + "supports": { + "stateBackend": true, + "dryRun": true + }, + "artifacts": { + "registry": { + "@bradygaster/squad-cli": { + "version": "0.13.1", + "shasum": "c8f6427bab4cae989934574315eabda98495fb37", + "integrity": "sha512-ogfx2nhyx6fqMyqlaI5tDnOzUbRdh+a6Uh/rMQ0aNJwAXnvQp4N37Xp/eOTyB5CxcB7E34NBtAfCxyXbWSiS8w==" + }, + "@bradygaster/squad-sdk": { + "version": "0.13.1", + "shasum": "90e98450971e921ca2072673b325fcc8126a2fb6", + "integrity": "sha512-1Jt6c9kahJq1JCcE77j/50D79YcXVG6Rt1genuLJ3O9J2SKTZKemlbvHgftkc5LgGH22DkWqHh5zLNEQ/p+ukA==" + } + } + } + }, + { + "version": "1.0.0", + "source": "npm", + "why": "First 1.x stable; last version published to npm (dist-tag latest).", + "supports": { + "stateBackend": true, + "dryRun": true + }, + "artifacts": { + "registry": { + "@bradygaster/squad-cli": { + "version": "1.0.0", + "shasum": "e4e776ef938cbe8c65d8d2c729f2249a48bca638", + "integrity": "sha512-mnZXnA+GAtp4KSLf7a4Zw0BpeOaHFAxYV5oxAy58ancZ9C2LguUGbcTxWIG0djM96XWET8K9fcncif3Vm6Z9RQ==" + }, + "@bradygaster/squad-sdk": { + "version": "1.0.0", + "shasum": "4abb8e450e79827b865a7846af6589789d079cab", + "integrity": "sha512-9f4NthJaQDq6ifXU3f6VfsZz2bbWs2u57fHhXKtDgDMSvZ3Xq8FPjMvddxhRvseWqaSlkYcWsag3M/lzTtKaJA==" + } + } + } + }, + { + "version": "1.0.1", + "source": "github-release", + "why": "Patch release whose CLI never reached npm (release run 37222633918: SDK publish failed, CLI publish skipped); only the standalone bundles exist.", + "supports": { + "stateBackend": true, + "dryRun": true + }, + "artifacts": { + "release": { + "repository": "bradygaster/squad", + "tag": "v1.0.1", + "assets": { + "squad-darwin-arm64.tar.gz": { + "id": 610330560, + "sha256": "47a8d44d0a9a338ae2c2b6192cb761187ebd71edbd1171a4800cee11cb001b43" + }, + "squad-darwin-x64.tar.gz": { + "id": 610330564, + "sha256": "b88db7e8949878ba2cdf756758651cd50a0e9b5fc83f4c759bd7438d596b4951" + }, + "squad-linux-arm64.tar.gz": { + "id": 610330558, + "sha256": "9d8105c903eeac4d1762e43432f54827a6c4676f4aa42cfdf34bf645e497e756" + }, + "squad-linux-x64.tar.gz": { + "id": 610330551, + "sha256": "283a1893be9ddad11056dcc8bd0673eff6b48a789bbf523b3f7ec31994d27ac7" + }, + "squad-win32-arm64.zip": { + "id": 610330674, + "sha256": "586c4e2d3d708f4d3f08b2c8ffb965d9312d6586d0830a1b8a817b27c3c14bd0" + }, + "squad-win32-x64.zip": { + "id": 610330559, + "sha256": "f38a8f85e60982dba340187b761488f01de1aec68ea50c1b85725783d32df2fe" + } + } + } + } + }, + { + "version": "1.1.0", + "source": "github-release", + "why": "Latest 1.x release; npm CLI publish failed in release run 39442663504 (\"Could not verify @bradygaster/squad-cli@1.1.0 on npm dist-tag 'latest' after 20 attempts\"), so the standalone bundle is the only published artifact.", + "supports": { + "stateBackend": true, + "dryRun": true + }, + "artifacts": { + "release": { + "repository": "bradygaster/squad", + "tag": "v1.1.0", + "assets": { + "squad-darwin-arm64.tar.gz": { + "id": 659607638, + "sha256": "057c8d2296e1c95cd77933bae55475409a2dce88aa5b3443cc6ca6cfcd545631" + }, + "squad-darwin-x64.tar.gz": { + "id": 659607644, + "sha256": "4e4e8574f6a339f5537c82eb586d66ecf2ee3005e86b058b834b965be33a43cd" + }, + "squad-linux-arm64.tar.gz": { + "id": 659607639, + "sha256": "992e9dbfe677e3c772446960051329aaed1578d5e4dbcb3350827dbb755e8b00" + }, + "squad-linux-x64.tar.gz": { + "id": 659607641, + "sha256": "2de647a3feaef334051eb1b3084acba37a511818aad029a16abfe32fadb41aff" + }, + "squad-win32-arm64.zip": { + "id": 659607808, + "sha256": "3c922c37863b255546403cffb2aa1c7dff2a347be87ca582759c149aec7bb1da" + }, + "squad-win32-x64.zip": { + "id": 659607643, + "sha256": "571d734f7ae718fefefdfa9864f9f82a3d1fb1546f01ec8bf8866fa7e1829c57" + } + } + } + } + }, + { + "version": "HEAD", + "source": "pack", + "why": "Same-version idempotency: the CLI packed from this tree scaffolds and then upgrades itself.", + "supports": { + "stateBackend": true, + "dryRun": true + }, + "artifacts": { + "pack": "npm pack of packages/squad-sdk and packages/squad-cli from the tree under test (targetSha)" + } + } + ], + "shapes": { + "default": "`squad init` with an empty profile directory, plus a custom agent (`agents/probe/`), an appended decision, a hand-written `.mcp.json` server, `AGENTS.md`, `CLAUDE.md` and a `.gitignore` rule.", + "profile": "As `default`, but the platform config dir is pre-seeded with the 1.x user-profile layout (a roster directory and a `.squad/` under `/squad/`); the whole tree must stay sha256-identical and the project must not be linked to it.", + "legacy-casting": "As `default`, then `.squad/casting/` is replaced by a hand-written `registry.json` (`{universe, agents:[{name, role, status}]}`) with no history — the shape an agent-edited 1.x registry has.", + "orphan": "`squad init --state-backend orphan` (producers that have the flag); the `squad-state` branch and `stateBackend` must survive.", + "two-layer": "`squad init --state-backend two-layer` (producers that have the flag); the `squad-state` branch and `stateBackend` must survive.", + "ai-team": "As `default`, then `.squad/` is renamed to the legacy `.ai-team/` and the upgrade runs with `--migrate-directory`.", + "upstream": "HEAD only: a second squad is declared via `squad upstream add --name org`; `upstream.json` must survive the upgrade byte-exact." + }, + "checks": { + "producer-artifact-verified": "The pinned artifact that was executed matched its recorded hashes (npm: sha1 shasum + sha512 integrity of both tarballs; GitHub Release: sha256 of the asset downloaded by id). A mismatch FAILS every row of that producer — it is never a skip; only an unreachable registry/release skips, with the reason.", + "scenario-setup": "The matrix could set up the shape (e.g. configure the `upstream` squad). Only recorded when setup fails: a row can never be `fail` without a failing check (LD-2).", + "producer-init": "The OLD CLI (the pinned producer artifact) scaffolds the project non-interactively (exit 0, `.squad/team.md` present).", + "upgrade-plain": "`squad upgrade` (plus `--migrate-directory` for `ai-team`) with the NEW packed CLI exits 0.", + "coordinator-is-new-template": "`.github/agents/squad.agent.md` carries the new version stamp (``, `Squad (vX)`).", + "user-state-preserved": "Full manifest compare (path + sha256 of every file outside `.git/`) before/after the plain upgrade: every seeded user file — active and retired agent folders, nested unknown files, filenames with spaces and Unicode, appended `decisions.md`/`team.md`/`routing.md`, a user skill — is byte-identical; every changed path matches a declared Squad-owned pattern (`squadOwned`); the custom `config.json` key is kept. For `orphan`/`two-layer` the mutable state (`decisions.md`, `agents/*/history.md`) is seeded as git blobs on `squad-state` and every seeded blob OID must be unchanged and no branch path deleted. For `ai-team` the legacy directory is mapped onto `.squad/` first so the migration itself is not counted.", + "casting-pair-valid": "`readCastingRegistryPair` from the packed SDK reads a numbered registry + history; for `legacy-casting` the hand-written original is archived and `probe` is in the repaired registry.", + "state-backend-preserved": "`config.json` `stateBackend` is unchanged; for `orphan`/`two-layer` the `squad-state` branch still exists.", + "ai-team-migrated": "`.ai-team/` is gone and `.squad/team.md` exists (only for the `ai-team` shape).", + "team-root-absent": "`config.json` has no `teamRoot` after the upgrade (Squad 2.0 never links a project to a profile directory).", + "profile-untouched": "The seeded 1.x `/squad/` profile tree (roster directory + `.squad/`) is sha256-identical after EVERY phase (upgrade, second upgrade, downgrade probe, projection, doctor, uninstall — the offending phase is named) AND the fs access logger loaded into every Squad 2.0 process (`NODE_OPTIONS=--require test/fixtures/upgrade-matrix/fs-access-log.cjs`) recorded zero reads, probes or writes under it (only for `profile`).", + "idempotent-second-upgrade": "A second plain `squad upgrade` changes nothing: sha256 of every file outside `.git/`, every git ref OID (`for-each-ref`, incl. the `squad-state` branch and any notes refs), `git status --porcelain` and the `squad-state` commit count are identical.", + "downgrade-probe": "UNSUPPORTED PATH, recorded not endorsed: the OLD CLI's `upgrade --dry-run` against a copy of the upgraded project must modify nothing; its exit code and message are recorded as a note. Squad has no supported downgrade.", + "clients-projected": "`squad upgrade --client all --force` exits 0, `config.json` `clients` lists all 7 hosts, and every projection PARSES with its host's format — JSON (`.mcp.json`, `.kiro/agents/squad.json`, `.kiro/settings/mcp.json`, `opencode.json`, `.cursor/mcp.json`), TOML (`.codex/agents/squad.toml`, `.codex/config.toml`), YAML frontmatter (`.github/agents/squad.agent.md`, `.claude/agents/squad.md`, `.kiro/steering/squad.md`, `.opencode/agent/squad.md`, `.cursor/agents/squad.md`, `.cursor/rules/squad.mdc`, `.agents/agents/squad.md`) — with a `squad_state` MCP entry where the host has one; mirrors the per-host assertions in `test/clients.test.ts`.", + "user-config-preserved": "After projection the user's `.mcp.json` server, `AGENTS.md`/`CLAUDE.md` paragraphs and `.gitignore` rule are intact (for `upstream`: `upstream.json` is byte-identical).", + "doctor": "`squad doctor` exits 0 with no ❌ line.", + "projection-uninstall-byte-exact": "`uninstallClients(CLIENT_IDS)` from the packed SDK, judged against the tree right after the plain upgrade (before any projection): `AGENTS.md`, `CLAUDE.md`, `GEMINI.md`, `.gitignore` byte-identical to that baseline (absent stays absent); all 8 projection files removed (agents, Kiro steering, Cursor rules, Kiro agent JSON); `.mcp.json` equal to the baseline minus the `squad_state` entry Squad installed, with the user's server intact; Kiro/Cursor MCP JSON, `opencode.json` and `.codex/config.toml` created by the projection carry no `squad_state`; seeded `.squad/` files and `squad-state` blobs untouched. This is projection uninstall — NOT a package or repository rollback and NOT a downgrade." + }, + "squadOwned": [ + "^\\.github/agents/squad\\.agent\\.md(\\.local-backup)?$", + "^\\.github/copilot-instructions\\.md$", + "^\\.github/skills/", + "^\\.copilot/skills/", + "^\\.github/workflows/(squad-[\\w-]+|sync-squad-labels)\\.yml(\\.local-backup)?$", + "^\\.squad/\\.first-run$", + "^\\.squad/config\\.json$", + "^\\.squad/casting/", + "^\\.squad/templates/", + "^\\.squad/casting-(history|policy|registry)\\.json$", + "^\\.squad/agents/(rai|Rai|fact-checker|ralph|scribe)/", + "^\\.squad/(rai|fact-checker|identity|memory)/", + "^\\.squad/(ceremonies|ralph-instructions)\\.md$", + "^\\.gitignore$", + "^\\.gitattributes$", + "^\\.mcp\\.json$", + "^\\.copilot/mcp-config\\.json$", + "^\\.vscode/" + ], + "inapplicable": [ + { + "producer": "0.9.1", + "shape": "orphan", + "reason": "`@bradygaster/squad-cli@0.9.1` has no `--state-backend` flag (first shipped in 0.13.x), so an orphan-backend project cannot be produced by it." + }, + { + "producer": "0.9.1", + "shape": "two-layer", + "reason": "`@bradygaster/squad-cli@0.9.1` has no `--state-backend` flag (first shipped in 0.13.x), so a two-layer-backend project cannot be produced by it." + }, + { + "producer": "0.9.1", + "shape": "upstream", + "reason": "Upstream inheritance (`.squad/upstream.json`, `squad upstream add`) is a Squad 2.0 feature; 0.9.1 cannot declare an upstream, so there is nothing for the upgrade to preserve." + }, + { + "producer": "0.13.1", + "shape": "upstream", + "reason": "Upstream inheritance (`.squad/upstream.json`, `squad upstream add`) is a Squad 2.0 feature; 0.13.1 cannot declare an upstream, so there is nothing for the upgrade to preserve." + }, + { + "producer": "1.0.0", + "shape": "upstream", + "reason": "Upstream inheritance (`.squad/upstream.json`, `squad upstream add`) is a Squad 2.0 feature; 1.0.0 cannot declare an upstream, so there is nothing for the upgrade to preserve." + }, + { + "producer": "1.0.1", + "shape": "upstream", + "reason": "Upstream inheritance (`.squad/upstream.json`, `squad upstream add`) is a Squad 2.0 feature; 1.0.1 cannot declare an upstream, so there is nothing for the upgrade to preserve." + }, + { + "producer": "1.1.0", + "shape": "upstream", + "reason": "Upstream inheritance (`.squad/upstream.json`, `squad upstream add`) is a Squad 2.0 feature; 1.1.0 cannot declare an upstream, so there is nothing for the upgrade to preserve." + } + ], + "findings": [ + { + "id": "F1", + "title": "Stale `teamRoot` written by 0.13.x–1.x `init` survives `squad upgrade`; the upgraded project resolves in remote mode against the removed profile squad", + "severity": "high", + "layer": "C — user-profile squad removal (`runUpgrade` / `resolveSquadPaths`)", + "repro": [ + "Seed a 1.x profile tree under the platform config dir: the user-profile roster directory and `.squad/` under `/squad/` (APPDATA on Windows, XDG_CONFIG_HOME on Linux).", + "`squad init` with @bradygaster/squad-cli 0.13.1 or 1.0.0 (npm) or the 1.0.1 / 1.1.0 standalone bundle: it prints its profile-detected notice and writes `.squad/config.json` → `{ \"version\": 1, \"teamRoot\": \"\\\\squad\" }`.", + "Upgrade with the 2.0 CLI packed from this tree: `squad upgrade` (exit 0, no warning about teamRoot).", + "`cat .squad/config.json` still contains `teamRoot: /squad`; `squad doctor` reports `Mode: remote` and ❌ `routing.md`, `casting/registry.json`, `decisions.md` not found — it is reading `/squad/.squad/`, not the project." + ], + "observed": "`team-root-absent` fails (`teamRoot=/squad`) and `doctor` fails for the `profile` shape on every 0.13.1+ producer. The profile tree itself stays sha256-identical (`profile-untouched` passes), so this is a resolution defect, not a write: Squad 2.0 keeps honouring a link to a directory it no longer supports. Expected: `squad upgrade` drops (or refuses with guidance) a `teamRoot` that points at the platform config dir, and the project resolves in local mode.", + "resolved": "Fixed on squad2-c @ 21ecca77 (PR #2279): `squad upgrade` removes a `teamRoot` that resolves to the platform config dir / `~/.squad` / `SQUAD_HOME` (`isRetiredProfileTeamRoot()` in `@bradygaster/squad-sdk/resolution`) and prints `Removed stale teamRoot pointing to ; project state is now local`; `--dry-run` previews it; `resolveSquadPaths` treats such a teamRoot as local mode. Dual-root targets are untouched. Verified by this matrix: `team-root-absent` and `doctor` now pass for every 0.13.1+ producer × `profile` on both OSes (the former pins produced `xpass` before removal)." + }, + { + "id": "F2", + "title": "`squad upgrade` never recognizes its own stamped coordinator: every run reports \"local customizations\" and overwrites `squad.agent.md.local-backup`", + "severity": "medium", + "layer": "CLI core — `packages/squad-cli/src/cli/core/upgrade.ts` `writeAgentTemplate`", + "repro": [ + "`squad init` (any producer, including the 2.0 CLI itself), then `squad upgrade` once — a genuine pre-upgrade coordinator is saved to `.github/agents/squad.agent.md.local-backup`.", + "Run `squad upgrade` again with nothing changed.", + "Output again says `⚠️ squad.agent.md has local customizations — backed up to squad.agent.md.local-backup`; the backup is overwritten with the pristine current coordinator, so the user's real pre-upgrade copy is gone. The only file whose sha256 changes between run 1 and run 2 is `.github/agents/squad.agent.md.local-backup`." + ], + "observed": "`idempotent-second-upgrade` fails with exactly `.github/agents/squad.agent.md.local-backup` for every old producer × shape × platform. On a project initialized by the 2.0 CLI itself the warning is printed on every run too (`Already up to date` + `⚠️ … local customizations`), but the overwritten backup is byte-identical, so the HEAD rows are not pinned. Cause: `writeAgentTemplate` strips `` before comparing, but `stampVersion` writes ``, `- **Version:** X` and `Squad vX`, so a stamped coordinator never equals the template. Expected: a second `squad upgrade` is a byte-for-byte no-op, prints no customization warning, and never touches an existing `.local-backup`.", + "resolved": "Fixed on squad2-c @ 21ecca77 (PR #2279): `stripAgentStamps()` (`@bradygaster/squad-cli/core/upgrade`) normalizes ``, the `- **Version:**` token, `` `Squad vX` ``, `@bradygaster/squad-cli@X` and the legacy `` before the customization compare, and an identical `.local-backup` is not rewritten. Verified by this matrix: `idempotent-second-upgrade` now passes for every producer × shape on both OSes." + }, + { + "id": "F3", + "title": "Squad 0.9.1 `squad upgrade --dry-run` is not dry: pointed at a 2.0 project it silently downgrades the coordinator, skills, workflows and templates in place", + "severity": "info (historical producer defect — not fixable in 2.0; recorded for the downgrade story)", + "layer": "none — behavior of the published 0.9.1 CLI", + "repro": [ + "Scaffold with 0.9.1 (`npm install @bradygaster/squad-cli@0.9.1`), upgrade with the 2.0 CLI, copy the project.", + "In the copy run the 0.9.1 CLI: `squad upgrade --dry-run`.", + "It exits 0, prints the normal upgrade summary (`Upgrade complete: v1.2.0-preview.1 → v0.9.1 … Preserves user state: …`) and rewrites ~44 files with the pinned 0.9.1 SDK (~83 when 0.9.1's `>=0.9.0` SDK range floats to 1.0.1): `.github/agents/squad.agent.md` (back to 0.9.1), `.copilot/skills/*`, `.github/workflows/squad-*.yml`, `.squad/templates/*`, plus stray `.squad/charter.md`, `.squad/roster.md`, `.squad/history.md` … at the state root." + ], + "observed": "`downgrade-probe` fails for every 0.9.1 row because the dry-run modified files (destructive: it IS the downgrade) and printed `Upgrade complete: v1.2.0-preview.1 → v0.9.1`; 0.13.1, 1.0.0, 1.0.1 and 1.1.0 honor `--dry-run` (`Run without --dry-run to apply.`, nothing modified) but their preview lists `overwrite .github/workflows/squad-*.yml` etc. — none of the old CLIs has a newer-version guard, so running any of them for real against a 2.0 project downgrades the Squad-owned files. Guidance for users: never run an older CLI against an upgraded repository; re-run `squad upgrade` with the 2.0 CLI to repair a downgraded coordinator (team state is not touched by the downgrade)." + }, + { + "id": "F4", + "title": "First `squad upgrade` of a 0.9.x project (empty `.squad/casting/`) does not converge: the coordinator's team-capabilities block is only rendered by the second upgrade", + "severity": "medium", + "layer": "CLI core — `packages/squad-cli/src/cli/core/upgrade.ts` (coordinator write + `syncTeamCapabilities` run before the casting pair is scaffolded)", + "repro": [ + "`npx @bradygaster/squad-cli@0.9.1 init` with `@bradygaster/squad-sdk@0.9.1` pinned (the faithful 0.9.1 layout): `.squad/casting/` exists but contains no `registry.json`/`history.json`.", + "Run the 2.0 CLI `squad upgrade`: it prints `⚠️ Could not refresh squad.agent.md team capabilities: Cannot read a consistent casting registry/history pair: both files are required`, then creates the casting pair later in the same run.", + "Run `squad upgrade` again: `✓ upgraded squad.agent.md` — the coordinator gains the 17-line `` block that run 1 could not render. Nothing else changes." + ], + "observed": "`idempotent-second-upgrade` fails with exactly `.github/agents/squad.agent.md` for 0.9.1 × `default`/`profile`/`ai-team` on both OSes. 0.9.1 × `legacy-casting` is unaffected because the hand-written registry is repaired in the preflight, so the pair exists before the coordinator is written. The project is correct after the second run; the defect is ordering — the capabilities sync should run after the casting pair is ensured (or the pair should be ensured in the preflight).", + "resolved": "Fixed on squad2-c @ 50afa91a (PR #2279): `runUpgrade` runs `preflightCastingPair()` (ensureCastingDefaults) before `writeAgentTemplate` in both the already-current and the normal path, so a 0.9.x project with an empty `.squad/casting/` gets the `` block on the first upgrade and the second upgrade is byte-identical (C test: `test/upgrade-profile-teamroot.test.ts` › \"F4: a 0.9.x project (empty casting/) upgrades in ONE pass\"). Verified by this matrix: with the pin still in place, `idempotent-second-upgrade` produced `xpass` on exactly 0.9.1 × default/profile/ai-team; the pin is removed." + } + ], + "expectedFailures": [ + { + "finding": "F3", + "check": "downgrade-probe", + "producers": [ + "0.9.1" + ], + "shapes": "*", + "platforms": "*" + } + ], + "runs": [ + { + "platform": "linux", + "arch": "x64", + "node": "v22.23.3", + "target": "1.2.0-preview.1", + "targetSha": "9da01a2e41ba15794d31b0b8251ddaa3abd34ba8", + "harnessHash": "bca27c39dc2dcbffc3bf652bf1f45b471c09911a4bb9ba7e5b7247d25705a66e", + "event": "workflow_dispatch", + "runUrl": "https://github.com/bradygaster/squad/actions/runs/39903943542", + "recordedAt": "2026-10-23T05:08:38.415Z", + "results": [ + { + "producer": "0.9.1", + "source": "npm", + "shape": "orphan", + "status": "skipped", + "reason": "`@bradygaster/squad-cli@0.9.1` has no `--state-backend` flag (first shipped in 0.13.x), so an orphan-backend project cannot be produced by it.", + "provenance": "npm: @bradygaster/squad-cli@0.9.1 sha512-TcZqRbJk3XBg4N8Y5/5OWJeA52ARAMuzk4MGtB0bfdr87e7iJRV6lJEw8+OIYz9IQ+1JxN/Q30dZDALMOcdi2w==; @bradygaster/squad-sdk@0.9.1 sha512-1KqZ2N6Lt/B4uYlU/Su9JjA1s/7ng0OAEb9CZrkRk19YSPi6tg7K5QWdTKl5gY/gp8zPAJqfjVffOwrZwGlzmw==", + "checks": [], + "notes": [] + }, + { + "producer": "0.9.1", + "source": "npm", + "shape": "two-layer", + "status": "skipped", + "reason": "`@bradygaster/squad-cli@0.9.1` has no `--state-backend` flag (first shipped in 0.13.x), so a two-layer-backend project cannot be produced by it.", + "provenance": "npm: @bradygaster/squad-cli@0.9.1 sha512-TcZqRbJk3XBg4N8Y5/5OWJeA52ARAMuzk4MGtB0bfdr87e7iJRV6lJEw8+OIYz9IQ+1JxN/Q30dZDALMOcdi2w==; @bradygaster/squad-sdk@0.9.1 sha512-1KqZ2N6Lt/B4uYlU/Su9JjA1s/7ng0OAEb9CZrkRk19YSPi6tg7K5QWdTKl5gY/gp8zPAJqfjVffOwrZwGlzmw==", + "checks": [], + "notes": [] + }, + { + "producer": "0.9.1", + "source": "npm", + "shape": "upstream", + "status": "skipped", + "reason": "Upstream inheritance (`.squad/upstream.json`, `squad upstream add`) is a Squad 2.0 feature; 0.9.1 cannot declare an upstream, so there is nothing for the upgrade to preserve.", + "provenance": "npm: @bradygaster/squad-cli@0.9.1 sha512-TcZqRbJk3XBg4N8Y5/5OWJeA52ARAMuzk4MGtB0bfdr87e7iJRV6lJEw8+OIYz9IQ+1JxN/Q30dZDALMOcdi2w==; @bradygaster/squad-sdk@0.9.1 sha512-1KqZ2N6Lt/B4uYlU/Su9JjA1s/7ng0OAEb9CZrkRk19YSPi6tg7K5QWdTKl5gY/gp8zPAJqfjVffOwrZwGlzmw==", + "checks": [], + "notes": [] + }, + { + "producer": "0.9.1", + "source": "npm", + "shape": "default", + "status": "pass", + "provenance": "npm: @bradygaster/squad-cli@0.9.1 sha512-TcZqRbJk3XBg4N8Y5/5OWJeA52ARAMuzk4MGtB0bfdr87e7iJRV6lJEw8+OIYz9IQ+1JxN/Q30dZDALMOcdi2w==; @bradygaster/squad-sdk@0.9.1 sha512-1KqZ2N6Lt/B4uYlU/Su9JjA1s/7ng0OAEb9CZrkRk19YSPi6tg7K5QWdTKl5gY/gp8zPAJqfjVffOwrZwGlzmw==", + "checks": [ + { + "id": "producer-artifact-verified", + "status": "pass" + }, + { + "id": "producer-init", + "status": "pass" + }, + { + "id": "upgrade-plain", + "status": "pass" + }, + { + "id": "coordinator-is-new-template", + "status": "pass" + }, + { + "id": "user-state-preserved", + "status": "pass" + }, + { + "id": "casting-pair-valid", + "status": "pass" + }, + { + "id": "state-backend-preserved", + "status": "pass" + }, + { + "id": "ai-team-migrated", + "status": "skipped", + "detail": "not an .ai-team shape" + }, + { + "id": "team-root-absent", + "status": "pass" + }, + { + "id": "idempotent-second-upgrade", + "status": "pass" + }, + { + "id": "downgrade-probe", + "status": "xfail", + "detail": "dry-run MODIFIED 45 files (20 coordinator/templates/workflows, e.g. .copilot/skills/agent-collaboration/SKILL.md, .copilot/skills/cross-squad/SKILL.md, .copilot/skills/git-workflow/SKILL.md …)" + }, + { + "id": "clients-projected", + "status": "pass" + }, + { + "id": "user-config-preserved", + "status": "pass" + }, + { + "id": "doctor", + "status": "pass" + }, + { + "id": "projection-uninstall-byte-exact", + "status": "pass" + }, + { + "id": "profile-untouched", + "status": "skipped", + "detail": "no profile squad seeded in this shape" + } + ], + "notes": [ + "plain upgrade changed 100 working-tree path(s), all Squad-owned: .copilot/skills/agent-collaboration/SKILL.md, .copilot/skills/cross-squad/SKILL.md, .copilot/skills/git-workflow/SKILL.md, .copilot/skills/reviewer-protocol/SKILL.md … (+96)", + "old 0.9.1 upgrade --dry-run on upgraded project: exit 0, last lines \"\u001b[32m✓\u001b[0m refreshed .squad/templates/ | Upgrade complete: v1.2.0-preview.1 → v0.9.1 | \u001b[2mPreserves user state: team.md, decisions/, agents/*/history.md\u001b[0m\"; MODIFIED 45 files (20 coordinator/templates/workflows, e.g. .copilot/skills/agent-collaboration/SKILL.md, .copilot/skills/cross-squad/SKILL.md, .copilot/skills/git-workflow/SKILL.md …)", + "downgrade-probe: F3" + ] + }, + { + "producer": "0.9.1", + "source": "npm", + "shape": "profile", + "status": "pass", + "provenance": "npm: @bradygaster/squad-cli@0.9.1 sha512-TcZqRbJk3XBg4N8Y5/5OWJeA52ARAMuzk4MGtB0bfdr87e7iJRV6lJEw8+OIYz9IQ+1JxN/Q30dZDALMOcdi2w==; @bradygaster/squad-sdk@0.9.1 sha512-1KqZ2N6Lt/B4uYlU/Su9JjA1s/7ng0OAEb9CZrkRk19YSPi6tg7K5QWdTKl5gY/gp8zPAJqfjVffOwrZwGlzmw==", + "checks": [ + { + "id": "producer-artifact-verified", + "status": "pass" + }, + { + "id": "producer-init", + "status": "pass" + }, + { + "id": "upgrade-plain", + "status": "pass" + }, + { + "id": "coordinator-is-new-template", + "status": "pass" + }, + { + "id": "user-state-preserved", + "status": "pass" + }, + { + "id": "casting-pair-valid", + "status": "pass" + }, + { + "id": "state-backend-preserved", + "status": "pass" + }, + { + "id": "ai-team-migrated", + "status": "skipped", + "detail": "not an .ai-team shape" + }, + { + "id": "team-root-absent", + "status": "pass" + }, + { + "id": "idempotent-second-upgrade", + "status": "pass" + }, + { + "id": "downgrade-probe", + "status": "xfail", + "detail": "dry-run MODIFIED 45 files (20 coordinator/templates/workflows, e.g. .copilot/skills/agent-collaboration/SKILL.md, .copilot/skills/cross-squad/SKILL.md, .copilot/skills/git-workflow/SKILL.md …)" + }, + { + "id": "clients-projected", + "status": "pass" + }, + { + "id": "user-config-preserved", + "status": "pass" + }, + { + "id": "doctor", + "status": "pass" + }, + { + "id": "projection-uninstall-byte-exact", + "status": "pass" + }, + { + "id": "profile-untouched", + "status": "pass" + } + ], + "notes": [ + "plain upgrade changed 100 working-tree path(s), all Squad-owned: .copilot/skills/agent-collaboration/SKILL.md, .copilot/skills/cross-squad/SKILL.md, .copilot/skills/git-workflow/SKILL.md, .copilot/skills/reviewer-protocol/SKILL.md … (+96)", + "old 0.9.1 upgrade --dry-run on upgraded project: exit 0, last lines \"\u001b[32m✓\u001b[0m refreshed .squad/templates/ | Upgrade complete: v1.2.0-preview.1 → v0.9.1 | \u001b[2mPreserves user state: team.md, decisions/, agents/*/history.md\u001b[0m\"; MODIFIED 45 files (20 coordinator/templates/workflows, e.g. .copilot/skills/agent-collaboration/SKILL.md, .copilot/skills/cross-squad/SKILL.md, .copilot/skills/git-workflow/SKILL.md …)", + "downgrade-probe: F3" + ] + }, + { + "producer": "0.13.1", + "source": "npm", + "shape": "default", + "status": "pass", + "provenance": "npm: @bradygaster/squad-cli@0.13.1 sha512-ogfx2nhyx6fqMyqlaI5tDnOzUbRdh+a6Uh/rMQ0aNJwAXnvQp4N37Xp/eOTyB5CxcB7E34NBtAfCxyXbWSiS8w==; @bradygaster/squad-sdk@0.13.1 sha512-1Jt6c9kahJq1JCcE77j/50D79YcXVG6Rt1genuLJ3O9J2SKTZKemlbvHgftkc5LgGH22DkWqHh5zLNEQ/p+ukA==", + "checks": [ + { + "id": "producer-artifact-verified", + "status": "pass" + }, + { + "id": "producer-init", + "status": "pass" + }, + { + "id": "upgrade-plain", + "status": "pass" + }, + { + "id": "coordinator-is-new-template", + "status": "pass" + }, + { + "id": "user-state-preserved", + "status": "pass" + }, + { + "id": "casting-pair-valid", + "status": "pass" + }, + { + "id": "state-backend-preserved", + "status": "pass" + }, + { + "id": "ai-team-migrated", + "status": "skipped", + "detail": "not an .ai-team shape" + }, + { + "id": "team-root-absent", + "status": "pass" + }, + { + "id": "idempotent-second-upgrade", + "status": "pass" + }, + { + "id": "downgrade-probe", + "status": "pass" + }, + { + "id": "clients-projected", + "status": "pass" + }, + { + "id": "user-config-preserved", + "status": "pass" + }, + { + "id": "doctor", + "status": "pass" + }, + { + "id": "projection-uninstall-byte-exact", + "status": "pass" + }, + { + "id": "profile-untouched", + "status": "skipped", + "detail": "no profile squad seeded in this shape" + } + ], + "notes": [ + "plain upgrade changed 58 working-tree path(s), all Squad-owned: .github/agents/squad.agent.md, .github/agents/squad.agent.md.local-backup, .github/skills/coordinator-response-mode/SKILL.md, .github/skills/coordinator-source-of-truth/SKILL.md … (+54)", + "old 0.13.1 upgrade --dry-run on upgraded project: exit 0, last lines \"overwrite ../.github/workflows/squad-triage.yml | overwrite ../.github/workflows/sync-squad-labels.yml | Run without --dry-run to apply.\"; modified nothing" + ] + }, + { + "producer": "0.9.1", + "source": "npm", + "shape": "legacy-casting", + "status": "pass", + "provenance": "npm: @bradygaster/squad-cli@0.9.1 sha512-TcZqRbJk3XBg4N8Y5/5OWJeA52ARAMuzk4MGtB0bfdr87e7iJRV6lJEw8+OIYz9IQ+1JxN/Q30dZDALMOcdi2w==; @bradygaster/squad-sdk@0.9.1 sha512-1KqZ2N6Lt/B4uYlU/Su9JjA1s/7ng0OAEb9CZrkRk19YSPi6tg7K5QWdTKl5gY/gp8zPAJqfjVffOwrZwGlzmw==", + "checks": [ + { + "id": "producer-artifact-verified", + "status": "pass" + }, + { + "id": "producer-init", + "status": "pass" + }, + { + "id": "upgrade-plain", + "status": "pass" + }, + { + "id": "coordinator-is-new-template", + "status": "pass" + }, + { + "id": "user-state-preserved", + "status": "pass" + }, + { + "id": "casting-pair-valid", + "status": "pass" + }, + { + "id": "state-backend-preserved", + "status": "pass" + }, + { + "id": "ai-team-migrated", + "status": "skipped", + "detail": "not an .ai-team shape" + }, + { + "id": "team-root-absent", + "status": "pass" + }, + { + "id": "idempotent-second-upgrade", + "status": "pass" + }, + { + "id": "downgrade-probe", + "status": "xfail", + "detail": "dry-run MODIFIED 45 files (20 coordinator/templates/workflows, e.g. .copilot/skills/agent-collaboration/SKILL.md, .copilot/skills/cross-squad/SKILL.md, .copilot/skills/git-workflow/SKILL.md …)" + }, + { + "id": "clients-projected", + "status": "pass" + }, + { + "id": "user-config-preserved", + "status": "pass" + }, + { + "id": "doctor", + "status": "pass" + }, + { + "id": "projection-uninstall-byte-exact", + "status": "pass" + }, + { + "id": "profile-untouched", + "status": "skipped", + "detail": "no profile squad seeded in this shape" + } + ], + "notes": [ + "plain upgrade changed 101 working-tree path(s), all Squad-owned: .copilot/skills/agent-collaboration/SKILL.md, .copilot/skills/cross-squad/SKILL.md, .copilot/skills/git-workflow/SKILL.md, .copilot/skills/reviewer-protocol/SKILL.md … (+97)", + "old 0.9.1 upgrade --dry-run on upgraded project: exit 0, last lines \"\u001b[32m✓\u001b[0m refreshed .squad/templates/ | Upgrade complete: v1.2.0-preview.1 → v0.9.1 | \u001b[2mPreserves user state: team.md, decisions/, agents/*/history.md\u001b[0m\"; MODIFIED 45 files (20 coordinator/templates/workflows, e.g. .copilot/skills/agent-collaboration/SKILL.md, .copilot/skills/cross-squad/SKILL.md, .copilot/skills/git-workflow/SKILL.md …)", + "downgrade-probe: F3" + ] + }, + { + "producer": "0.9.1", + "source": "npm", + "shape": "ai-team", + "status": "pass", + "provenance": "npm: @bradygaster/squad-cli@0.9.1 sha512-TcZqRbJk3XBg4N8Y5/5OWJeA52ARAMuzk4MGtB0bfdr87e7iJRV6lJEw8+OIYz9IQ+1JxN/Q30dZDALMOcdi2w==; @bradygaster/squad-sdk@0.9.1 sha512-1KqZ2N6Lt/B4uYlU/Su9JjA1s/7ng0OAEb9CZrkRk19YSPi6tg7K5QWdTKl5gY/gp8zPAJqfjVffOwrZwGlzmw==", + "checks": [ + { + "id": "producer-artifact-verified", + "status": "pass" + }, + { + "id": "producer-init", + "status": "pass" + }, + { + "id": "upgrade-plain", + "status": "pass" + }, + { + "id": "coordinator-is-new-template", + "status": "pass" + }, + { + "id": "user-state-preserved", + "status": "pass" + }, + { + "id": "casting-pair-valid", + "status": "pass" + }, + { + "id": "state-backend-preserved", + "status": "pass" + }, + { + "id": "ai-team-migrated", + "status": "pass" + }, + { + "id": "team-root-absent", + "status": "pass" + }, + { + "id": "idempotent-second-upgrade", + "status": "pass" + }, + { + "id": "downgrade-probe", + "status": "xfail", + "detail": "dry-run MODIFIED 45 files (20 coordinator/templates/workflows, e.g. .copilot/skills/agent-collaboration/SKILL.md, .copilot/skills/cross-squad/SKILL.md, .copilot/skills/git-workflow/SKILL.md …)" + }, + { + "id": "clients-projected", + "status": "pass" + }, + { + "id": "user-config-preserved", + "status": "pass" + }, + { + "id": "doctor", + "status": "pass" + }, + { + "id": "projection-uninstall-byte-exact", + "status": "pass" + }, + { + "id": "profile-untouched", + "status": "skipped", + "detail": "no profile squad seeded in this shape" + } + ], + "notes": [ + "plain upgrade changed 100 working-tree path(s), all Squad-owned: .copilot/skills/agent-collaboration/SKILL.md, .copilot/skills/cross-squad/SKILL.md, .copilot/skills/git-workflow/SKILL.md, .copilot/skills/reviewer-protocol/SKILL.md … (+96)", + "old 0.9.1 upgrade --dry-run on upgraded project: exit 0, last lines \"\u001b[32m✓\u001b[0m refreshed .squad/templates/ | Upgrade complete: v1.2.0-preview.1 → v0.9.1 | \u001b[2mPreserves user state: team.md, decisions/, agents/*/history.md\u001b[0m\"; MODIFIED 45 files (20 coordinator/templates/workflows, e.g. .copilot/skills/agent-collaboration/SKILL.md, .copilot/skills/cross-squad/SKILL.md, .copilot/skills/git-workflow/SKILL.md …)", + "downgrade-probe: F3" + ] + }, + { + "producer": "0.13.1", + "source": "npm", + "shape": "profile", + "status": "pass", + "provenance": "npm: @bradygaster/squad-cli@0.13.1 sha512-ogfx2nhyx6fqMyqlaI5tDnOzUbRdh+a6Uh/rMQ0aNJwAXnvQp4N37Xp/eOTyB5CxcB7E34NBtAfCxyXbWSiS8w==; @bradygaster/squad-sdk@0.13.1 sha512-1Jt6c9kahJq1JCcE77j/50D79YcXVG6Rt1genuLJ3O9J2SKTZKemlbvHgftkc5LgGH22DkWqHh5zLNEQ/p+ukA==", + "checks": [ + { + "id": "producer-artifact-verified", + "status": "pass" + }, + { + "id": "producer-init", + "status": "pass" + }, + { + "id": "upgrade-plain", + "status": "pass" + }, + { + "id": "coordinator-is-new-template", + "status": "pass" + }, + { + "id": "user-state-preserved", + "status": "pass" + }, + { + "id": "casting-pair-valid", + "status": "pass" + }, + { + "id": "state-backend-preserved", + "status": "pass" + }, + { + "id": "ai-team-migrated", + "status": "skipped", + "detail": "not an .ai-team shape" + }, + { + "id": "team-root-absent", + "status": "pass" + }, + { + "id": "idempotent-second-upgrade", + "status": "pass" + }, + { + "id": "downgrade-probe", + "status": "pass" + }, + { + "id": "clients-projected", + "status": "pass" + }, + { + "id": "user-config-preserved", + "status": "pass" + }, + { + "id": "doctor", + "status": "pass" + }, + { + "id": "projection-uninstall-byte-exact", + "status": "pass" + }, + { + "id": "profile-untouched", + "status": "pass" + } + ], + "notes": [ + "producer wrote teamRoot=/squad at init", + "plain upgrade changed 59 working-tree path(s), all Squad-owned: .github/agents/squad.agent.md, .github/agents/squad.agent.md.local-backup, .github/skills/coordinator-response-mode/SKILL.md, .github/skills/coordinator-source-of-truth/SKILL.md … (+55)", + "old 0.13.1 upgrade --dry-run on upgraded project: exit 0, last lines \"overwrite ../.github/workflows/squad-triage.yml | overwrite ../.github/workflows/sync-squad-labels.yml | Run without --dry-run to apply.\"; modified nothing" + ] + }, + { + "producer": "0.13.1", + "source": "npm", + "shape": "upstream", + "status": "skipped", + "reason": "Upstream inheritance (`.squad/upstream.json`, `squad upstream add`) is a Squad 2.0 feature; 0.13.1 cannot declare an upstream, so there is nothing for the upgrade to preserve.", + "provenance": "npm: @bradygaster/squad-cli@0.13.1 sha512-ogfx2nhyx6fqMyqlaI5tDnOzUbRdh+a6Uh/rMQ0aNJwAXnvQp4N37Xp/eOTyB5CxcB7E34NBtAfCxyXbWSiS8w==; @bradygaster/squad-sdk@0.13.1 sha512-1Jt6c9kahJq1JCcE77j/50D79YcXVG6Rt1genuLJ3O9J2SKTZKemlbvHgftkc5LgGH22DkWqHh5zLNEQ/p+ukA==", + "checks": [], + "notes": [] + }, + { + "producer": "0.13.1", + "source": "npm", + "shape": "legacy-casting", + "status": "pass", + "provenance": "npm: @bradygaster/squad-cli@0.13.1 sha512-ogfx2nhyx6fqMyqlaI5tDnOzUbRdh+a6Uh/rMQ0aNJwAXnvQp4N37Xp/eOTyB5CxcB7E34NBtAfCxyXbWSiS8w==; @bradygaster/squad-sdk@0.13.1 sha512-1Jt6c9kahJq1JCcE77j/50D79YcXVG6Rt1genuLJ3O9J2SKTZKemlbvHgftkc5LgGH22DkWqHh5zLNEQ/p+ukA==", + "checks": [ + { + "id": "producer-artifact-verified", + "status": "pass" + }, + { + "id": "producer-init", + "status": "pass" + }, + { + "id": "upgrade-plain", + "status": "pass" + }, + { + "id": "coordinator-is-new-template", + "status": "pass" + }, + { + "id": "user-state-preserved", + "status": "pass" + }, + { + "id": "casting-pair-valid", + "status": "pass" + }, + { + "id": "state-backend-preserved", + "status": "pass" + }, + { + "id": "ai-team-migrated", + "status": "skipped", + "detail": "not an .ai-team shape" + }, + { + "id": "team-root-absent", + "status": "pass" + }, + { + "id": "idempotent-second-upgrade", + "status": "pass" + }, + { + "id": "downgrade-probe", + "status": "pass" + }, + { + "id": "clients-projected", + "status": "pass" + }, + { + "id": "user-config-preserved", + "status": "pass" + }, + { + "id": "doctor", + "status": "pass" + }, + { + "id": "projection-uninstall-byte-exact", + "status": "pass" + }, + { + "id": "profile-untouched", + "status": "skipped", + "detail": "no profile squad seeded in this shape" + } + ], + "notes": [ + "plain upgrade changed 58 working-tree path(s), all Squad-owned: .github/agents/squad.agent.md, .github/agents/squad.agent.md.local-backup, .github/skills/coordinator-response-mode/SKILL.md, .github/skills/coordinator-source-of-truth/SKILL.md … (+54)", + "old 0.13.1 upgrade --dry-run on upgraded project: exit 0, last lines \"overwrite ../.github/workflows/squad-triage.yml | overwrite ../.github/workflows/sync-squad-labels.yml | Run without --dry-run to apply.\"; modified nothing" + ] + }, + { + "producer": "0.13.1", + "source": "npm", + "shape": "ai-team", + "status": "pass", + "provenance": "npm: @bradygaster/squad-cli@0.13.1 sha512-ogfx2nhyx6fqMyqlaI5tDnOzUbRdh+a6Uh/rMQ0aNJwAXnvQp4N37Xp/eOTyB5CxcB7E34NBtAfCxyXbWSiS8w==; @bradygaster/squad-sdk@0.13.1 sha512-1Jt6c9kahJq1JCcE77j/50D79YcXVG6Rt1genuLJ3O9J2SKTZKemlbvHgftkc5LgGH22DkWqHh5zLNEQ/p+ukA==", + "checks": [ + { + "id": "producer-artifact-verified", + "status": "pass" + }, + { + "id": "producer-init", + "status": "pass" + }, + { + "id": "upgrade-plain", + "status": "pass" + }, + { + "id": "coordinator-is-new-template", + "status": "pass" + }, + { + "id": "user-state-preserved", + "status": "pass" + }, + { + "id": "casting-pair-valid", + "status": "pass" + }, + { + "id": "state-backend-preserved", + "status": "pass" + }, + { + "id": "ai-team-migrated", + "status": "pass" + }, + { + "id": "team-root-absent", + "status": "pass" + }, + { + "id": "idempotent-second-upgrade", + "status": "pass" + }, + { + "id": "downgrade-probe", + "status": "pass" + }, + { + "id": "clients-projected", + "status": "pass" + }, + { + "id": "user-config-preserved", + "status": "pass" + }, + { + "id": "doctor", + "status": "pass" + }, + { + "id": "projection-uninstall-byte-exact", + "status": "pass" + }, + { + "id": "profile-untouched", + "status": "skipped", + "detail": "no profile squad seeded in this shape" + } + ], + "notes": [ + "plain upgrade changed 58 working-tree path(s), all Squad-owned: .github/agents/squad.agent.md, .github/agents/squad.agent.md.local-backup, .github/skills/coordinator-response-mode/SKILL.md, .github/skills/coordinator-source-of-truth/SKILL.md … (+54)", + "old 0.13.1 upgrade --dry-run on upgraded project: exit 0, last lines \"overwrite ../.github/workflows/squad-triage.yml | overwrite ../.github/workflows/sync-squad-labels.yml | Run without --dry-run to apply.\"; modified nothing" + ] + }, + { + "producer": "0.13.1", + "source": "npm", + "shape": "orphan", + "status": "pass", + "provenance": "npm: @bradygaster/squad-cli@0.13.1 sha512-ogfx2nhyx6fqMyqlaI5tDnOzUbRdh+a6Uh/rMQ0aNJwAXnvQp4N37Xp/eOTyB5CxcB7E34NBtAfCxyXbWSiS8w==; @bradygaster/squad-sdk@0.13.1 sha512-1Jt6c9kahJq1JCcE77j/50D79YcXVG6Rt1genuLJ3O9J2SKTZKemlbvHgftkc5LgGH22DkWqHh5zLNEQ/p+ukA==", + "checks": [ + { + "id": "producer-artifact-verified", + "status": "pass" + }, + { + "id": "producer-init", + "status": "pass" + }, + { + "id": "upgrade-plain", + "status": "pass" + }, + { + "id": "coordinator-is-new-template", + "status": "pass" + }, + { + "id": "user-state-preserved", + "status": "pass" + }, + { + "id": "casting-pair-valid", + "status": "pass" + }, + { + "id": "state-backend-preserved", + "status": "pass" + }, + { + "id": "ai-team-migrated", + "status": "skipped", + "detail": "not an .ai-team shape" + }, + { + "id": "team-root-absent", + "status": "pass" + }, + { + "id": "idempotent-second-upgrade", + "status": "pass" + }, + { + "id": "downgrade-probe", + "status": "pass" + }, + { + "id": "clients-projected", + "status": "pass" + }, + { + "id": "user-config-preserved", + "status": "pass" + }, + { + "id": "doctor", + "status": "pass" + }, + { + "id": "projection-uninstall-byte-exact", + "status": "pass" + }, + { + "id": "profile-untouched", + "status": "skipped", + "detail": "no profile squad seeded in this shape" + } + ], + "notes": [ + "plain upgrade changed 56 working-tree path(s), all Squad-owned: .github/agents/squad.agent.md, .github/agents/squad.agent.md.local-backup, .github/skills/coordinator-response-mode/SKILL.md, .github/skills/coordinator-source-of-truth/SKILL.md … (+52); squad-state blobs for 3 seeded path(s) identical", + "old 0.13.1 upgrade --dry-run on upgraded project: exit 0, last lines \"overwrite ../.github/workflows/squad-triage.yml | overwrite ../.github/workflows/sync-squad-labels.yml | Run without --dry-run to apply.\"; modified nothing" + ] + }, + { + "producer": "0.13.1", + "source": "npm", + "shape": "two-layer", + "status": "pass", + "provenance": "npm: @bradygaster/squad-cli@0.13.1 sha512-ogfx2nhyx6fqMyqlaI5tDnOzUbRdh+a6Uh/rMQ0aNJwAXnvQp4N37Xp/eOTyB5CxcB7E34NBtAfCxyXbWSiS8w==; @bradygaster/squad-sdk@0.13.1 sha512-1Jt6c9kahJq1JCcE77j/50D79YcXVG6Rt1genuLJ3O9J2SKTZKemlbvHgftkc5LgGH22DkWqHh5zLNEQ/p+ukA==", + "checks": [ + { + "id": "producer-artifact-verified", + "status": "pass" + }, + { + "id": "producer-init", + "status": "pass" + }, + { + "id": "upgrade-plain", + "status": "pass" + }, + { + "id": "coordinator-is-new-template", + "status": "pass" + }, + { + "id": "user-state-preserved", + "status": "pass" + }, + { + "id": "casting-pair-valid", + "status": "pass" + }, + { + "id": "state-backend-preserved", + "status": "pass" + }, + { + "id": "ai-team-migrated", + "status": "skipped", + "detail": "not an .ai-team shape" + }, + { + "id": "team-root-absent", + "status": "pass" + }, + { + "id": "idempotent-second-upgrade", + "status": "pass" + }, + { + "id": "downgrade-probe", + "status": "pass" + }, + { + "id": "clients-projected", + "status": "pass" + }, + { + "id": "user-config-preserved", + "status": "pass" + }, + { + "id": "doctor", + "status": "pass" + }, + { + "id": "projection-uninstall-byte-exact", + "status": "pass" + }, + { + "id": "profile-untouched", + "status": "skipped", + "detail": "no profile squad seeded in this shape" + } + ], + "notes": [ + "plain upgrade changed 56 working-tree path(s), all Squad-owned: .github/agents/squad.agent.md, .github/agents/squad.agent.md.local-backup, .github/skills/coordinator-response-mode/SKILL.md, .github/skills/coordinator-source-of-truth/SKILL.md … (+52); squad-state blobs for 3 seeded path(s) identical", + "old 0.13.1 upgrade --dry-run on upgraded project: exit 0, last lines \"overwrite ../.github/workflows/squad-triage.yml | overwrite ../.github/workflows/sync-squad-labels.yml | Run without --dry-run to apply.\"; modified nothing" + ] + }, + { + "producer": "1.0.0", + "source": "npm", + "shape": "default", + "status": "pass", + "provenance": "npm: @bradygaster/squad-cli@1.0.0 sha512-mnZXnA+GAtp4KSLf7a4Zw0BpeOaHFAxYV5oxAy58ancZ9C2LguUGbcTxWIG0djM96XWET8K9fcncif3Vm6Z9RQ==; @bradygaster/squad-sdk@1.0.0 sha512-9f4NthJaQDq6ifXU3f6VfsZz2bbWs2u57fHhXKtDgDMSvZ3Xq8FPjMvddxhRvseWqaSlkYcWsag3M/lzTtKaJA==", + "checks": [ + { + "id": "producer-artifact-verified", + "status": "pass" + }, + { + "id": "producer-init", + "status": "pass" + }, + { + "id": "upgrade-plain", + "status": "pass" + }, + { + "id": "coordinator-is-new-template", + "status": "pass" + }, + { + "id": "user-state-preserved", + "status": "pass" + }, + { + "id": "casting-pair-valid", + "status": "pass" + }, + { + "id": "state-backend-preserved", + "status": "pass" + }, + { + "id": "ai-team-migrated", + "status": "skipped", + "detail": "not an .ai-team shape" + }, + { + "id": "team-root-absent", + "status": "pass" + }, + { + "id": "idempotent-second-upgrade", + "status": "pass" + }, + { + "id": "downgrade-probe", + "status": "pass" + }, + { + "id": "clients-projected", + "status": "pass" + }, + { + "id": "user-config-preserved", + "status": "pass" + }, + { + "id": "doctor", + "status": "pass" + }, + { + "id": "projection-uninstall-byte-exact", + "status": "pass" + }, + { + "id": "profile-untouched", + "status": "skipped", + "detail": "no profile squad seeded in this shape" + } + ], + "notes": [ + "plain upgrade changed 37 working-tree path(s), all Squad-owned: .github/agents/squad.agent.md, .github/agents/squad.agent.md.local-backup, .github/skills/coordinator-source-of-truth/SKILL.md, .github/workflows/squad-ci.yml … (+33)", + "old 1.0.0 upgrade --dry-run on upgraded project: exit 0, last lines \"overwrite ../.github/workflows/squad-triage.yml | overwrite ../.github/workflows/sync-squad-labels.yml | Run without --dry-run to apply.\"; modified nothing" + ] + }, + { + "producer": "1.0.0", + "source": "npm", + "shape": "profile", + "status": "pass", + "provenance": "npm: @bradygaster/squad-cli@1.0.0 sha512-mnZXnA+GAtp4KSLf7a4Zw0BpeOaHFAxYV5oxAy58ancZ9C2LguUGbcTxWIG0djM96XWET8K9fcncif3Vm6Z9RQ==; @bradygaster/squad-sdk@1.0.0 sha512-9f4NthJaQDq6ifXU3f6VfsZz2bbWs2u57fHhXKtDgDMSvZ3Xq8FPjMvddxhRvseWqaSlkYcWsag3M/lzTtKaJA==", + "checks": [ + { + "id": "producer-artifact-verified", + "status": "pass" + }, + { + "id": "producer-init", + "status": "pass" + }, + { + "id": "upgrade-plain", + "status": "pass" + }, + { + "id": "coordinator-is-new-template", + "status": "pass" + }, + { + "id": "user-state-preserved", + "status": "pass" + }, + { + "id": "casting-pair-valid", + "status": "pass" + }, + { + "id": "state-backend-preserved", + "status": "pass" + }, + { + "id": "ai-team-migrated", + "status": "skipped", + "detail": "not an .ai-team shape" + }, + { + "id": "team-root-absent", + "status": "pass" + }, + { + "id": "idempotent-second-upgrade", + "status": "pass" + }, + { + "id": "downgrade-probe", + "status": "pass" + }, + { + "id": "clients-projected", + "status": "pass" + }, + { + "id": "user-config-preserved", + "status": "pass" + }, + { + "id": "doctor", + "status": "pass" + }, + { + "id": "projection-uninstall-byte-exact", + "status": "pass" + }, + { + "id": "profile-untouched", + "status": "pass" + } + ], + "notes": [ + "producer wrote teamRoot=/squad at init", + "plain upgrade changed 38 working-tree path(s), all Squad-owned: .github/agents/squad.agent.md, .github/agents/squad.agent.md.local-backup, .github/skills/coordinator-source-of-truth/SKILL.md, .github/workflows/squad-ci.yml … (+34)", + "old 1.0.0 upgrade --dry-run on upgraded project: exit 0, last lines \"overwrite ../.github/workflows/squad-triage.yml | overwrite ../.github/workflows/sync-squad-labels.yml | Run without --dry-run to apply.\"; modified nothing" + ] + }, + { + "producer": "1.0.0", + "source": "npm", + "shape": "upstream", + "status": "skipped", + "reason": "Upstream inheritance (`.squad/upstream.json`, `squad upstream add`) is a Squad 2.0 feature; 1.0.0 cannot declare an upstream, so there is nothing for the upgrade to preserve.", + "provenance": "npm: @bradygaster/squad-cli@1.0.0 sha512-mnZXnA+GAtp4KSLf7a4Zw0BpeOaHFAxYV5oxAy58ancZ9C2LguUGbcTxWIG0djM96XWET8K9fcncif3Vm6Z9RQ==; @bradygaster/squad-sdk@1.0.0 sha512-9f4NthJaQDq6ifXU3f6VfsZz2bbWs2u57fHhXKtDgDMSvZ3Xq8FPjMvddxhRvseWqaSlkYcWsag3M/lzTtKaJA==", + "checks": [], + "notes": [] + }, + { + "producer": "1.0.0", + "source": "npm", + "shape": "legacy-casting", + "status": "pass", + "provenance": "npm: @bradygaster/squad-cli@1.0.0 sha512-mnZXnA+GAtp4KSLf7a4Zw0BpeOaHFAxYV5oxAy58ancZ9C2LguUGbcTxWIG0djM96XWET8K9fcncif3Vm6Z9RQ==; @bradygaster/squad-sdk@1.0.0 sha512-9f4NthJaQDq6ifXU3f6VfsZz2bbWs2u57fHhXKtDgDMSvZ3Xq8FPjMvddxhRvseWqaSlkYcWsag3M/lzTtKaJA==", + "checks": [ + { + "id": "producer-artifact-verified", + "status": "pass" + }, + { + "id": "producer-init", + "status": "pass" + }, + { + "id": "upgrade-plain", + "status": "pass" + }, + { + "id": "coordinator-is-new-template", + "status": "pass" + }, + { + "id": "user-state-preserved", + "status": "pass" + }, + { + "id": "casting-pair-valid", + "status": "pass" + }, + { + "id": "state-backend-preserved", + "status": "pass" + }, + { + "id": "ai-team-migrated", + "status": "skipped", + "detail": "not an .ai-team shape" + }, + { + "id": "team-root-absent", + "status": "pass" + }, + { + "id": "idempotent-second-upgrade", + "status": "pass" + }, + { + "id": "downgrade-probe", + "status": "pass" + }, + { + "id": "clients-projected", + "status": "pass" + }, + { + "id": "user-config-preserved", + "status": "pass" + }, + { + "id": "doctor", + "status": "pass" + }, + { + "id": "projection-uninstall-byte-exact", + "status": "pass" + }, + { + "id": "profile-untouched", + "status": "skipped", + "detail": "no profile squad seeded in this shape" + } + ], + "notes": [ + "plain upgrade changed 42 working-tree path(s), all Squad-owned: .github/agents/squad.agent.md, .github/agents/squad.agent.md.local-backup, .github/skills/coordinator-source-of-truth/SKILL.md, .github/workflows/squad-ci.yml … (+38)", + "old 1.0.0 upgrade --dry-run on upgraded project: exit 0, last lines \"overwrite ../.github/workflows/squad-triage.yml | overwrite ../.github/workflows/sync-squad-labels.yml | Run without --dry-run to apply.\"; modified nothing" + ] + }, + { + "producer": "1.0.0", + "source": "npm", + "shape": "orphan", + "status": "pass", + "provenance": "npm: @bradygaster/squad-cli@1.0.0 sha512-mnZXnA+GAtp4KSLf7a4Zw0BpeOaHFAxYV5oxAy58ancZ9C2LguUGbcTxWIG0djM96XWET8K9fcncif3Vm6Z9RQ==; @bradygaster/squad-sdk@1.0.0 sha512-9f4NthJaQDq6ifXU3f6VfsZz2bbWs2u57fHhXKtDgDMSvZ3Xq8FPjMvddxhRvseWqaSlkYcWsag3M/lzTtKaJA==", + "checks": [ + { + "id": "producer-artifact-verified", + "status": "pass" + }, + { + "id": "producer-init", + "status": "pass" + }, + { + "id": "upgrade-plain", + "status": "pass" + }, + { + "id": "coordinator-is-new-template", + "status": "pass" + }, + { + "id": "user-state-preserved", + "status": "pass" + }, + { + "id": "casting-pair-valid", + "status": "pass" + }, + { + "id": "state-backend-preserved", + "status": "pass" + }, + { + "id": "ai-team-migrated", + "status": "skipped", + "detail": "not an .ai-team shape" + }, + { + "id": "team-root-absent", + "status": "pass" + }, + { + "id": "idempotent-second-upgrade", + "status": "pass" + }, + { + "id": "downgrade-probe", + "status": "pass" + }, + { + "id": "clients-projected", + "status": "pass" + }, + { + "id": "user-config-preserved", + "status": "pass" + }, + { + "id": "doctor", + "status": "pass" + }, + { + "id": "projection-uninstall-byte-exact", + "status": "pass" + }, + { + "id": "profile-untouched", + "status": "skipped", + "detail": "no profile squad seeded in this shape" + } + ], + "notes": [ + "plain upgrade changed 35 working-tree path(s), all Squad-owned: .github/agents/squad.agent.md, .github/agents/squad.agent.md.local-backup, .github/skills/coordinator-source-of-truth/SKILL.md, .github/workflows/squad-ci.yml … (+31); squad-state blobs for 3 seeded path(s) identical", + "old 1.0.0 upgrade --dry-run on upgraded project: exit 0, last lines \"overwrite ../.github/workflows/squad-triage.yml | overwrite ../.github/workflows/sync-squad-labels.yml | Run without --dry-run to apply.\"; modified nothing" + ] + }, + { + "producer": "1.0.0", + "source": "npm", + "shape": "two-layer", + "status": "pass", + "provenance": "npm: @bradygaster/squad-cli@1.0.0 sha512-mnZXnA+GAtp4KSLf7a4Zw0BpeOaHFAxYV5oxAy58ancZ9C2LguUGbcTxWIG0djM96XWET8K9fcncif3Vm6Z9RQ==; @bradygaster/squad-sdk@1.0.0 sha512-9f4NthJaQDq6ifXU3f6VfsZz2bbWs2u57fHhXKtDgDMSvZ3Xq8FPjMvddxhRvseWqaSlkYcWsag3M/lzTtKaJA==", + "checks": [ + { + "id": "producer-artifact-verified", + "status": "pass" + }, + { + "id": "producer-init", + "status": "pass" + }, + { + "id": "upgrade-plain", + "status": "pass" + }, + { + "id": "coordinator-is-new-template", + "status": "pass" + }, + { + "id": "user-state-preserved", + "status": "pass" + }, + { + "id": "casting-pair-valid", + "status": "pass" + }, + { + "id": "state-backend-preserved", + "status": "pass" + }, + { + "id": "ai-team-migrated", + "status": "skipped", + "detail": "not an .ai-team shape" + }, + { + "id": "team-root-absent", + "status": "pass" + }, + { + "id": "idempotent-second-upgrade", + "status": "pass" + }, + { + "id": "downgrade-probe", + "status": "pass" + }, + { + "id": "clients-projected", + "status": "pass" + }, + { + "id": "user-config-preserved", + "status": "pass" + }, + { + "id": "doctor", + "status": "pass" + }, + { + "id": "projection-uninstall-byte-exact", + "status": "pass" + }, + { + "id": "profile-untouched", + "status": "skipped", + "detail": "no profile squad seeded in this shape" + } + ], + "notes": [ + "plain upgrade changed 35 working-tree path(s), all Squad-owned: .github/agents/squad.agent.md, .github/agents/squad.agent.md.local-backup, .github/skills/coordinator-source-of-truth/SKILL.md, .github/workflows/squad-ci.yml … (+31); squad-state blobs for 3 seeded path(s) identical", + "old 1.0.0 upgrade --dry-run on upgraded project: exit 0, last lines \"overwrite ../.github/workflows/squad-triage.yml | overwrite ../.github/workflows/sync-squad-labels.yml | Run without --dry-run to apply.\"; modified nothing" + ] + }, + { + "producer": "1.0.1", + "source": "github-release", + "shape": "default", + "status": "pass", + "provenance": "github-release: bradygaster/squad@v1.0.1 asset squad-linux-x64.tar.gz id 610330551 sha256 283a1893be9ddad11056dcc8bd0673eff6b48a789bbf523b3f7ec31994d27ac7 (bundled node 22.20.0)", + "checks": [ + { + "id": "producer-artifact-verified", + "status": "pass" + }, + { + "id": "producer-init", + "status": "pass" + }, + { + "id": "upgrade-plain", + "status": "pass" + }, + { + "id": "coordinator-is-new-template", + "status": "pass" + }, + { + "id": "user-state-preserved", + "status": "pass" + }, + { + "id": "casting-pair-valid", + "status": "pass" + }, + { + "id": "state-backend-preserved", + "status": "pass" + }, + { + "id": "ai-team-migrated", + "status": "skipped", + "detail": "not an .ai-team shape" + }, + { + "id": "team-root-absent", + "status": "pass" + }, + { + "id": "idempotent-second-upgrade", + "status": "pass" + }, + { + "id": "downgrade-probe", + "status": "pass" + }, + { + "id": "clients-projected", + "status": "pass" + }, + { + "id": "user-config-preserved", + "status": "pass" + }, + { + "id": "doctor", + "status": "pass" + }, + { + "id": "projection-uninstall-byte-exact", + "status": "pass" + }, + { + "id": "profile-untouched", + "status": "skipped", + "detail": "no profile squad seeded in this shape" + } + ], + "notes": [ + "plain upgrade changed 37 working-tree path(s), all Squad-owned: .github/agents/squad.agent.md, .github/agents/squad.agent.md.local-backup, .github/skills/coordinator-source-of-truth/SKILL.md, .github/workflows/squad-ci.yml … (+33)", + "old 1.0.1 upgrade --dry-run on upgraded project: exit 0, last lines \"overwrite ../.github/workflows/squad-triage.yml | overwrite ../.github/workflows/sync-squad-labels.yml | Run without --dry-run to apply.\"; modified nothing" + ] + }, + { + "producer": "1.0.0", + "source": "npm", + "shape": "ai-team", + "status": "pass", + "provenance": "npm: @bradygaster/squad-cli@1.0.0 sha512-mnZXnA+GAtp4KSLf7a4Zw0BpeOaHFAxYV5oxAy58ancZ9C2LguUGbcTxWIG0djM96XWET8K9fcncif3Vm6Z9RQ==; @bradygaster/squad-sdk@1.0.0 sha512-9f4NthJaQDq6ifXU3f6VfsZz2bbWs2u57fHhXKtDgDMSvZ3Xq8FPjMvddxhRvseWqaSlkYcWsag3M/lzTtKaJA==", + "checks": [ + { + "id": "producer-artifact-verified", + "status": "pass" + }, + { + "id": "producer-init", + "status": "pass" + }, + { + "id": "upgrade-plain", + "status": "pass" + }, + { + "id": "coordinator-is-new-template", + "status": "pass" + }, + { + "id": "user-state-preserved", + "status": "pass" + }, + { + "id": "casting-pair-valid", + "status": "pass" + }, + { + "id": "state-backend-preserved", + "status": "pass" + }, + { + "id": "ai-team-migrated", + "status": "pass" + }, + { + "id": "team-root-absent", + "status": "pass" + }, + { + "id": "idempotent-second-upgrade", + "status": "pass" + }, + { + "id": "downgrade-probe", + "status": "pass" + }, + { + "id": "clients-projected", + "status": "pass" + }, + { + "id": "user-config-preserved", + "status": "pass" + }, + { + "id": "doctor", + "status": "pass" + }, + { + "id": "projection-uninstall-byte-exact", + "status": "pass" + }, + { + "id": "profile-untouched", + "status": "skipped", + "detail": "no profile squad seeded in this shape" + } + ], + "notes": [ + "plain upgrade changed 37 working-tree path(s), all Squad-owned: .github/agents/squad.agent.md, .github/agents/squad.agent.md.local-backup, .github/skills/coordinator-source-of-truth/SKILL.md, .github/workflows/squad-ci.yml … (+33)", + "old 1.0.0 upgrade --dry-run on upgraded project: exit 0, last lines \"overwrite ../.github/workflows/squad-triage.yml | overwrite ../.github/workflows/sync-squad-labels.yml | Run without --dry-run to apply.\"; modified nothing" + ] + }, + { + "producer": "1.0.1", + "source": "github-release", + "shape": "profile", + "status": "pass", + "provenance": "github-release: bradygaster/squad@v1.0.1 asset squad-linux-x64.tar.gz id 610330551 sha256 283a1893be9ddad11056dcc8bd0673eff6b48a789bbf523b3f7ec31994d27ac7 (bundled node 22.20.0)", + "checks": [ + { + "id": "producer-artifact-verified", + "status": "pass" + }, + { + "id": "producer-init", + "status": "pass" + }, + { + "id": "upgrade-plain", + "status": "pass" + }, + { + "id": "coordinator-is-new-template", + "status": "pass" + }, + { + "id": "user-state-preserved", + "status": "pass" + }, + { + "id": "casting-pair-valid", + "status": "pass" + }, + { + "id": "state-backend-preserved", + "status": "pass" + }, + { + "id": "ai-team-migrated", + "status": "skipped", + "detail": "not an .ai-team shape" + }, + { + "id": "team-root-absent", + "status": "pass" + }, + { + "id": "idempotent-second-upgrade", + "status": "pass" + }, + { + "id": "downgrade-probe", + "status": "pass" + }, + { + "id": "clients-projected", + "status": "pass" + }, + { + "id": "user-config-preserved", + "status": "pass" + }, + { + "id": "doctor", + "status": "pass" + }, + { + "id": "projection-uninstall-byte-exact", + "status": "pass" + }, + { + "id": "profile-untouched", + "status": "pass" + } + ], + "notes": [ + "producer wrote teamRoot=/squad at init", + "plain upgrade changed 38 working-tree path(s), all Squad-owned: .github/agents/squad.agent.md, .github/agents/squad.agent.md.local-backup, .github/skills/coordinator-source-of-truth/SKILL.md, .github/workflows/squad-ci.yml … (+34)", + "old 1.0.1 upgrade --dry-run on upgraded project: exit 0, last lines \"overwrite ../.github/workflows/squad-triage.yml | overwrite ../.github/workflows/sync-squad-labels.yml | Run without --dry-run to apply.\"; modified nothing" + ] + }, + { + "producer": "1.0.1", + "source": "github-release", + "shape": "upstream", + "status": "skipped", + "reason": "Upstream inheritance (`.squad/upstream.json`, `squad upstream add`) is a Squad 2.0 feature; 1.0.1 cannot declare an upstream, so there is nothing for the upgrade to preserve.", + "provenance": "github-release: bradygaster/squad@v1.0.1 asset squad-linux-x64.tar.gz id 610330551 sha256 283a1893be9ddad11056dcc8bd0673eff6b48a789bbf523b3f7ec31994d27ac7 (bundled node 22.20.0)", + "checks": [], + "notes": [] + }, + { + "producer": "1.0.1", + "source": "github-release", + "shape": "legacy-casting", + "status": "pass", + "provenance": "github-release: bradygaster/squad@v1.0.1 asset squad-linux-x64.tar.gz id 610330551 sha256 283a1893be9ddad11056dcc8bd0673eff6b48a789bbf523b3f7ec31994d27ac7 (bundled node 22.20.0)", + "checks": [ + { + "id": "producer-artifact-verified", + "status": "pass" + }, + { + "id": "producer-init", + "status": "pass" + }, + { + "id": "upgrade-plain", + "status": "pass" + }, + { + "id": "coordinator-is-new-template", + "status": "pass" + }, + { + "id": "user-state-preserved", + "status": "pass" + }, + { + "id": "casting-pair-valid", + "status": "pass" + }, + { + "id": "state-backend-preserved", + "status": "pass" + }, + { + "id": "ai-team-migrated", + "status": "skipped", + "detail": "not an .ai-team shape" + }, + { + "id": "team-root-absent", + "status": "pass" + }, + { + "id": "idempotent-second-upgrade", + "status": "pass" + }, + { + "id": "downgrade-probe", + "status": "pass" + }, + { + "id": "clients-projected", + "status": "pass" + }, + { + "id": "user-config-preserved", + "status": "pass" + }, + { + "id": "doctor", + "status": "pass" + }, + { + "id": "projection-uninstall-byte-exact", + "status": "pass" + }, + { + "id": "profile-untouched", + "status": "skipped", + "detail": "no profile squad seeded in this shape" + } + ], + "notes": [ + "plain upgrade changed 42 working-tree path(s), all Squad-owned: .github/agents/squad.agent.md, .github/agents/squad.agent.md.local-backup, .github/skills/coordinator-source-of-truth/SKILL.md, .github/workflows/squad-ci.yml … (+38)", + "old 1.0.1 upgrade --dry-run on upgraded project: exit 0, last lines \"overwrite ../.github/workflows/squad-triage.yml | overwrite ../.github/workflows/sync-squad-labels.yml | Run without --dry-run to apply.\"; modified nothing" + ] + }, + { + "producer": "1.0.1", + "source": "github-release", + "shape": "orphan", + "status": "pass", + "provenance": "github-release: bradygaster/squad@v1.0.1 asset squad-linux-x64.tar.gz id 610330551 sha256 283a1893be9ddad11056dcc8bd0673eff6b48a789bbf523b3f7ec31994d27ac7 (bundled node 22.20.0)", + "checks": [ + { + "id": "producer-artifact-verified", + "status": "pass" + }, + { + "id": "producer-init", + "status": "pass" + }, + { + "id": "upgrade-plain", + "status": "pass" + }, + { + "id": "coordinator-is-new-template", + "status": "pass" + }, + { + "id": "user-state-preserved", + "status": "pass" + }, + { + "id": "casting-pair-valid", + "status": "pass" + }, + { + "id": "state-backend-preserved", + "status": "pass" + }, + { + "id": "ai-team-migrated", + "status": "skipped", + "detail": "not an .ai-team shape" + }, + { + "id": "team-root-absent", + "status": "pass" + }, + { + "id": "idempotent-second-upgrade", + "status": "pass" + }, + { + "id": "downgrade-probe", + "status": "pass" + }, + { + "id": "clients-projected", + "status": "pass" + }, + { + "id": "user-config-preserved", + "status": "pass" + }, + { + "id": "doctor", + "status": "pass" + }, + { + "id": "projection-uninstall-byte-exact", + "status": "pass" + }, + { + "id": "profile-untouched", + "status": "skipped", + "detail": "no profile squad seeded in this shape" + } + ], + "notes": [ + "plain upgrade changed 35 working-tree path(s), all Squad-owned: .github/agents/squad.agent.md, .github/agents/squad.agent.md.local-backup, .github/skills/coordinator-source-of-truth/SKILL.md, .github/workflows/squad-ci.yml … (+31); squad-state blobs for 3 seeded path(s) identical", + "old 1.0.1 upgrade --dry-run on upgraded project: exit 0, last lines \"overwrite ../.github/workflows/squad-triage.yml | overwrite ../.github/workflows/sync-squad-labels.yml | Run without --dry-run to apply.\"; modified nothing" + ] + }, + { + "producer": "1.0.1", + "source": "github-release", + "shape": "two-layer", + "status": "pass", + "provenance": "github-release: bradygaster/squad@v1.0.1 asset squad-linux-x64.tar.gz id 610330551 sha256 283a1893be9ddad11056dcc8bd0673eff6b48a789bbf523b3f7ec31994d27ac7 (bundled node 22.20.0)", + "checks": [ + { + "id": "producer-artifact-verified", + "status": "pass" + }, + { + "id": "producer-init", + "status": "pass" + }, + { + "id": "upgrade-plain", + "status": "pass" + }, + { + "id": "coordinator-is-new-template", + "status": "pass" + }, + { + "id": "user-state-preserved", + "status": "pass" + }, + { + "id": "casting-pair-valid", + "status": "pass" + }, + { + "id": "state-backend-preserved", + "status": "pass" + }, + { + "id": "ai-team-migrated", + "status": "skipped", + "detail": "not an .ai-team shape" + }, + { + "id": "team-root-absent", + "status": "pass" + }, + { + "id": "idempotent-second-upgrade", + "status": "pass" + }, + { + "id": "downgrade-probe", + "status": "pass" + }, + { + "id": "clients-projected", + "status": "pass" + }, + { + "id": "user-config-preserved", + "status": "pass" + }, + { + "id": "doctor", + "status": "pass" + }, + { + "id": "projection-uninstall-byte-exact", + "status": "pass" + }, + { + "id": "profile-untouched", + "status": "skipped", + "detail": "no profile squad seeded in this shape" + } + ], + "notes": [ + "plain upgrade changed 35 working-tree path(s), all Squad-owned: .github/agents/squad.agent.md, .github/agents/squad.agent.md.local-backup, .github/skills/coordinator-source-of-truth/SKILL.md, .github/workflows/squad-ci.yml … (+31); squad-state blobs for 3 seeded path(s) identical", + "old 1.0.1 upgrade --dry-run on upgraded project: exit 0, last lines \"overwrite ../.github/workflows/squad-triage.yml | overwrite ../.github/workflows/sync-squad-labels.yml | Run without --dry-run to apply.\"; modified nothing" + ] + }, + { + "producer": "1.1.0", + "source": "github-release", + "shape": "default", + "status": "pass", + "provenance": "github-release: bradygaster/squad@v1.1.0 asset squad-linux-x64.tar.gz id 659607641 sha256 2de647a3feaef334051eb1b3084acba37a511818aad029a16abfe32fadb41aff (bundled node 22.20.0)", + "checks": [ + { + "id": "producer-artifact-verified", + "status": "pass" + }, + { + "id": "producer-init", + "status": "pass" + }, + { + "id": "upgrade-plain", + "status": "pass" + }, + { + "id": "coordinator-is-new-template", + "status": "pass" + }, + { + "id": "user-state-preserved", + "status": "pass" + }, + { + "id": "casting-pair-valid", + "status": "pass" + }, + { + "id": "state-backend-preserved", + "status": "pass" + }, + { + "id": "ai-team-migrated", + "status": "skipped", + "detail": "not an .ai-team shape" + }, + { + "id": "team-root-absent", + "status": "pass" + }, + { + "id": "idempotent-second-upgrade", + "status": "pass" + }, + { + "id": "downgrade-probe", + "status": "pass" + }, + { + "id": "clients-projected", + "status": "pass" + }, + { + "id": "user-config-preserved", + "status": "pass" + }, + { + "id": "doctor", + "status": "pass" + }, + { + "id": "projection-uninstall-byte-exact", + "status": "pass" + }, + { + "id": "profile-untouched", + "status": "skipped", + "detail": "no profile squad seeded in this shape" + } + ], + "notes": [ + "plain upgrade changed 27 working-tree path(s), all Squad-owned: .github/agents/squad.agent.md, .github/agents/squad.agent.md.local-backup, .github/workflows/squad-ci.yml, .github/workflows/squad-ci.yml.local-backup … (+23)", + "old 1.1.0 upgrade --dry-run on upgraded project: exit 0, last lines \"overwrite ../.github/workflows/squad-triage.yml | overwrite ../.github/workflows/sync-squad-labels.yml | Run without --dry-run to apply.\"; modified nothing" + ] + }, + { + "producer": "1.0.1", + "source": "github-release", + "shape": "ai-team", + "status": "pass", + "provenance": "github-release: bradygaster/squad@v1.0.1 asset squad-linux-x64.tar.gz id 610330551 sha256 283a1893be9ddad11056dcc8bd0673eff6b48a789bbf523b3f7ec31994d27ac7 (bundled node 22.20.0)", + "checks": [ + { + "id": "producer-artifact-verified", + "status": "pass" + }, + { + "id": "producer-init", + "status": "pass" + }, + { + "id": "upgrade-plain", + "status": "pass" + }, + { + "id": "coordinator-is-new-template", + "status": "pass" + }, + { + "id": "user-state-preserved", + "status": "pass" + }, + { + "id": "casting-pair-valid", + "status": "pass" + }, + { + "id": "state-backend-preserved", + "status": "pass" + }, + { + "id": "ai-team-migrated", + "status": "pass" + }, + { + "id": "team-root-absent", + "status": "pass" + }, + { + "id": "idempotent-second-upgrade", + "status": "pass" + }, + { + "id": "downgrade-probe", + "status": "pass" + }, + { + "id": "clients-projected", + "status": "pass" + }, + { + "id": "user-config-preserved", + "status": "pass" + }, + { + "id": "doctor", + "status": "pass" + }, + { + "id": "projection-uninstall-byte-exact", + "status": "pass" + }, + { + "id": "profile-untouched", + "status": "skipped", + "detail": "no profile squad seeded in this shape" + } + ], + "notes": [ + "plain upgrade changed 37 working-tree path(s), all Squad-owned: .github/agents/squad.agent.md, .github/agents/squad.agent.md.local-backup, .github/skills/coordinator-source-of-truth/SKILL.md, .github/workflows/squad-ci.yml … (+33)", + "old 1.0.1 upgrade --dry-run on upgraded project: exit 0, last lines \"overwrite ../.github/workflows/squad-triage.yml | overwrite ../.github/workflows/sync-squad-labels.yml | Run without --dry-run to apply.\"; modified nothing" + ] + }, + { + "producer": "1.1.0", + "source": "github-release", + "shape": "profile", + "status": "pass", + "provenance": "github-release: bradygaster/squad@v1.1.0 asset squad-linux-x64.tar.gz id 659607641 sha256 2de647a3feaef334051eb1b3084acba37a511818aad029a16abfe32fadb41aff (bundled node 22.20.0)", + "checks": [ + { + "id": "producer-artifact-verified", + "status": "pass" + }, + { + "id": "producer-init", + "status": "pass" + }, + { + "id": "upgrade-plain", + "status": "pass" + }, + { + "id": "coordinator-is-new-template", + "status": "pass" + }, + { + "id": "user-state-preserved", + "status": "pass" + }, + { + "id": "casting-pair-valid", + "status": "pass" + }, + { + "id": "state-backend-preserved", + "status": "pass" + }, + { + "id": "ai-team-migrated", + "status": "skipped", + "detail": "not an .ai-team shape" + }, + { + "id": "team-root-absent", + "status": "pass" + }, + { + "id": "idempotent-second-upgrade", + "status": "pass" + }, + { + "id": "downgrade-probe", + "status": "pass" + }, + { + "id": "clients-projected", + "status": "pass" + }, + { + "id": "user-config-preserved", + "status": "pass" + }, + { + "id": "doctor", + "status": "pass" + }, + { + "id": "projection-uninstall-byte-exact", + "status": "pass" + }, + { + "id": "profile-untouched", + "status": "pass" + } + ], + "notes": [ + "producer wrote teamRoot=/squad at init", + "plain upgrade changed 28 working-tree path(s), all Squad-owned: .github/agents/squad.agent.md, .github/agents/squad.agent.md.local-backup, .github/workflows/squad-ci.yml, .github/workflows/squad-ci.yml.local-backup … (+24)", + "old 1.1.0 upgrade --dry-run on upgraded project: exit 0, last lines \"overwrite ../.github/workflows/squad-triage.yml | overwrite ../.github/workflows/sync-squad-labels.yml | Run without --dry-run to apply.\"; modified nothing" + ] + }, + { + "producer": "1.1.0", + "source": "github-release", + "shape": "upstream", + "status": "skipped", + "reason": "Upstream inheritance (`.squad/upstream.json`, `squad upstream add`) is a Squad 2.0 feature; 1.1.0 cannot declare an upstream, so there is nothing for the upgrade to preserve.", + "provenance": "github-release: bradygaster/squad@v1.1.0 asset squad-linux-x64.tar.gz id 659607641 sha256 2de647a3feaef334051eb1b3084acba37a511818aad029a16abfe32fadb41aff (bundled node 22.20.0)", + "checks": [], + "notes": [] + }, + { + "producer": "1.1.0", + "source": "github-release", + "shape": "legacy-casting", + "status": "pass", + "provenance": "github-release: bradygaster/squad@v1.1.0 asset squad-linux-x64.tar.gz id 659607641 sha256 2de647a3feaef334051eb1b3084acba37a511818aad029a16abfe32fadb41aff (bundled node 22.20.0)", + "checks": [ + { + "id": "producer-artifact-verified", + "status": "pass" + }, + { + "id": "producer-init", + "status": "pass" + }, + { + "id": "upgrade-plain", + "status": "pass" + }, + { + "id": "coordinator-is-new-template", + "status": "pass" + }, + { + "id": "user-state-preserved", + "status": "pass" + }, + { + "id": "casting-pair-valid", + "status": "pass" + }, + { + "id": "state-backend-preserved", + "status": "pass" + }, + { + "id": "ai-team-migrated", + "status": "skipped", + "detail": "not an .ai-team shape" + }, + { + "id": "team-root-absent", + "status": "pass" + }, + { + "id": "idempotent-second-upgrade", + "status": "pass" + }, + { + "id": "downgrade-probe", + "status": "pass" + }, + { + "id": "clients-projected", + "status": "pass" + }, + { + "id": "user-config-preserved", + "status": "pass" + }, + { + "id": "doctor", + "status": "pass" + }, + { + "id": "projection-uninstall-byte-exact", + "status": "pass" + }, + { + "id": "profile-untouched", + "status": "skipped", + "detail": "no profile squad seeded in this shape" + } + ], + "notes": [ + "plain upgrade changed 32 working-tree path(s), all Squad-owned: .github/agents/squad.agent.md, .github/agents/squad.agent.md.local-backup, .github/workflows/squad-ci.yml, .github/workflows/squad-ci.yml.local-backup … (+28)", + "old 1.1.0 upgrade --dry-run on upgraded project: exit 0, last lines \"overwrite ../.github/workflows/squad-triage.yml | overwrite ../.github/workflows/sync-squad-labels.yml | Run without --dry-run to apply.\"; modified nothing" + ] + }, + { + "producer": "HEAD", + "source": "pack", + "shape": "default", + "status": "pass", + "provenance": "pack: npm pack of packages/squad-sdk + packages/squad-cli at 9da01a2e41ba15794d31b0b8251ddaa3abd34ba8 (v1.2.0-preview.1)", + "checks": [ + { + "id": "producer-artifact-verified", + "status": "pass" + }, + { + "id": "producer-init", + "status": "pass" + }, + { + "id": "upgrade-plain", + "status": "pass" + }, + { + "id": "coordinator-is-new-template", + "status": "pass" + }, + { + "id": "user-state-preserved", + "status": "pass" + }, + { + "id": "casting-pair-valid", + "status": "pass" + }, + { + "id": "state-backend-preserved", + "status": "pass" + }, + { + "id": "ai-team-migrated", + "status": "skipped", + "detail": "not an .ai-team shape" + }, + { + "id": "team-root-absent", + "status": "pass" + }, + { + "id": "idempotent-second-upgrade", + "status": "pass" + }, + { + "id": "downgrade-probe", + "status": "skipped", + "detail": "producer is the target version" + }, + { + "id": "clients-projected", + "status": "pass" + }, + { + "id": "user-config-preserved", + "status": "pass" + }, + { + "id": "doctor", + "status": "pass" + }, + { + "id": "projection-uninstall-byte-exact", + "status": "pass" + }, + { + "id": "profile-untouched", + "status": "skipped", + "detail": "no profile squad seeded in this shape" + } + ], + "notes": [ + "plain upgrade changed 13 working-tree path(s), all Squad-owned: .github/workflows/squad-ci.yml, .github/workflows/squad-docs.yml, .github/workflows/squad-insider-release.yml, .github/workflows/squad-label-enforce.yml … (+9)" + ] + }, + { + "producer": "1.1.0", + "source": "github-release", + "shape": "ai-team", + "status": "pass", + "provenance": "github-release: bradygaster/squad@v1.1.0 asset squad-linux-x64.tar.gz id 659607641 sha256 2de647a3feaef334051eb1b3084acba37a511818aad029a16abfe32fadb41aff (bundled node 22.20.0)", + "checks": [ + { + "id": "producer-artifact-verified", + "status": "pass" + }, + { + "id": "producer-init", + "status": "pass" + }, + { + "id": "upgrade-plain", + "status": "pass" + }, + { + "id": "coordinator-is-new-template", + "status": "pass" + }, + { + "id": "user-state-preserved", + "status": "pass" + }, + { + "id": "casting-pair-valid", + "status": "pass" + }, + { + "id": "state-backend-preserved", + "status": "pass" + }, + { + "id": "ai-team-migrated", + "status": "pass" + }, + { + "id": "team-root-absent", + "status": "pass" + }, + { + "id": "idempotent-second-upgrade", + "status": "pass" + }, + { + "id": "downgrade-probe", + "status": "pass" + }, + { + "id": "clients-projected", + "status": "pass" + }, + { + "id": "user-config-preserved", + "status": "pass" + }, + { + "id": "doctor", + "status": "pass" + }, + { + "id": "projection-uninstall-byte-exact", + "status": "pass" + }, + { + "id": "profile-untouched", + "status": "skipped", + "detail": "no profile squad seeded in this shape" + } + ], + "notes": [ + "plain upgrade changed 27 working-tree path(s), all Squad-owned: .github/agents/squad.agent.md, .github/agents/squad.agent.md.local-backup, .github/workflows/squad-ci.yml, .github/workflows/squad-ci.yml.local-backup … (+23)", + "old 1.1.0 upgrade --dry-run on upgraded project: exit 0, last lines \"overwrite ../.github/workflows/squad-triage.yml | overwrite ../.github/workflows/sync-squad-labels.yml | Run without --dry-run to apply.\"; modified nothing" + ] + }, + { + "producer": "1.1.0", + "source": "github-release", + "shape": "two-layer", + "status": "pass", + "provenance": "github-release: bradygaster/squad@v1.1.0 asset squad-linux-x64.tar.gz id 659607641 sha256 2de647a3feaef334051eb1b3084acba37a511818aad029a16abfe32fadb41aff (bundled node 22.20.0)", + "checks": [ + { + "id": "producer-artifact-verified", + "status": "pass" + }, + { + "id": "producer-init", + "status": "pass" + }, + { + "id": "upgrade-plain", + "status": "pass" + }, + { + "id": "coordinator-is-new-template", + "status": "pass" + }, + { + "id": "user-state-preserved", + "status": "pass" + }, + { + "id": "casting-pair-valid", + "status": "pass" + }, + { + "id": "state-backend-preserved", + "status": "pass" + }, + { + "id": "ai-team-migrated", + "status": "skipped", + "detail": "not an .ai-team shape" + }, + { + "id": "team-root-absent", + "status": "pass" + }, + { + "id": "idempotent-second-upgrade", + "status": "pass" + }, + { + "id": "downgrade-probe", + "status": "pass" + }, + { + "id": "clients-projected", + "status": "pass" + }, + { + "id": "user-config-preserved", + "status": "pass" + }, + { + "id": "doctor", + "status": "pass" + }, + { + "id": "projection-uninstall-byte-exact", + "status": "pass" + }, + { + "id": "profile-untouched", + "status": "skipped", + "detail": "no profile squad seeded in this shape" + } + ], + "notes": [ + "plain upgrade changed 25 working-tree path(s), all Squad-owned: .github/agents/squad.agent.md, .github/agents/squad.agent.md.local-backup, .github/workflows/squad-ci.yml, .github/workflows/squad-ci.yml.local-backup … (+21); squad-state blobs for 3 seeded path(s) identical", + "old 1.1.0 upgrade --dry-run on upgraded project: exit 0, last lines \"overwrite ../.github/workflows/squad-triage.yml | overwrite ../.github/workflows/sync-squad-labels.yml | Run without --dry-run to apply.\"; modified nothing" + ] + }, + { + "producer": "1.1.0", + "source": "github-release", + "shape": "orphan", + "status": "pass", + "provenance": "github-release: bradygaster/squad@v1.1.0 asset squad-linux-x64.tar.gz id 659607641 sha256 2de647a3feaef334051eb1b3084acba37a511818aad029a16abfe32fadb41aff (bundled node 22.20.0)", + "checks": [ + { + "id": "producer-artifact-verified", + "status": "pass" + }, + { + "id": "producer-init", + "status": "pass" + }, + { + "id": "upgrade-plain", + "status": "pass" + }, + { + "id": "coordinator-is-new-template", + "status": "pass" + }, + { + "id": "user-state-preserved", + "status": "pass" + }, + { + "id": "casting-pair-valid", + "status": "pass" + }, + { + "id": "state-backend-preserved", + "status": "pass" + }, + { + "id": "ai-team-migrated", + "status": "skipped", + "detail": "not an .ai-team shape" + }, + { + "id": "team-root-absent", + "status": "pass" + }, + { + "id": "idempotent-second-upgrade", + "status": "pass" + }, + { + "id": "downgrade-probe", + "status": "pass" + }, + { + "id": "clients-projected", + "status": "pass" + }, + { + "id": "user-config-preserved", + "status": "pass" + }, + { + "id": "doctor", + "status": "pass" + }, + { + "id": "projection-uninstall-byte-exact", + "status": "pass" + }, + { + "id": "profile-untouched", + "status": "skipped", + "detail": "no profile squad seeded in this shape" + } + ], + "notes": [ + "plain upgrade changed 25 working-tree path(s), all Squad-owned: .github/agents/squad.agent.md, .github/agents/squad.agent.md.local-backup, .github/workflows/squad-ci.yml, .github/workflows/squad-ci.yml.local-backup … (+21); squad-state blobs for 3 seeded path(s) identical", + "old 1.1.0 upgrade --dry-run on upgraded project: exit 0, last lines \"overwrite ../.github/workflows/squad-triage.yml | overwrite ../.github/workflows/sync-squad-labels.yml | Run without --dry-run to apply.\"; modified nothing" + ] + }, + { + "producer": "HEAD", + "source": "pack", + "shape": "profile", + "status": "pass", + "provenance": "pack: npm pack of packages/squad-sdk + packages/squad-cli at 9da01a2e41ba15794d31b0b8251ddaa3abd34ba8 (v1.2.0-preview.1)", + "checks": [ + { + "id": "producer-artifact-verified", + "status": "pass" + }, + { + "id": "producer-init", + "status": "pass" + }, + { + "id": "upgrade-plain", + "status": "pass" + }, + { + "id": "coordinator-is-new-template", + "status": "pass" + }, + { + "id": "user-state-preserved", + "status": "pass" + }, + { + "id": "casting-pair-valid", + "status": "pass" + }, + { + "id": "state-backend-preserved", + "status": "pass" + }, + { + "id": "ai-team-migrated", + "status": "skipped", + "detail": "not an .ai-team shape" + }, + { + "id": "team-root-absent", + "status": "pass" + }, + { + "id": "idempotent-second-upgrade", + "status": "pass" + }, + { + "id": "downgrade-probe", + "status": "skipped", + "detail": "producer is the target version" + }, + { + "id": "clients-projected", + "status": "pass" + }, + { + "id": "user-config-preserved", + "status": "pass" + }, + { + "id": "doctor", + "status": "pass" + }, + { + "id": "projection-uninstall-byte-exact", + "status": "pass" + }, + { + "id": "profile-untouched", + "status": "pass" + } + ], + "notes": [ + "plain upgrade changed 13 working-tree path(s), all Squad-owned: .github/workflows/squad-ci.yml, .github/workflows/squad-docs.yml, .github/workflows/squad-insider-release.yml, .github/workflows/squad-label-enforce.yml … (+9)" + ] + }, + { + "producer": "HEAD", + "source": "pack", + "shape": "legacy-casting", + "status": "pass", + "provenance": "pack: npm pack of packages/squad-sdk + packages/squad-cli at 9da01a2e41ba15794d31b0b8251ddaa3abd34ba8 (v1.2.0-preview.1)", + "checks": [ + { + "id": "producer-artifact-verified", + "status": "pass" + }, + { + "id": "producer-init", + "status": "pass" + }, + { + "id": "upgrade-plain", + "status": "pass" + }, + { + "id": "coordinator-is-new-template", + "status": "pass" + }, + { + "id": "user-state-preserved", + "status": "pass" + }, + { + "id": "casting-pair-valid", + "status": "pass" + }, + { + "id": "state-backend-preserved", + "status": "pass" + }, + { + "id": "ai-team-migrated", + "status": "skipped", + "detail": "not an .ai-team shape" + }, + { + "id": "team-root-absent", + "status": "pass" + }, + { + "id": "idempotent-second-upgrade", + "status": "pass" + }, + { + "id": "downgrade-probe", + "status": "skipped", + "detail": "producer is the target version" + }, + { + "id": "clients-projected", + "status": "pass" + }, + { + "id": "user-config-preserved", + "status": "pass" + }, + { + "id": "doctor", + "status": "pass" + }, + { + "id": "projection-uninstall-byte-exact", + "status": "pass" + }, + { + "id": "profile-untouched", + "status": "skipped", + "detail": "no profile squad seeded in this shape" + } + ], + "notes": [ + "plain upgrade changed 18 working-tree path(s), all Squad-owned: .github/workflows/squad-ci.yml, .github/workflows/squad-docs.yml, .github/workflows/squad-insider-release.yml, .github/workflows/squad-label-enforce.yml … (+14)" + ] + }, + { + "producer": "HEAD", + "source": "pack", + "shape": "ai-team", + "status": "pass", + "provenance": "pack: npm pack of packages/squad-sdk + packages/squad-cli at 9da01a2e41ba15794d31b0b8251ddaa3abd34ba8 (v1.2.0-preview.1)", + "checks": [ + { + "id": "producer-artifact-verified", + "status": "pass" + }, + { + "id": "producer-init", + "status": "pass" + }, + { + "id": "upgrade-plain", + "status": "pass" + }, + { + "id": "coordinator-is-new-template", + "status": "pass" + }, + { + "id": "user-state-preserved", + "status": "pass" + }, + { + "id": "casting-pair-valid", + "status": "pass" + }, + { + "id": "state-backend-preserved", + "status": "pass" + }, + { + "id": "ai-team-migrated", + "status": "pass" + }, + { + "id": "team-root-absent", + "status": "pass" + }, + { + "id": "idempotent-second-upgrade", + "status": "pass" + }, + { + "id": "downgrade-probe", + "status": "skipped", + "detail": "producer is the target version" + }, + { + "id": "clients-projected", + "status": "pass" + }, + { + "id": "user-config-preserved", + "status": "pass" + }, + { + "id": "doctor", + "status": "pass" + }, + { + "id": "projection-uninstall-byte-exact", + "status": "pass" + }, + { + "id": "profile-untouched", + "status": "skipped", + "detail": "no profile squad seeded in this shape" + } + ], + "notes": [ + "plain upgrade changed 13 working-tree path(s), all Squad-owned: .github/workflows/squad-ci.yml, .github/workflows/squad-docs.yml, .github/workflows/squad-insider-release.yml, .github/workflows/squad-label-enforce.yml … (+9)" + ] + }, + { + "producer": "HEAD", + "source": "pack", + "shape": "orphan", + "status": "pass", + "provenance": "pack: npm pack of packages/squad-sdk + packages/squad-cli at 9da01a2e41ba15794d31b0b8251ddaa3abd34ba8 (v1.2.0-preview.1)", + "checks": [ + { + "id": "producer-artifact-verified", + "status": "pass" + }, + { + "id": "producer-init", + "status": "pass" + }, + { + "id": "upgrade-plain", + "status": "pass" + }, + { + "id": "coordinator-is-new-template", + "status": "pass" + }, + { + "id": "user-state-preserved", + "status": "pass" + }, + { + "id": "casting-pair-valid", + "status": "pass" + }, + { + "id": "state-backend-preserved", + "status": "pass" + }, + { + "id": "ai-team-migrated", + "status": "skipped", + "detail": "not an .ai-team shape" + }, + { + "id": "team-root-absent", + "status": "pass" + }, + { + "id": "idempotent-second-upgrade", + "status": "pass" + }, + { + "id": "downgrade-probe", + "status": "skipped", + "detail": "producer is the target version" + }, + { + "id": "clients-projected", + "status": "pass" + }, + { + "id": "user-config-preserved", + "status": "pass" + }, + { + "id": "doctor", + "status": "pass" + }, + { + "id": "projection-uninstall-byte-exact", + "status": "pass" + }, + { + "id": "profile-untouched", + "status": "skipped", + "detail": "no profile squad seeded in this shape" + } + ], + "notes": [ + "plain upgrade changed 11 working-tree path(s), all Squad-owned: .github/workflows/squad-ci.yml, .github/workflows/squad-docs.yml, .github/workflows/squad-insider-release.yml, .github/workflows/squad-label-enforce.yml … (+7); squad-state blobs for 3 seeded path(s) identical" + ] + }, + { + "producer": "HEAD", + "source": "pack", + "shape": "two-layer", + "status": "pass", + "provenance": "pack: npm pack of packages/squad-sdk + packages/squad-cli at 9da01a2e41ba15794d31b0b8251ddaa3abd34ba8 (v1.2.0-preview.1)", + "checks": [ + { + "id": "producer-artifact-verified", + "status": "pass" + }, + { + "id": "producer-init", + "status": "pass" + }, + { + "id": "upgrade-plain", + "status": "pass" + }, + { + "id": "coordinator-is-new-template", + "status": "pass" + }, + { + "id": "user-state-preserved", + "status": "pass" + }, + { + "id": "casting-pair-valid", + "status": "pass" + }, + { + "id": "state-backend-preserved", + "status": "pass" + }, + { + "id": "ai-team-migrated", + "status": "skipped", + "detail": "not an .ai-team shape" + }, + { + "id": "team-root-absent", + "status": "pass" + }, + { + "id": "idempotent-second-upgrade", + "status": "pass" + }, + { + "id": "downgrade-probe", + "status": "skipped", + "detail": "producer is the target version" + }, + { + "id": "clients-projected", + "status": "pass" + }, + { + "id": "user-config-preserved", + "status": "pass" + }, + { + "id": "doctor", + "status": "pass" + }, + { + "id": "projection-uninstall-byte-exact", + "status": "pass" + }, + { + "id": "profile-untouched", + "status": "skipped", + "detail": "no profile squad seeded in this shape" + } + ], + "notes": [ + "plain upgrade changed 11 working-tree path(s), all Squad-owned: .github/workflows/squad-ci.yml, .github/workflows/squad-docs.yml, .github/workflows/squad-insider-release.yml, .github/workflows/squad-label-enforce.yml … (+7); squad-state blobs for 3 seeded path(s) identical" + ] + }, + { + "producer": "HEAD", + "source": "pack", + "shape": "upstream", + "status": "pass", + "provenance": "pack: npm pack of packages/squad-sdk + packages/squad-cli at 9da01a2e41ba15794d31b0b8251ddaa3abd34ba8 (v1.2.0-preview.1)", + "checks": [ + { + "id": "producer-artifact-verified", + "status": "pass" + }, + { + "id": "producer-init", + "status": "pass" + }, + { + "id": "upgrade-plain", + "status": "pass" + }, + { + "id": "coordinator-is-new-template", + "status": "pass" + }, + { + "id": "user-state-preserved", + "status": "pass" + }, + { + "id": "casting-pair-valid", + "status": "pass" + }, + { + "id": "state-backend-preserved", + "status": "pass" + }, + { + "id": "ai-team-migrated", + "status": "skipped", + "detail": "not an .ai-team shape" + }, + { + "id": "team-root-absent", + "status": "pass" + }, + { + "id": "user-config-preserved", + "status": "pass" + }, + { + "id": "idempotent-second-upgrade", + "status": "pass" + }, + { + "id": "downgrade-probe", + "status": "skipped", + "detail": "producer is the target version" + }, + { + "id": "clients-projected", + "status": "pass" + }, + { + "id": "doctor", + "status": "pass" + }, + { + "id": "projection-uninstall-byte-exact", + "status": "pass" + }, + { + "id": "profile-untouched", + "status": "skipped", + "detail": "no profile squad seeded in this shape" + } + ], + "notes": [ + "plain upgrade changed 13 working-tree path(s), all Squad-owned: .github/workflows/squad-ci.yml, .github/workflows/squad-docs.yml, .github/workflows/squad-insider-release.yml, .github/workflows/squad-label-enforce.yml … (+9)" + ] + } + ] + }, + { + "platform": "win32", + "arch": "x64", + "node": "v22.23.4", + "target": "1.2.0-preview.1", + "targetSha": "9da01a2e41ba15794d31b0b8251ddaa3abd34ba8", + "harnessHash": "bca27c39dc2dcbffc3bf652bf1f45b471c09911a4bb9ba7e5b7247d25705a66e", + "event": "workflow_dispatch", + "runUrl": "https://github.com/bradygaster/squad/actions/runs/39903943542", + "recordedAt": "2026-10-23T05:10:56.011Z", + "results": [ + { + "producer": "0.9.1", + "source": "npm", + "shape": "orphan", + "status": "skipped", + "reason": "`@bradygaster/squad-cli@0.9.1` has no `--state-backend` flag (first shipped in 0.13.x), so an orphan-backend project cannot be produced by it.", + "provenance": "npm: @bradygaster/squad-cli@0.9.1 sha512-TcZqRbJk3XBg4N8Y5/5OWJeA52ARAMuzk4MGtB0bfdr87e7iJRV6lJEw8+OIYz9IQ+1JxN/Q30dZDALMOcdi2w==; @bradygaster/squad-sdk@0.9.1 sha512-1KqZ2N6Lt/B4uYlU/Su9JjA1s/7ng0OAEb9CZrkRk19YSPi6tg7K5QWdTKl5gY/gp8zPAJqfjVffOwrZwGlzmw==", + "checks": [], + "notes": [] + }, + { + "producer": "0.9.1", + "source": "npm", + "shape": "two-layer", + "status": "skipped", + "reason": "`@bradygaster/squad-cli@0.9.1` has no `--state-backend` flag (first shipped in 0.13.x), so a two-layer-backend project cannot be produced by it.", + "provenance": "npm: @bradygaster/squad-cli@0.9.1 sha512-TcZqRbJk3XBg4N8Y5/5OWJeA52ARAMuzk4MGtB0bfdr87e7iJRV6lJEw8+OIYz9IQ+1JxN/Q30dZDALMOcdi2w==; @bradygaster/squad-sdk@0.9.1 sha512-1KqZ2N6Lt/B4uYlU/Su9JjA1s/7ng0OAEb9CZrkRk19YSPi6tg7K5QWdTKl5gY/gp8zPAJqfjVffOwrZwGlzmw==", + "checks": [], + "notes": [] + }, + { + "producer": "0.9.1", + "source": "npm", + "shape": "upstream", + "status": "skipped", + "reason": "Upstream inheritance (`.squad/upstream.json`, `squad upstream add`) is a Squad 2.0 feature; 0.9.1 cannot declare an upstream, so there is nothing for the upgrade to preserve.", + "provenance": "npm: @bradygaster/squad-cli@0.9.1 sha512-TcZqRbJk3XBg4N8Y5/5OWJeA52ARAMuzk4MGtB0bfdr87e7iJRV6lJEw8+OIYz9IQ+1JxN/Q30dZDALMOcdi2w==; @bradygaster/squad-sdk@0.9.1 sha512-1KqZ2N6Lt/B4uYlU/Su9JjA1s/7ng0OAEb9CZrkRk19YSPi6tg7K5QWdTKl5gY/gp8zPAJqfjVffOwrZwGlzmw==", + "checks": [], + "notes": [] + }, + { + "producer": "0.9.1", + "source": "npm", + "shape": "profile", + "status": "pass", + "provenance": "npm: @bradygaster/squad-cli@0.9.1 sha512-TcZqRbJk3XBg4N8Y5/5OWJeA52ARAMuzk4MGtB0bfdr87e7iJRV6lJEw8+OIYz9IQ+1JxN/Q30dZDALMOcdi2w==; @bradygaster/squad-sdk@0.9.1 sha512-1KqZ2N6Lt/B4uYlU/Su9JjA1s/7ng0OAEb9CZrkRk19YSPi6tg7K5QWdTKl5gY/gp8zPAJqfjVffOwrZwGlzmw==", + "checks": [ + { + "id": "producer-artifact-verified", + "status": "pass" + }, + { + "id": "producer-init", + "status": "pass" + }, + { + "id": "upgrade-plain", + "status": "pass" + }, + { + "id": "coordinator-is-new-template", + "status": "pass" + }, + { + "id": "user-state-preserved", + "status": "pass" + }, + { + "id": "casting-pair-valid", + "status": "pass" + }, + { + "id": "state-backend-preserved", + "status": "pass" + }, + { + "id": "ai-team-migrated", + "status": "skipped", + "detail": "not an .ai-team shape" + }, + { + "id": "team-root-absent", + "status": "pass" + }, + { + "id": "idempotent-second-upgrade", + "status": "pass" + }, + { + "id": "downgrade-probe", + "status": "xfail", + "detail": "dry-run MODIFIED 44 files (20 coordinator/templates/workflows, e.g. .copilot/skills/agent-collaboration/SKILL.md, .copilot/skills/cross-squad/SKILL.md, .copilot/skills/git-workflow/SKILL.md …)" + }, + { + "id": "clients-projected", + "status": "pass" + }, + { + "id": "user-config-preserved", + "status": "pass" + }, + { + "id": "doctor", + "status": "pass" + }, + { + "id": "projection-uninstall-byte-exact", + "status": "pass" + }, + { + "id": "profile-untouched", + "status": "pass" + } + ], + "notes": [ + "plain upgrade changed 100 working-tree path(s), all Squad-owned: .copilot/skills/agent-collaboration/SKILL.md, .copilot/skills/cross-squad/SKILL.md, .copilot/skills/git-workflow/SKILL.md, .copilot/skills/reviewer-protocol/SKILL.md … (+96)", + "old 0.9.1 upgrade --dry-run on upgraded project: exit 0, last lines \"\u001b[32m✓\u001b[0m refreshed .squad/templates/ | Upgrade complete: v1.2.0-preview.1 → v0.9.1 | \u001b[2mPreserves user state: team.md, decisions/, agents/*/history.md\u001b[0m\"; MODIFIED 44 files (20 coordinator/templates/workflows, e.g. .copilot/skills/agent-collaboration/SKILL.md, .copilot/skills/cross-squad/SKILL.md, .copilot/skills/git-workflow/SKILL.md …)", + "downgrade-probe: F3" + ] + }, + { + "producer": "0.9.1", + "source": "npm", + "shape": "default", + "status": "pass", + "provenance": "npm: @bradygaster/squad-cli@0.9.1 sha512-TcZqRbJk3XBg4N8Y5/5OWJeA52ARAMuzk4MGtB0bfdr87e7iJRV6lJEw8+OIYz9IQ+1JxN/Q30dZDALMOcdi2w==; @bradygaster/squad-sdk@0.9.1 sha512-1KqZ2N6Lt/B4uYlU/Su9JjA1s/7ng0OAEb9CZrkRk19YSPi6tg7K5QWdTKl5gY/gp8zPAJqfjVffOwrZwGlzmw==", + "checks": [ + { + "id": "producer-artifact-verified", + "status": "pass" + }, + { + "id": "producer-init", + "status": "pass" + }, + { + "id": "upgrade-plain", + "status": "pass" + }, + { + "id": "coordinator-is-new-template", + "status": "pass" + }, + { + "id": "user-state-preserved", + "status": "pass" + }, + { + "id": "casting-pair-valid", + "status": "pass" + }, + { + "id": "state-backend-preserved", + "status": "pass" + }, + { + "id": "ai-team-migrated", + "status": "skipped", + "detail": "not an .ai-team shape" + }, + { + "id": "team-root-absent", + "status": "pass" + }, + { + "id": "idempotent-second-upgrade", + "status": "pass" + }, + { + "id": "downgrade-probe", + "status": "xfail", + "detail": "dry-run MODIFIED 44 files (20 coordinator/templates/workflows, e.g. .copilot/skills/agent-collaboration/SKILL.md, .copilot/skills/cross-squad/SKILL.md, .copilot/skills/git-workflow/SKILL.md …)" + }, + { + "id": "clients-projected", + "status": "pass" + }, + { + "id": "user-config-preserved", + "status": "pass" + }, + { + "id": "doctor", + "status": "pass" + }, + { + "id": "projection-uninstall-byte-exact", + "status": "pass" + }, + { + "id": "profile-untouched", + "status": "skipped", + "detail": "no profile squad seeded in this shape" + } + ], + "notes": [ + "plain upgrade changed 100 working-tree path(s), all Squad-owned: .copilot/skills/agent-collaboration/SKILL.md, .copilot/skills/cross-squad/SKILL.md, .copilot/skills/git-workflow/SKILL.md, .copilot/skills/reviewer-protocol/SKILL.md … (+96)", + "old 0.9.1 upgrade --dry-run on upgraded project: exit 0, last lines \"\u001b[32m✓\u001b[0m refreshed .squad/templates/ | Upgrade complete: v1.2.0-preview.1 → v0.9.1 | \u001b[2mPreserves user state: team.md, decisions/, agents/*/history.md\u001b[0m\"; MODIFIED 44 files (20 coordinator/templates/workflows, e.g. .copilot/skills/agent-collaboration/SKILL.md, .copilot/skills/cross-squad/SKILL.md, .copilot/skills/git-workflow/SKILL.md …)", + "downgrade-probe: F3" + ] + }, + { + "producer": "0.13.1", + "source": "npm", + "shape": "default", + "status": "pass", + "provenance": "npm: @bradygaster/squad-cli@0.13.1 sha512-ogfx2nhyx6fqMyqlaI5tDnOzUbRdh+a6Uh/rMQ0aNJwAXnvQp4N37Xp/eOTyB5CxcB7E34NBtAfCxyXbWSiS8w==; @bradygaster/squad-sdk@0.13.1 sha512-1Jt6c9kahJq1JCcE77j/50D79YcXVG6Rt1genuLJ3O9J2SKTZKemlbvHgftkc5LgGH22DkWqHh5zLNEQ/p+ukA==", + "checks": [ + { + "id": "producer-artifact-verified", + "status": "pass" + }, + { + "id": "producer-init", + "status": "pass" + }, + { + "id": "upgrade-plain", + "status": "pass" + }, + { + "id": "coordinator-is-new-template", + "status": "pass" + }, + { + "id": "user-state-preserved", + "status": "pass" + }, + { + "id": "casting-pair-valid", + "status": "pass" + }, + { + "id": "state-backend-preserved", + "status": "pass" + }, + { + "id": "ai-team-migrated", + "status": "skipped", + "detail": "not an .ai-team shape" + }, + { + "id": "team-root-absent", + "status": "pass" + }, + { + "id": "idempotent-second-upgrade", + "status": "pass" + }, + { + "id": "downgrade-probe", + "status": "pass" + }, + { + "id": "clients-projected", + "status": "pass" + }, + { + "id": "user-config-preserved", + "status": "pass" + }, + { + "id": "doctor", + "status": "pass" + }, + { + "id": "projection-uninstall-byte-exact", + "status": "pass" + }, + { + "id": "profile-untouched", + "status": "skipped", + "detail": "no profile squad seeded in this shape" + } + ], + "notes": [ + "plain upgrade changed 58 working-tree path(s), all Squad-owned: .github/agents/squad.agent.md, .github/agents/squad.agent.md.local-backup, .github/skills/coordinator-response-mode/SKILL.md, .github/skills/coordinator-source-of-truth/SKILL.md … (+54)", + "old 0.13.1 upgrade --dry-run on upgraded project: exit 0, last lines \"overwrite ../.github/workflows/squad-triage.yml | overwrite ../.github/workflows/sync-squad-labels.yml | Run without --dry-run to apply.\"; modified nothing" + ] + }, + { + "producer": "0.9.1", + "source": "npm", + "shape": "ai-team", + "status": "pass", + "provenance": "npm: @bradygaster/squad-cli@0.9.1 sha512-TcZqRbJk3XBg4N8Y5/5OWJeA52ARAMuzk4MGtB0bfdr87e7iJRV6lJEw8+OIYz9IQ+1JxN/Q30dZDALMOcdi2w==; @bradygaster/squad-sdk@0.9.1 sha512-1KqZ2N6Lt/B4uYlU/Su9JjA1s/7ng0OAEb9CZrkRk19YSPi6tg7K5QWdTKl5gY/gp8zPAJqfjVffOwrZwGlzmw==", + "checks": [ + { + "id": "producer-artifact-verified", + "status": "pass" + }, + { + "id": "producer-init", + "status": "pass" + }, + { + "id": "upgrade-plain", + "status": "pass" + }, + { + "id": "coordinator-is-new-template", + "status": "pass" + }, + { + "id": "user-state-preserved", + "status": "pass" + }, + { + "id": "casting-pair-valid", + "status": "pass" + }, + { + "id": "state-backend-preserved", + "status": "pass" + }, + { + "id": "ai-team-migrated", + "status": "pass" + }, + { + "id": "team-root-absent", + "status": "pass" + }, + { + "id": "idempotent-second-upgrade", + "status": "pass" + }, + { + "id": "downgrade-probe", + "status": "xfail", + "detail": "dry-run MODIFIED 44 files (20 coordinator/templates/workflows, e.g. .copilot/skills/agent-collaboration/SKILL.md, .copilot/skills/cross-squad/SKILL.md, .copilot/skills/git-workflow/SKILL.md …)" + }, + { + "id": "clients-projected", + "status": "pass" + }, + { + "id": "user-config-preserved", + "status": "pass" + }, + { + "id": "doctor", + "status": "pass" + }, + { + "id": "projection-uninstall-byte-exact", + "status": "pass" + }, + { + "id": "profile-untouched", + "status": "skipped", + "detail": "no profile squad seeded in this shape" + } + ], + "notes": [ + "plain upgrade changed 100 working-tree path(s), all Squad-owned: .copilot/skills/agent-collaboration/SKILL.md, .copilot/skills/cross-squad/SKILL.md, .copilot/skills/git-workflow/SKILL.md, .copilot/skills/reviewer-protocol/SKILL.md … (+96)", + "old 0.9.1 upgrade --dry-run on upgraded project: exit 0, last lines \"\u001b[32m✓\u001b[0m refreshed .squad/templates/ | Upgrade complete: v1.2.0-preview.1 → v0.9.1 | \u001b[2mPreserves user state: team.md, decisions/, agents/*/history.md\u001b[0m\"; MODIFIED 44 files (20 coordinator/templates/workflows, e.g. .copilot/skills/agent-collaboration/SKILL.md, .copilot/skills/cross-squad/SKILL.md, .copilot/skills/git-workflow/SKILL.md …)", + "downgrade-probe: F3" + ] + }, + { + "producer": "0.9.1", + "source": "npm", + "shape": "legacy-casting", + "status": "pass", + "provenance": "npm: @bradygaster/squad-cli@0.9.1 sha512-TcZqRbJk3XBg4N8Y5/5OWJeA52ARAMuzk4MGtB0bfdr87e7iJRV6lJEw8+OIYz9IQ+1JxN/Q30dZDALMOcdi2w==; @bradygaster/squad-sdk@0.9.1 sha512-1KqZ2N6Lt/B4uYlU/Su9JjA1s/7ng0OAEb9CZrkRk19YSPi6tg7K5QWdTKl5gY/gp8zPAJqfjVffOwrZwGlzmw==", + "checks": [ + { + "id": "producer-artifact-verified", + "status": "pass" + }, + { + "id": "producer-init", + "status": "pass" + }, + { + "id": "upgrade-plain", + "status": "pass" + }, + { + "id": "coordinator-is-new-template", + "status": "pass" + }, + { + "id": "user-state-preserved", + "status": "pass" + }, + { + "id": "casting-pair-valid", + "status": "pass" + }, + { + "id": "state-backend-preserved", + "status": "pass" + }, + { + "id": "ai-team-migrated", + "status": "skipped", + "detail": "not an .ai-team shape" + }, + { + "id": "team-root-absent", + "status": "pass" + }, + { + "id": "idempotent-second-upgrade", + "status": "pass" + }, + { + "id": "downgrade-probe", + "status": "xfail", + "detail": "dry-run MODIFIED 44 files (20 coordinator/templates/workflows, e.g. .copilot/skills/agent-collaboration/SKILL.md, .copilot/skills/cross-squad/SKILL.md, .copilot/skills/git-workflow/SKILL.md …)" + }, + { + "id": "clients-projected", + "status": "pass" + }, + { + "id": "user-config-preserved", + "status": "pass" + }, + { + "id": "doctor", + "status": "pass" + }, + { + "id": "projection-uninstall-byte-exact", + "status": "pass" + }, + { + "id": "profile-untouched", + "status": "skipped", + "detail": "no profile squad seeded in this shape" + } + ], + "notes": [ + "plain upgrade changed 101 working-tree path(s), all Squad-owned: .copilot/skills/agent-collaboration/SKILL.md, .copilot/skills/cross-squad/SKILL.md, .copilot/skills/git-workflow/SKILL.md, .copilot/skills/reviewer-protocol/SKILL.md … (+97)", + "old 0.9.1 upgrade --dry-run on upgraded project: exit 0, last lines \"\u001b[32m✓\u001b[0m refreshed .squad/templates/ | Upgrade complete: v1.2.0-preview.1 → v0.9.1 | \u001b[2mPreserves user state: team.md, decisions/, agents/*/history.md\u001b[0m\"; MODIFIED 44 files (20 coordinator/templates/workflows, e.g. .copilot/skills/agent-collaboration/SKILL.md, .copilot/skills/cross-squad/SKILL.md, .copilot/skills/git-workflow/SKILL.md …)", + "downgrade-probe: F3" + ] + }, + { + "producer": "0.13.1", + "source": "npm", + "shape": "profile", + "status": "pass", + "provenance": "npm: @bradygaster/squad-cli@0.13.1 sha512-ogfx2nhyx6fqMyqlaI5tDnOzUbRdh+a6Uh/rMQ0aNJwAXnvQp4N37Xp/eOTyB5CxcB7E34NBtAfCxyXbWSiS8w==; @bradygaster/squad-sdk@0.13.1 sha512-1Jt6c9kahJq1JCcE77j/50D79YcXVG6Rt1genuLJ3O9J2SKTZKemlbvHgftkc5LgGH22DkWqHh5zLNEQ/p+ukA==", + "checks": [ + { + "id": "producer-artifact-verified", + "status": "pass" + }, + { + "id": "producer-init", + "status": "pass" + }, + { + "id": "upgrade-plain", + "status": "pass" + }, + { + "id": "coordinator-is-new-template", + "status": "pass" + }, + { + "id": "user-state-preserved", + "status": "pass" + }, + { + "id": "casting-pair-valid", + "status": "pass" + }, + { + "id": "state-backend-preserved", + "status": "pass" + }, + { + "id": "ai-team-migrated", + "status": "skipped", + "detail": "not an .ai-team shape" + }, + { + "id": "team-root-absent", + "status": "pass" + }, + { + "id": "idempotent-second-upgrade", + "status": "pass" + }, + { + "id": "downgrade-probe", + "status": "pass" + }, + { + "id": "clients-projected", + "status": "pass" + }, + { + "id": "user-config-preserved", + "status": "pass" + }, + { + "id": "doctor", + "status": "pass" + }, + { + "id": "projection-uninstall-byte-exact", + "status": "pass" + }, + { + "id": "profile-untouched", + "status": "pass" + } + ], + "notes": [ + "producer wrote teamRoot=\\squad at init", + "plain upgrade changed 59 working-tree path(s), all Squad-owned: .github/agents/squad.agent.md, .github/agents/squad.agent.md.local-backup, .github/skills/coordinator-response-mode/SKILL.md, .github/skills/coordinator-source-of-truth/SKILL.md … (+55)", + "old 0.13.1 upgrade --dry-run on upgraded project: exit 0, last lines \"overwrite ../.github/workflows/squad-triage.yml | overwrite ../.github/workflows/sync-squad-labels.yml | Run without --dry-run to apply.\"; modified nothing" + ] + }, + { + "producer": "0.13.1", + "source": "npm", + "shape": "upstream", + "status": "skipped", + "reason": "Upstream inheritance (`.squad/upstream.json`, `squad upstream add`) is a Squad 2.0 feature; 0.13.1 cannot declare an upstream, so there is nothing for the upgrade to preserve.", + "provenance": "npm: @bradygaster/squad-cli@0.13.1 sha512-ogfx2nhyx6fqMyqlaI5tDnOzUbRdh+a6Uh/rMQ0aNJwAXnvQp4N37Xp/eOTyB5CxcB7E34NBtAfCxyXbWSiS8w==; @bradygaster/squad-sdk@0.13.1 sha512-1Jt6c9kahJq1JCcE77j/50D79YcXVG6Rt1genuLJ3O9J2SKTZKemlbvHgftkc5LgGH22DkWqHh5zLNEQ/p+ukA==", + "checks": [], + "notes": [] + }, + { + "producer": "0.13.1", + "source": "npm", + "shape": "legacy-casting", + "status": "pass", + "provenance": "npm: @bradygaster/squad-cli@0.13.1 sha512-ogfx2nhyx6fqMyqlaI5tDnOzUbRdh+a6Uh/rMQ0aNJwAXnvQp4N37Xp/eOTyB5CxcB7E34NBtAfCxyXbWSiS8w==; @bradygaster/squad-sdk@0.13.1 sha512-1Jt6c9kahJq1JCcE77j/50D79YcXVG6Rt1genuLJ3O9J2SKTZKemlbvHgftkc5LgGH22DkWqHh5zLNEQ/p+ukA==", + "checks": [ + { + "id": "producer-artifact-verified", + "status": "pass" + }, + { + "id": "producer-init", + "status": "pass" + }, + { + "id": "upgrade-plain", + "status": "pass" + }, + { + "id": "coordinator-is-new-template", + "status": "pass" + }, + { + "id": "user-state-preserved", + "status": "pass" + }, + { + "id": "casting-pair-valid", + "status": "pass" + }, + { + "id": "state-backend-preserved", + "status": "pass" + }, + { + "id": "ai-team-migrated", + "status": "skipped", + "detail": "not an .ai-team shape" + }, + { + "id": "team-root-absent", + "status": "pass" + }, + { + "id": "idempotent-second-upgrade", + "status": "pass" + }, + { + "id": "downgrade-probe", + "status": "pass" + }, + { + "id": "clients-projected", + "status": "pass" + }, + { + "id": "user-config-preserved", + "status": "pass" + }, + { + "id": "doctor", + "status": "pass" + }, + { + "id": "projection-uninstall-byte-exact", + "status": "pass" + }, + { + "id": "profile-untouched", + "status": "skipped", + "detail": "no profile squad seeded in this shape" + } + ], + "notes": [ + "plain upgrade changed 58 working-tree path(s), all Squad-owned: .github/agents/squad.agent.md, .github/agents/squad.agent.md.local-backup, .github/skills/coordinator-response-mode/SKILL.md, .github/skills/coordinator-source-of-truth/SKILL.md … (+54)", + "old 0.13.1 upgrade --dry-run on upgraded project: exit 0, last lines \"overwrite ../.github/workflows/squad-triage.yml | overwrite ../.github/workflows/sync-squad-labels.yml | Run without --dry-run to apply.\"; modified nothing" + ] + }, + { + "producer": "0.13.1", + "source": "npm", + "shape": "ai-team", + "status": "pass", + "provenance": "npm: @bradygaster/squad-cli@0.13.1 sha512-ogfx2nhyx6fqMyqlaI5tDnOzUbRdh+a6Uh/rMQ0aNJwAXnvQp4N37Xp/eOTyB5CxcB7E34NBtAfCxyXbWSiS8w==; @bradygaster/squad-sdk@0.13.1 sha512-1Jt6c9kahJq1JCcE77j/50D79YcXVG6Rt1genuLJ3O9J2SKTZKemlbvHgftkc5LgGH22DkWqHh5zLNEQ/p+ukA==", + "checks": [ + { + "id": "producer-artifact-verified", + "status": "pass" + }, + { + "id": "producer-init", + "status": "pass" + }, + { + "id": "upgrade-plain", + "status": "pass" + }, + { + "id": "coordinator-is-new-template", + "status": "pass" + }, + { + "id": "user-state-preserved", + "status": "pass" + }, + { + "id": "casting-pair-valid", + "status": "pass" + }, + { + "id": "state-backend-preserved", + "status": "pass" + }, + { + "id": "ai-team-migrated", + "status": "pass" + }, + { + "id": "team-root-absent", + "status": "pass" + }, + { + "id": "idempotent-second-upgrade", + "status": "pass" + }, + { + "id": "downgrade-probe", + "status": "pass" + }, + { + "id": "clients-projected", + "status": "pass" + }, + { + "id": "user-config-preserved", + "status": "pass" + }, + { + "id": "doctor", + "status": "pass" + }, + { + "id": "projection-uninstall-byte-exact", + "status": "pass" + }, + { + "id": "profile-untouched", + "status": "skipped", + "detail": "no profile squad seeded in this shape" + } + ], + "notes": [ + "plain upgrade changed 58 working-tree path(s), all Squad-owned: .github/agents/squad.agent.md, .github/agents/squad.agent.md.local-backup, .github/skills/coordinator-response-mode/SKILL.md, .github/skills/coordinator-source-of-truth/SKILL.md … (+54)", + "old 0.13.1 upgrade --dry-run on upgraded project: exit 0, last lines \"overwrite ../.github/workflows/squad-triage.yml | overwrite ../.github/workflows/sync-squad-labels.yml | Run without --dry-run to apply.\"; modified nothing" + ] + }, + { + "producer": "0.13.1", + "source": "npm", + "shape": "orphan", + "status": "pass", + "provenance": "npm: @bradygaster/squad-cli@0.13.1 sha512-ogfx2nhyx6fqMyqlaI5tDnOzUbRdh+a6Uh/rMQ0aNJwAXnvQp4N37Xp/eOTyB5CxcB7E34NBtAfCxyXbWSiS8w==; @bradygaster/squad-sdk@0.13.1 sha512-1Jt6c9kahJq1JCcE77j/50D79YcXVG6Rt1genuLJ3O9J2SKTZKemlbvHgftkc5LgGH22DkWqHh5zLNEQ/p+ukA==", + "checks": [ + { + "id": "producer-artifact-verified", + "status": "pass" + }, + { + "id": "producer-init", + "status": "pass" + }, + { + "id": "upgrade-plain", + "status": "pass" + }, + { + "id": "coordinator-is-new-template", + "status": "pass" + }, + { + "id": "user-state-preserved", + "status": "pass" + }, + { + "id": "casting-pair-valid", + "status": "pass" + }, + { + "id": "state-backend-preserved", + "status": "pass" + }, + { + "id": "ai-team-migrated", + "status": "skipped", + "detail": "not an .ai-team shape" + }, + { + "id": "team-root-absent", + "status": "pass" + }, + { + "id": "idempotent-second-upgrade", + "status": "pass" + }, + { + "id": "downgrade-probe", + "status": "pass" + }, + { + "id": "clients-projected", + "status": "pass" + }, + { + "id": "user-config-preserved", + "status": "pass" + }, + { + "id": "doctor", + "status": "pass" + }, + { + "id": "projection-uninstall-byte-exact", + "status": "pass" + }, + { + "id": "profile-untouched", + "status": "skipped", + "detail": "no profile squad seeded in this shape" + } + ], + "notes": [ + "plain upgrade changed 56 working-tree path(s), all Squad-owned: .github/agents/squad.agent.md, .github/agents/squad.agent.md.local-backup, .github/skills/coordinator-response-mode/SKILL.md, .github/skills/coordinator-source-of-truth/SKILL.md … (+52); squad-state blobs for 3 seeded path(s) identical", + "old 0.13.1 upgrade --dry-run on upgraded project: exit 0, last lines \"overwrite ../.github/workflows/squad-triage.yml | overwrite ../.github/workflows/sync-squad-labels.yml | Run without --dry-run to apply.\"; modified nothing" + ] + }, + { + "producer": "0.13.1", + "source": "npm", + "shape": "two-layer", + "status": "pass", + "provenance": "npm: @bradygaster/squad-cli@0.13.1 sha512-ogfx2nhyx6fqMyqlaI5tDnOzUbRdh+a6Uh/rMQ0aNJwAXnvQp4N37Xp/eOTyB5CxcB7E34NBtAfCxyXbWSiS8w==; @bradygaster/squad-sdk@0.13.1 sha512-1Jt6c9kahJq1JCcE77j/50D79YcXVG6Rt1genuLJ3O9J2SKTZKemlbvHgftkc5LgGH22DkWqHh5zLNEQ/p+ukA==", + "checks": [ + { + "id": "producer-artifact-verified", + "status": "pass" + }, + { + "id": "producer-init", + "status": "pass" + }, + { + "id": "upgrade-plain", + "status": "pass" + }, + { + "id": "coordinator-is-new-template", + "status": "pass" + }, + { + "id": "user-state-preserved", + "status": "pass" + }, + { + "id": "casting-pair-valid", + "status": "pass" + }, + { + "id": "state-backend-preserved", + "status": "pass" + }, + { + "id": "ai-team-migrated", + "status": "skipped", + "detail": "not an .ai-team shape" + }, + { + "id": "team-root-absent", + "status": "pass" + }, + { + "id": "idempotent-second-upgrade", + "status": "pass" + }, + { + "id": "downgrade-probe", + "status": "pass" + }, + { + "id": "clients-projected", + "status": "pass" + }, + { + "id": "user-config-preserved", + "status": "pass" + }, + { + "id": "doctor", + "status": "pass" + }, + { + "id": "projection-uninstall-byte-exact", + "status": "pass" + }, + { + "id": "profile-untouched", + "status": "skipped", + "detail": "no profile squad seeded in this shape" + } + ], + "notes": [ + "plain upgrade changed 56 working-tree path(s), all Squad-owned: .github/agents/squad.agent.md, .github/agents/squad.agent.md.local-backup, .github/skills/coordinator-response-mode/SKILL.md, .github/skills/coordinator-source-of-truth/SKILL.md … (+52); squad-state blobs for 3 seeded path(s) identical", + "old 0.13.1 upgrade --dry-run on upgraded project: exit 0, last lines \"overwrite ../.github/workflows/squad-triage.yml | overwrite ../.github/workflows/sync-squad-labels.yml | Run without --dry-run to apply.\"; modified nothing" + ] + }, + { + "producer": "1.0.0", + "source": "npm", + "shape": "default", + "status": "pass", + "provenance": "npm: @bradygaster/squad-cli@1.0.0 sha512-mnZXnA+GAtp4KSLf7a4Zw0BpeOaHFAxYV5oxAy58ancZ9C2LguUGbcTxWIG0djM96XWET8K9fcncif3Vm6Z9RQ==; @bradygaster/squad-sdk@1.0.0 sha512-9f4NthJaQDq6ifXU3f6VfsZz2bbWs2u57fHhXKtDgDMSvZ3Xq8FPjMvddxhRvseWqaSlkYcWsag3M/lzTtKaJA==", + "checks": [ + { + "id": "producer-artifact-verified", + "status": "pass" + }, + { + "id": "producer-init", + "status": "pass" + }, + { + "id": "upgrade-plain", + "status": "pass" + }, + { + "id": "coordinator-is-new-template", + "status": "pass" + }, + { + "id": "user-state-preserved", + "status": "pass" + }, + { + "id": "casting-pair-valid", + "status": "pass" + }, + { + "id": "state-backend-preserved", + "status": "pass" + }, + { + "id": "ai-team-migrated", + "status": "skipped", + "detail": "not an .ai-team shape" + }, + { + "id": "team-root-absent", + "status": "pass" + }, + { + "id": "idempotent-second-upgrade", + "status": "pass" + }, + { + "id": "downgrade-probe", + "status": "pass" + }, + { + "id": "clients-projected", + "status": "pass" + }, + { + "id": "user-config-preserved", + "status": "pass" + }, + { + "id": "doctor", + "status": "pass" + }, + { + "id": "projection-uninstall-byte-exact", + "status": "pass" + }, + { + "id": "profile-untouched", + "status": "skipped", + "detail": "no profile squad seeded in this shape" + } + ], + "notes": [ + "plain upgrade changed 38 working-tree path(s), all Squad-owned: .github/agents/squad.agent.md, .github/agents/squad.agent.md.local-backup, .github/skills/coordinator-source-of-truth/SKILL.md, .github/workflows/squad-ci.yml … (+34)", + "old 1.0.0 upgrade --dry-run on upgraded project: exit 0, last lines \"overwrite ../.github/workflows/squad-triage.yml | overwrite ../.github/workflows/sync-squad-labels.yml | Run without --dry-run to apply.\"; modified nothing" + ] + }, + { + "producer": "1.0.0", + "source": "npm", + "shape": "profile", + "status": "pass", + "provenance": "npm: @bradygaster/squad-cli@1.0.0 sha512-mnZXnA+GAtp4KSLf7a4Zw0BpeOaHFAxYV5oxAy58ancZ9C2LguUGbcTxWIG0djM96XWET8K9fcncif3Vm6Z9RQ==; @bradygaster/squad-sdk@1.0.0 sha512-9f4NthJaQDq6ifXU3f6VfsZz2bbWs2u57fHhXKtDgDMSvZ3Xq8FPjMvddxhRvseWqaSlkYcWsag3M/lzTtKaJA==", + "checks": [ + { + "id": "producer-artifact-verified", + "status": "pass" + }, + { + "id": "producer-init", + "status": "pass" + }, + { + "id": "upgrade-plain", + "status": "pass" + }, + { + "id": "coordinator-is-new-template", + "status": "pass" + }, + { + "id": "user-state-preserved", + "status": "pass" + }, + { + "id": "casting-pair-valid", + "status": "pass" + }, + { + "id": "state-backend-preserved", + "status": "pass" + }, + { + "id": "ai-team-migrated", + "status": "skipped", + "detail": "not an .ai-team shape" + }, + { + "id": "team-root-absent", + "status": "pass" + }, + { + "id": "idempotent-second-upgrade", + "status": "pass" + }, + { + "id": "downgrade-probe", + "status": "pass" + }, + { + "id": "clients-projected", + "status": "pass" + }, + { + "id": "user-config-preserved", + "status": "pass" + }, + { + "id": "doctor", + "status": "pass" + }, + { + "id": "projection-uninstall-byte-exact", + "status": "pass" + }, + { + "id": "profile-untouched", + "status": "pass" + } + ], + "notes": [ + "producer wrote teamRoot=\\squad at init", + "plain upgrade changed 39 working-tree path(s), all Squad-owned: .github/agents/squad.agent.md, .github/agents/squad.agent.md.local-backup, .github/skills/coordinator-source-of-truth/SKILL.md, .github/workflows/squad-ci.yml … (+35)", + "old 1.0.0 upgrade --dry-run on upgraded project: exit 0, last lines \"overwrite ../.github/workflows/squad-triage.yml | overwrite ../.github/workflows/sync-squad-labels.yml | Run without --dry-run to apply.\"; modified nothing" + ] + }, + { + "producer": "1.0.0", + "source": "npm", + "shape": "upstream", + "status": "skipped", + "reason": "Upstream inheritance (`.squad/upstream.json`, `squad upstream add`) is a Squad 2.0 feature; 1.0.0 cannot declare an upstream, so there is nothing for the upgrade to preserve.", + "provenance": "npm: @bradygaster/squad-cli@1.0.0 sha512-mnZXnA+GAtp4KSLf7a4Zw0BpeOaHFAxYV5oxAy58ancZ9C2LguUGbcTxWIG0djM96XWET8K9fcncif3Vm6Z9RQ==; @bradygaster/squad-sdk@1.0.0 sha512-9f4NthJaQDq6ifXU3f6VfsZz2bbWs2u57fHhXKtDgDMSvZ3Xq8FPjMvddxhRvseWqaSlkYcWsag3M/lzTtKaJA==", + "checks": [], + "notes": [] + }, + { + "producer": "1.0.0", + "source": "npm", + "shape": "legacy-casting", + "status": "pass", + "provenance": "npm: @bradygaster/squad-cli@1.0.0 sha512-mnZXnA+GAtp4KSLf7a4Zw0BpeOaHFAxYV5oxAy58ancZ9C2LguUGbcTxWIG0djM96XWET8K9fcncif3Vm6Z9RQ==; @bradygaster/squad-sdk@1.0.0 sha512-9f4NthJaQDq6ifXU3f6VfsZz2bbWs2u57fHhXKtDgDMSvZ3Xq8FPjMvddxhRvseWqaSlkYcWsag3M/lzTtKaJA==", + "checks": [ + { + "id": "producer-artifact-verified", + "status": "pass" + }, + { + "id": "producer-init", + "status": "pass" + }, + { + "id": "upgrade-plain", + "status": "pass" + }, + { + "id": "coordinator-is-new-template", + "status": "pass" + }, + { + "id": "user-state-preserved", + "status": "pass" + }, + { + "id": "casting-pair-valid", + "status": "pass" + }, + { + "id": "state-backend-preserved", + "status": "pass" + }, + { + "id": "ai-team-migrated", + "status": "skipped", + "detail": "not an .ai-team shape" + }, + { + "id": "team-root-absent", + "status": "pass" + }, + { + "id": "idempotent-second-upgrade", + "status": "pass" + }, + { + "id": "downgrade-probe", + "status": "pass" + }, + { + "id": "clients-projected", + "status": "pass" + }, + { + "id": "user-config-preserved", + "status": "pass" + }, + { + "id": "doctor", + "status": "pass" + }, + { + "id": "projection-uninstall-byte-exact", + "status": "pass" + }, + { + "id": "profile-untouched", + "status": "skipped", + "detail": "no profile squad seeded in this shape" + } + ], + "notes": [ + "plain upgrade changed 43 working-tree path(s), all Squad-owned: .github/agents/squad.agent.md, .github/agents/squad.agent.md.local-backup, .github/skills/coordinator-source-of-truth/SKILL.md, .github/workflows/squad-ci.yml … (+39)", + "old 1.0.0 upgrade --dry-run on upgraded project: exit 0, last lines \"overwrite ../.github/workflows/squad-triage.yml | overwrite ../.github/workflows/sync-squad-labels.yml | Run without --dry-run to apply.\"; modified nothing" + ] + }, + { + "producer": "1.0.0", + "source": "npm", + "shape": "ai-team", + "status": "pass", + "provenance": "npm: @bradygaster/squad-cli@1.0.0 sha512-mnZXnA+GAtp4KSLf7a4Zw0BpeOaHFAxYV5oxAy58ancZ9C2LguUGbcTxWIG0djM96XWET8K9fcncif3Vm6Z9RQ==; @bradygaster/squad-sdk@1.0.0 sha512-9f4NthJaQDq6ifXU3f6VfsZz2bbWs2u57fHhXKtDgDMSvZ3Xq8FPjMvddxhRvseWqaSlkYcWsag3M/lzTtKaJA==", + "checks": [ + { + "id": "producer-artifact-verified", + "status": "pass" + }, + { + "id": "producer-init", + "status": "pass" + }, + { + "id": "upgrade-plain", + "status": "pass" + }, + { + "id": "coordinator-is-new-template", + "status": "pass" + }, + { + "id": "user-state-preserved", + "status": "pass" + }, + { + "id": "casting-pair-valid", + "status": "pass" + }, + { + "id": "state-backend-preserved", + "status": "pass" + }, + { + "id": "ai-team-migrated", + "status": "pass" + }, + { + "id": "team-root-absent", + "status": "pass" + }, + { + "id": "idempotent-second-upgrade", + "status": "pass" + }, + { + "id": "downgrade-probe", + "status": "pass" + }, + { + "id": "clients-projected", + "status": "pass" + }, + { + "id": "user-config-preserved", + "status": "pass" + }, + { + "id": "doctor", + "status": "pass" + }, + { + "id": "projection-uninstall-byte-exact", + "status": "pass" + }, + { + "id": "profile-untouched", + "status": "skipped", + "detail": "no profile squad seeded in this shape" + } + ], + "notes": [ + "plain upgrade changed 38 working-tree path(s), all Squad-owned: .github/agents/squad.agent.md, .github/agents/squad.agent.md.local-backup, .github/skills/coordinator-source-of-truth/SKILL.md, .github/workflows/squad-ci.yml … (+34)", + "old 1.0.0 upgrade --dry-run on upgraded project: exit 0, last lines \"overwrite ../.github/workflows/squad-triage.yml | overwrite ../.github/workflows/sync-squad-labels.yml | Run without --dry-run to apply.\"; modified nothing" + ] + }, + { + "producer": "1.0.1", + "source": "github-release", + "shape": "default", + "status": "pass", + "provenance": "github-release: bradygaster/squad@v1.0.1 asset squad-win32-x64.zip id 610330559 sha256 f38a8f85e60982dba340187b761488f01de1aec68ea50c1b85725783d32df2fe (bundled node 22.20.0)", + "checks": [ + { + "id": "producer-artifact-verified", + "status": "pass" + }, + { + "id": "producer-init", + "status": "pass" + }, + { + "id": "upgrade-plain", + "status": "pass" + }, + { + "id": "coordinator-is-new-template", + "status": "pass" + }, + { + "id": "user-state-preserved", + "status": "pass" + }, + { + "id": "casting-pair-valid", + "status": "pass" + }, + { + "id": "state-backend-preserved", + "status": "pass" + }, + { + "id": "ai-team-migrated", + "status": "skipped", + "detail": "not an .ai-team shape" + }, + { + "id": "team-root-absent", + "status": "pass" + }, + { + "id": "idempotent-second-upgrade", + "status": "pass" + }, + { + "id": "downgrade-probe", + "status": "pass" + }, + { + "id": "clients-projected", + "status": "pass" + }, + { + "id": "user-config-preserved", + "status": "pass" + }, + { + "id": "doctor", + "status": "pass" + }, + { + "id": "projection-uninstall-byte-exact", + "status": "pass" + }, + { + "id": "profile-untouched", + "status": "skipped", + "detail": "no profile squad seeded in this shape" + } + ], + "notes": [ + "plain upgrade changed 38 working-tree path(s), all Squad-owned: .github/agents/squad.agent.md, .github/agents/squad.agent.md.local-backup, .github/skills/coordinator-source-of-truth/SKILL.md, .github/workflows/squad-ci.yml … (+34)", + "old 1.0.1 upgrade --dry-run on upgraded project: exit 0, last lines \"overwrite ../.github/workflows/squad-triage.yml | overwrite ../.github/workflows/sync-squad-labels.yml | Run without --dry-run to apply.\"; modified nothing" + ] + }, + { + "producer": "1.0.0", + "source": "npm", + "shape": "orphan", + "status": "pass", + "provenance": "npm: @bradygaster/squad-cli@1.0.0 sha512-mnZXnA+GAtp4KSLf7a4Zw0BpeOaHFAxYV5oxAy58ancZ9C2LguUGbcTxWIG0djM96XWET8K9fcncif3Vm6Z9RQ==; @bradygaster/squad-sdk@1.0.0 sha512-9f4NthJaQDq6ifXU3f6VfsZz2bbWs2u57fHhXKtDgDMSvZ3Xq8FPjMvddxhRvseWqaSlkYcWsag3M/lzTtKaJA==", + "checks": [ + { + "id": "producer-artifact-verified", + "status": "pass" + }, + { + "id": "producer-init", + "status": "pass" + }, + { + "id": "upgrade-plain", + "status": "pass" + }, + { + "id": "coordinator-is-new-template", + "status": "pass" + }, + { + "id": "user-state-preserved", + "status": "pass" + }, + { + "id": "casting-pair-valid", + "status": "pass" + }, + { + "id": "state-backend-preserved", + "status": "pass" + }, + { + "id": "ai-team-migrated", + "status": "skipped", + "detail": "not an .ai-team shape" + }, + { + "id": "team-root-absent", + "status": "pass" + }, + { + "id": "idempotent-second-upgrade", + "status": "pass" + }, + { + "id": "downgrade-probe", + "status": "pass" + }, + { + "id": "clients-projected", + "status": "pass" + }, + { + "id": "user-config-preserved", + "status": "pass" + }, + { + "id": "doctor", + "status": "pass" + }, + { + "id": "projection-uninstall-byte-exact", + "status": "pass" + }, + { + "id": "profile-untouched", + "status": "skipped", + "detail": "no profile squad seeded in this shape" + } + ], + "notes": [ + "plain upgrade changed 36 working-tree path(s), all Squad-owned: .github/agents/squad.agent.md, .github/agents/squad.agent.md.local-backup, .github/skills/coordinator-source-of-truth/SKILL.md, .github/workflows/squad-ci.yml … (+32); squad-state blobs for 3 seeded path(s) identical", + "old 1.0.0 upgrade --dry-run on upgraded project: exit 0, last lines \"overwrite ../.github/workflows/squad-triage.yml | overwrite ../.github/workflows/sync-squad-labels.yml | Run without --dry-run to apply.\"; modified nothing" + ] + }, + { + "producer": "1.0.0", + "source": "npm", + "shape": "two-layer", + "status": "pass", + "provenance": "npm: @bradygaster/squad-cli@1.0.0 sha512-mnZXnA+GAtp4KSLf7a4Zw0BpeOaHFAxYV5oxAy58ancZ9C2LguUGbcTxWIG0djM96XWET8K9fcncif3Vm6Z9RQ==; @bradygaster/squad-sdk@1.0.0 sha512-9f4NthJaQDq6ifXU3f6VfsZz2bbWs2u57fHhXKtDgDMSvZ3Xq8FPjMvddxhRvseWqaSlkYcWsag3M/lzTtKaJA==", + "checks": [ + { + "id": "producer-artifact-verified", + "status": "pass" + }, + { + "id": "producer-init", + "status": "pass" + }, + { + "id": "upgrade-plain", + "status": "pass" + }, + { + "id": "coordinator-is-new-template", + "status": "pass" + }, + { + "id": "user-state-preserved", + "status": "pass" + }, + { + "id": "casting-pair-valid", + "status": "pass" + }, + { + "id": "state-backend-preserved", + "status": "pass" + }, + { + "id": "ai-team-migrated", + "status": "skipped", + "detail": "not an .ai-team shape" + }, + { + "id": "team-root-absent", + "status": "pass" + }, + { + "id": "idempotent-second-upgrade", + "status": "pass" + }, + { + "id": "downgrade-probe", + "status": "pass" + }, + { + "id": "clients-projected", + "status": "pass" + }, + { + "id": "user-config-preserved", + "status": "pass" + }, + { + "id": "doctor", + "status": "pass" + }, + { + "id": "projection-uninstall-byte-exact", + "status": "pass" + }, + { + "id": "profile-untouched", + "status": "skipped", + "detail": "no profile squad seeded in this shape" + } + ], + "notes": [ + "plain upgrade changed 36 working-tree path(s), all Squad-owned: .github/agents/squad.agent.md, .github/agents/squad.agent.md.local-backup, .github/skills/coordinator-source-of-truth/SKILL.md, .github/workflows/squad-ci.yml … (+32); squad-state blobs for 3 seeded path(s) identical", + "old 1.0.0 upgrade --dry-run on upgraded project: exit 0, last lines \"overwrite ../.github/workflows/squad-triage.yml | overwrite ../.github/workflows/sync-squad-labels.yml | Run without --dry-run to apply.\"; modified nothing" + ] + }, + { + "producer": "1.0.1", + "source": "github-release", + "shape": "profile", + "status": "pass", + "provenance": "github-release: bradygaster/squad@v1.0.1 asset squad-win32-x64.zip id 610330559 sha256 f38a8f85e60982dba340187b761488f01de1aec68ea50c1b85725783d32df2fe (bundled node 22.20.0)", + "checks": [ + { + "id": "producer-artifact-verified", + "status": "pass" + }, + { + "id": "producer-init", + "status": "pass" + }, + { + "id": "upgrade-plain", + "status": "pass" + }, + { + "id": "coordinator-is-new-template", + "status": "pass" + }, + { + "id": "user-state-preserved", + "status": "pass" + }, + { + "id": "casting-pair-valid", + "status": "pass" + }, + { + "id": "state-backend-preserved", + "status": "pass" + }, + { + "id": "ai-team-migrated", + "status": "skipped", + "detail": "not an .ai-team shape" + }, + { + "id": "team-root-absent", + "status": "pass" + }, + { + "id": "idempotent-second-upgrade", + "status": "pass" + }, + { + "id": "downgrade-probe", + "status": "pass" + }, + { + "id": "clients-projected", + "status": "pass" + }, + { + "id": "user-config-preserved", + "status": "pass" + }, + { + "id": "doctor", + "status": "pass" + }, + { + "id": "projection-uninstall-byte-exact", + "status": "pass" + }, + { + "id": "profile-untouched", + "status": "pass" + } + ], + "notes": [ + "producer wrote teamRoot=\\squad at init", + "plain upgrade changed 39 working-tree path(s), all Squad-owned: .github/agents/squad.agent.md, .github/agents/squad.agent.md.local-backup, .github/skills/coordinator-source-of-truth/SKILL.md, .github/workflows/squad-ci.yml … (+35)", + "old 1.0.1 upgrade --dry-run on upgraded project: exit 0, last lines \"overwrite ../.github/workflows/squad-triage.yml | overwrite ../.github/workflows/sync-squad-labels.yml | Run without --dry-run to apply.\"; modified nothing" + ] + }, + { + "producer": "1.0.1", + "source": "github-release", + "shape": "upstream", + "status": "skipped", + "reason": "Upstream inheritance (`.squad/upstream.json`, `squad upstream add`) is a Squad 2.0 feature; 1.0.1 cannot declare an upstream, so there is nothing for the upgrade to preserve.", + "provenance": "github-release: bradygaster/squad@v1.0.1 asset squad-win32-x64.zip id 610330559 sha256 f38a8f85e60982dba340187b761488f01de1aec68ea50c1b85725783d32df2fe (bundled node 22.20.0)", + "checks": [], + "notes": [] + }, + { + "producer": "1.0.1", + "source": "github-release", + "shape": "legacy-casting", + "status": "pass", + "provenance": "github-release: bradygaster/squad@v1.0.1 asset squad-win32-x64.zip id 610330559 sha256 f38a8f85e60982dba340187b761488f01de1aec68ea50c1b85725783d32df2fe (bundled node 22.20.0)", + "checks": [ + { + "id": "producer-artifact-verified", + "status": "pass" + }, + { + "id": "producer-init", + "status": "pass" + }, + { + "id": "upgrade-plain", + "status": "pass" + }, + { + "id": "coordinator-is-new-template", + "status": "pass" + }, + { + "id": "user-state-preserved", + "status": "pass" + }, + { + "id": "casting-pair-valid", + "status": "pass" + }, + { + "id": "state-backend-preserved", + "status": "pass" + }, + { + "id": "ai-team-migrated", + "status": "skipped", + "detail": "not an .ai-team shape" + }, + { + "id": "team-root-absent", + "status": "pass" + }, + { + "id": "idempotent-second-upgrade", + "status": "pass" + }, + { + "id": "downgrade-probe", + "status": "pass" + }, + { + "id": "clients-projected", + "status": "pass" + }, + { + "id": "user-config-preserved", + "status": "pass" + }, + { + "id": "doctor", + "status": "pass" + }, + { + "id": "projection-uninstall-byte-exact", + "status": "pass" + }, + { + "id": "profile-untouched", + "status": "skipped", + "detail": "no profile squad seeded in this shape" + } + ], + "notes": [ + "plain upgrade changed 43 working-tree path(s), all Squad-owned: .github/agents/squad.agent.md, .github/agents/squad.agent.md.local-backup, .github/skills/coordinator-source-of-truth/SKILL.md, .github/workflows/squad-ci.yml … (+39)", + "old 1.0.1 upgrade --dry-run on upgraded project: exit 0, last lines \"overwrite ../.github/workflows/squad-triage.yml | overwrite ../.github/workflows/sync-squad-labels.yml | Run without --dry-run to apply.\"; modified nothing" + ] + }, + { + "producer": "1.0.1", + "source": "github-release", + "shape": "ai-team", + "status": "pass", + "provenance": "github-release: bradygaster/squad@v1.0.1 asset squad-win32-x64.zip id 610330559 sha256 f38a8f85e60982dba340187b761488f01de1aec68ea50c1b85725783d32df2fe (bundled node 22.20.0)", + "checks": [ + { + "id": "producer-artifact-verified", + "status": "pass" + }, + { + "id": "producer-init", + "status": "pass" + }, + { + "id": "upgrade-plain", + "status": "pass" + }, + { + "id": "coordinator-is-new-template", + "status": "pass" + }, + { + "id": "user-state-preserved", + "status": "pass" + }, + { + "id": "casting-pair-valid", + "status": "pass" + }, + { + "id": "state-backend-preserved", + "status": "pass" + }, + { + "id": "ai-team-migrated", + "status": "pass" + }, + { + "id": "team-root-absent", + "status": "pass" + }, + { + "id": "idempotent-second-upgrade", + "status": "pass" + }, + { + "id": "downgrade-probe", + "status": "pass" + }, + { + "id": "clients-projected", + "status": "pass" + }, + { + "id": "user-config-preserved", + "status": "pass" + }, + { + "id": "doctor", + "status": "pass" + }, + { + "id": "projection-uninstall-byte-exact", + "status": "pass" + }, + { + "id": "profile-untouched", + "status": "skipped", + "detail": "no profile squad seeded in this shape" + } + ], + "notes": [ + "plain upgrade changed 38 working-tree path(s), all Squad-owned: .github/agents/squad.agent.md, .github/agents/squad.agent.md.local-backup, .github/skills/coordinator-source-of-truth/SKILL.md, .github/workflows/squad-ci.yml … (+34)", + "old 1.0.1 upgrade --dry-run on upgraded project: exit 0, last lines \"overwrite ../.github/workflows/squad-triage.yml | overwrite ../.github/workflows/sync-squad-labels.yml | Run without --dry-run to apply.\"; modified nothing" + ] + }, + { + "producer": "1.1.0", + "source": "github-release", + "shape": "default", + "status": "pass", + "provenance": "github-release: bradygaster/squad@v1.1.0 asset squad-win32-x64.zip id 659607643 sha256 571d734f7ae718fefefdfa9864f9f82a3d1fb1546f01ec8bf8866fa7e1829c57 (bundled node 22.20.0)", + "checks": [ + { + "id": "producer-artifact-verified", + "status": "pass" + }, + { + "id": "producer-init", + "status": "pass" + }, + { + "id": "upgrade-plain", + "status": "pass" + }, + { + "id": "coordinator-is-new-template", + "status": "pass" + }, + { + "id": "user-state-preserved", + "status": "pass" + }, + { + "id": "casting-pair-valid", + "status": "pass" + }, + { + "id": "state-backend-preserved", + "status": "pass" + }, + { + "id": "ai-team-migrated", + "status": "skipped", + "detail": "not an .ai-team shape" + }, + { + "id": "team-root-absent", + "status": "pass" + }, + { + "id": "idempotent-second-upgrade", + "status": "pass" + }, + { + "id": "downgrade-probe", + "status": "pass" + }, + { + "id": "clients-projected", + "status": "pass" + }, + { + "id": "user-config-preserved", + "status": "pass" + }, + { + "id": "doctor", + "status": "pass" + }, + { + "id": "projection-uninstall-byte-exact", + "status": "pass" + }, + { + "id": "profile-untouched", + "status": "skipped", + "detail": "no profile squad seeded in this shape" + } + ], + "notes": [ + "plain upgrade changed 31 working-tree path(s), all Squad-owned: .github/agents/squad.agent.md, .github/agents/squad.agent.md.local-backup, .github/workflows/squad-ci.yml, .github/workflows/squad-ci.yml.local-backup … (+27)", + "old 1.1.0 upgrade --dry-run on upgraded project: exit 0, last lines \"overwrite ../.github/workflows/squad-triage.yml | overwrite ../.github/workflows/sync-squad-labels.yml | Run without --dry-run to apply.\"; modified nothing" + ] + }, + { + "producer": "1.0.1", + "source": "github-release", + "shape": "orphan", + "status": "pass", + "provenance": "github-release: bradygaster/squad@v1.0.1 asset squad-win32-x64.zip id 610330559 sha256 f38a8f85e60982dba340187b761488f01de1aec68ea50c1b85725783d32df2fe (bundled node 22.20.0)", + "checks": [ + { + "id": "producer-artifact-verified", + "status": "pass" + }, + { + "id": "producer-init", + "status": "pass" + }, + { + "id": "upgrade-plain", + "status": "pass" + }, + { + "id": "coordinator-is-new-template", + "status": "pass" + }, + { + "id": "user-state-preserved", + "status": "pass" + }, + { + "id": "casting-pair-valid", + "status": "pass" + }, + { + "id": "state-backend-preserved", + "status": "pass" + }, + { + "id": "ai-team-migrated", + "status": "skipped", + "detail": "not an .ai-team shape" + }, + { + "id": "team-root-absent", + "status": "pass" + }, + { + "id": "idempotent-second-upgrade", + "status": "pass" + }, + { + "id": "downgrade-probe", + "status": "pass" + }, + { + "id": "clients-projected", + "status": "pass" + }, + { + "id": "user-config-preserved", + "status": "pass" + }, + { + "id": "doctor", + "status": "pass" + }, + { + "id": "projection-uninstall-byte-exact", + "status": "pass" + }, + { + "id": "profile-untouched", + "status": "skipped", + "detail": "no profile squad seeded in this shape" + } + ], + "notes": [ + "plain upgrade changed 36 working-tree path(s), all Squad-owned: .github/agents/squad.agent.md, .github/agents/squad.agent.md.local-backup, .github/skills/coordinator-source-of-truth/SKILL.md, .github/workflows/squad-ci.yml … (+32); squad-state blobs for 3 seeded path(s) identical", + "old 1.0.1 upgrade --dry-run on upgraded project: exit 0, last lines \"overwrite ../.github/workflows/squad-triage.yml | overwrite ../.github/workflows/sync-squad-labels.yml | Run without --dry-run to apply.\"; modified nothing" + ] + }, + { + "producer": "1.0.1", + "source": "github-release", + "shape": "two-layer", + "status": "pass", + "provenance": "github-release: bradygaster/squad@v1.0.1 asset squad-win32-x64.zip id 610330559 sha256 f38a8f85e60982dba340187b761488f01de1aec68ea50c1b85725783d32df2fe (bundled node 22.20.0)", + "checks": [ + { + "id": "producer-artifact-verified", + "status": "pass" + }, + { + "id": "producer-init", + "status": "pass" + }, + { + "id": "upgrade-plain", + "status": "pass" + }, + { + "id": "coordinator-is-new-template", + "status": "pass" + }, + { + "id": "user-state-preserved", + "status": "pass" + }, + { + "id": "casting-pair-valid", + "status": "pass" + }, + { + "id": "state-backend-preserved", + "status": "pass" + }, + { + "id": "ai-team-migrated", + "status": "skipped", + "detail": "not an .ai-team shape" + }, + { + "id": "team-root-absent", + "status": "pass" + }, + { + "id": "idempotent-second-upgrade", + "status": "pass" + }, + { + "id": "downgrade-probe", + "status": "pass" + }, + { + "id": "clients-projected", + "status": "pass" + }, + { + "id": "user-config-preserved", + "status": "pass" + }, + { + "id": "doctor", + "status": "pass" + }, + { + "id": "projection-uninstall-byte-exact", + "status": "pass" + }, + { + "id": "profile-untouched", + "status": "skipped", + "detail": "no profile squad seeded in this shape" + } + ], + "notes": [ + "plain upgrade changed 36 working-tree path(s), all Squad-owned: .github/agents/squad.agent.md, .github/agents/squad.agent.md.local-backup, .github/skills/coordinator-source-of-truth/SKILL.md, .github/workflows/squad-ci.yml … (+32); squad-state blobs for 3 seeded path(s) identical", + "old 1.0.1 upgrade --dry-run on upgraded project: exit 0, last lines \"overwrite ../.github/workflows/squad-triage.yml | overwrite ../.github/workflows/sync-squad-labels.yml | Run without --dry-run to apply.\"; modified nothing" + ] + }, + { + "producer": "1.1.0", + "source": "github-release", + "shape": "profile", + "status": "pass", + "provenance": "github-release: bradygaster/squad@v1.1.0 asset squad-win32-x64.zip id 659607643 sha256 571d734f7ae718fefefdfa9864f9f82a3d1fb1546f01ec8bf8866fa7e1829c57 (bundled node 22.20.0)", + "checks": [ + { + "id": "producer-artifact-verified", + "status": "pass" + }, + { + "id": "producer-init", + "status": "pass" + }, + { + "id": "upgrade-plain", + "status": "pass" + }, + { + "id": "coordinator-is-new-template", + "status": "pass" + }, + { + "id": "user-state-preserved", + "status": "pass" + }, + { + "id": "casting-pair-valid", + "status": "pass" + }, + { + "id": "state-backend-preserved", + "status": "pass" + }, + { + "id": "ai-team-migrated", + "status": "skipped", + "detail": "not an .ai-team shape" + }, + { + "id": "team-root-absent", + "status": "pass" + }, + { + "id": "idempotent-second-upgrade", + "status": "pass" + }, + { + "id": "downgrade-probe", + "status": "pass" + }, + { + "id": "clients-projected", + "status": "pass" + }, + { + "id": "user-config-preserved", + "status": "pass" + }, + { + "id": "doctor", + "status": "pass" + }, + { + "id": "projection-uninstall-byte-exact", + "status": "pass" + }, + { + "id": "profile-untouched", + "status": "pass" + } + ], + "notes": [ + "producer wrote teamRoot=\\squad at init", + "plain upgrade changed 32 working-tree path(s), all Squad-owned: .github/agents/squad.agent.md, .github/agents/squad.agent.md.local-backup, .github/workflows/squad-ci.yml, .github/workflows/squad-ci.yml.local-backup … (+28)", + "old 1.1.0 upgrade --dry-run on upgraded project: exit 0, last lines \"overwrite ../.github/workflows/squad-triage.yml | overwrite ../.github/workflows/sync-squad-labels.yml | Run without --dry-run to apply.\"; modified nothing" + ] + }, + { + "producer": "1.1.0", + "source": "github-release", + "shape": "upstream", + "status": "skipped", + "reason": "Upstream inheritance (`.squad/upstream.json`, `squad upstream add`) is a Squad 2.0 feature; 1.1.0 cannot declare an upstream, so there is nothing for the upgrade to preserve.", + "provenance": "github-release: bradygaster/squad@v1.1.0 asset squad-win32-x64.zip id 659607643 sha256 571d734f7ae718fefefdfa9864f9f82a3d1fb1546f01ec8bf8866fa7e1829c57 (bundled node 22.20.0)", + "checks": [], + "notes": [] + }, + { + "producer": "1.1.0", + "source": "github-release", + "shape": "legacy-casting", + "status": "pass", + "provenance": "github-release: bradygaster/squad@v1.1.0 asset squad-win32-x64.zip id 659607643 sha256 571d734f7ae718fefefdfa9864f9f82a3d1fb1546f01ec8bf8866fa7e1829c57 (bundled node 22.20.0)", + "checks": [ + { + "id": "producer-artifact-verified", + "status": "pass" + }, + { + "id": "producer-init", + "status": "pass" + }, + { + "id": "upgrade-plain", + "status": "pass" + }, + { + "id": "coordinator-is-new-template", + "status": "pass" + }, + { + "id": "user-state-preserved", + "status": "pass" + }, + { + "id": "casting-pair-valid", + "status": "pass" + }, + { + "id": "state-backend-preserved", + "status": "pass" + }, + { + "id": "ai-team-migrated", + "status": "skipped", + "detail": "not an .ai-team shape" + }, + { + "id": "team-root-absent", + "status": "pass" + }, + { + "id": "idempotent-second-upgrade", + "status": "pass" + }, + { + "id": "downgrade-probe", + "status": "pass" + }, + { + "id": "clients-projected", + "status": "pass" + }, + { + "id": "user-config-preserved", + "status": "pass" + }, + { + "id": "doctor", + "status": "pass" + }, + { + "id": "projection-uninstall-byte-exact", + "status": "pass" + }, + { + "id": "profile-untouched", + "status": "skipped", + "detail": "no profile squad seeded in this shape" + } + ], + "notes": [ + "plain upgrade changed 36 working-tree path(s), all Squad-owned: .github/agents/squad.agent.md, .github/agents/squad.agent.md.local-backup, .github/workflows/squad-ci.yml, .github/workflows/squad-ci.yml.local-backup … (+32)", + "old 1.1.0 upgrade --dry-run on upgraded project: exit 0, last lines \"overwrite ../.github/workflows/squad-triage.yml | overwrite ../.github/workflows/sync-squad-labels.yml | Run without --dry-run to apply.\"; modified nothing" + ] + }, + { + "producer": "1.1.0", + "source": "github-release", + "shape": "ai-team", + "status": "pass", + "provenance": "github-release: bradygaster/squad@v1.1.0 asset squad-win32-x64.zip id 659607643 sha256 571d734f7ae718fefefdfa9864f9f82a3d1fb1546f01ec8bf8866fa7e1829c57 (bundled node 22.20.0)", + "checks": [ + { + "id": "producer-artifact-verified", + "status": "pass" + }, + { + "id": "producer-init", + "status": "pass" + }, + { + "id": "upgrade-plain", + "status": "pass" + }, + { + "id": "coordinator-is-new-template", + "status": "pass" + }, + { + "id": "user-state-preserved", + "status": "pass" + }, + { + "id": "casting-pair-valid", + "status": "pass" + }, + { + "id": "state-backend-preserved", + "status": "pass" + }, + { + "id": "ai-team-migrated", + "status": "pass" + }, + { + "id": "team-root-absent", + "status": "pass" + }, + { + "id": "idempotent-second-upgrade", + "status": "pass" + }, + { + "id": "downgrade-probe", + "status": "pass" + }, + { + "id": "clients-projected", + "status": "pass" + }, + { + "id": "user-config-preserved", + "status": "pass" + }, + { + "id": "doctor", + "status": "pass" + }, + { + "id": "projection-uninstall-byte-exact", + "status": "pass" + }, + { + "id": "profile-untouched", + "status": "skipped", + "detail": "no profile squad seeded in this shape" + } + ], + "notes": [ + "plain upgrade changed 31 working-tree path(s), all Squad-owned: .github/agents/squad.agent.md, .github/agents/squad.agent.md.local-backup, .github/workflows/squad-ci.yml, .github/workflows/squad-ci.yml.local-backup … (+27)", + "old 1.1.0 upgrade --dry-run on upgraded project: exit 0, last lines \"overwrite ../.github/workflows/squad-triage.yml | overwrite ../.github/workflows/sync-squad-labels.yml | Run without --dry-run to apply.\"; modified nothing" + ] + }, + { + "producer": "1.1.0", + "source": "github-release", + "shape": "orphan", + "status": "pass", + "provenance": "github-release: bradygaster/squad@v1.1.0 asset squad-win32-x64.zip id 659607643 sha256 571d734f7ae718fefefdfa9864f9f82a3d1fb1546f01ec8bf8866fa7e1829c57 (bundled node 22.20.0)", + "checks": [ + { + "id": "producer-artifact-verified", + "status": "pass" + }, + { + "id": "producer-init", + "status": "pass" + }, + { + "id": "upgrade-plain", + "status": "pass" + }, + { + "id": "coordinator-is-new-template", + "status": "pass" + }, + { + "id": "user-state-preserved", + "status": "pass" + }, + { + "id": "casting-pair-valid", + "status": "pass" + }, + { + "id": "state-backend-preserved", + "status": "pass" + }, + { + "id": "ai-team-migrated", + "status": "skipped", + "detail": "not an .ai-team shape" + }, + { + "id": "team-root-absent", + "status": "pass" + }, + { + "id": "idempotent-second-upgrade", + "status": "pass" + }, + { + "id": "downgrade-probe", + "status": "pass" + }, + { + "id": "clients-projected", + "status": "pass" + }, + { + "id": "user-config-preserved", + "status": "pass" + }, + { + "id": "doctor", + "status": "pass" + }, + { + "id": "projection-uninstall-byte-exact", + "status": "pass" + }, + { + "id": "profile-untouched", + "status": "skipped", + "detail": "no profile squad seeded in this shape" + } + ], + "notes": [ + "plain upgrade changed 29 working-tree path(s), all Squad-owned: .github/agents/squad.agent.md, .github/agents/squad.agent.md.local-backup, .github/workflows/squad-ci.yml, .github/workflows/squad-ci.yml.local-backup … (+25); squad-state blobs for 3 seeded path(s) identical", + "old 1.1.0 upgrade --dry-run on upgraded project: exit 0, last lines \"overwrite ../.github/workflows/squad-triage.yml | overwrite ../.github/workflows/sync-squad-labels.yml | Run without --dry-run to apply.\"; modified nothing" + ] + }, + { + "producer": "HEAD", + "source": "pack", + "shape": "default", + "status": "pass", + "provenance": "pack: npm pack of packages/squad-sdk + packages/squad-cli at 9da01a2e41ba15794d31b0b8251ddaa3abd34ba8 (v1.2.0-preview.1)", + "checks": [ + { + "id": "producer-artifact-verified", + "status": "pass" + }, + { + "id": "producer-init", + "status": "pass" + }, + { + "id": "upgrade-plain", + "status": "pass" + }, + { + "id": "coordinator-is-new-template", + "status": "pass" + }, + { + "id": "user-state-preserved", + "status": "pass" + }, + { + "id": "casting-pair-valid", + "status": "pass" + }, + { + "id": "state-backend-preserved", + "status": "pass" + }, + { + "id": "ai-team-migrated", + "status": "skipped", + "detail": "not an .ai-team shape" + }, + { + "id": "team-root-absent", + "status": "pass" + }, + { + "id": "idempotent-second-upgrade", + "status": "pass" + }, + { + "id": "downgrade-probe", + "status": "skipped", + "detail": "producer is the target version" + }, + { + "id": "clients-projected", + "status": "pass" + }, + { + "id": "user-config-preserved", + "status": "pass" + }, + { + "id": "doctor", + "status": "pass" + }, + { + "id": "projection-uninstall-byte-exact", + "status": "pass" + }, + { + "id": "profile-untouched", + "status": "skipped", + "detail": "no profile squad seeded in this shape" + } + ], + "notes": [ + "plain upgrade changed 17 working-tree path(s), all Squad-owned: .github/workflows/squad-ci.yml, .github/workflows/squad-docs.yml, .github/workflows/squad-heartbeat.yml, .github/workflows/squad-insider-release.yml … (+13)" + ] + }, + { + "producer": "1.1.0", + "source": "github-release", + "shape": "two-layer", + "status": "pass", + "provenance": "github-release: bradygaster/squad@v1.1.0 asset squad-win32-x64.zip id 659607643 sha256 571d734f7ae718fefefdfa9864f9f82a3d1fb1546f01ec8bf8866fa7e1829c57 (bundled node 22.20.0)", + "checks": [ + { + "id": "producer-artifact-verified", + "status": "pass" + }, + { + "id": "producer-init", + "status": "pass" + }, + { + "id": "upgrade-plain", + "status": "pass" + }, + { + "id": "coordinator-is-new-template", + "status": "pass" + }, + { + "id": "user-state-preserved", + "status": "pass" + }, + { + "id": "casting-pair-valid", + "status": "pass" + }, + { + "id": "state-backend-preserved", + "status": "pass" + }, + { + "id": "ai-team-migrated", + "status": "skipped", + "detail": "not an .ai-team shape" + }, + { + "id": "team-root-absent", + "status": "pass" + }, + { + "id": "idempotent-second-upgrade", + "status": "pass" + }, + { + "id": "downgrade-probe", + "status": "pass" + }, + { + "id": "clients-projected", + "status": "pass" + }, + { + "id": "user-config-preserved", + "status": "pass" + }, + { + "id": "doctor", + "status": "pass" + }, + { + "id": "projection-uninstall-byte-exact", + "status": "pass" + }, + { + "id": "profile-untouched", + "status": "skipped", + "detail": "no profile squad seeded in this shape" + } + ], + "notes": [ + "plain upgrade changed 29 working-tree path(s), all Squad-owned: .github/agents/squad.agent.md, .github/agents/squad.agent.md.local-backup, .github/workflows/squad-ci.yml, .github/workflows/squad-ci.yml.local-backup … (+25); squad-state blobs for 3 seeded path(s) identical", + "old 1.1.0 upgrade --dry-run on upgraded project: exit 0, last lines \"overwrite ../.github/workflows/squad-triage.yml | overwrite ../.github/workflows/sync-squad-labels.yml | Run without --dry-run to apply.\"; modified nothing" + ] + }, + { + "producer": "HEAD", + "source": "pack", + "shape": "profile", + "status": "pass", + "provenance": "pack: npm pack of packages/squad-sdk + packages/squad-cli at 9da01a2e41ba15794d31b0b8251ddaa3abd34ba8 (v1.2.0-preview.1)", + "checks": [ + { + "id": "producer-artifact-verified", + "status": "pass" + }, + { + "id": "producer-init", + "status": "pass" + }, + { + "id": "upgrade-plain", + "status": "pass" + }, + { + "id": "coordinator-is-new-template", + "status": "pass" + }, + { + "id": "user-state-preserved", + "status": "pass" + }, + { + "id": "casting-pair-valid", + "status": "pass" + }, + { + "id": "state-backend-preserved", + "status": "pass" + }, + { + "id": "ai-team-migrated", + "status": "skipped", + "detail": "not an .ai-team shape" + }, + { + "id": "team-root-absent", + "status": "pass" + }, + { + "id": "idempotent-second-upgrade", + "status": "pass" + }, + { + "id": "downgrade-probe", + "status": "skipped", + "detail": "producer is the target version" + }, + { + "id": "clients-projected", + "status": "pass" + }, + { + "id": "user-config-preserved", + "status": "pass" + }, + { + "id": "doctor", + "status": "pass" + }, + { + "id": "projection-uninstall-byte-exact", + "status": "pass" + }, + { + "id": "profile-untouched", + "status": "pass" + } + ], + "notes": [ + "plain upgrade changed 17 working-tree path(s), all Squad-owned: .github/workflows/squad-ci.yml, .github/workflows/squad-docs.yml, .github/workflows/squad-heartbeat.yml, .github/workflows/squad-insider-release.yml … (+13)" + ] + }, + { + "producer": "HEAD", + "source": "pack", + "shape": "legacy-casting", + "status": "pass", + "provenance": "pack: npm pack of packages/squad-sdk + packages/squad-cli at 9da01a2e41ba15794d31b0b8251ddaa3abd34ba8 (v1.2.0-preview.1)", + "checks": [ + { + "id": "producer-artifact-verified", + "status": "pass" + }, + { + "id": "producer-init", + "status": "pass" + }, + { + "id": "upgrade-plain", + "status": "pass" + }, + { + "id": "coordinator-is-new-template", + "status": "pass" + }, + { + "id": "user-state-preserved", + "status": "pass" + }, + { + "id": "casting-pair-valid", + "status": "pass" + }, + { + "id": "state-backend-preserved", + "status": "pass" + }, + { + "id": "ai-team-migrated", + "status": "skipped", + "detail": "not an .ai-team shape" + }, + { + "id": "team-root-absent", + "status": "pass" + }, + { + "id": "idempotent-second-upgrade", + "status": "pass" + }, + { + "id": "downgrade-probe", + "status": "skipped", + "detail": "producer is the target version" + }, + { + "id": "clients-projected", + "status": "pass" + }, + { + "id": "user-config-preserved", + "status": "pass" + }, + { + "id": "doctor", + "status": "pass" + }, + { + "id": "projection-uninstall-byte-exact", + "status": "pass" + }, + { + "id": "profile-untouched", + "status": "skipped", + "detail": "no profile squad seeded in this shape" + } + ], + "notes": [ + "plain upgrade changed 22 working-tree path(s), all Squad-owned: .github/workflows/squad-ci.yml, .github/workflows/squad-docs.yml, .github/workflows/squad-heartbeat.yml, .github/workflows/squad-insider-release.yml … (+18)" + ] + }, + { + "producer": "HEAD", + "source": "pack", + "shape": "ai-team", + "status": "pass", + "provenance": "pack: npm pack of packages/squad-sdk + packages/squad-cli at 9da01a2e41ba15794d31b0b8251ddaa3abd34ba8 (v1.2.0-preview.1)", + "checks": [ + { + "id": "producer-artifact-verified", + "status": "pass" + }, + { + "id": "producer-init", + "status": "pass" + }, + { + "id": "upgrade-plain", + "status": "pass" + }, + { + "id": "coordinator-is-new-template", + "status": "pass" + }, + { + "id": "user-state-preserved", + "status": "pass" + }, + { + "id": "casting-pair-valid", + "status": "pass" + }, + { + "id": "state-backend-preserved", + "status": "pass" + }, + { + "id": "ai-team-migrated", + "status": "pass" + }, + { + "id": "team-root-absent", + "status": "pass" + }, + { + "id": "idempotent-second-upgrade", + "status": "pass" + }, + { + "id": "downgrade-probe", + "status": "skipped", + "detail": "producer is the target version" + }, + { + "id": "clients-projected", + "status": "pass" + }, + { + "id": "user-config-preserved", + "status": "pass" + }, + { + "id": "doctor", + "status": "pass" + }, + { + "id": "projection-uninstall-byte-exact", + "status": "pass" + }, + { + "id": "profile-untouched", + "status": "skipped", + "detail": "no profile squad seeded in this shape" + } + ], + "notes": [ + "plain upgrade changed 17 working-tree path(s), all Squad-owned: .github/workflows/squad-ci.yml, .github/workflows/squad-docs.yml, .github/workflows/squad-heartbeat.yml, .github/workflows/squad-insider-release.yml … (+13)" + ] + }, + { + "producer": "HEAD", + "source": "pack", + "shape": "orphan", + "status": "pass", + "provenance": "pack: npm pack of packages/squad-sdk + packages/squad-cli at 9da01a2e41ba15794d31b0b8251ddaa3abd34ba8 (v1.2.0-preview.1)", + "checks": [ + { + "id": "producer-artifact-verified", + "status": "pass" + }, + { + "id": "producer-init", + "status": "pass" + }, + { + "id": "upgrade-plain", + "status": "pass" + }, + { + "id": "coordinator-is-new-template", + "status": "pass" + }, + { + "id": "user-state-preserved", + "status": "pass" + }, + { + "id": "casting-pair-valid", + "status": "pass" + }, + { + "id": "state-backend-preserved", + "status": "pass" + }, + { + "id": "ai-team-migrated", + "status": "skipped", + "detail": "not an .ai-team shape" + }, + { + "id": "team-root-absent", + "status": "pass" + }, + { + "id": "idempotent-second-upgrade", + "status": "pass" + }, + { + "id": "downgrade-probe", + "status": "skipped", + "detail": "producer is the target version" + }, + { + "id": "clients-projected", + "status": "pass" + }, + { + "id": "user-config-preserved", + "status": "pass" + }, + { + "id": "doctor", + "status": "pass" + }, + { + "id": "projection-uninstall-byte-exact", + "status": "pass" + }, + { + "id": "profile-untouched", + "status": "skipped", + "detail": "no profile squad seeded in this shape" + } + ], + "notes": [ + "plain upgrade changed 15 working-tree path(s), all Squad-owned: .github/workflows/squad-ci.yml, .github/workflows/squad-docs.yml, .github/workflows/squad-heartbeat.yml, .github/workflows/squad-insider-release.yml … (+11); squad-state blobs for 3 seeded path(s) identical" + ] + }, + { + "producer": "HEAD", + "source": "pack", + "shape": "two-layer", + "status": "pass", + "provenance": "pack: npm pack of packages/squad-sdk + packages/squad-cli at 9da01a2e41ba15794d31b0b8251ddaa3abd34ba8 (v1.2.0-preview.1)", + "checks": [ + { + "id": "producer-artifact-verified", + "status": "pass" + }, + { + "id": "producer-init", + "status": "pass" + }, + { + "id": "upgrade-plain", + "status": "pass" + }, + { + "id": "coordinator-is-new-template", + "status": "pass" + }, + { + "id": "user-state-preserved", + "status": "pass" + }, + { + "id": "casting-pair-valid", + "status": "pass" + }, + { + "id": "state-backend-preserved", + "status": "pass" + }, + { + "id": "ai-team-migrated", + "status": "skipped", + "detail": "not an .ai-team shape" + }, + { + "id": "team-root-absent", + "status": "pass" + }, + { + "id": "idempotent-second-upgrade", + "status": "pass" + }, + { + "id": "downgrade-probe", + "status": "skipped", + "detail": "producer is the target version" + }, + { + "id": "clients-projected", + "status": "pass" + }, + { + "id": "user-config-preserved", + "status": "pass" + }, + { + "id": "doctor", + "status": "pass" + }, + { + "id": "projection-uninstall-byte-exact", + "status": "pass" + }, + { + "id": "profile-untouched", + "status": "skipped", + "detail": "no profile squad seeded in this shape" + } + ], + "notes": [ + "plain upgrade changed 15 working-tree path(s), all Squad-owned: .github/workflows/squad-ci.yml, .github/workflows/squad-docs.yml, .github/workflows/squad-heartbeat.yml, .github/workflows/squad-insider-release.yml … (+11); squad-state blobs for 3 seeded path(s) identical" + ] + }, + { + "producer": "HEAD", + "source": "pack", + "shape": "upstream", + "status": "pass", + "provenance": "pack: npm pack of packages/squad-sdk + packages/squad-cli at 9da01a2e41ba15794d31b0b8251ddaa3abd34ba8 (v1.2.0-preview.1)", + "checks": [ + { + "id": "producer-artifact-verified", + "status": "pass" + }, + { + "id": "producer-init", + "status": "pass" + }, + { + "id": "upgrade-plain", + "status": "pass" + }, + { + "id": "coordinator-is-new-template", + "status": "pass" + }, + { + "id": "user-state-preserved", + "status": "pass" + }, + { + "id": "casting-pair-valid", + "status": "pass" + }, + { + "id": "state-backend-preserved", + "status": "pass" + }, + { + "id": "ai-team-migrated", + "status": "skipped", + "detail": "not an .ai-team shape" + }, + { + "id": "team-root-absent", + "status": "pass" + }, + { + "id": "user-config-preserved", + "status": "pass" + }, + { + "id": "idempotent-second-upgrade", + "status": "pass" + }, + { + "id": "downgrade-probe", + "status": "skipped", + "detail": "producer is the target version" + }, + { + "id": "clients-projected", + "status": "pass" + }, + { + "id": "doctor", + "status": "pass" + }, + { + "id": "projection-uninstall-byte-exact", + "status": "pass" + }, + { + "id": "profile-untouched", + "status": "skipped", + "detail": "no profile squad seeded in this shape" + } + ], + "notes": [ + "plain upgrade changed 17 working-tree path(s), all Squad-owned: .github/workflows/squad-ci.yml, .github/workflows/squad-docs.yml, .github/workflows/squad-heartbeat.yml, .github/workflows/squad-insider-release.yml … (+13)" + ] + } + ] + } + ] +} diff --git a/test/fixtures/upgrade-matrix/fs-access-log.cjs b/test/fixtures/upgrade-matrix/fs-access-log.cjs new file mode 100644 index 000000000..6c632eaf2 --- /dev/null +++ b/test/fixtures/upgrade-matrix/fs-access-log.cjs @@ -0,0 +1,52 @@ +'use strict'; +/** + * fs access logger for the upgrade matrix (loaded into spawned Squad 2.0 CLI processes via + * `NODE_OPTIONS=--require`). Records every `node:fs` call — sync, callback and promise APIs — + * whose path argument resolves under `SQUAD_MATRIX_FS_WATCH_ROOT`, appending one line per hit to + * `SQUAD_MATRIX_FS_WATCH_LOG`. The matrix asserts that log is empty for the retired user-profile + * tree: the sha256 fingerprint already proves no writes; this proves no reads or existence probes. + * + * Plain CommonJS, Node built-ins only: it must load before anything else in the target process. + */ +const fs = require('fs'); +const path = require('path'); +const { syncBuiltinESMExports } = require('module'); + +const root = process.env.SQUAD_MATRIX_FS_WATCH_ROOT; +const log = process.env.SQUAD_MATRIX_FS_WATCH_LOG; +if (root && log) { + const prefix = path.resolve(root).toLowerCase() + path.sep; + const inside = (p) => { + if (typeof p !== 'string' && !Buffer.isBuffer(p) && !(p instanceof URL)) return false; + try { + const s = p instanceof URL ? require('url').fileURLToPath(p) : p.toString(); + const abs = path.resolve(s).toLowerCase(); + return abs === prefix.slice(0, -1) || abs.startsWith(prefix); + } catch { return false; } + }; + const record = (api, args) => { + const hit = args.find(inside); + if (hit === undefined) return; + try { fs.appendFileSync(log, `${process.pid} ${api}(${String(hit)})\n`); } catch { /* never break the CLI */ } + }; + const wrap = (obj, name, label) => { + const original = obj[name]; + if (typeof original !== 'function') return; + const wrapped = function (...args) { record(`${label}.${name}`, args.slice(0, 2)); return original.apply(this, args); }; + Object.defineProperty(wrapped, 'name', { value: original.name }); + obj[name] = wrapped; + }; + const names = [ + 'access', 'appendFile', 'chmod', 'chown', 'copyFile', 'cp', 'exists', 'lstat', 'mkdir', 'mkdtemp', 'open', 'opendir', + 'readdir', 'readFile', 'readlink', 'realpath', 'rename', 'rm', 'rmdir', 'stat', 'statfs', 'symlink', 'truncate', + 'unlink', 'utimes', 'writeFile', 'createReadStream', 'createWriteStream', 'watch', 'watchFile', + ]; + for (const n of names) { + wrap(fs, n, 'fs'); + wrap(fs, `${n}Sync`, 'fs'); + wrap(fs.promises, n, 'fs.promises'); + } + if (fs.realpath && fs.realpath.native) wrap(fs.realpath, 'native', 'fs.realpath'); + if (fs.realpathSync && fs.realpathSync.native) wrap(fs.realpathSync, 'native', 'fs.realpathSync'); + syncBuiltinESMExports(); +} diff --git a/test/squad-20-upgrade-matrix.test.ts b/test/squad-20-upgrade-matrix.test.ts new file mode 100644 index 000000000..487bcbf2b --- /dev/null +++ b/test/squad-20-upgrade-matrix.test.ts @@ -0,0 +1,1305 @@ +/** + * Squad 2.0 upgrade matrix: real old releases × OS × project state shape. + * + * Two layers: + * + * 1. **Integrity (always runs)** — `test-fixtures/spec/suite-v0.2/upgrade-matrix.json` is the + * committed record of the last matrix runs. It must parse, every expected failure must point at a + * recorded finding, and the "Tested upgrade paths" section of + * `docs/src/content/docs/scenarios/upgrading.md` must be the rendered output of that JSON + * (`node scripts/render-upgrade-matrix.mjs --check`). + * + * 2. **Matrix (opt-in: `SQUAD_UPGRADE_MATRIX=1`)** — for every producer version that is still + * obtainable as a *published artifact* (npm tarball through the registry npm is configured for — + * `NPM_CONFIG_REGISTRY` wins — or a standalone bundle from the GitHub Release when npm never got + * that version), scaffold a project with the OLD CLI under several state shapes, then upgrade it + * with the NEW CLI packed from this tree (`npm pack` of squad-sdk + squad-cli) and assert: + * user state byte-exact, casting pair valid, state backend preserved, `.ai-team/` migrated, + * `teamRoot` absent, profile squad sha256-identical, user config merged not clobbered, all 7 host + * projections, `squad doctor` green, `uninstallClients` rollback byte-exact, second `upgrade` a + * no-op, and the OLD CLI's behavior against the upgraded project recorded. + * + * Producers that cannot be obtained (registry unreachable, release asset missing, checksum + * mismatch) are **skipped loudly** with the reason recorded in the results JSON — never silently + * passed. Known product bugs are pinned in the fixture's `expectedFailures`: the matching check is + * reported as `xfail`; if it starts passing the suite fails (`xpass`) so the pin must be removed. + * + * Results are written to `SQUAD_UPGRADE_MATRIX_OUT` (default + * `node_modules/.cache/squad-upgrade-matrix/results-.json`) and folded into the fixture with + * `node scripts/render-upgrade-matrix.mjs --merge `. + */ + +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; +import { execFile, execFileSync } from 'node:child_process'; +import http from 'node:http'; +import type { AddressInfo } from 'node:net'; +import { createHash } from 'node:crypto'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { pathToFileURL } from 'node:url'; +import { promisify } from 'node:util'; +import { parse as tomlParse } from 'smol-toml'; +import { parse as yamlParse } from 'yaml'; + +const execFileAsync = promisify(execFile); + +const ROOT = path.resolve(__dirname, '..'); +const MATRIX_PATH = path.join(ROOT, 'test-fixtures', 'spec', 'suite-v0.2', 'upgrade-matrix.json'); +const PAGE_PATH = path.join(ROOT, 'docs', 'src', 'content', 'docs', 'scenarios', 'upgrading.md'); +const RENDERER = pathToFileURL(path.join(ROOT, 'scripts', 'render-upgrade-matrix.mjs')).href; +const ENABLED = process.env['SQUAD_UPGRADE_MATRIX'] === '1'; +const PLATFORM = process.platform; + +// ─── fixture types ─────────────────────────────────────────────────────────── + +type CheckStatus = 'pass' | 'fail' | 'xfail' | 'xpass' | 'skipped'; +type ScenarioStatus = 'pass' | 'fail' | 'skipped'; +interface Check { id: string; status: CheckStatus; detail?: string } +interface ScenarioResult { + producer: string; + source: ProducerSource; + shape: string; + status: ScenarioStatus; + reason?: string; + /** Exact artifact that was executed for this row (pinned version + integrity, or release tag + asset id + sha256). */ + provenance?: string; + checks: Check[]; + notes: string[]; +} +interface ExpectedFailure { + finding: string; + check: string; + producers: string[] | '*'; + shapes: string[] | '*'; + platforms: string[] | '*'; + /** For set-valued checks: the exact detail that is known/expected. Any other failure is a real `fail`. */ + knownDetail?: string; +} +interface Finding { id: string; title: string; severity: string; layer: string; repro: string[]; observed: string; resolved?: string } +interface Inapplicable { producer: string; shape: string; reason: string } +interface Run { + platform: string; arch: string; node: string; target: string; + /** Commit of the tree under test (branch head, not the PR merge ref). */ + targetSha: string; + /** sha256 over the harness files (suite, renderer, workflow, fixture minus `runs`) the run executed with. */ + harnessHash: string; + event: string; runUrl?: string; recordedAt: string; results: ScenarioResult[]; +} +interface Matrix { + protocol: string; + producers: ProducerSpec[]; + shapes: Record; + checks: Record; + /** Regexes (as strings) of paths a plain `squad upgrade` may create or rewrite. */ + squadOwned: string[]; + inapplicable: Inapplicable[]; + expectedFailures: ExpectedFailure[]; + findings: Finding[]; + runs: Run[]; +} + +type ProducerSource = 'npm' | 'github-release' | 'pack'; +interface RegistryArtifact { version: string; shasum: string; integrity: string } +interface ReleaseAsset { id: number; sha256: string } +interface ProducerSpec { + version: string; + source: ProducerSource; + /** Why this producer is in the matrix. */ + why: string; + /** Feature support of the OLD CLI (determines which shapes are attempted). */ + supports: { stateBackend: boolean; dryRun: boolean }; + /** Immutable provenance of the artifact that is executed. */ + artifacts: + | { registry: Record } + | { release: { repository: string; tag: string; assets: Record } } + | { pack: string }; +} + +const matrix = JSON.parse(fs.readFileSync(MATRIX_PATH, 'utf8')) as Matrix; +const SHAPES = Object.keys(matrix.shapes); +const CHECK_IDS = Object.keys(matrix.checks); +const CLIENT_IDS = ['copilot', 'claude', 'kiro', 'opencode', 'cursor', 'codex', 'antigravity']; +const WORKFLOW_PATH = path.join(ROOT, '.github', 'workflows', 'squad-upgrade-matrix.yml'); +const FS_HOOK = path.join(ROOT, 'test', 'fixtures', 'upgrade-matrix', 'fs-access-log.cjs'); + +/** + * Why a producer × shape cell cannot be exercised, derived from the producers' capabilities. + * The fixture's `inapplicable` list must match this exactly (asserted below), so every skip the + * suite emits is explained on the rendered page and none reads as "not run". + */ +function structuralSkip(spec: ProducerSpec, shape: string): string | null { + if ((shape === 'orphan' || shape === 'two-layer') && !spec.supports.stateBackend) return `producer ${spec.version} has no --state-backend flag`; + if (shape === 'upstream' && spec.source !== 'pack') return `upstream inheritance is a Squad 2.0 feature; producer ${spec.version} cannot declare upstream.json`; + return null; +} + +function inapplicableReason(producer: string, shape: string): string | undefined { + return matrix.inapplicable.find((x) => x.producer === producer && x.shape === shape)?.reason; +} + +/** Canonical JSON (sorted keys) so the hash is independent of key order and formatting. */ +function canonical(value: unknown): string { + if (Array.isArray(value)) return `[${value.map(canonical).join(',')}]`; + if (value && typeof value === 'object') { + return `{${Object.keys(value as Record).sort().map((k) => `${JSON.stringify(k)}:${canonical((value as Record)[k])}`).join(',')}}`; + } + return JSON.stringify(value); +} + +/** + * Attestation hash of the harness: the suite, the renderer, the CI workflow and the fixture without + * its `runs`. A recorded run is only accepted when its `harnessHash` equals this value, so the fold + * commit that merges CI results may change nothing but the runs data. + */ +function harnessHash(): string { + const text = (p: string) => fs.readFileSync(p, 'utf8').replace(/\r\n/g, '\n'); + const fixture = JSON.parse(text(MATRIX_PATH)) as Matrix; + const { runs: _runs, ...rest } = fixture; + void _runs; + return createHash('sha256') + .update(text(__filename)).update('\0') + .update(text(path.join(ROOT, 'scripts', 'render-upgrade-matrix.mjs'))).update('\0') + .update(text(WORKFLOW_PATH)).update('\0') + .update(text(FS_HOOK)).update('\0') + .update(canonical(rest)) + .digest('hex'); +} + +// ─── 1. integrity (always) ────────────────────────────────────────────────── + +describe('upgrade-matrix.json integrity', () => { + it('declares producers, shapes, checks, and a protocol', () => { + expect(matrix.protocol).toBe('upgrade-matrix/v1'); + expect(matrix.producers.length).toBeGreaterThanOrEqual(5); + expect(matrix.producers.some((p) => p.source === 'pack'), 'HEAD→HEAD (same-version) producer must be present').toBe(true); + for (const p of matrix.producers) { + expect(p.version, `${p.version}: version`).toMatch(/^(\d+\.\d+\.\d+(-[\w.]+)?|HEAD)$/); + expect(['npm', 'github-release', 'pack']).toContain(p.source); + } + for (const id of ['default', 'profile', 'legacy-casting', 'orphan', 'two-layer', 'ai-team', 'upstream']) { + expect(SHAPES, `shape ${id} must be declared`).toContain(id); + } + for (const id of ['producer-init', 'upgrade-plain', 'coordinator-is-new-template', 'user-state-preserved', 'casting-pair-valid', + 'state-backend-preserved', 'ai-team-migrated', 'team-root-absent', 'profile-untouched', 'idempotent-second-upgrade', + 'clients-projected', 'user-config-preserved', 'doctor', 'projection-uninstall-byte-exact', 'downgrade-probe']) { + expect(CHECK_IDS, `check ${id} must be declared`).toContain(id); + } + expect(CHECK_IDS, 'uninstallClients restores host projections; it is not a rollback — the check must not be called one').not.toContain('rollback-byte-exact'); + expect(matrix.squadOwned.length, 'squadOwned patterns').toBeGreaterThan(5); + for (const p of matrix.squadOwned) expect(() => new RegExp(p), `squadOwned pattern ${p} must be a valid RegExp`).not.toThrow(); + expect(fs.existsSync(FS_HOOK), 'fs access hook must exist').toBe(true); + }); + + it('every producer carries immutable provenance (exact version + integrity, or release tag + asset id + sha256)', () => { + for (const p of matrix.producers) { + if (p.source === 'npm') { + expect('registry' in p.artifacts, `${p.version}: npm producer needs artifacts.registry`).toBe(true); + const reg = (p.artifacts as { registry: Record }).registry; + for (const name of ['@bradygaster/squad-cli', '@bradygaster/squad-sdk']) { + const a = reg[name]; + expect(a, `${p.version}: missing pinned ${name}`).toBeDefined(); + expect(a!.version, `${p.version}: ${name} must be pinned to the exact producer version`).toBe(p.version); + expect(a!.shasum, `${p.version}: ${name} sha1 shasum`).toMatch(/^[0-9a-f]{40}$/); + expect(a!.integrity, `${p.version}: ${name} sha512 integrity`).toMatch(/^sha512-[A-Za-z0-9+/]{86}==$/); + } + } else if (p.source === 'github-release') { + expect('release' in p.artifacts, `${p.version}: release producer needs artifacts.release`).toBe(true); + const rel = (p.artifacts as { release: { repository: string; tag: string; assets: Record } }).release; + expect(rel.repository).toBe('bradygaster/squad'); + expect(rel.tag).toBe(`v${p.version}`); + for (const name of ['squad-linux-x64.tar.gz', 'squad-linux-arm64.tar.gz', 'squad-darwin-x64.tar.gz', 'squad-darwin-arm64.tar.gz', 'squad-win32-x64.zip', 'squad-win32-arm64.zip']) { + const a = rel.assets[name]; + expect(a, `${p.version}: missing asset ${name}`).toBeDefined(); + expect(Number.isInteger(a!.id) && a!.id > 0, `${p.version}: ${name} needs a GitHub asset id`).toBe(true); + expect(a!.sha256, `${p.version}: ${name} sha256`).toMatch(/^[0-9a-f]{64}$/); + } + } else { + expect('pack' in p.artifacts, `${p.version}: pack producer describes its provenance`).toBe(true); + } + } + }); + + it('every structurally inapplicable cell is explained individually, and nothing else is', () => { + const derived = matrix.producers.flatMap((p) => SHAPES.filter((s) => structuralSkip(p, s) !== null).map((s) => `${p.version} × ${s}`)).sort(); + const declared = matrix.inapplicable.map((x) => `${x.producer} × ${x.shape}`).sort(); + expect(declared, 'fixture `inapplicable` must list exactly the cells the suite cannot exercise (derived from producer capabilities)').toEqual(derived); + for (const x of matrix.inapplicable) expect(x.reason.length, `${x.producer} × ${x.shape}: reason`).toBeGreaterThan(40); + expect(derived).toHaveLength(7); + }); + + it('every expected failure names a declared check and a recorded finding', () => { + const findingIds = new Set(matrix.findings.map((f) => f.id)); + for (const xf of matrix.expectedFailures) { + expect(CHECK_IDS, `expectedFailure ${xf.finding}: unknown check ${xf.check}`).toContain(xf.check); + expect(findingIds.has(xf.finding), `expectedFailure for ${xf.check} references unrecorded finding ${xf.finding}`).toBe(true); + if (xf.producers !== '*') for (const p of xf.producers) expect(matrix.producers.map((x) => x.version), `${xf.finding}: unknown producer ${p}`).toContain(p); + if (xf.shapes !== '*') for (const s of xf.shapes) expect(SHAPES, `${xf.finding}: unknown shape ${s}`).toContain(s); + } + for (const f of matrix.findings) { + expect(f.repro.length, `finding ${f.id} needs a repro`).toBeGreaterThan(0); + expect(f.observed.length, `finding ${f.id} needs an observed outcome`).toBeGreaterThan(0); + const pinned = matrix.expectedFailures.some((xf) => xf.finding === f.id); + expect(pinned || !!f.resolved, `finding ${f.id} is neither pinned as an expected failure nor marked resolved`).toBe(true); + expect(!(pinned && f.resolved), `finding ${f.id} is marked resolved but still pinned — remove its expectedFailures entries`).toBe(true); + } + }); + + it('recorded runs are attributable to one target SHA, ran with this exact harness, and use honest statuses', () => { + const hash = harnessHash(); + const targets = new Set(matrix.runs.map((r) => r.targetSha)); + expect(targets.size <= 1, `recorded runs must all come from one target SHA, found ${[...targets].join(', ')}`).toBe(true); + for (const run of matrix.runs) { + expect(run.platform).toMatch(/^(win32|linux|darwin)$/); + expect(run.targetSha, `${run.platform}: targetSha`).toMatch(/^[0-9a-f]{40}$/); + expect(run.harnessHash, `${run.platform}: recorded run executed with a different harness (suite/renderer/workflow/fixture) than the one committed — re-run the matrix at this head and fold the results`).toBe(hash); + for (const r of run.results) { + expect(matrix.producers.map((p) => p.version), `${run.platform}: unknown producer ${r.producer}`).toContain(r.producer); + expect(SHAPES, `${run.platform}: unknown shape ${r.shape}`).toContain(r.shape); + expect(['pass', 'fail', 'skipped']).toContain(r.status); + if (r.status === 'skipped') { + expect(r.reason, `${run.platform} ${r.producer}/${r.shape}: skipped without a reason`).toBeTruthy(); + expect(inapplicableReason(r.producer, r.shape), `${run.platform} ${r.producer}/${r.shape}: skipped for a reason that is not a declared structural inapplicability: ${r.reason}`).toBeDefined(); + } + for (const c of r.checks) { + expect(CHECK_IDS, `${run.platform} ${r.producer}/${r.shape}: unknown check ${c.id}`).toContain(c.id); + expect(['pass', 'fail', 'xfail', 'xpass', 'skipped']).toContain(c.status); + } + const failed = r.checks.filter((c) => c.status === 'fail' || c.status === 'xpass'); + if (r.status === 'pass') expect(failed, `${run.platform} ${r.producer}/${r.shape} recorded as pass but has failing checks`).toEqual([]); + } + const skipped = run.results.filter((r) => r.status === 'skipped').map((r) => `${r.producer} × ${r.shape}`).sort(); + expect(skipped, `${run.platform}: the skipped rows must be exactly the declared inapplicable cells`).toEqual(matrix.inapplicable.map((x) => `${x.producer} × ${x.shape}`).sort()); + } + }); + + it('the "Tested upgrade paths" section of upgrading.md is up to date with the JSON', async () => { + const { renderSection, extractSection } = (await import(RENDERER)) as { + renderSection: (m: unknown) => string; + extractSection: (page: string) => string | null; + }; + const expected = renderSection(matrix).replace(/\r\n/g, '\n'); + const actual = extractSection(fs.readFileSync(PAGE_PATH, 'utf8').replace(/\r\n/g, '\n')); + expect(actual, 'docs/src/content/docs/scenarios/upgrading.md upgrade-matrix section is stale; run node scripts/render-upgrade-matrix.mjs').toBe(expected); + }); +}); + +// ─── harness guards (always on) ───────────────────────────────────────────── + +describe('upgrade-matrix harness guards', () => { + const fakeSpec = (over: Partial = {}): ProducerSpec => ({ + version: '0.0.0-guard', source: 'github-release', why: 'guard test', supports: { stateBackend: true, dryRun: true }, + artifacts: { release: { repository: 'bradygaster/squad', tag: 'v0.0.0-guard', assets: {} } }, + ...over, + }); + + it('LD-1: a corrupted recorded hash is a verification FAILURE, not a skip (npm sha1/sha512 and release sha256)', () => { + const bytes = Buffer.from('tarball bytes'); + const goodIntegrity = `sha512-${createHash('sha512').update(bytes).digest('base64')}`; + const goodShasum = createHash('sha1').update(bytes).digest('hex'); + expect(verifyArtifact('npm', 'x@1', bytes, { integrity: goodIntegrity, shasum: goodShasum })).toEqual({ ok: true }); + const badIntegrity = verifyArtifact('npm', 'x@1', bytes, { integrity: goodIntegrity.replace(/.$/, (c) => (c === 'A' ? 'B' : 'A')), shasum: goodShasum }); + expect(badIntegrity.ok).toBe(false); + expect((badIntegrity as { failReason: string }).failReason).toMatch(/sha512 integrity expected .* got sha512-/); + const badShasum = verifyArtifact('npm', 'x@1', bytes, { integrity: goodIntegrity, shasum: '0'.repeat(40) }); + expect((badShasum as { failReason: string }).failReason).toMatch(/sha1 shasum expected 0{40}, got [0-9a-f]{40}/); + const badSha256 = verifyArtifact('release', 'v1 asset', bytes, { sha256: 'f'.repeat(64) }); + expect((badSha256 as { failReason: string }).failReason).toMatch(/sha256 expected f{64}, got [0-9a-f]{64}/); + }); + + it('LD-1: the release resolver turns a checksum mismatch into failReason (rows FAIL) and a 404 into skipReason (rows skip)', async () => { + const served = Buffer.from('not the real bundle'); + const server = http.createServer((req, res) => { + if (req.url?.endsWith('/assets/1')) { res.writeHead(200, { 'content-type': 'application/octet-stream' }); res.end(served); } + else { res.writeHead(404); res.end('missing'); } + }); + await new Promise((r) => server.listen(0, '127.0.0.1', r)); + const port = (server.address() as AddressInfo).port; + const prev = process.env['SQUAD_UPGRADE_MATRIX_GITHUB_API']; + process.env['SQUAD_UPGRADE_MATRIX_GITHUB_API'] = `http://127.0.0.1:${port}`; + try { + const assetName = releaseAssetName(); + if (!assetName) return; + const mismatch = await resolveReleaseProducer(fakeSpec({ artifacts: { release: { repository: 'bradygaster/squad', tag: 'v0.0.0-guard', assets: { [assetName]: { id: 1, sha256: 'a'.repeat(64) } } } } })); + expect(mismatch.exec).toBeNull(); + expect(mismatch.skipReason, 'a mismatch must never be reported as a skip').toBeUndefined(); + expect(mismatch.failReason).toMatch(/sha256 expected a{64}, got [0-9a-f]{64}/); + const missing = await resolveReleaseProducer(fakeSpec({ artifacts: { release: { repository: 'bradygaster/squad', tag: 'v0.0.0-guard', assets: { [assetName]: { id: 2, sha256: sha256(served) } } } } })); + expect(missing.exec).toBeNull(); + expect(missing.failReason).toBeUndefined(); + expect(missing.skipReason).toMatch(/unavailable: HTTP 404/); + } finally { + if (prev === undefined) delete process.env['SQUAD_UPGRADE_MATRIX_GITHUB_API']; else process.env['SQUAD_UPGRADE_MATRIX_GITHUB_API'] = prev; + await new Promise((r) => server.close(() => r())); + rmrf(path.join(CACHE_ROOT, 'producers', 'release-0.0.0-guard')); + } + }); + + it('LD-1/LD-2: a producer that failed verification yields a FAILING row that the outer assertion rejects', () => { + const s = new Scenario({ spec: fakeSpec() }, 'default'); + s.check('producer-artifact-verified', false, 'release artifact v0 failed verification: sha256 expected a, got b'); + const row = s.result('fail', 'release artifact v0 failed verification: sha256 expected a, got b'); + expect(row.status).toBe('fail'); + expect(row.checks.filter((c) => c.status === 'fail').map((c) => c.id)).toEqual(['producer-artifact-verified']); + expect(rowVerdict(row)).toMatch(/producer-artifact-verified: release artifact v0 failed verification/); + }); + + it('LD-2: a row that fails during setup (no natural check) still carries a failing check and is rejected', () => { + const s = new Scenario({ spec: fakeSpec({ source: 'pack', artifacts: { pack: 'x' } }) }, 'upstream'); + s.check('producer-init', true); + const row = s.result('fail', 'could not configure upstream: init exit 1'); + expect(row.status).toBe('fail'); + expect(row.checks.some((c) => c.id === 'scenario-setup' && c.status === 'fail')).toBe(true); + expect(rowVerdict(row)).toMatch(/scenario-setup: could not configure upstream/); + expect(() => { const ok = new Scenario({ spec: fakeSpec() }, 'default'); ok.check('x', false, 'boom'); ok.result('pass'); }).toThrow(/cannot be pass with failing checks/); + }); + + it('LD-2: the verdict accepts pass rows (including pinned xfail) and rejects xpass, fail and non-pass statuses', () => { + const base = { producer: '0.9.1', source: 'npm' as const, shape: 'default', notes: [] as string[] }; + expect(rowVerdict({ ...base, status: 'pass', checks: [{ id: 'a', status: 'pass' }, { id: 'downgrade-probe', status: 'xfail', detail: 'dry-run MODIFIED' }] })).toBeNull(); + expect(rowVerdict({ ...base, status: 'skipped', reason: 'n/a', checks: [] })).toBeNull(); + expect(rowVerdict({ ...base, status: 'pass', checks: [{ id: 'a', status: 'xpass', detail: 'F3' }] })).toMatch(/now PASS — remove the matching expectedFailures pin/); + expect(rowVerdict({ ...base, status: 'fail', checks: [{ id: 'a', status: 'fail', detail: 'bad' }] })).toMatch(/a: bad/); + expect(rowVerdict({ ...base, status: 'fail', reason: 'setup', checks: [] })).toMatch(/status fail without a failing check — setup/); + }); +}); + +// ─── helpers ──────────────────────────────────────────────────────────────── + +interface Cmd { code: number; stdout: string; stderr: string } + +async function run(file: string, args: string[], opts: { cwd: string; env: NodeJS.ProcessEnv; timeout?: number }): Promise { + try { + const { stdout, stderr } = await execFileAsync(file, args, { + cwd: opts.cwd, env: opts.env, encoding: 'utf8', timeout: opts.timeout ?? 120_000, maxBuffer: 32 * 1024 * 1024, + windowsHide: true, + }); + return { code: 0, stdout, stderr }; + } catch (err) { + const e = err as NodeJS.ErrnoException & { code?: number | string; stdout?: string; stderr?: string; killed?: boolean }; + const code = typeof e.code === 'number' ? e.code : e.killed ? 124 : 1; + return { code, stdout: e.stdout ?? '', stderr: (e.stderr ?? '') + (typeof e.code === 'string' ? `\n${e.code}: ${e.message}` : '') }; + } +} + +function sha256(buf: Buffer | string): string { + return createHash('sha256').update(buf).digest('hex'); +} + +/** rel path → sha256 for every file under `dir` (skips `.git/`). */ +function snapshot(dir: string): Map { + const out = new Map(); + const walk = (d: string) => { + for (const e of fs.readdirSync(d, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) { + if (e.name === '.git') continue; + const p = path.join(d, e.name); + const rel = path.relative(dir, p).replace(/\\/g, '/'); + if (e.isSymbolicLink()) out.set(rel, `link:${fs.readlinkSync(p)}`); + else if (e.isDirectory()) walk(p); + else out.set(rel, sha256(fs.readFileSync(p))); + } + }; + if (fs.existsSync(dir)) walk(dir); + return out; +} + +function fingerprint(dir: string): string { + return [...snapshot(dir)].map(([k, v]) => `${k}:${v}`).join('\n'); +} + +function diffSnapshots(a: Map, b: Map): string[] { + const changed = new Set(); + for (const [k, v] of a) if (b.get(k) !== v) changed.add(k); + for (const k of b.keys()) if (!a.has(k)) changed.add(k); + return [...changed].sort(); +} + +function read(p: string): string | null { + return fs.existsSync(p) ? fs.readFileSync(p, 'utf8') : null; +} + +function readJson>(p: string): T | null { + const raw = read(p); + if (raw === null) return null; + try { return JSON.parse(raw) as T; } catch { return null; } +} + +function tail(s: string, n = 3): string { + return s.split(/\r?\n/).map((l) => l.trim()).filter(Boolean).slice(-n).join(' | '); +} + +function rmrf(p: string): void { + try { fs.rmSync(p, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 }); } catch { /* best effort on Windows */ } +} + +/** + * Extract an archive. On Windows prefer the system bsdtar: under GitHub's bash shell `tar` on PATH is + * Git's GNU tar, which reads `D:\...` as a `host:path` remote spec. Paths are passed relative to `cwd` + * so either implementation accepts them. + */ +function extract(archive: string, dest: string): void { + const sysTar = PLATFORM === 'win32' ? path.join(process.env['SystemRoot'] ?? 'C:\\Windows', 'System32', 'tar.exe') : null; + const bin = sysTar && fs.existsSync(sysTar) ? sysTar : 'tar'; + const cwd = path.dirname(archive); + execFileSync(bin, ['-xf', path.basename(archive), '-C', path.relative(cwd, dest) || '.'], { cwd, stdio: 'pipe' }); +} + +const GIT_ENV = { + GIT_AUTHOR_NAME: 'Squad Matrix', GIT_AUTHOR_EMAIL: 'matrix@example.invalid', + GIT_COMMITTER_NAME: 'Squad Matrix', GIT_COMMITTER_EMAIL: 'matrix@example.invalid', + GIT_CONFIG_GLOBAL: PLATFORM === 'win32' ? 'NUL' : '/dev/null', GIT_CONFIG_NOSYSTEM: '1', GIT_TERMINAL_PROMPT: '0', +}; + +/** Child-process environment that cannot see the developer's real profile squad or home. */ +function isolatedEnv(configHome: string, extra: NodeJS.ProcessEnv = {}): NodeJS.ProcessEnv { + const env: NodeJS.ProcessEnv = { + ...process.env, ...GIT_ENV, + CI: '1', SQUAD_NONINTERACTIVE: '1', NO_COLOR: '1', FORCE_COLOR: '0', SQUAD_INIT_NO_COLOR: '1', TERM: 'dumb', + APPDATA: configHome, LOCALAPPDATA: configHome, XDG_CONFIG_HOME: configHome, + SQUAD_HOME_DIR_OVERRIDE: path.join(configHome, 'squad-home'), + ...extra, + }; + if (PLATFORM === 'darwin') env['HOME'] = configHome; + delete env['NODE_PATH']; + delete env['NODE_OPTIONS']; + delete env['SQUAD_TEAM_ROOT']; + for (const k of Object.keys(env)) if (k.startsWith('npm_')) delete env[k]; + return env; +} + +// ─── scenario bookkeeping ─────────────────────────────────────────────────── + +function expected(check: string, producer: string, shape: string): ExpectedFailure | undefined { + return matrix.expectedFailures.find((xf) => xf.check === check + && (xf.producers === '*' || xf.producers.includes(producer)) + && (xf.shapes === '*' || xf.shapes.includes(shape)) + && (xf.platforms === '*' || xf.platforms.includes(PLATFORM))); +} + +interface ProducerRef { spec: ProducerSpec; provenance?: string } + +export class Scenario { + readonly checks: Check[] = []; + readonly notes: string[] = []; + constructor(readonly producer: ProducerRef, readonly shape: string) {} + + /** Record a boolean check, applying the expected-failure pin. */ + check(id: string, ok: boolean, detail?: string): boolean { + const xf = expected(id, this.producer.spec.version, this.shape); + let status: CheckStatus; + if (ok) status = xf ? 'xpass' : 'pass'; + else if (xf && (!xf.knownDetail || xf.knownDetail === detail)) status = 'xfail'; + else status = 'fail'; + this.checks.push({ id, status, detail: status === 'pass' ? undefined : detail ?? (xf ? xf.finding : undefined) }); + if (status === 'xfail') this.notes.push(`${id}: ${xf!.finding}`); + return ok; + } + + skip(id: string, why: string): void { + this.checks.push({ id, status: 'skipped', detail: why }); + } + + /** + * Build the row. Invariant (LD-2): a row may only be `fail` when at least one check is `fail`/`xpass`; + * a setup or verification failure that has no natural check is recorded under `scenario-setup`. + */ + result(status?: ScenarioStatus, reason?: string): ScenarioResult { + const hasFailing = () => this.checks.some((c) => c.status === 'fail' || c.status === 'xpass'); + if (status === 'fail' && !hasFailing()) this.checks.push({ id: 'scenario-setup', status: 'fail', detail: reason ?? 'scenario failed without a recorded check' }); + const final: ScenarioStatus = status ?? (hasFailing() ? 'fail' : 'pass'); + if (final === 'pass' && hasFailing()) throw new Error(`row ${this.producer.spec.version} × ${this.shape} cannot be pass with failing checks`); + return { + producer: this.producer.spec.version, source: this.producer.spec.source, shape: this.shape, + status: final, reason, + ...(this.producer.provenance ? { provenance: this.producer.provenance } : {}), + checks: this.checks, notes: this.notes, + }; + } +} + +/** + * The single place the outer test judges a row (LD-2): skipped rows are reported as skips by the + * caller; everything else must be `pass` with no `fail`/`xpass` check. Returns the failure message + * instead of throwing so it can be unit-tested. + */ +export function rowVerdict(result: ScenarioResult): string | null { + if (result.status === 'skipped') return null; + const xpass = result.checks.filter((c) => c.status === 'xpass'); + if (xpass.length) return `${result.producer} × ${result.shape}: ${xpass.map((c) => c.id).join(', ')} now PASS — remove the matching expectedFailures pin(s) from ${path.relative(ROOT, MATRIX_PATH)}`; + const failed = result.checks.filter((c) => c.status === 'fail'); + if (failed.length) return `${result.producer} (${result.source}) × ${result.shape} on ${PLATFORM}: ${failed.map((c) => `${c.id}: ${c.detail ?? ''}`).join('; ')}${result.reason ? ` — ${result.reason}` : ''}`; + if (result.status !== 'pass') return `${result.producer} × ${result.shape} on ${PLATFORM}: status ${result.status} without a failing check — ${result.reason ?? 'no reason'}`; + return null; +} + +/** + * Artifact verification (LD-1). Pure so it can be unit-tested; a mismatch is a FAIL reason, never a skip. + */ +export function verifyArtifact( + kind: 'npm' | 'release', + label: string, + bytes: Buffer, + expectedHashes: { integrity?: string; shasum?: string; sha256?: string }, +): { ok: true } | { ok: false; failReason: string } { + const got: Record = {}; + const problems: string[] = []; + if (expectedHashes.integrity !== undefined) { + got['integrity'] = `sha512-${createHash('sha512').update(bytes).digest('base64')}`; + if (got['integrity'] !== expectedHashes.integrity) problems.push(`sha512 integrity expected ${expectedHashes.integrity}, got ${got['integrity']}`); + } + if (expectedHashes.shasum !== undefined) { + got['shasum'] = createHash('sha1').update(bytes).digest('hex'); + if (got['shasum'] !== expectedHashes.shasum) problems.push(`sha1 shasum expected ${expectedHashes.shasum}, got ${got['shasum']}`); + } + if (expectedHashes.sha256 !== undefined) { + got['sha256'] = sha256(bytes); + if (got['sha256'] !== expectedHashes.sha256) problems.push(`sha256 expected ${expectedHashes.sha256}, got ${got['sha256']}`); + } + if (!problems.length) return { ok: true }; + return { ok: false, failReason: `${kind} artifact ${label} failed verification: ${problems.join('; ')}` }; +} + +// ─── producers ────────────────────────────────────────────────────────────── + +interface Producer { + spec: ProducerSpec; + /** How to invoke the OLD CLI: [file, ...leadingArgs] */ + exec: { file: string; args: string[]; env: NodeJS.ProcessEnv } | null; + /** Artifact could not be obtained (network / not published for this platform): every row is skipped loudly. */ + skipReason?: string; + /** Artifact was obtained but failed verification (integrity / checksum): every row FAILS. */ + failReason?: string; + /** Human-readable provenance of what is executed, recorded per row. */ + provenance?: string; +} + +const CACHE_ROOT = path.join(ROOT, 'node_modules', '.cache', 'squad-upgrade-matrix'); +const githubApi = () => process.env['SQUAD_UPGRADE_MATRIX_GITHUB_API'] ?? 'https://api.github.com'; + +function releaseAssetName(): string | null { + const arch = process.arch === 'arm64' ? 'arm64' : process.arch === 'x64' ? 'x64' : null; + if (!arch) return null; + if (PLATFORM === 'win32') return `squad-win32-${arch}.zip`; + if (PLATFORM === 'linux') return `squad-linux-${arch}.tar.gz`; + if (PLATFORM === 'darwin') return `squad-darwin-${arch}.tar.gz`; + return null; +} + +async function fetchBuffer(url: string, headers: Record = {}): Promise { + const res = await fetch(url, { redirect: 'follow', headers: { 'user-agent': 'squad-upgrade-matrix', ...headers } }); + if (!res.ok) throw new Error(`HTTP ${res.status} for ${url}`); + return Buffer.from(await res.arrayBuffer()); +} + +async function npmCmd(args: string[], cwd: string, env: NodeJS.ProcessEnv): Promise { + const npm = PLATFORM === 'win32' ? 'npm.cmd' : 'npm'; + // Absolute tarball paths: a bare `dir/file.tgz` would be read by npm as a GitHub `owner/repo` shorthand. + const quoted = args.map((a) => (/\s/.test(a) ? `"${a}"` : a)); + return PLATFORM === 'win32' + ? run(process.env['ComSpec'] ?? 'cmd.exe', ['/d', '/s', '/c', `${npm} ${quoted.join(' ')}`], { cwd, env, timeout: 600_000 }) + : run(npm, args, { cwd, env, timeout: 600_000 }); +} + +/** + * npm producer: fetch the exact pinned tarballs of @bradygaster/squad-cli and @bradygaster/squad-sdk + * (`npm pack`, through whatever registry npm is configured for), verify sha1 + sha512 against the + * fixture (mismatch → FAIL, not skip), then install both tarballs into an isolated prefix so the old + * CLI runs against the SDK version it shipped with — never a floating `>=` resolution. + */ +async function resolveNpmProducer(spec: ProducerSpec): Promise { + const reg = (spec.artifacts as { registry: Record }).registry; + const dir = path.join(CACHE_ROOT, 'producers', `npm-${spec.version}`); + const entry = path.join(dir, 'node_modules', '@bradygaster', 'squad-cli', 'dist', 'cli-entry.js'); + const registry = process.env['NPM_CONFIG_REGISTRY'] ?? process.env['npm_config_registry']; + const provenance = Object.entries(reg).map(([n, a]) => `${n}@${a.version} ${a.integrity}`).join('; '); + const verifiedMarker = path.join(dir, '.verified'); + if (!fs.existsSync(entry) || !fs.existsSync(verifiedMarker)) { + rmrf(dir); + fs.mkdirSync(dir, { recursive: true }); + fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({ name: 'squad-upgrade-matrix-producer', private: true }, null, 2) + '\n'); + const env: NodeJS.ProcessEnv = { ...process.env, NO_COLOR: '1' }; + if (registry) env['NPM_CONFIG_REGISTRY'] = registry; + const tarballs = path.join(dir, 'tarballs'); + fs.mkdirSync(tarballs); + const specs = Object.entries(reg).map(([n, a]) => `${n}@${a.version}`); + const packed = await npmCmd(['pack', ...specs, '--pack-destination', tarballs, '--ignore-scripts', '--loglevel=error'], dir, env); + const files = fs.existsSync(tarballs) ? fs.readdirSync(tarballs).filter((f) => f.endsWith('.tgz')) : []; + if (packed.code !== 0 || files.length !== specs.length) { + rmrf(dir); + return { spec, exec: null, skipReason: `could not fetch ${specs.join(' + ')} via ${registry ?? 'the npm-configured registry'}: ${tail(packed.stderr || packed.stdout, 2) || `exit ${packed.code}`}` }; + } + for (const [name, a] of Object.entries(reg)) { + const file = files.find((f) => f.startsWith(`${name.replace('@', '').replace('/', '-')}-${a.version}.tgz`)); + if (!file) return { spec, exec: null, failReason: `npm pack produced no tarball for ${name}@${a.version} (got ${files.join(', ')})` }; + const verdict = verifyArtifact('npm', `${name}@${a.version}`, fs.readFileSync(path.join(tarballs, file)), { integrity: a.integrity, shasum: a.shasum }); + if (!verdict.ok) { + rmrf(dir); + return { spec, exec: null, failReason: verdict.failReason }; + } + } + const installed = await npmCmd(['install', '--prefix', dir, ...files.map((f) => path.join(tarballs, f)), '--ignore-scripts', '--no-audit', '--no-fund', '--no-package-lock', '--loglevel=error'], dir, env); + if (installed.code !== 0 || !fs.existsSync(entry)) { + rmrf(dir); + return { spec, exec: null, skipReason: `npm install of the verified ${spec.version} tarballs failed: ${tail(installed.stderr || installed.stdout, 2) || `exit ${installed.code}`}` }; + } + const sdkVersion = readJson<{ version: string }>(path.join(dir, 'node_modules', '@bradygaster', 'squad-sdk', 'package.json'))?.version; + if (sdkVersion !== reg['@bradygaster/squad-sdk']!.version) { + rmrf(dir); + return { spec, exec: null, failReason: `installed @bradygaster/squad-sdk@${sdkVersion ?? '?'} instead of the pinned ${reg['@bradygaster/squad-sdk']!.version}` }; + } + fs.writeFileSync(verifiedMarker, provenance + '\n'); + } + return { spec, exec: { file: process.execPath, args: [entry], env: {} }, provenance: `npm: ${provenance}` }; +} + +/** + * GitHub Release producer: download the bundle by immutable asset id + * (`/repos///releases/assets/`), verify sha256 against the fixture (mismatch → FAIL, + * not skip), extract, and run the real launcher. + */ +async function resolveReleaseProducer(spec: ProducerSpec): Promise { + const rel = (spec.artifacts as { release: { repository: string; tag: string; assets: Record } }).release; + const assetName = releaseAssetName(); + const asset = assetName ? rel.assets[assetName] : undefined; + if (!assetName || !asset) return { spec, exec: null, skipReason: `no standalone bundle is published for ${PLATFORM}/${process.arch} in ${rel.tag}` }; + const provenance = `github-release: ${rel.repository}@${rel.tag} asset ${assetName} id ${asset.id} sha256 ${asset.sha256}`; + const dir = path.join(CACHE_ROOT, 'producers', `release-${spec.version}`); + const bundleDir = path.join(dir, assetName.replace(/\.(zip|tar\.gz)$/, '')); + const launcher = PLATFORM === 'win32' ? path.join(bundleDir, 'squad.exe') : path.join(bundleDir, 'squad'); + const verifiedMarker = path.join(dir, '.verified'); + if (!fs.existsSync(launcher) || !fs.existsSync(verifiedMarker)) { + rmrf(dir); + fs.mkdirSync(dir, { recursive: true }); + let bytes: Buffer; + try { + const token = process.env['SQUAD_UPGRADE_MATRIX_GITHUB_TOKEN'] ?? process.env['GITHUB_TOKEN'] ?? process.env['GH_TOKEN']; + const headers: Record = { accept: 'application/octet-stream', 'x-github-api-version': '2022-11-28' }; + if (token) headers['authorization'] = `Bearer ${token}`; + bytes = await fetchBuffer(`${githubApi()}/repos/${rel.repository}/releases/assets/${asset.id}`, headers); + } catch (err) { + rmrf(dir); + return { spec, exec: null, skipReason: `GitHub Release ${rel.tag} asset ${assetName} (id ${asset.id}) unavailable: ${(err as Error).message}` }; + } + const verdict = verifyArtifact('release', `${rel.tag} ${assetName} (asset id ${asset.id})`, bytes, { sha256: asset.sha256 }); + if (!verdict.ok) { + rmrf(dir); + return { spec, exec: null, failReason: verdict.failReason }; + } + try { + const archive = path.join(dir, assetName); + fs.writeFileSync(archive, bytes); + extract(archive, dir); + fs.rmSync(archive, { force: true }); + if (!fs.existsSync(launcher)) throw new Error(`bundle did not contain ${path.basename(launcher)}`); + if (PLATFORM !== 'win32') fs.chmodSync(launcher, 0o755); + } catch (err) { + rmrf(dir); + return { spec, exec: null, failReason: `verified bundle ${assetName} could not be extracted: ${(err as Error).message}` }; + } + fs.writeFileSync(verifiedMarker, provenance + '\n'); + } + const info = readJson<{ nodeVersion?: string }>(path.join(bundleDir, 'BUNDLE-INFO.json')); + return { + spec, + exec: { file: launcher, args: [], env: { SQUAD_STANDALONE_HOME: bundleDir + path.sep } }, + provenance: `${provenance} (bundled node ${info?.nodeVersion ?? '?'})`, + }; +} + +// ─── the matrix ───────────────────────────────────────────────────────────── + +describe.skipIf(!ENABLED)('old-version × OS × state-shape upgrade matrix (SQUAD_UPGRADE_MATRIX=1)', () => { + let staging: string; + let newCli: { file: string; args: string[]; env: NodeJS.ProcessEnv }; + let newVersion: string; + const producers = new Map(); + const results: ScenarioResult[] = []; + const workRoots: string[] = []; + + beforeAll(async () => { + const sdkDir = path.join(ROOT, 'packages', 'squad-sdk'); + const cliDir = path.join(ROOT, 'packages', 'squad-cli'); + const buildEnv = { ...process.env, SKIP_BUILD_BUMP: '1', NO_COLOR: '1', FORCE_COLOR: '0' }; + const npmShell = PLATFORM === 'win32'; + if (!fs.existsSync(path.join(sdkDir, 'dist', 'index.js'))) execFileSync('npm', ['run', 'build'], { cwd: sdkDir, stdio: 'inherit', env: buildEnv, shell: npmShell }); + if (!fs.existsSync(path.join(cliDir, 'dist', 'cli-entry.js'))) execFileSync('npm', ['run', 'build'], { cwd: cliDir, stdio: 'inherit', env: buildEnv, shell: npmShell }); + + fs.mkdirSync(CACHE_ROOT, { recursive: true }); + staging = fs.mkdtempSync(path.join(CACHE_ROOT, 'staging-')); + const tarballs = path.join(staging, 'tarballs'); + fs.mkdirSync(tarballs); + const pack = (pkgDir: string): string => { + const out = execFileSync('npm', ['pack', '--quiet', '--ignore-scripts', '--pack-destination', tarballs], { cwd: pkgDir, encoding: 'utf8', shell: npmShell, env: buildEnv }).trim(); + return path.join(tarballs, out.split(/\r?\n/).filter(Boolean).pop()!.trim()); + }; + const unpack = (tarball: string, dest: string) => { + const tmp = fs.mkdtempSync(path.join(tarballs, 'x-')); + extract(tarball, tmp); + fs.mkdirSync(path.dirname(dest), { recursive: true }); + fs.renameSync(path.join(tmp, 'package'), dest); + rmrf(tmp); + }; + unpack(pack(sdkDir), path.join(staging, 'node_modules', '@bradygaster', 'squad-sdk')); + unpack(pack(cliDir), path.join(staging, 'node_modules', '@bradygaster', 'squad-cli')); + fs.writeFileSync(path.join(staging, 'package.json'), JSON.stringify({ name: 'squad-upgrade-matrix-consumer', private: true, type: 'module' }, null, 2) + '\n'); + newVersion = (readJson<{ version: string }>(path.join(staging, 'node_modules', '@bradygaster', 'squad-cli', 'package.json')))!.version; + newCli = { + file: process.execPath, + args: [path.join(staging, 'node_modules', '@bradygaster', 'squad-cli', 'dist', 'cli-entry.js')], + env: { NODE_PATH: [path.join(staging, 'node_modules'), path.join(ROOT, 'node_modules')].join(path.delimiter) }, + }; + + const only = process.env['SQUAD_UPGRADE_MATRIX_PRODUCERS']?.split(',').map((s) => s.trim()).filter(Boolean); + await Promise.all(matrix.producers.map(async (spec) => { + if (only && only.length && !only.includes(spec.version)) { + producers.set(spec.version, { spec, exec: null, skipReason: `excluded by SQUAD_UPGRADE_MATRIX_PRODUCERS=${only.join(',')}` }); + return; + } + const p: Producer = spec.source === 'pack' + ? { spec, exec: newCli, provenance: `pack: npm pack of packages/squad-sdk + packages/squad-cli at ${targetSha()} (v${newVersion})` } + : spec.source === 'npm' ? await resolveNpmProducer(spec) : await resolveReleaseProducer(spec); + producers.set(spec.version, p); + if (p.failReason) console.error(`❌ upgrade-matrix: producer ${spec.version} FAILED verification — ${p.failReason}`); + else if (p.skipReason) console.warn(`⏭️ upgrade-matrix: producer ${spec.version} skipped — ${p.skipReason}`); + else process.stdout.write(`✅ upgrade-matrix: producer ${spec.version} ready — ${p.provenance}\n`); + })); + }, 20 * 60_000); + + /** Branch head under test: CI passes it explicitly (PR checkouts are merge refs); locally it is HEAD. */ + function targetSha(): string { + const fromEnv = process.env['SQUAD_UPGRADE_MATRIX_TARGET_SHA']; + if (fromEnv && /^[0-9a-f]{40}$/.test(fromEnv)) return fromEnv; + try { return execFileSync('git', ['rev-parse', 'HEAD'], { cwd: ROOT, encoding: 'utf8' }).trim(); } catch { return 'unknown'; } + } + + afterAll(() => { + const out = process.env['SQUAD_UPGRADE_MATRIX_OUT'] ?? path.join(CACHE_ROOT, `results-${PLATFORM}.json`); + const runUrl = process.env['GITHUB_RUN_ID'] && process.env['GITHUB_REPOSITORY'] + ? `${process.env['GITHUB_SERVER_URL'] ?? 'https://github.com'}/${process.env['GITHUB_REPOSITORY']}/actions/runs/${process.env['GITHUB_RUN_ID']}` + : undefined; + const run: Run = { + platform: PLATFORM, arch: process.arch, node: process.version, target: newVersion ?? 'unbuilt', + targetSha: targetSha(), harnessHash: harnessHash(), + event: process.env['GITHUB_EVENT_NAME'] ?? 'local', ...(runUrl ? { runUrl } : {}), + recordedAt: new Date().toISOString(), results, + }; + fs.mkdirSync(path.dirname(out), { recursive: true }); + fs.writeFileSync(out, JSON.stringify(run, null, 2) + '\n'); + const passed = results.filter((r) => r.status === 'pass'); + const withKnown = passed.filter((r) => r.checks.some((c) => c.status === 'xfail')); + const knownIds = [...new Set(withKnown.flatMap((r) => r.notes.map((n) => /^[\w-]+: (F\d+)$/.exec(n)?.[1]).filter((x): x is string => !!x)))].sort(); + const failed = results.filter((r) => r.status === 'fail'); + const skipped = results.filter((r) => r.status === 'skipped'); + const inapplicable = skipped.filter((r) => inapplicableReason(r.producer, r.shape) !== undefined); + const unavailable = skipped.length - inapplicable.length; + const lines = [ + `\nupgrade-matrix (${PLATFORM}/${process.arch}, target v${run.target} @ ${run.targetSha.slice(0, 8)}, harness ${run.harnessHash.slice(0, 12)}):`, + ` ${passed.length} rows passed all non-xfail assertions; ${withKnown.length} of them carry known expected failures${knownIds.length ? ` (${knownIds.join(', ')})` : ''}; ${failed.length} rows failed; ${inapplicable.length} rows are structurally inapplicable${unavailable ? `; ${unavailable} rows SKIPPED because the producer was unavailable/excluded (never counts as a pass)` : ''} → ${path.relative(ROOT, out)}`, + ]; + for (const r of results) { + const bad = r.checks.filter((c) => c.status !== 'pass' && c.status !== 'skipped').map((c) => `${c.id}=${c.status}${c.detail ? ` (${c.detail})` : ''}`); + lines.push(` ${r.status.padEnd(7)} ${r.producer.padEnd(8)} ${r.shape.padEnd(15)}${r.reason ? ` ${r.reason}` : ''}${bad.length ? ` ${bad.join('; ')}` : ''}`); + } + process.stdout.write(lines.join('\n') + '\n'); + if (process.env['SQUAD_UPGRADE_MATRIX_KEEP'] !== '1') { + for (const w of workRoots) rmrf(w); + rmrf(staging); + } + }, 10 * 60_000); + + const USER_MCP_SERVER = { command: 'node', args: ['my-server.js'], env: { TOKEN_FILE: 'secrets/token' } }; + const USER_AGENTS_MD = '# My project\n\nHand-written AGENTS.md paragraph that Squad must keep.\n'; + const USER_CLAUDE_MD = '# Claude notes\n\nKeep this paragraph verbatim.\n'; + const USER_GITIGNORE_LINE = 'my-private-notes/'; + const PROBE_CHARTER = '# Probe\n\n## Identity\n- **Name:** Probe\n- **Role:** Upgrade Probe\n\n## Notes\nWritten by the matrix before upgrade.\n'; + const PROBE_HISTORY = '# History\n\n- pre-upgrade learning (must survive byte-exact)\n'; + + /** + * The 1.x user-profile roster directory name, as the old producers spell it. It is seed data: old CLIs + * only write `teamRoot` when they find this exact directory, which is what exercises the retired + * profile-link path. Assembled from parts so the retired-symbol gate (test/retired-symbols.test.ts) + * keeps proving the product itself never names it. + */ + const LEGACY_ROSTER_DIR = ['personal', 'squad'].join('-'); + + function seedProfile(configHome: string): string { + const root = path.join(configHome, 'squad'); + const personal = path.join(root, LEGACY_ROSTER_DIR); + const global = path.join(root, '.squad'); + fs.mkdirSync(path.join(personal, 'agents', 'ghostwriter'), { recursive: true }); + fs.writeFileSync(path.join(personal, 'config.json'), JSON.stringify({ defaultModel: 'auto' }, null, 2) + '\n'); + fs.writeFileSync(path.join(personal, 'agents', 'ghostwriter', 'charter.md'), '# Ghostwriter\n\n## Identity\n- **Name:** Ghostwriter\n- **Role:** Advisor\n'); + fs.writeFileSync(path.join(personal, 'agents', 'ghostwriter', 'history.md'), '# History\n\n- learned something\n'); + fs.mkdirSync(path.join(global, 'agents', 'globetrotter'), { recursive: true }); + fs.writeFileSync(path.join(global, 'config.json'), JSON.stringify({ version: 1 }, null, 2) + '\n'); + fs.writeFileSync(path.join(global, 'team.md'), '# Profile Squad\n\n## Members\n\n| Name | Role |\n|---|---|\n| Globetrotter | Advisor |\n'); + fs.writeFileSync(path.join(global, 'agents', 'globetrotter', 'charter.md'), '# Globetrotter\n'); + return root; + } + + async function git(cwd: string, env: NodeJS.ProcessEnv, ...args: string[]): Promise { + return run('git', args, { cwd, env, timeout: 30_000 }); + } + + /** Every ref OID, the porcelain status and the `squad-state` commit count: idempotency must hold here too. */ + async function gitState(cwd: string, env: NodeJS.ProcessEnv): Promise<{ refs: string; status: string; stateCommits: string }> { + const refs = await git(cwd, env, 'for-each-ref', '--format=%(refname) %(objectname)'); + const status = await git(cwd, env, 'status', '--porcelain=v1', '--untracked-files=all'); + const branch = await git(cwd, env, 'show-ref', '--verify', '--quiet', 'refs/heads/squad-state'); + const count = branch.code === 0 ? await git(cwd, env, 'rev-list', '--count', 'squad-state') : null; + return { refs: refs.stdout.trim(), status: status.stdout.trim(), stateCommits: count ? count.stdout.trim() : 'no squad-state branch' }; + } + + function gitDiff(a: { refs: string; status: string; stateCommits: string }, b: { refs: string; status: string; stateCommits: string }): string[] { + const out: string[] = []; + if (a.refs !== b.refs) { + const before = new Map(a.refs.split('\n').filter(Boolean).map((l) => l.split(' ') as [string, string])); + const after = new Map(b.refs.split('\n').filter(Boolean).map((l) => l.split(' ') as [string, string])); + for (const [ref, oid] of after) if (before.get(ref) !== oid) out.push(`ref ${ref} ${before.get(ref)?.slice(0, 8) ?? '(new)'} → ${oid.slice(0, 8)}`); + for (const ref of before.keys()) if (!after.has(ref)) out.push(`ref ${ref} deleted`); + } + if (a.status !== b.status) out.push(`worktree status changed: ${JSON.stringify(b.status.slice(0, 200))}`); + if (a.stateCommits !== b.stateCommits) out.push(`squad-state commits ${a.stateCommits} → ${b.stateCommits}`); + return out; + } + + /** path → blob OID for every file on a ref (no checkout). */ + async function branchManifest(cwd: string, env: NodeJS.ProcessEnv, ref: string): Promise> { + const out = await git(cwd, env, 'ls-tree', '-r', '-z', ref); + const m = new Map(); + for (const entry of out.stdout.split('\0').filter(Boolean)) { + const [meta, file] = entry.split('\t') as [string, string]; + m.set(file, meta.split(' ')[2]!); + } + return m; + } + + /** + * Write files onto a ref as git blobs (plumbing only, working tree untouched): the way two-layer / + * orphan projects hold decisions.md and agent histories. + */ + async function seedBranchBlobs(cwd: string, env: NodeJS.ProcessEnv, ref: string, files: Record): Promise<{ ok: true } | { ok: false; error: string }> { + const indexFile = path.join(cwd, '.git', `matrix-seed-${process.pid}.idx`); + const ienv = { ...env, GIT_INDEX_FILE: indexFile }; + try { + const rt = await git(cwd, ienv, 'read-tree', ref); + if (rt.code !== 0) return { ok: false, error: `read-tree: ${tail(rt.stderr, 1)}` }; + for (const [rel, content] of Object.entries(files)) { + const tmp = path.join(cwd, '.git', `matrix-blob-${process.pid}`); + fs.writeFileSync(tmp, content); + const ho = await git(cwd, ienv, 'hash-object', '-w', tmp); + fs.rmSync(tmp, { force: true }); + if (ho.code !== 0) return { ok: false, error: `hash-object ${rel}: ${tail(ho.stderr, 1)}` }; + const ui = await git(cwd, ienv, 'update-index', '--add', '--cacheinfo', `100644,${ho.stdout.trim()},${rel}`); + if (ui.code !== 0) return { ok: false, error: `update-index ${rel}: ${tail(ui.stderr, 1)}` }; + } + const wt = await git(cwd, ienv, 'write-tree'); + if (wt.code !== 0) return { ok: false, error: `write-tree: ${tail(wt.stderr, 1)}` }; + const ct = await git(cwd, env, 'commit-tree', wt.stdout.trim(), '-p', ref, '-m', 'matrix: seed user state'); + if (ct.code !== 0) return { ok: false, error: `commit-tree: ${tail(ct.stderr, 1)}` }; + const ur = await git(cwd, env, 'update-ref', ref, ct.stdout.trim()); + if (ur.code !== 0) return { ok: false, error: `update-ref: ${tail(ur.stderr, 1)}` }; + return { ok: true }; + } finally { + fs.rmSync(indexFile, { force: true }); + } + } + + /** Squad-owned paths that a plain `squad upgrade` may legitimately create or rewrite (documented in the fixture). */ + const SQUAD_OWNED: RegExp[] = matrix.squadOwned.map((p) => new RegExp(p)); + const isSquadOwned = (rel: string) => SQUAD_OWNED.some((re) => re.test(rel)); + + function parseFrontmatter(text: string): Record | null { + const m = /^---\r?\n([\s\S]*?)\r?\n---(\r?\n|$)/.exec(text); + if (!m) return null; + try { return (yamlParse(m[1]!) ?? {}) as Record; } catch { return null; } + } + + /** + * Host projections must *parse* with the format each host uses, not merely exist + * (mirrors the per-host assertions in test/clients.test.ts). + */ + function validateProjections(project: string): string[] { + const problems: string[] = []; + const json = (rel: string, check: (v: Record) => string | null) => { + const raw = read(path.join(project, rel)); + if (raw === null) return problems.push(`${rel} missing`); + try { const v = JSON.parse(raw) as Record; const why = check(v); if (why) problems.push(`${rel}: ${why}`); } catch (e) { problems.push(`${rel}: invalid JSON (${(e as Error).message})`); } + }; + const toml = (rel: string, check: (v: Record) => string | null) => { + const raw = read(path.join(project, rel)); + if (raw === null) return problems.push(`${rel} missing`); + try { const v = tomlParse(raw) as Record; const why = check(v); if (why) problems.push(`${rel}: ${why}`); } catch (e) { problems.push(`${rel}: invalid TOML (${(e as Error).message})`); } + }; + const agentMd = (rel: string, requiredKeys: string[] = ['name', 'description']) => { + const raw = read(path.join(project, rel)); + if (raw === null) return problems.push(`${rel} missing`); + const fm = parseFrontmatter(raw); + if (!fm) return problems.push(`${rel}: no parseable YAML frontmatter`); + if (!requiredKeys.some((k) => fm[k] !== undefined && fm[k] !== null)) problems.push(`${rel}: frontmatter lacks ${requiredKeys.join('/')}`); + if (!/Squad/i.test(raw)) problems.push(`${rel}: body does not mention Squad`); + }; + const hasSquadState = (v: Record, key = 'mcpServers') => { + const servers = v[key] as Record | undefined; + return servers && typeof servers === 'object' && 'squad_state' in servers ? null : `no ${key}.squad_state entry`; + }; + // copilot + agentMd('.github/agents/squad.agent.md'); + json('.mcp.json', (v) => hasSquadState(v)); + // claude + agentMd('.claude/agents/squad.md'); + // kiro + agentMd('.kiro/steering/squad.md', ['inclusion']); + json('.kiro/agents/squad.json', (v) => (typeof v['name'] === 'string' ? null : 'agent definition lacks name')); + json('.kiro/settings/mcp.json', (v) => hasSquadState(v)); + // opencode + agentMd('.opencode/agent/squad.md'); + json('opencode.json', (v) => hasSquadState(v, 'mcp')); + // cursor + agentMd('.cursor/agents/squad.md'); + agentMd('.cursor/rules/squad.mdc', ['description', 'alwaysApply', 'globs']); + json('.cursor/mcp.json', (v) => hasSquadState(v)); + // codex + toml('.codex/agents/squad.toml', (v) => (typeof v['name'] === 'string' || typeof v['description'] === 'string' ? null : 'agent definition lacks name/description')); + toml('.codex/config.toml', (v) => { + const servers = v['mcp_servers'] as Record | undefined; + return servers && 'squad_state' in servers ? null : 'no mcp_servers.squad_state table'; + }); + if (read(path.join(project, 'AGENTS.md')) === null) problems.push('AGENTS.md missing'); + // antigravity + agentMd('.agents/agents/squad.md'); + if (read(path.join(project, 'GEMINI.md')) === null) problems.push('GEMINI.md missing'); + return problems; + } + + async function runScenario(producer: Producer, shape: string): Promise { + const s = new Scenario(producer, shape); + const { spec } = producer; + const structural = structuralSkip(spec, shape); + if (structural) return s.result('skipped', inapplicableReason(spec.version, shape) ?? structural); + if (producer.failReason) { + s.check('producer-artifact-verified', false, producer.failReason); + return s.result('fail', producer.failReason); + } + s.check('producer-artifact-verified', true); + if (!producer.exec) return s.result('skipped', producer.skipReason); + + const work = fs.mkdtempSync(path.join(os.tmpdir(), `squad-um-${spec.version.replace(/[^\w.]/g, '_')}-${shape}-`)); + workRoots.push(work); + const project = path.join(work, 'project'); + const configHome = path.join(work, 'config-home'); + fs.mkdirSync(project); + fs.mkdirSync(configHome); + const profileRoot = path.join(configHome, 'squad'); + const fsLog = path.join(work, 'profile-fs-access.log'); + const oldEnv = isolatedEnv(configHome, producer.exec.env); + // Every Squad 2.0 process (upgrade, doctor, projection, uninstall) runs with the fs access logger + // pointed at the retired profile tree: zero reads, probes or writes may land there. + const newEnv = isolatedEnv(configHome, { + ...newCli.env, + NODE_OPTIONS: `--require ${JSON.stringify(FS_HOOK)}`, + SQUAD_MATRIX_FS_WATCH_ROOT: profileRoot, + SQUAD_MATRIX_FS_WATCH_LOG: fsLog, + }); + const oldCli = (...args: string[]) => run(producer.exec!.file, [...producer.exec!.args, ...args], { cwd: project, env: oldEnv, timeout: 180_000 }); + const squad = (cwd: string, ...args: string[]) => run(newCli.file, [...newCli.args, ...args], { cwd, env: newEnv, timeout: 180_000 }); + + await git(project, newEnv, 'init', '-q', '-b', 'main'); + await git(project, newEnv, 'commit', '-q', '--allow-empty', '-m', 'init'); + + const seededProfile = shape === 'profile' ? seedProfile(configHome) : null; + const profileBefore = seededProfile ? fingerprint(seededProfile) : null; + + // 1. OLD CLI scaffolds the project ───────────────────────────────────── + const initArgs = ['init', ...(shape === 'orphan' || shape === 'two-layer' ? ['--state-backend', shape] : [])]; + const init = await oldCli(...initArgs); + const teamMd = path.join(project, '.squad', 'team.md'); + if (!s.check('producer-init', init.code === 0 && fs.existsSync(teamMd), `exit ${init.code}: ${tail(init.stderr || init.stdout)}`)) { + return s.result('fail', `old CLI ${spec.version} ${initArgs.join(' ')} failed: exit ${init.code} — ${tail(init.stderr || init.stdout)}`); + } + const cfgAfterInit = readJson(path.join(project, '.squad', 'config.json')) ?? {}; + if (typeof cfgAfterInit['teamRoot'] === 'string') s.notes.push(`producer wrote teamRoot=${String(cfgAfterInit['teamRoot']).replace(configHome, '')} at init`); + + // 2. Seed user state (active + retired agents, nested unknown files, spaces/Unicode, custom config keys) ─ + const squadDir = path.join(project, '.squad'); + const onBackend = shape === 'orphan' || shape === 'two-layer'; + // In backend shapes the mutable state (decisions.md, agents/*/history.md) lives ONLY on the + // `squad-state` branch; seed those as git blobs there and keep everything else in the working tree. + const mutableState: Record = { + 'agents/probe/history.md': PROBE_HISTORY, + 'agents/emeritus/history.md': '# History\n\n- retired before the upgrade; must survive untouched\n', + }; + const userFiles: Record = { + 'agents/probe/charter.md': PROBE_CHARTER, + 'agents/probe/notes/日本語 メモ.md': '# メモ\n\nUnicode filename under an agent folder.\n', + 'agents/emeritus/charter.md': '# Emeritus\n\n## Identity\n- **Name:** Emeritus\n- **Role:** Retired Advisor\n- **Status:** retired\n', + 'skills/custom-review/SKILL.md': '---\nname: custom-review\ndescription: user-authored skill\n---\n\nUser skill body.\n', + 'notes/file with spaces.md': 'Spaces in the filename.\n', + 'decisions/2026-10 ünïcödé décision.md': '# Décision\n\nAccents in the filename.\n', + 'orchestration/unknown-tool.json': '{"vendor":"user","keep":true}\n', + ...(onBackend ? {} : mutableState), + }; + for (const [rel, content] of Object.entries(userFiles)) { + fs.mkdirSync(path.dirname(path.join(squadDir, rel)), { recursive: true }); + fs.writeFileSync(path.join(squadDir, rel), content); + } + const DECISION = '\n## Matrix decision\n\n- recorded before upgrade; must survive byte-exact\n'; + let branchSeeded: Record = {}; + if (onBackend) { + const existingDecisions = (await git(project, newEnv, 'show', 'refs/heads/squad-state:decisions.md')).stdout; + branchSeeded = { ...mutableState, 'decisions.md': existingDecisions + DECISION }; + const seeded = await seedBranchBlobs(project, newEnv, 'refs/heads/squad-state', branchSeeded); + if (!seeded.ok) return s.result('fail', `could not seed user state onto squad-state: ${seeded.error}`); + } else { + fs.appendFileSync(path.join(squadDir, 'decisions.md'), DECISION); + } + fs.appendFileSync(teamMd, '| Probe | Upgrade Probe | .squad/agents/probe/charter.md | ✅ Active |\n| Emeritus | Retired Advisor | .squad/agents/emeritus/charter.md | 🪦 Retired |\n'); + fs.appendFileSync(path.join(squadDir, 'routing.md'), '\n**Upgrade probes** → Probe\n'); + const cfgPath = path.join(squadDir, 'config.json'); + const cfgSeed = readJson(cfgPath) ?? {}; + const CUSTOM_KEY = { owner: 'matrix', nested: { keep: true, list: [1, 2, 3] } }; + cfgSeed['userCustom'] = CUSTOM_KEY; + fs.writeFileSync(cfgPath, JSON.stringify(cfgSeed, null, 2) + '\n'); + for (const rel of ['decisions.md', 'team.md', 'routing.md']) userFiles[rel] = read(path.join(squadDir, rel))!; + const mcpPath = path.join(project, '.mcp.json'); + const mcp = readJson<{ mcpServers?: Record }>(mcpPath) ?? { mcpServers: {} }; + mcp.mcpServers = { ...(mcp.mcpServers ?? {}), 'user-custom': USER_MCP_SERVER }; + fs.writeFileSync(mcpPath, JSON.stringify(mcp, null, 2) + '\n'); + fs.writeFileSync(path.join(project, 'AGENTS.md'), USER_AGENTS_MD); + fs.writeFileSync(path.join(project, 'CLAUDE.md'), USER_CLAUDE_MD); + fs.appendFileSync(path.join(project, '.gitignore'), `\n# user rule\n${USER_GITIGNORE_LINE}\n`); + + if (shape === 'legacy-casting') { + const casting = path.join(squadDir, 'casting'); + for (const f of fs.readdirSync(casting)) fs.rmSync(path.join(casting, f), { recursive: true, force: true }); + fs.writeFileSync(path.join(casting, 'registry.json'), JSON.stringify({ universe: 'Star Trek', agents: [{ name: 'Probe', role: 'Upgrade Probe', status: 'active' }, { name: 'Emeritus', role: 'Retired Advisor', status: 'retired' }] }, null, 2) + '\n'); + } + if (shape === 'ai-team') { + fs.renameSync(squadDir, path.join(project, '.ai-team')); + } + let upstreamJsonBefore: string | null = null; + if (shape === 'upstream') { + const upstream = path.join(work, 'upstream-squad'); + fs.mkdirSync(upstream); + await git(upstream, newEnv, 'init', '-q', '-b', 'main'); + const ui = await squad(upstream, 'init'); + const add = await squad(project, 'upstream', 'add', upstream, '--name', 'org'); + upstreamJsonBefore = read(path.join(project, '.squad', 'upstream.json')); + if (ui.code !== 0 || add.code !== 0 || !upstreamJsonBefore) return s.result('fail', `could not configure upstream: init exit ${ui.code}, add exit ${add.code} — ${tail(add.stderr || add.stdout)}`); + } + + const stateDirBefore = shape === 'ai-team' ? '.ai-team' : '.squad'; + // Manifest before the upgrade, with the legacy state dir already mapped onto `.squad/` so the + // migration itself is not counted as a change. + const beforeRaw = snapshot(project); + const before = new Map([...beforeRaw].map(([k, v]) => [k.startsWith(`${stateDirBefore}/`) ? `.squad/${k.slice(stateDirBefore.length + 1)}` : k, v])); + const branchBefore = onBackend ? await branchManifest(project, newEnv, 'refs/heads/squad-state') : null; + const backendBefore = (readJson(path.join(project, stateDirBefore, 'config.json')) ?? {})['stateBackend']; + const profilePhases: string[] = []; + const profileCheckpoint = (phase: string) => { if (seededProfile && fingerprint(seededProfile) !== profileBefore) profilePhases.push(phase); }; + profileCheckpoint('producer init + seeding'); + + // 3. NEW CLI: plain upgrade ──────────────────────────────────────────── + const up = await squad(project, 'upgrade', ...(shape === 'ai-team' ? ['--migrate-directory'] : [])); + if (!s.check('upgrade-plain', up.code === 0, `exit ${up.code}: ${tail(up.stderr || up.stdout)}`)) { + return s.result('fail', `squad upgrade failed: exit ${up.code} — ${tail(up.stderr || up.stdout)}`); + } + profileCheckpoint('squad upgrade'); + const afterPlain = snapshot(project); + const afterPlainMcp = read(mcpPath); + const gitAfterPlain = await gitState(project, newEnv); + const branchAfterPlain = onBackend ? await branchManifest(project, newEnv, 'refs/heads/squad-state') : null; + const coordinator = read(path.join(project, '.github', 'agents', 'squad.agent.md')) ?? ''; + const stampRe = //.exec(coordinator); + s.check('coordinator-is-new-template', stampRe?.[1] === newVersion && coordinator.includes(`- **Version:** ${newVersion}`), `coordinator stamp is ${stampRe?.[1] ?? 'missing'}, expected ${newVersion}`); + + // Full manifest delta: every seeded user file byte-identical (working tree AND squad-state blobs), + // every changed path Squad-owned, custom config keys kept. + const delta = diffSnapshots(before, afterPlain); + const userTouched = Object.keys(userFiles).map((rel) => `.squad/${rel}`).filter((rel) => afterPlain.get(rel) !== before.get(rel)); + const unexplained = delta.filter((rel) => !isSquadOwned(rel)); + const cfgAfter = readJson(path.join(project, '.squad', 'config.json')) ?? {}; + const customKept = JSON.stringify(cfgAfter['userCustom']) === JSON.stringify(CUSTOM_KEY); + const branchProblems = branchBefore && branchAfterPlain + ? [ + ...Object.keys(branchSeeded).filter((rel) => branchAfterPlain.get(rel) !== branchBefore.get(rel)).map((rel) => `squad-state blob changed or lost: ${rel} (${branchBefore.get(rel)?.slice(0, 8) ?? 'absent'} → ${branchAfterPlain.get(rel)?.slice(0, 8) ?? 'absent'})`), + ...[...branchBefore.keys()].filter((rel) => !branchAfterPlain.has(rel)).map((rel) => `squad-state path deleted: ${rel}`), + ] + : []; + const stateProblems = [ + ...userTouched.map((rel) => `user file rewritten: ${rel}`), + ...unexplained.map((rel) => `changed path is not Squad-owned: ${rel}`), + ...branchProblems, + ...(customKept ? [] : ['config.json custom key `userCustom` lost or altered']), + ]; + s.check('user-state-preserved', stateProblems.length === 0, stateProblems.join('; ') || undefined); + s.notes.push(`plain upgrade changed ${delta.length} working-tree path(s), all Squad-owned: ${delta.length <= 6 ? delta.join(', ') : `${delta.slice(0, 4).join(', ')} … (+${delta.length - 4})`}${branchBefore ? `; squad-state blobs for ${Object.keys(branchSeeded).length} seeded path(s) identical` : ''}`); + + const castingScript = [ + 'import { readCastingRegistryPair } from "@bradygaster/squad-sdk/casting";', + `const pair = readCastingRegistryPair(${JSON.stringify(path.join(project, '.squad', 'casting'))});`, + 'const reg = pair.registry; const agents = reg && reg.agents ? Object.keys(reg.agents) : [];', + 'if (!reg || typeof reg.revision !== "number" || !pair.history) throw new Error("registry/history pair invalid: " + JSON.stringify({ hasRegistry: !!reg, revision: reg && reg.revision, hasHistory: !!pair.history }));', + 'console.log(JSON.stringify({ revision: reg.revision, agents }));', + ].join(' '); + const casting = await run(process.execPath, ['--input-type=module', '-e', castingScript], { cwd: staging, env: newEnv, timeout: 30_000 }); + const castingInfo = casting.code === 0 ? (JSON.parse(casting.stdout.trim()) as { revision: number; agents: string[] }) : null; + const castingDir = path.join(project, '.squad', 'casting'); + const archive = fs.existsSync(castingDir) ? fs.readdirSync(castingDir).find((n) => /^(handwritten|legacy)-archive-/.test(n)) : undefined; + const legacyOk = shape !== 'legacy-casting' || (!!archive && !!castingInfo?.agents.includes('probe') && !!castingInfo?.agents.includes('emeritus')); + s.check('casting-pair-valid', casting.code === 0 && legacyOk, casting.code !== 0 ? tail(casting.stderr, 1) : !legacyOk ? (!archive ? 'handwritten registry was not archived' : `repaired registry lacks probe/emeritus (${castingInfo?.agents.join(',')})`) : undefined); + + if (shape === 'orphan' || shape === 'two-layer') { + const branch = await git(project, newEnv, 'show-ref', '--verify', '--quiet', 'refs/heads/squad-state'); + s.check('state-backend-preserved', cfgAfter['stateBackend'] === shape && branch.code === 0, `stateBackend=${String(cfgAfter['stateBackend'])} (was ${String(backendBefore)}), squad-state branch ${branch.code === 0 ? 'present' : 'missing'}`); + } else { + s.check('state-backend-preserved', (cfgAfter['stateBackend'] ?? 'local') === (backendBefore ?? 'local'), `stateBackend ${String(backendBefore)} → ${String(cfgAfter['stateBackend'])}`); + } + + if (shape === 'ai-team') { + s.check('ai-team-migrated', fs.existsSync(path.join(project, '.squad', 'team.md')) && !fs.existsSync(path.join(project, '.ai-team')), fs.existsSync(path.join(project, '.ai-team')) ? '.ai-team/ still present' : '.squad/team.md missing after migration'); + } else { + s.skip('ai-team-migrated', 'not an .ai-team shape'); + } + + s.check('team-root-absent', !('teamRoot' in cfgAfter), 'teamRoot' in cfgAfter ? `teamRoot=${String(cfgAfter['teamRoot']).replace(configHome, '')}` : undefined); + + if (shape === 'upstream') { + const upstreamAfter = read(path.join(project, '.squad', 'upstream.json')); + s.check('user-config-preserved', upstreamAfter === upstreamJsonBefore, 'upstream.json rewritten by upgrade'); + } + + // 4. Idempotency: a second plain upgrade changes neither the working tree nor git state ── + const again = await squad(project, 'upgrade'); + profileCheckpoint('second squad upgrade'); + const changedAgain = diffSnapshots(afterPlain, snapshot(project)); + const gitChangedAgain = gitDiff(gitAfterPlain, await gitState(project, newEnv)); + const idemProblems = [...(again.code !== 0 ? [`exit ${again.code}`] : []), ...changedAgain, ...gitChangedAgain]; + s.check('idempotent-second-upgrade', idemProblems.length === 0, idemProblems.join(', ') || undefined); + + // 5. Downgrade probe: OLD CLI against the upgraded project (dry-run, on a copy) — unsupported path, recorded ─ + if (spec.source === 'pack') { + s.skip('downgrade-probe', 'producer is the target version'); + } else if (!spec.supports.dryRun) { + s.skip('downgrade-probe', `producer ${spec.version} has no --dry-run`); + } else { + const copy = path.join(work, 'downgrade-copy'); + fs.cpSync(project, copy, { recursive: true }); + const copyBefore = snapshot(copy); + const dg = await run(producer.exec.file, [...producer.exec.args, 'upgrade', '--dry-run'], { cwd: copy, env: oldEnv, timeout: 180_000 }); + const dgChanged = diffSnapshots(copyBefore, snapshot(copy)); + const mentions = /downgrad|newer|older|already up to date|would (upgrade|downgrade|refresh)[^\n]*/i.exec(dg.stdout + dg.stderr)?.[0]?.trim(); + const modified = dgChanged.length === 0 ? 'modified nothing' + : dgChanged.length <= 4 ? `MODIFIED ${dgChanged.join(', ')}` + : `MODIFIED ${dgChanged.length} files (${dgChanged.filter((f) => /squad\.agent\.md$|^\.squad\/templates\/|^\.github\/workflows\//.test(f)).length} coordinator/templates/workflows, e.g. ${dgChanged.slice(0, 3).join(', ')} …)`; + s.notes.push(`old ${spec.version} upgrade --dry-run on upgraded project: exit ${dg.code}${mentions ? `, says "${mentions.slice(0, 120)}"` : `, last lines "${tail(dg.stdout || dg.stderr, 3).slice(0, 200)}"`}; ${modified}`); + s.check('downgrade-probe', dgChanged.length === 0, dgChanged.length ? `dry-run ${modified}` : undefined); + profileCheckpoint('old CLI --dry-run (downgrade probe)'); + } + + // 6. Project all 7 hosts — and prove every projection parses ────────── + const clients = await squad(project, 'upgrade', '--client', 'all', '--force'); + profileCheckpoint('squad upgrade --client all'); + const cfgClients = (readJson(path.join(project, '.squad', 'config.json')) ?? {})['clients']; + const projectionProblems = clients.code === 0 ? validateProjections(project) : []; + s.check('clients-projected', clients.code === 0 && JSON.stringify(cfgClients) === JSON.stringify(CLIENT_IDS) && projectionProblems.length === 0, + clients.code !== 0 ? `exit ${clients.code}: ${tail(clients.stderr || clients.stdout)}` : projectionProblems.length ? projectionProblems.join('; ') : `clients=${JSON.stringify(cfgClients)}`); + + const mergedMcp = readJson<{ mcpServers?: Record }>(mcpPath); + const agentsNow = read(path.join(project, 'AGENTS.md')) ?? ''; + const claudeNow = read(path.join(project, 'CLAUDE.md')) ?? ''; + const gitignoreNow = read(path.join(project, '.gitignore')) ?? ''; + const cfgProblems = [ + JSON.stringify(mergedMcp?.mcpServers?.['user-custom']) !== JSON.stringify(USER_MCP_SERVER) ? '.mcp.json user-custom server lost' : null, + !mergedMcp?.mcpServers?.['squad_state'] ? '.mcp.json squad_state missing' : null, + !agentsNow.startsWith(USER_AGENTS_MD) ? 'AGENTS.md user paragraph lost' : null, + !claudeNow.startsWith(USER_CLAUDE_MD) ? 'CLAUDE.md user paragraph lost' : null, + !gitignoreNow.includes(USER_GITIGNORE_LINE) ? '.gitignore user rule lost' : null, + ].filter((x): x is string => x !== null); + if (shape !== 'upstream') s.check('user-config-preserved', cfgProblems.length === 0, cfgProblems.join('; ') || undefined); + + // 7. doctor ───────────────────────────────────────────────────────────── + const doctor = await squad(project, 'doctor'); + profileCheckpoint('squad doctor'); + const failedLines = doctor.stdout.split(/\r?\n/).filter((l) => /^❌/.test(l.trim())).map((l) => l.trim()); + s.check('doctor', doctor.code === 0 && failedLines.length === 0, failedLines.length ? failedLines.join(' | ') : doctor.code !== 0 ? `exit ${doctor.code}: ${tail(doctor.stderr || doctor.stdout)}` : undefined); + + // 8. Projection uninstall (uninstallClients) — this is NOT a downgrade ──────────────────── + // Baseline = the tree right after the plain upgrade, i.e. before any projection. uninstall must + // (a) restore every shared file to those bytes, except that squad_state entries Squad itself + // installed into shared MCP configs are removed, (b) remove every host projection and every + // Squad-owned entry from host configs, and (c) leave .squad/ user state untouched. + const uninstallScript = [ + 'import { uninstallClients, CLIENT_IDS } from "@bradygaster/squad-sdk/client";', + `const res = uninstallClients(CLIENT_IDS, { root: ${JSON.stringify(project)}, mcp: { command: "squad", args: ["state-mcp"] } });`, + 'const errors = res.filter(r => r.action === "error"); if (errors.length) throw new Error(JSON.stringify(errors));', + ].join(' '); + const un = await run(process.execPath, ['--input-type=module', '-e', uninstallScript], { cwd: staging, env: newEnv, timeout: 30_000 }); + profileCheckpoint('uninstallClients'); + const afterUninstall = snapshot(project); + const branchAfterUninstall = onBackend ? await branchManifest(project, newEnv, 'refs/heads/squad-state') : null; + const sharedFiles = ['AGENTS.md', 'CLAUDE.md', 'GEMINI.md', '.gitignore']; + const projectionFiles = [ + '.claude/agents/squad.md', '.kiro/steering/squad.md', '.kiro/agents/squad.json', '.opencode/agent/squad.md', + '.cursor/agents/squad.md', '.cursor/rules/squad.mdc', '.codex/agents/squad.toml', '.agents/agents/squad.md', + ]; + const hostMcpJson = ['.kiro/settings/mcp.json', '.cursor/mcp.json']; + const withoutSquadState = (raw: string | null): unknown => { + if (raw === null) return null; + try { + const v = JSON.parse(raw) as Record; + for (const key of ['mcpServers', 'mcp']) { + const servers = v[key] as Record | undefined; + if (servers && typeof servers === 'object') delete servers['squad_state']; + } + return v; + } catch { return raw; } + }; + const mcpNow = readJson<{ mcpServers?: Record }>(mcpPath); + const unProblems = [ + un.code !== 0 ? `uninstallClients failed: ${tail(un.stderr, 1)}` : null, + ...sharedFiles.filter((rel) => afterUninstall.get(rel) !== afterPlain.get(rel)).map((rel) => `${rel} not restored to its pre-projection bytes (${afterPlain.has(rel) ? 'differs' : 'did not exist before, exists now'})`), + ...projectionFiles.filter((rel) => afterUninstall.has(rel)).map((rel) => `${rel} still present`), + // .mcp.json: identical to pre-projection content minus the squad_state entry Squad installed; the user's server intact. + JSON.stringify(withoutSquadState(read(mcpPath))) !== JSON.stringify(withoutSquadState(afterPlainMcp)) ? '.mcp.json differs from its pre-projection content (ignoring squad_state)' : null, + JSON.stringify(mcpNow?.mcpServers?.['user-custom']) !== JSON.stringify(USER_MCP_SERVER) ? '.mcp.json user-custom server lost on uninstall' : null, + (mcpNow?.mcpServers ?? {})['squad_state'] !== undefined ? '.mcp.json still carries squad_state after uninstalling every client' : null, + // Host MCP configs created by the projection must be gone or free of Squad entries. + ...hostMcpJson.filter((rel) => afterUninstall.has(rel) && !afterPlain.has(rel) && (readJson<{ mcpServers?: Record }>(path.join(project, rel))?.mcpServers ?? {})['squad_state'] !== undefined).map((rel) => `${rel} still carries squad_state`), + afterUninstall.has('opencode.json') && !afterPlain.has('opencode.json') && (readJson<{ mcp?: Record }>(path.join(project, 'opencode.json'))?.mcp ?? {})['squad_state'] !== undefined ? 'opencode.json still carries mcp.squad_state' : null, + afterUninstall.has('.codex/config.toml') && !afterPlain.has('.codex/config.toml') && /squad_state/.test(read(path.join(project, '.codex', 'config.toml')) ?? '') ? '.codex/config.toml still carries mcp_servers.squad_state' : null, + ...Object.keys(userFiles).map((rel) => `.squad/${rel}`).filter((rel) => afterUninstall.get(rel) !== before.get(rel)).map((rel) => `${rel} changed by projection/uninstall`), + ...(branchBefore ? Object.keys(branchSeeded).filter((rel) => branchAfterUninstall?.get(rel) !== branchBefore.get(rel)).map((rel) => `squad-state blob changed by projection/uninstall: ${rel}`) : []), + ].filter((x): x is string => x !== null); + s.check('projection-uninstall-byte-exact', unProblems.length === 0, unProblems.join('; ') || undefined); + + // 9. Profile integrity: no writes (fingerprint, checked after every phase) AND no reads/probes + // (fs access log) by any 2.0 process ───────────────────────────────────────────────────── + if (seededProfile) { + const hits = (read(fsLog) ?? '').split(/\r?\n/).filter(Boolean); + const problems = [ + profilePhases.length ? `profile tree changed (sha256 fingerprint differs) after: ${profilePhases.join(', ')}` : null, + hits.length ? `${hits.length} fs access(es) under the profile root by Squad 2.0 processes, e.g. ${hits.slice(0, 3).map((h) => h.replace(configHome, '')).join('; ')}` : null, + ].filter((x): x is string => x !== null); + s.check('profile-untouched', problems.length === 0, problems.join('; ') || undefined); + } else { + s.skip('profile-untouched', 'no profile squad seeded in this shape'); + } + + return s.result(); + } + + const cells = matrix.producers.flatMap((p) => SHAPES.map((shape) => ({ version: p.version, shape }))); + + it.concurrent.for(cells)('$version × $shape', async ({ version, shape }, ctx) => { + const producer = producers.get(version)!; + const result = await runScenario(producer, shape); + results.push(result); + if (result.status === 'skipped') { + ctx.skip(`${version} × ${shape}: ${result.reason}`); + return; + } + const verdict = rowVerdict(result); + expect(verdict, verdict ?? '').toBeNull(); + }, 15 * 60_000); +});