Initial Commit
This commit is contained in:
@@ -0,0 +1,253 @@
|
||||
# Design: add-installer-upgrade
|
||||
|
||||
This document captures the non-normative design for the deferred upgrade flow.
|
||||
Normative requirements live in `specs/installer/spec.md`.
|
||||
|
||||
## Flow diagram
|
||||
|
||||
```text
|
||||
suaveplan-install upgrade <plugin> [--from X] [--to Y] [--dry-run] [--force]
|
||||
│
|
||||
▼
|
||||
┌────────────────────────────────────────────────────────────────────────┐
|
||||
│ Host.upgrade(plugin, options) │
|
||||
└────────────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
├─► loadState(fs, cwd)
|
||||
│ state-file: .suaveplan/installer-state.json
|
||||
│ missing entry → INSTALLER.UPGRADE_NOT_INSTALLED
|
||||
│
|
||||
├─► resolveTargetVersion(plugin, cwd)
|
||||
│ reads node_modules/@<scope>/<pkg>/package.json
|
||||
│ missing → INSTALLER.UPGRADE_VERSION_NOT_FOUND
|
||||
│
|
||||
├─► if installed === target && !force
|
||||
│ → INSTALLER.UPGRADE_SAME_VERSION (idempotent no-op)
|
||||
│
|
||||
├─► preflight
|
||||
│ ├─ detectLockfile() → UPGRADE_LOCKFILE_MISSING
|
||||
│ └─ detectDirtyTree() → UPGRADE_DIRTY_TREE (unless --allow-dirty)
|
||||
│
|
||||
├─► hashTree(targetDir) → currentHashes (rollback snapshot key)
|
||||
│
|
||||
├─► plan(answers, ctx) at fromVersion → fromOps
|
||||
│ plan(answers, ctx) at toVersion → toOps
|
||||
│
|
||||
├─► computeDiff(fromOps, toOps)
|
||||
│ { added, removed, changed, unchanged }
|
||||
│
|
||||
├─► materialiseDiff(diff) → FileOp[]
|
||||
│ additions → write
|
||||
│ removals → delete
|
||||
│ changes → write (overwrite) | patch-json (merge)
|
||||
│
|
||||
├─► Host.applyOps(ops) ◄── EXISTING TRANSACTIONAL PATH
|
||||
│ staged-tmp + atomic rename (INSTALLER-04)
|
||||
│
|
||||
├─► runMigrations(selected, ctx)
|
||||
│ throws → rollback transaction → UPGRADE_MIGRATION_FAILED
|
||||
│
|
||||
├─► writeState({ pluginId, version: to, installedAt, fileHashes })
|
||||
│ atomic write → fsync → rename
|
||||
│
|
||||
└─► OTel: span "installer.upgrade" closes with attributes
|
||||
plugin.id, plugin.version.from, plugin.version.to, dry_run,
|
||||
files.added, files.removed, files.changed
|
||||
```
|
||||
|
||||
## Atomicity model
|
||||
|
||||
The upgrade is **one** transaction, not two. The diff-apply step and the
|
||||
migration-run step share the same staged-tmp directory created by
|
||||
`Host.applyOps`. The window is:
|
||||
|
||||
1. **Stage** — every `write`/`patch-*` op writes its content to
|
||||
`<targetDir>/.tmp-<txn>/<path>`. Deletions are recorded as
|
||||
"move-aside to `<targetDir>/.tmp-<txn>/.deleted/<path>`" — files are
|
||||
**moved**, not removed, so rollback can restore them. No live files have
|
||||
been mutated yet.
|
||||
2. **Migrate** — `MigrationScript`s run against the staged tree (their
|
||||
`MigrationContext.fs` is rooted at the tmp directory). They can mutate
|
||||
staged files or emit additional `FileOp`s, which are appended to the
|
||||
plan and re-staged into the same tmp directory.
|
||||
3. **Commit** — once every stage step has succeeded, the host issues atomic
|
||||
`rename(tmp, live)` for every staged path **in dependency order** (parent
|
||||
dirs before children for `write`, children before parents for `delete`).
|
||||
4. **Persist state** — only after every rename commits does the host write
|
||||
the new `installer-state.json` (itself via the same write-fsync-rename
|
||||
dance scoped to one file).
|
||||
|
||||
Any failure in steps 1–3 walks the journal in reverse: staged tmp files are
|
||||
unlinked; moved-aside files are renamed back; the state-file is untouched.
|
||||
The user's tree is bit-identical to the pre-upgrade state.
|
||||
|
||||
A rollback failure (e.g. fs returns EIO on the restore rename) surfaces as
|
||||
`InstallerError(INSTALLER.ROLLBACK_FAILED, { cause: <original apply error> })`
|
||||
exactly as `INSTALLER-04` already prescribes for the `add` flow.
|
||||
|
||||
## Snapshot / rollback strategy
|
||||
|
||||
The "snapshot" is not a copy of the tree — that would be linear in target
|
||||
size and unacceptable for a skills bundle. Instead, the snapshot is:
|
||||
|
||||
1. **The pre-upgrade `hashTree`** — recorded before staging so the upgrade
|
||||
has a content-addressed manifest of what existed.
|
||||
2. **The move-aside journal** — every file the diff intends to delete or
|
||||
overwrite is moved to `<tmp>/.deleted/<path>` (rename is O(1) on the
|
||||
same filesystem). Disk usage during upgrade equals
|
||||
`size(diff.changed ∪ diff.removed)` rather than `size(entire tree)`.
|
||||
3. **The new staged content** — every `write` lands in `<tmp>/<path>` and
|
||||
is `rename`d in only on commit.
|
||||
|
||||
Rollback walks the journal in reverse: rename moved-aside files back to
|
||||
their live paths, unlink staged files, rmdir the tmp directory.
|
||||
|
||||
## Migration script contract
|
||||
|
||||
```ts
|
||||
// In a plugin: src/upgrade.ts
|
||||
import type { MigrationTable, MigrationScript } from "@suaveplan/installer/migrations";
|
||||
|
||||
const v1_to_v2: MigrationScript = async (ctx) => {
|
||||
// ctx.fs is rooted at the staged tmp directory.
|
||||
// ctx.signal honours --abort.
|
||||
// ctx.logger is the host's structured logger.
|
||||
// ctx.from = "1.0.0", ctx.to = "2.0.0"
|
||||
const old = await ctx.fs.readFile("skills/legacy.json");
|
||||
await ctx.fs.writeFile("skills/legacy.yaml", convert(old));
|
||||
await ctx.fs.delete("skills/legacy.json");
|
||||
};
|
||||
|
||||
export const upgradeTable: MigrationTable = {
|
||||
"1.0.0->2.0.0": v1_to_v2,
|
||||
};
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- Keys are `"<from>-><to>"` semver strings. The selector finds the shortest
|
||||
path of contiguous edges from the installed version to the target.
|
||||
- Scripts MUST be pure with respect to outside-the-tmp state. Network
|
||||
access is permitted (e.g. fetching upstream renames) but discouraged.
|
||||
- Scripts MAY throw. A throw rolls the transaction back and surfaces
|
||||
`INSTALLER.UPGRADE_MIGRATION_FAILED` with `{ cause }`.
|
||||
- Scripts MAY return additional `FileOp[]` that the host appends to the
|
||||
plan and re-stages before commit.
|
||||
|
||||
## CLI behaviour
|
||||
|
||||
| Flag | Effect |
|
||||
|---|---|
|
||||
| `--dry-run` | Computes diff + migration list, prints the plan, exits before staging. |
|
||||
| `--force` | Collapses conflict policies to `overwrite` AND bypasses the dirty-tree guard. Equivalent to `--allow-dirty` + per-file overwrite. |
|
||||
| `--allow-dirty` | Bypasses dirty-tree guard but does NOT change conflict policies. Used when the operator knows their edits are reconcilable. |
|
||||
| `--no-interactive` | Any prompt op fails with `INSTALLER.APPLY_CONFLICT` (existing semantics). |
|
||||
| `--from <version>` | Overrides the version detected from the state file. Useful when the state file is missing or stale. |
|
||||
| `--to <version>` | Overrides the version resolved from `node_modules`. Useful for pin-rollforward simulations. |
|
||||
| `--answers <file>` | Same as `add` — JSON file supplying answers for any prompts the plan emits. |
|
||||
|
||||
## Idempotency
|
||||
|
||||
Two notions, both held:
|
||||
|
||||
1. **Same-version upgrade is a no-op.** `installed == target` exits with
|
||||
`INSTALLER.UPGRADE_SAME_VERSION` without staging anything.
|
||||
2. **Successful upgrade is replayable.** If the user runs the same upgrade
|
||||
again, the state file already reflects `target`, so step 1 fires. There
|
||||
is no way to "double-apply" a migration script.
|
||||
|
||||
## State file shape
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"$schema": "installer-state/v1",
|
||||
"plugins": {
|
||||
"agent-skills": {
|
||||
"version": "0.7.0",
|
||||
"installedAt": "2026-05-17T12:34:56Z",
|
||||
"targetDir": ".suaveplan/skills",
|
||||
"fileHashes": {
|
||||
"skills/crypto/SKILL.md": "blake3:...",
|
||||
"skills/events/SKILL.md": "blake3:..."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The schema version (`$schema`) is bumped any time the shape changes; the
|
||||
loader rejects unknown versions with `INSTALLER.UPGRADE_STATE_INVALID`.
|
||||
|
||||
## Coverage strategy
|
||||
|
||||
Two fixture plugins live under `src/testing/fixtures/`:
|
||||
|
||||
- `upgrade-fixture-plugin@1.0.0` — emits two files
|
||||
(`a.txt`, `b.txt`) and one `patch-json` op against `data.json`.
|
||||
- `upgrade-fixture-plugin@2.0.0` — drops `b.txt`, renames `a.txt` →
|
||||
`a-renamed.txt`, adds `c.txt`, changes the `patch-json` merge, and ships
|
||||
one migration script `1.0.0->2.0.0` that rewrites `data.json` schema.
|
||||
|
||||
Together they exercise every branch in `computeDiff`, every conflict policy
|
||||
in the diff materialiser, both the migration-success and migration-throw
|
||||
paths, and both the same-version-noop and version-not-found paths. 100%
|
||||
istanbul falls out naturally.
|
||||
|
||||
## File layout
|
||||
|
||||
```text
|
||||
packages/tooling/installer/src/
|
||||
state/
|
||||
state.types.ts
|
||||
state.schemas.ts
|
||||
state-file.ts
|
||||
state-file.md
|
||||
state-file.spec-conformance.test.ts
|
||||
hash-tree.ts
|
||||
hash-tree.md
|
||||
host/
|
||||
upgrade/
|
||||
upgrade.ts
|
||||
upgrade.md
|
||||
upgrade.spec-conformance.test.ts
|
||||
preflight.ts
|
||||
preflight.md
|
||||
preflight.spec-conformance.test.ts
|
||||
dirty-tree.spec-conformance.test.ts
|
||||
diff.ts
|
||||
diff.md
|
||||
apply-diff.spec-conformance.test.ts
|
||||
migrations.ts
|
||||
migrations.types.ts
|
||||
migrations.md
|
||||
migrations.spec-conformance.test.ts
|
||||
index.ts
|
||||
testing/
|
||||
upgrade-plugin.ts
|
||||
upgrade-plugin.md
|
||||
fixtures/
|
||||
upgrade-fixture-plugin.ts
|
||||
upgrade-fixture-plugin.md
|
||||
```
|
||||
|
||||
## Decisions and trade-offs
|
||||
|
||||
- **Move-aside vs full snapshot.** Move-aside wastes one rename per
|
||||
affected file but is O(1) per file. A full snapshot would be safer
|
||||
against concurrent writes but the installer already requires the user
|
||||
to not be editing files concurrently — `INSTALLER-04` makes the same
|
||||
trade-off for `add`. Consistency with `add` wins.
|
||||
- **BLAKE3 over SHA-256.** `@suaveplan/crypto-hash` ships both. BLAKE3 is
|
||||
~3× faster for the small-file case (skill files average <4 KB) and
|
||||
parallelisable. No security requirement (this is a change-detection
|
||||
hash, not a tamper-proof signature).
|
||||
- **Migration scripts run AFTER diff stage, not before.** Running them
|
||||
before would mean diffing against a tree the migration script has
|
||||
already mutated, which makes the "did the user modify this file?"
|
||||
question ambiguous. The order is: diff first (against the unmodified
|
||||
staged tree), then migrate.
|
||||
- **No network resolution.** Resolving "latest" from npm would couple the
|
||||
installer to a registry shape, force network access in CI, and conflict
|
||||
with the project's air-gapped use cases. The target version is
|
||||
whatever resolves from `node_modules`; the operator bumps the dep
|
||||
with their package manager first.
|
||||
@@ -0,0 +1,145 @@
|
||||
# OpenSpec Change: add-installer-upgrade
|
||||
|
||||
- **Change ID**: add-installer-upgrade
|
||||
- **Package**: `@suaveplan/installer`
|
||||
- **Status**: PROPOSED
|
||||
- **Date**: 2026-05-17
|
||||
- **Design**: ./design.md
|
||||
- **Spec delta**: ./specs/installer/spec.md
|
||||
|
||||
## Why
|
||||
|
||||
`openspec/archive/2026-04-28-installer-port/proposal.md` §"Out of scope" deferred
|
||||
the upgrade flow:
|
||||
|
||||
> Plugin upgrade flow (`upgrade <plugin>` to bring an existing install up to
|
||||
> current bundle version) is a follow-up — initial scope is install + reinstall
|
||||
> (idempotent re-run with conflict policy).
|
||||
|
||||
The installer therefore ships `add <plugin>` and `list` but has no answer for a
|
||||
user who installed `@suaveplan/install-agent-skills@0.5.0` six months ago and
|
||||
wants the changes that landed in `0.7.0`. Today their only options are
|
||||
`add agent-skills --force` (which overwrites local edits without diffing) or a
|
||||
manual `rm -rf` (which destroys their in-flight skill edits). Both are unsafe.
|
||||
|
||||
The `add` path is also unable to run plugin-authored migration scripts, so any
|
||||
breaking layout change in a plugin bundle (e.g. `skills/<id>/SKILL.md` →
|
||||
`skills/<id>/<id>.md`) silently leaves stale files behind that the user has to
|
||||
hand-prune.
|
||||
|
||||
## What Changes
|
||||
|
||||
Add a third top-level installer command, `suaveplan-install upgrade <plugin>`,
|
||||
that resolves the **installed** plugin version from the on-disk state file,
|
||||
resolves the **target** version from `node_modules`, computes the file-level
|
||||
diff between the two bundles (added / removed / changed), and applies it
|
||||
transactionally with the same staged-tmp + rollback strategy `apply` already
|
||||
uses for `add`. Pre-flight gates protect against in-flight edits (dirty target
|
||||
tree) and missing lockfiles. Plugins MAY ship an optional `upgrade.ts`
|
||||
migration script per from-version → to-version pair; the host invokes them in
|
||||
order inside the same transaction.
|
||||
|
||||
### Surface added
|
||||
|
||||
- `suaveplan-install upgrade <plugin>` CLI verb with flags
|
||||
`--dry-run`, `--force`, `--no-interactive`, `--answers <file>`,
|
||||
`--from <version>` (override detected), `--to <version>` (target; defaults
|
||||
to whatever resolves from `node_modules`), `--allow-dirty` (skip dirty-tree
|
||||
guard — destructive, opt-in only).
|
||||
- `Host.upgrade(plugin, options)` public entry point alongside `Host.run`.
|
||||
- A persisted state file (`.suaveplan/installer-state.json`) capturing
|
||||
`{ pluginId, version, installedAt, fileHashes }` per installed plugin so
|
||||
diff and dirty-tree detection are deterministic on subsequent upgrades.
|
||||
- A `MigrationContext` and `MigrationScript` contract under
|
||||
`@suaveplan/installer/migrations` for plugin-authored scripts.
|
||||
- 7 new `INSTALLER.*` codes: `UPGRADE_NOT_INSTALLED`,
|
||||
`UPGRADE_VERSION_NOT_FOUND`, `UPGRADE_SAME_VERSION`, `UPGRADE_DIRTY_TREE`,
|
||||
`UPGRADE_STATE_INVALID`, `UPGRADE_MIGRATION_FAILED`,
|
||||
`UPGRADE_LOCKFILE_MISSING`.
|
||||
|
||||
### Behaviour guarantees
|
||||
|
||||
- Re-running `upgrade <plugin>` when installed == target is a no-op exiting
|
||||
with `INSTALLER.UPGRADE_SAME_VERSION` (treated as success under `--dry-run`).
|
||||
- Failure at any point (diff compute, migration script throw, atomic rename
|
||||
error) rolls the target tree back to the pre-upgrade snapshot. Partial
|
||||
state is never persisted.
|
||||
- The state file is updated atomically (`write → fsync → rename`) only after
|
||||
the transaction commits.
|
||||
|
||||
## Source mapping
|
||||
|
||||
| Source / archive reference | What it tells us |
|
||||
|---|---|
|
||||
| `openspec/archive/2026-04-28-installer-port/proposal.md` §"Out of scope" bullet 2 | The deferred upgrade flow is the scope of this change. |
|
||||
| `openspec/archive/2026-04-28-installer-port/proposal.md` §"Architecture → Lifecycle" | Reuse the existing `detect → prompts → plan → apply → postInstall` pipeline; upgrade adds a diff-and-migrate stage between `plan` and `apply`. |
|
||||
| `openspec/specs/installer/spec.md` `INSTALLER-04` (transactional apply) | Upgrade MUST reuse the same staged-tmp + rollback machinery — no parallel transaction model. |
|
||||
| `openspec/specs/installer/spec.md` `INSTALLER-05` (conflict policy) | Per-file conflict policy already exists; upgrade extends it with a "user-modified-since-install" case detected via the state-file hashes. |
|
||||
| `openspec/specs/installer/spec.md` `INSTALLER-09` (frozen registry) | New codes MUST extend `defineCodes("INSTALLER", [...])` — no parallel registry. |
|
||||
| `packages/tooling/installer/src/bin/suaveplan-install.ts` | Existing CLI shape: `cli().command("add <plugin>")…`. The new verb attaches identically: `.command("upgrade <plugin>")`. |
|
||||
| `packages/tooling/installer/src/host/host.ts` | `Host` class is the orchestration seam; `Host.upgrade` slots in alongside `Host.run`. |
|
||||
| `packages/tooling/installer/src/host/apply/apply.ts` | Staged-tmp transaction implementation is reused as-is; upgrade composes it. |
|
||||
| `packages/tooling/installer/src/manifest/discover.ts` | Version resolution from `node_modules` already exists for `add`; upgrade reuses `findPlugin(cwd, pluginId)`. |
|
||||
| `packages/tooling/installer/src/error-codes.ts` | New codes APPEND to the existing tuple; the registry stays frozen. |
|
||||
|
||||
## Impact
|
||||
|
||||
### Affected dependents
|
||||
|
||||
Run `bunx turbo ls --filter=...@suaveplan/installer` to enumerate. Current
|
||||
dependents:
|
||||
|
||||
- `@suaveplan/install-agent-skills` — [x] tests run / [x] docs reviewed /
|
||||
[ ] no action needed. The skills plugin is the canonical upgrade target;
|
||||
its existing `runPlugin` harness gains an `upgradePlugin` sibling so its
|
||||
conformance tests cover both flows. The plugin itself receives a tiny
|
||||
optional `upgrade.ts` exporting an empty migration table (no breaking
|
||||
layout changes yet); this proves the migration contract loads even when
|
||||
the table is empty.
|
||||
|
||||
No other dependents currently consume `@suaveplan/installer`.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- **Downgrade.** `upgrade` only moves forward. A separate
|
||||
`add-installer-downgrade` change can extend the diff engine in reverse;
|
||||
the migration contract is intentionally directional (`from`/`to` versions)
|
||||
so downgrade is a different scope.
|
||||
- **Cross-plugin coordination.** Upgrading `install-agent-skills` does not
|
||||
trigger an upgrade of a hypothetical `install-web-server`. Each plugin is
|
||||
upgraded independently; `peerInstallers` field warnings are a future
|
||||
packaging change.
|
||||
- **Background / scheduled upgrade daemon.** Upgrade is operator-initiated
|
||||
via the CLI; there is no auto-update behaviour.
|
||||
- **Network resolution.** The "target version" is whatever resolves from
|
||||
`node_modules`; the installer never talks to npm directly. Bumping the
|
||||
installed package version remains the user's package-manager step.
|
||||
|
||||
## Verification
|
||||
|
||||
Every requirement in `specs/installer/spec.md` carries a `**Verified by:**`
|
||||
line pointing to a conformance test in `src/host/upgrade/` or
|
||||
`src/state/`. The full set:
|
||||
|
||||
- `INSTALLER-UPGRADE-01` Lifecycle order → `src/host/upgrade/upgrade.spec-conformance.test.ts::"runs detect → resolve → diff → migrate → apply → postInstall in order"`
|
||||
- `INSTALLER-UPGRADE-02` Idempotent same-version no-op → `src/host/upgrade/upgrade.spec-conformance.test.ts::"exits with UPGRADE_SAME_VERSION when installed equals target"`
|
||||
- `INSTALLER-UPGRADE-03` Dirty-tree guard → `src/host/upgrade/dirty-tree.spec-conformance.test.ts::"throws UPGRADE_DIRTY_TREE when state-file hash diverges from disk"`
|
||||
- `INSTALLER-UPGRADE-04` Lockfile pre-flight → `src/host/upgrade/preflight.spec-conformance.test.ts::"throws UPGRADE_LOCKFILE_MISSING when no bun.lockb / package-lock.json / pnpm-lock.yaml exists"`
|
||||
- `INSTALLER-UPGRADE-05` Transactional diff application → `src/host/upgrade/apply-diff.spec-conformance.test.ts::"rolls back snapshot on staged-rename failure"`
|
||||
- `INSTALLER-UPGRADE-06` Migration script contract → `src/host/upgrade/migrations.spec-conformance.test.ts::"runs upgrade.ts scripts in semver order between from and to"`
|
||||
- `INSTALLER-UPGRADE-07` Migration failure rollback → `src/host/upgrade/migrations.spec-conformance.test.ts::"rolls back transaction and surfaces UPGRADE_MIGRATION_FAILED on script throw"`
|
||||
- `INSTALLER-UPGRADE-08` State-file integrity → `src/state/state-file.spec-conformance.test.ts::"atomically writes installer-state.json only on successful commit"`
|
||||
- `INSTALLER-UPGRADE-09` Error-code registry namespacing → `src/error-codes.spec-conformance.test.ts::"upgrade codes are frozen and namespaced INSTALLER.*"`
|
||||
- `INSTALLER-UPGRADE-10` Version-not-found surface → `src/host/upgrade/upgrade.spec-conformance.test.ts::"throws UPGRADE_VERSION_NOT_FOUND when plugin not in node_modules"`
|
||||
- `INSTALLER-UPGRADE-11` Not-installed surface → `src/host/upgrade/upgrade.spec-conformance.test.ts::"throws UPGRADE_NOT_INSTALLED when no state-file entry exists"`
|
||||
- `INSTALLER-UPGRADE-12` CLI flag wiring → `src/bin/suaveplan-install.spec-conformance.test.ts::"upgrade verb forwards --dry-run, --force, --allow-dirty, --from, --to to Host.upgrade"`
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- `bunx turbo lint typecheck test --filter=@suaveplan/installer` green.
|
||||
- `bunx turbo test:coverage --filter=@suaveplan/installer` 100% istanbul on
|
||||
every new module under `src/host/upgrade/`, `src/state/`, and the
|
||||
upgrade-specific additions to `src/bin/suaveplan-install.ts`.
|
||||
- Sibling `.md` for every new implementation `.ts` per `openspec/AGENTS.md` §4.
|
||||
- `openspec validate add-installer-upgrade --strict` green.
|
||||
- Spec delta folded into `openspec/specs/installer/spec.md` on archive.
|
||||
@@ -0,0 +1,278 @@
|
||||
<!-- markdownlint-disable MD024 -->
|
||||
|
||||
# Capability Spec Delta: installer (upgrade flow)
|
||||
|
||||
**Package**: `@suaveplan/installer`
|
||||
**Status**: PROPOSED
|
||||
|
||||
> Every requirement below carries a `**Verified by:**` line at authoring time.
|
||||
> Test files are planned paths inside `packages/tooling/installer/`.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Upgrade lifecycle order
|
||||
|
||||
`Host.upgrade(plugin, options)` SHALL execute upgrade stages in the strict
|
||||
order `loadState → resolveTarget → preflight → hashTree → planFrom → planTo →
|
||||
diff → materialise → applyOps → runMigrations → commitState`. Optional stages
|
||||
(`detect`, `prompts`, `postInstall` from the install lifecycle) SHALL still be
|
||||
honoured before `loadState` and after `commitState` respectively, preserving
|
||||
plugin composability. Each stage SHALL be wrapped in an OpenTelemetry span
|
||||
named `installer.upgrade.<stage>` whose parent span `installer.upgrade`
|
||||
carries attributes `plugin.id`, `plugin.version.from`, `plugin.version.to`,
|
||||
and `dry_run`.
|
||||
|
||||
#### Scenario: pipeline executes in the documented order
|
||||
|
||||
- **WHEN** `Host.upgrade(fixturePlugin, { from: "1.0.0", to: "2.0.0" })` runs
|
||||
against an installed `1.0.0` fixture and a `2.0.0` bundle in `node_modules`
|
||||
- **THEN** the OTel exporter records spans in the order `loadState`,
|
||||
`resolveTarget`, `preflight`, `hashTree`, `planFrom`, `planTo`, `diff`,
|
||||
`materialise`, `applyOps`, `runMigrations`, `commitState`, all parented to
|
||||
one `installer.upgrade` span carrying `plugin.id`, `plugin.version.from`,
|
||||
`plugin.version.to`, and `dry_run`.
|
||||
|
||||
**Verified by:** `src/host/upgrade/upgrade.spec-conformance.test.ts::"runs detect → resolve → diff → migrate → apply → postInstall in order"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Same-version upgrade is idempotent
|
||||
|
||||
`Host.upgrade` SHALL short-circuit when the installed version recorded in
|
||||
the state file equals the target version resolved from `node_modules`, and
|
||||
surface `InstallerError(INSTALLER.UPGRADE_SAME_VERSION)`. Callers MAY treat
|
||||
this code as a success indicator. Re-running the same upgrade after a
|
||||
successful commit SHALL therefore always exit via this short-circuit. The
|
||||
short-circuit SHALL NOT fire when `--force` was supplied.
|
||||
|
||||
#### Scenario: re-running upgrade after success exits with UPGRADE_SAME_VERSION
|
||||
|
||||
- **WHEN** `Host.upgrade` is invoked twice in succession against the same
|
||||
fixture plugin with no `--force` flag
|
||||
- **THEN** the first invocation MUST commit and write the state file, and
|
||||
the second invocation MUST throw `InstallerError` with code
|
||||
`INSTALLER.UPGRADE_SAME_VERSION` and MUST NOT stage any file
|
||||
|
||||
**Verified by:** `src/host/upgrade/upgrade.spec-conformance.test.ts::"exits with UPGRADE_SAME_VERSION when installed equals target"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Dirty-tree pre-flight guard
|
||||
|
||||
Before staging any file op, `Host.upgrade` SHALL hash every path recorded in
|
||||
the state file's `fileHashes` and compare to the recorded value. Any
|
||||
mismatched hash SHALL surface as
|
||||
`InstallerError(INSTALLER.UPGRADE_DIRTY_TREE, { context: { paths: string[] } })`
|
||||
unless `--allow-dirty` or `--force` was supplied. The error context SHALL
|
||||
enumerate every divergent path so the operator can reconcile manually.
|
||||
|
||||
#### Scenario: divergent on-disk hash blocks the upgrade
|
||||
|
||||
- **GIVEN** a state file recording `skills/crypto/SKILL.md` at hash `H1`
|
||||
- **WHEN** the file on disk hashes to `H2` and `Host.upgrade` runs without
|
||||
`--allow-dirty` or `--force`
|
||||
- **THEN** an `InstallerError` with code `INSTALLER.UPGRADE_DIRTY_TREE` is
|
||||
thrown whose `context.paths` includes `skills/crypto/SKILL.md`
|
||||
|
||||
**Verified by:** `src/host/upgrade/dirty-tree.spec-conformance.test.ts::"throws UPGRADE_DIRTY_TREE when state-file hash diverges from disk"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Lockfile presence is required
|
||||
|
||||
`Host.upgrade` SHALL refuse to run unless at least one of
|
||||
`bun.lockb`, `bun.lock`, `package-lock.json`, `pnpm-lock.yaml`, or
|
||||
`yarn.lock` is present in `cwd`. Missing lockfile SHALL surface as
|
||||
`InstallerError(INSTALLER.UPGRADE_LOCKFILE_MISSING)`. This gate exists to
|
||||
prevent upgrades against a tree whose dependency graph is unpinned, where
|
||||
the resolved target version could shift between runs.
|
||||
|
||||
#### Scenario: missing lockfile blocks the upgrade
|
||||
|
||||
- **WHEN** `Host.upgrade` runs in a `cwd` with no lockfile of any supported
|
||||
format
|
||||
- **THEN** an `InstallerError` with code `INSTALLER.UPGRADE_LOCKFILE_MISSING`
|
||||
is thrown before any file is hashed
|
||||
|
||||
**Verified by:** `src/host/upgrade/preflight.spec-conformance.test.ts::"throws UPGRADE_LOCKFILE_MISSING when no bun.lockb / package-lock.json / pnpm-lock.yaml exists"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Transactional diff application with rollback
|
||||
|
||||
`Host.upgrade` SHALL materialise the computed diff (`added`, `removed`,
|
||||
`changed`) into the existing `FileOp` union and feed it through
|
||||
`Host.applyOps`, reusing the staged-tmp + atomic-rename transaction
|
||||
described in the canonical `INSTALLER-04` requirement. Removed files SHALL
|
||||
be moved aside into `<tmp>/.deleted/` rather than unlinked, so rollback can
|
||||
restore them. Any failure during staging or commit SHALL trigger a
|
||||
reverse-order rollback that unlinks staged files and renames the
|
||||
moved-aside files back into place. Rollback failures SHALL surface as
|
||||
`InstallerError(INSTALLER.ROLLBACK_FAILED, { cause })`.
|
||||
|
||||
#### Scenario: rename failure rolls the tree back
|
||||
|
||||
- **GIVEN** a fixture plugin whose final `rename` of `a-renamed.txt` is
|
||||
injected to throw EIO
|
||||
- **WHEN** `Host.upgrade` runs against an installed `1.0.0` tree containing
|
||||
`a.txt`
|
||||
- **THEN** `a.txt` exists at its original content after the throw and
|
||||
`a-renamed.txt` does not exist, and the throw surfaces as `InstallerError`
|
||||
with code `INSTALLER.APPLY_FAILED` (or `INSTALLER.ROLLBACK_FAILED` if the
|
||||
restore also fails) carrying the EIO as `cause`
|
||||
|
||||
**Verified by:** `src/host/upgrade/apply-diff.spec-conformance.test.ts::"rolls back snapshot on staged-rename failure"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Migration scripts run in semver order
|
||||
|
||||
`Host.upgrade` SHALL, when a plugin ships an `upgrade.ts` exporting a
|
||||
`MigrationTable`, load the table, select the shortest contiguous chain
|
||||
of edges from `from` to `to`, and execute each `MigrationScript` in order
|
||||
inside the same transaction as the diff apply. Each script SHALL receive a
|
||||
`MigrationContext` carrying the same `fs` (rooted at the staged tmp dir),
|
||||
the same `AbortSignal`, the host `logger`, and the `from`/`to` semver
|
||||
strings for the current edge.
|
||||
|
||||
#### Scenario: chained migrations execute from-to-target in order
|
||||
|
||||
- **GIVEN** a plugin with edges `1.0.0->1.1.0` and `1.1.0->2.0.0`
|
||||
- **WHEN** `Host.upgrade` runs from `1.0.0` to `2.0.0`
|
||||
- **THEN** both scripts MUST run, the `1.0.0->1.1.0` script MUST run
|
||||
first, both MUST receive an `fs` rooted at the same staged tmp
|
||||
directory, and both MUST observe the same `AbortSignal` instance
|
||||
|
||||
**Verified by:** `src/host/upgrade/migrations.spec-conformance.test.ts::"runs upgrade.ts scripts in semver order between from and to"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Migration failure rolls back the transaction
|
||||
|
||||
A `MigrationScript` throw SHALL roll back every staged op (including any
|
||||
ops emitted by earlier migration scripts in the same chain) and SHALL
|
||||
surface as `InstallerError(INSTALLER.UPGRADE_MIGRATION_FAILED, { cause })`.
|
||||
The state file SHALL NOT be written when a migration throws. Re-running
|
||||
the upgrade after a migration failure SHALL therefore see the original
|
||||
installed version in the state file and start the chain from the
|
||||
beginning.
|
||||
|
||||
#### Scenario: throwing migration leaves tree at pre-upgrade state
|
||||
|
||||
- **GIVEN** a plugin whose `1.0.0->2.0.0` migration throws on invocation
|
||||
- **WHEN** `Host.upgrade` runs from `1.0.0` to `2.0.0`
|
||||
- **THEN** the throw surfaces as `InstallerError` with code
|
||||
`INSTALLER.UPGRADE_MIGRATION_FAILED`, the live tree is bit-identical to
|
||||
its pre-upgrade content, and the state file still records version
|
||||
`1.0.0`
|
||||
|
||||
**Verified by:** `src/host/upgrade/migrations.spec-conformance.test.ts::"rolls back transaction and surfaces UPGRADE_MIGRATION_FAILED on script throw"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: State file is written atomically on commit only
|
||||
|
||||
`Host.upgrade` SHALL persist the new `installer-state.json` entry only
|
||||
after every diff op and migration script has committed successfully. The
|
||||
write SHALL use the write-fsync-rename pattern: serialise to
|
||||
`<cwd>/.suaveplan/installer-state.json.tmp-<txn>`, `fsync` the descriptor,
|
||||
then `rename` over the live path. Reads SHALL Zod-validate the loaded
|
||||
state; validation failures SHALL surface as
|
||||
`InstallerError(INSTALLER.UPGRADE_STATE_INVALID, { cause })`.
|
||||
|
||||
#### Scenario: state file appears only after successful commit
|
||||
|
||||
- **WHEN** `Host.upgrade` aborts mid-apply (any failure)
|
||||
- **THEN** the file at `.suaveplan/installer-state.json` is unchanged from
|
||||
its pre-upgrade content (either still recording the old version or
|
||||
still absent), and no `.tmp-*` sibling remains on disk
|
||||
|
||||
**Verified by:** `src/state/state-file.spec-conformance.test.ts::"atomically writes installer-state.json only on successful commit"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Upgrade error codes are namespaced and frozen
|
||||
|
||||
The `INSTALLER_CODES` registry SHALL include `UPGRADE_NOT_INSTALLED`,
|
||||
`UPGRADE_VERSION_NOT_FOUND`, `UPGRADE_SAME_VERSION`, `UPGRADE_DIRTY_TREE`,
|
||||
`UPGRADE_STATE_INVALID`, `UPGRADE_MIGRATION_FAILED`, and
|
||||
`UPGRADE_LOCKFILE_MISSING` in addition to the existing nine codes. The
|
||||
registry SHALL remain frozen (`Object.isFrozen(INSTALLER_CODES) === true`).
|
||||
Every new value SHALL start with `INSTALLER.`. `InstallerError` SHALL
|
||||
remain the only error class thrown by the upgrade path.
|
||||
|
||||
#### Scenario: registry contains the upgrade codes and stays frozen
|
||||
|
||||
- **WHEN** the registry is inspected at module-load time
|
||||
- **THEN** `INSTALLER_CODES.UPGRADE_NOT_INSTALLED` equals
|
||||
`"INSTALLER.UPGRADE_NOT_INSTALLED"`, every other upgrade code is
|
||||
present with the same `INSTALLER.` prefix, and
|
||||
`Object.isFrozen(INSTALLER_CODES)` returns `true`
|
||||
|
||||
**Verified by:** `src/error-codes.spec-conformance.test.ts::"upgrade codes are frozen and namespaced INSTALLER.*"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Missing target version surfaces a typed error
|
||||
|
||||
`Host.upgrade` SHALL surface
|
||||
`InstallerError(INSTALLER.UPGRADE_VERSION_NOT_FOUND, { context: { pluginId, requested } })`
|
||||
when the requested target version (whether supplied via `--to` or resolved
|
||||
from `node_modules`) does not exist.
|
||||
The error SHALL be thrown before any pre-flight check runs so callers can
|
||||
distinguish "plugin not installed at target" from "plugin tree dirty".
|
||||
|
||||
#### Scenario: requesting an unresolvable target version fails early
|
||||
|
||||
- **GIVEN** an installed plugin at `1.0.0` and `node_modules` containing
|
||||
only `1.0.0`
|
||||
- **WHEN** `Host.upgrade(plugin, { to: "9.9.9" })` runs
|
||||
- **THEN** an `InstallerError` with code
|
||||
`INSTALLER.UPGRADE_VERSION_NOT_FOUND` MUST be thrown and the
|
||||
`context.requested` field MUST equal `"9.9.9"`
|
||||
|
||||
**Verified by:** `src/host/upgrade/upgrade.spec-conformance.test.ts::"throws UPGRADE_VERSION_NOT_FOUND when plugin not in node_modules"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Upgrading an uninstalled plugin surfaces a typed error
|
||||
|
||||
`Host.upgrade` SHALL surface
|
||||
`InstallerError(INSTALLER.UPGRADE_NOT_INSTALLED, { context: { pluginId } })`
|
||||
when no entry for the requested `pluginId` exists in the state file.
|
||||
The error SHALL be thrown before any version resolution runs so the
|
||||
operator gets the most specific guidance ("run `add` first") without
|
||||
network or filesystem cost.
|
||||
|
||||
#### Scenario: upgrade against empty state file fails fast
|
||||
|
||||
- **GIVEN** an empty `.suaveplan/installer-state.json` (no `plugins` entry
|
||||
for the requested id)
|
||||
- **WHEN** `Host.upgrade(plugin)` runs
|
||||
- **THEN** an `InstallerError` with code
|
||||
`INSTALLER.UPGRADE_NOT_INSTALLED` MUST be thrown before
|
||||
`resolveTargetVersion` is called
|
||||
|
||||
**Verified by:** `src/host/upgrade/upgrade.spec-conformance.test.ts::"throws UPGRADE_NOT_INSTALLED when no state-file entry exists"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: CLI verb forwards every upgrade flag
|
||||
|
||||
`suaveplan-install upgrade <plugin>` SHALL parse and forward the flags
|
||||
`--dry-run`, `--force`, `--no-interactive`, `--answers <file>`,
|
||||
`--from <version>`, `--to <version>`, and `--allow-dirty` to
|
||||
`Host.upgrade`. The verb SHALL share the same `InstallerContext`
|
||||
construction path as `add`, so the underlying `fs`, `logger`, `cli`,
|
||||
`templates`, and `signal` are identical. `--dry-run` SHALL print the
|
||||
computed diff (counts of added/removed/changed plus the migration chain)
|
||||
and exit with code 0 before any staging occurs.
|
||||
|
||||
#### Scenario: each upgrade flag reaches Host.upgrade
|
||||
|
||||
- **WHEN** `main(["upgrade", "agent-skills", "--dry-run", "--force", "--allow-dirty", "--from", "1.0.0", "--to", "2.0.0", "--answers", "a.json"])`
|
||||
runs with a recording `Host.upgrade` stub
|
||||
- **THEN** the stub records one call whose options object contains
|
||||
`{ dryRun: true, force: true, allowDirty: true, from: "1.0.0", to: "2.0.0", answers: <parsed JSON> }`
|
||||
|
||||
**Verified by:** `src/bin/suaveplan-install.spec-conformance.test.ts::"upgrade verb forwards --dry-run, --force, --allow-dirty, --from, --to to Host.upgrade"`
|
||||
@@ -0,0 +1,134 @@
|
||||
# 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<path, hex>`
|
||||
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 <plugin>")` with flags `--dry-run`,
|
||||
`--force`, `--no-interactive`, `--answers <file>`, `--from <version>`,
|
||||
`--to <version>`, `--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/<yyyy-mm-dd>-add-installer-upgrade/`.
|
||||
Reference in New Issue
Block a user