# Tasks: add-installer-upgrade ## 1. Error codes - [ ] 1.1 Append `UPGRADE_NOT_INSTALLED`, `UPGRADE_VERSION_NOT_FOUND`, `UPGRADE_SAME_VERSION`, `UPGRADE_DIRTY_TREE`, `UPGRADE_STATE_INVALID`, `UPGRADE_MIGRATION_FAILED`, `UPGRADE_LOCKFILE_MISSING` to the `defineCodes("INSTALLER", [...])` tuple in `packages/tooling/installer/src/error-codes.ts`. - [ ] 1.2 Extend `src/error-codes.spec-conformance.test.ts` with an `INSTALLER-UPGRADE-09` block asserting the codes are present, frozen, and prefixed `INSTALLER.`. ## 2. State file (`src/state/`) - [ ] 2.1 `src/state/state.types.ts` — `InstallerState`, `InstalledPlugin`, `FileHash` types. - [ ] 2.2 `src/state/state.schemas.ts` — Zod schemas for boundary validation. - [ ] 2.3 `src/state/state-file.ts` — `loadState(fs, cwd)`, `writeState(fs, cwd, state)` (atomic write → fsync → rename), `updatePlugin(state, entry)`. Failure → `INSTALLER.UPGRADE_STATE_INVALID`. - [ ] 2.4 `src/state/hash-tree.ts` — `hashTree(fs, dir)` → `Record` using BLAKE3 via `@suaveplan/crypto-hash`. - [ ] 2.5 `src/state/state-file.spec-conformance.test.ts` — `INSTALLER-UPGRADE-08`. - [ ] 2.6 `.md` siblings for every implementation file. ## 3. Pre-flight checks (`src/host/upgrade/preflight.ts`) - [ ] 3.1 `detectLockfile(fs, cwd)` returning `"bun" | "npm" | "pnpm" | "yarn"` or throwing `INSTALLER.UPGRADE_LOCKFILE_MISSING`. - [ ] 3.2 `detectDirtyTree(state, currentHashes)` returning the list of user-modified paths. Throws `INSTALLER.UPGRADE_DIRTY_TREE` unless `--allow-dirty` or `--force` was supplied. - [ ] 3.3 `src/host/upgrade/preflight.spec-conformance.test.ts` — `INSTALLER-UPGRADE-04`. - [ ] 3.4 `src/host/upgrade/dirty-tree.spec-conformance.test.ts` — `INSTALLER-UPGRADE-03`. ## 4. Diff engine (`src/host/upgrade/diff.ts`) - [ ] 4.1 `computeDiff(fromOps, toOps)` returning `{ added: FileOp[], removed: FileOp[], changed: FileOp[], unchanged: FileOp[] }`. - [ ] 4.2 `materialiseDiff(diff, options)` translating the diff into the existing `FileOp` union: removals → `{ kind: "delete" }`, additions → `{ kind: "write" }`, changes → `{ kind: "write", conflict: "overwrite" }` (or `merge` for `patch-json` ops). - [ ] 4.3 Sibling `diff.md` covering algorithm and tie-breaking rules. ## 5. Migration runner (`src/host/upgrade/migrations.ts`) - [ ] 5.1 `MigrationScript`, `MigrationContext`, `MigrationTable` types in `src/host/upgrade/migrations.types.ts`. - [ ] 5.2 `loadMigrationTable(plugin)` dynamic-imports the plugin's `./upgrade.js` if declared in the manifest; absent table → no-op. - [ ] 5.3 `selectMigrations(table, from, to)` returns the ordered semver path. - [ ] 5.4 `runMigrations(selected, ctx)` runs each script inside an `AbortSignal`-propagating sandbox. Throw → `INSTALLER.UPGRADE_MIGRATION_FAILED`. - [ ] 5.5 `src/host/upgrade/migrations.spec-conformance.test.ts` — `INSTALLER-UPGRADE-06`, `INSTALLER-UPGRADE-07`. ## 6. Upgrade orchestration (`src/host/upgrade/upgrade.ts`) - [ ] 6.1 `Host.upgrade(plugin, options)` method: 1. Load state-file entry; throw `UPGRADE_NOT_INSTALLED` if absent. 2. Resolve target version; throw `UPGRADE_VERSION_NOT_FOUND` if `node_modules` resolution fails. 3. Short-circuit with `UPGRADE_SAME_VERSION` when installed === target and not `--force`. 4. Run preflight (lockfile + dirty-tree). 5. Hash current tree; compute snapshot for rollback. 6. Run `plugin.plan` for both versions (or load cached bundle manifests). 7. `computeDiff` → `materialiseDiff` → `Host.applyOps` (existing transactional path). 8. Run migrations between the same staged-tmp transaction window. 9. On success, `writeState` atomically. - [ ] 6.2 OTel span `installer.upgrade` wrapping the whole flow with `plugin.id`, `plugin.version.from`, `plugin.version.to`, `dry_run`. - [ ] 6.3 `src/host/upgrade/upgrade.spec-conformance.test.ts` — `INSTALLER-UPGRADE-01`, `INSTALLER-UPGRADE-02`, `INSTALLER-UPGRADE-10`, `INSTALLER-UPGRADE-11`. - [ ] 6.4 `src/host/upgrade/apply-diff.spec-conformance.test.ts` — `INSTALLER-UPGRADE-05`. ## 7. CLI wiring (`src/bin/suaveplan-install.ts`) - [ ] 7.1 Add `app.command("upgrade ")` with flags `--dry-run`, `--force`, `--no-interactive`, `--answers `, `--from `, `--to `, `--allow-dirty`. - [ ] 7.2 `runUpgrade(pluginId, flags)` mirrors `runAdd` but calls `host.upgrade(...)`. - [ ] 7.3 `src/bin/suaveplan-install.spec-conformance.test.ts` — `INSTALLER-UPGRADE-12`. ## 8. Testing helpers (`src/testing/`) - [ ] 8.1 `src/testing/upgrade-plugin.ts` — `upgradePlugin(plugin, { fromState, toBundle, answers? })` mirroring the existing `runPlugin` harness for upgrade tests. - [ ] 8.2 `src/testing/fixtures/upgrade-fixture-plugin.ts` — two-version fixture plugin (`1.0.0` + `2.0.0`) with one migration script, used by every upgrade conformance test for end-to-end coverage at 100%. ## 9. Docs - [ ] 9.1 Sibling `.md` for every new implementation `.ts` per `openspec/AGENTS.md` §4 (Purpose, Features, Basic, Advanced, API, Notes). - [ ] 9.2 Update root `packages/tooling/installer/README.md` Quickstart to include the `upgrade` verb and link the new module docs. ## 10. Quality gates - [ ] 10.1 `bunx turbo lint typecheck test --filter=@suaveplan/installer` green. - [ ] 10.2 `bunx turbo test:coverage --filter=@suaveplan/installer` 100% istanbul on every new module. - [ ] 10.3 No banned patterns (`openspec/AGENTS.md` §2) in new files. - [ ] 10.4 No `biome-ignore` in new files (`openspec/AGENTS.md` §2a). - [ ] 10.5 File-size check: every `src/host/upgrade/*.ts` ≤ 300 lines, every `*.test.ts` ≤ 500 lines. - [ ] 10.6 Sibling `.md` audit returns zero gaps for new files. ## 11. Dependent sweep - [ ] 11.1 `bunx turbo test --filter=...@suaveplan/installer` green (validates `@suaveplan/install-agent-skills` against the new surface). - [ ] 11.2 Add empty `upgrade.ts` migration table export to `@suaveplan/install-agent-skills` and wire it through its manifest so the migration-loader path is exercised even with zero migrations. ## 12. Archive - [ ] 12.1 Fold `specs/installer/spec.md` delta into `openspec/specs/installer/spec.md`. - [ ] 12.2 Move `openspec/changes/add-installer-upgrade/` to `openspec/archive/-add-installer-upgrade/`.