# `@suaveplan/api-lifecycle` — plugin lifecycle FSM definition **Package**: `@suaveplan/api-lifecycle` **Status**: PROPOSED **Type**: new-package **Stage**: 1 (Contracts) — see umbrella `openspec/changes/api-plugin-foundation/` ## Why Design §4 specifies the plugin lifecycle as a finite-state machine with 13 states (`uninstalled`, `installing`, `installed`, `starting`, `started`, `rolled_back`, `crashed`, `stopping`, `stopped`, `uninstalling`, `install_failed`, `start_failed`, `uninstall_failed`) and a documented set of transitions. The host driver, the admin API status renderer, the hot-reload primitive, and operator tooling all need to agree on which transitions are legal — and a graph that lives in only one place. This package owns the FSM definition: the states (as a discriminated union), the transition function, the side-effect descriptors per transition (so the host can wire them), and the per-plugin mutex contract that guarantees at-most-one transition at a time. Reuses `@suaveplan/fsm` for the state-machine primitive — this package is the FSM *definition*, not a new FSM runtime. Per design §4: "The FSM has a per-plugin mutex — at most one transition at a time. Concurrent triggers queue." ## What changes Net-new package. Pure definition: states + transitions + side-effect descriptors + retry policies. No concrete plugin code; that lives in `api-host` (Stage 2). - ADDED: `./states` — `PluginState` discriminated union (re-imported from `@suaveplan/api-types` for runtime introspection); state-shape descriptors. - ADDED: `./transitions` — `PluginTransitions`: the legal `(from, to, trigger)` set as a frozen table. Trigger names: `install`, `installCompleted`, `installFailed`, `start`, `startCompleted`, `startFailed`, `rollbackTriggered`, `stop`, `stopCompleted`, `uninstall`, `uninstallCompleted`, `uninstallFailed`, `crashDetected`, `crashRestart`, `clearRollback`, `retryInstall`, `retryStart`, `forceUninstall`, `repair`. - ADDED: `./fsm-factory` — `createPluginFsm({ initial, sideEffects, mutex })` returns an FSM bound to the genesis `@suaveplan/fsm` runtime, initialised with `PluginTransitions` and the side-effect bus from design §4's transition table. - ADDED: `./side-effects` — descriptors for each transition's side effects (per §4 table): `installing → installed` emits `plugin.installed`, completes OTel span; `stopping → stopped` force-cancels stragglers, disposes capabilities reverse-registration order; etc. Descriptors are data; the host runtime executes them. - ADDED: `./retry-policies` — `start_failed` auto-retry × 3 with `@suaveplan/retry` exponential backoff; `crashed` auto-restart × `crashRestartMax` (default 3); restart budget reset after `crashRestartCooldownMs` of healthy operation. Default values exported. - ADDED: `./mutex-contract` — `PluginMutex` interface: at most one transition per plugin in flight; concurrent triggers queue (FIFO). - ADDED: `./reinstall-criteria` — `requiresReinstall(prev, next): boolean` returning true when the new manifest bumps `database.schemaVersion`, declares new `scopes`/`events`/`jobs`/`secrets`, or changes `dependsOn` (per §4 reload semantics). - ADDED: `./error-codes` — frozen `LIFECYCLE_CODES`: `ILLEGAL_TRANSITION`, `MUTEX_HELD`, `RETRY_BUDGET_EXHAUSTED`, `REINSTALL_REQUIRED_VERSION_DOWNGRADE`. - ADDED: `./testing` — `runLifecycleConformance(factory)` reusable harness. ## Out of scope - The host's lifecycle DRIVER (the code that actually CALLS `install`/`start`/`stop` hooks and reads side-effect descriptors) — lives in `api-host` (Stage 2). - Plugin ledger persistence — `api-host`. - Worker-tier `crashed` state restart implementation — `api-host` + `api-worker-host` (Stage 3). ## Source mapping Genesis-original. Source of design intent: `docs/superpowers/specs/2026-05-13-api-plugin-foundation-design.md`. | Requirement | Design doc section | |---|---| | State machine (13 states) | §4 (FSM ASCII diagram, lines 409-473) | | Transition side-effects table | §4 (table, lines 475-484) | | Reinstall criteria (`schemaVersion` bumped, new `scopes`/`events`/`jobs`/`secrets`, `dependsOn` changed) | §4 (line 486) | | `start_failed` auto-retry × 3 + `rolled_back` path | §4 (failure recovery section, lines 491-493) | | `crashed` auto-restart × `crashRestartMax`, cooldown | §4 (line 493) | | Per-plugin mutex; concurrent triggers queue | §4 (final paragraph) | | `VERSION_DOWNGRADE` failure | §4 (line 486) | ### Requirements with no source line | Requirement | Justification | |---|---| | Trigger naming (`installCompleted` etc.) | Mechanically derived from the §4 table; pinned here so the host and admin renderers agree. | | `runLifecycleConformance` harness | Stage-5 deliverable (§13.4). | ## Verification approach - `src/states/states.test.ts` — `PluginState` union matches design §4 exactly (no missing / extra states). - `src/transitions/transitions.test.ts` — exhaustive transition table: every documented `(from, to)` pair is legal; an undocumented pair throws `LIFECYCLE.ILLEGAL_TRANSITION`. - `src/fsm-factory/fsm-factory.test.ts` — `createPluginFsm` wires `@suaveplan/fsm` correctly; transition emits the right side-effect descriptors. - `src/side-effects/side-effects.test.ts` — every documented side effect is in the table for the right transition (`installed → starting` mounts routes, etc.). - `src/retry-policies/retry-policies.test.ts` — `start_failed` retried up to 3 times then parked; `crashed` restarted up to `crashRestartMax`; budget reset after cooldown. - `src/mutex-contract/mutex-contract.test.ts` — concurrent triggers queue FIFO; second trigger waits until first releases mutex. - `src/reinstall-criteria/reinstall-criteria.test.ts` — every documented trigger (`schemaVersion`, new scope/event/job/secret, `dependsOn` change) yields `true`; identical manifest yields `false`; version downgrade yields `REINSTALL_REQUIRED_VERSION_DOWNGRADE`. - `tests/dep-graph.test.ts` — deps: `@suaveplan/api-types`, `@suaveplan/fsm`, `@suaveplan/retry`, `@suaveplan/error`, `@suaveplan/types`. ## Migration impact None — additive only. ## Impact ### Affected dependents - (none) — [x] no action needed (reason: net-new; first consumer is `api-host` in Stage 2)