diff --git a/.changeset/lifecycle-event-registry-enforced.md b/.changeset/lifecycle-event-registry-enforced.md
new file mode 100644
index 0000000000..904a48951b
--- /dev/null
+++ b/.changeset/lifecycle-event-registry-enforced.md
@@ -0,0 +1,64 @@
+---
+"@objectstack/spec": major
+"@objectstack/core": patch
+---
+
+fix(spec)!: retire the never-built typed-event system; the lifecycle registry now lists the events that actually fire (#4212 follow-up)
+
+The lifecycle-event surface promised a typed-event system that was never
+built, in three layers. `kernel/plugin-lifecycle-events.zod.ts` shipped ten
+payload schemas (`PluginRegisteredEvent`, `PluginErrorEvent`,
+`HookTriggeredEvent`, `KernelReadyEvent`, …) and a 21-name
+`PluginLifecycleEventType` enum — zero consumers for every export, and the
+enum was wrong in both directions: 17 names nothing fires, 10 real events
+missing. `contracts/plugin-lifecycle-events.ts` declared the same 17 dead
+names in `IPluginLifecycleEvents` next to 5 real ones, plus an
+`ITypedEventEmitter` interface nothing implements. All of it read as a
+promise; anyone who coded against it (hooking `plugin:started`, awaiting
+`plugin:error`) registered a handler that could never fire, with no error
+saying so — the same silent-drop shape as the #4212 lifecycle-hook family.
+
+Removed, with zero consumers verified repo-wide:
+
+- `kernel/plugin-lifecycle-events.zod.ts` and every export: `EventPhase`,
+ `PluginEventBase`, `PluginRegisteredEvent`, `PluginLifecyclePhaseEvent`,
+ `PluginErrorEvent`, `ServiceRegisteredEvent`, `ServiceUnregisteredEvent`,
+ `HookRegisteredEvent`, `HookTriggeredEvent`, `KernelEventBase`,
+ `KernelReadyEvent`, `KernelShutdownEvent`, `PluginLifecycleEventType`
+ (schemas and inferred types).
+- `ITypedEventEmitter` from `contracts/plugin-lifecycle-events.ts`.
+- The 17 never-fired names from `IPluginLifecycleEvents`.
+
+`IPluginLifecycleEvents` is now the registry of the **14 events with a real
+emitter** — `kernel:{ready,bootstrapped,listening,shutdown}`, `app:seeded`,
+`metadata:reloaded` (payload `metadata` now optional, matching the documented
+contract), `external.schema.drift`, `ai:routes`, `auth:configure`, and the
+`{service}:ready` convention family (`mcp`, `automation`, `analytics`,
+`external-datasource`, `datasource-admin`) — each payload as observed at its
+fire site. A new `LifecycleEventName` union types
+`PluginContext.hook`/`trigger` in `@objectstack/core` as
+`LifecycleEventName | (string & {})`: known names autocomplete, custom
+cross-plugin names stay legal, existing callers compile unchanged. A pinning
+test asserts two-way equality between the interface keys and the fire-site
+inventory.
+
+FROM → TO:
+
+- `PluginLifecycleEventType` → `LifecycleEventName` (the union of names that
+ fire). There is no runtime enum; the bus is open by design.
+- Event payload schemas (`KernelReadyEvent`, `PluginErrorEvent`, …) → the
+ payload tuples on `IPluginLifecycleEvents`. No wire format existed or
+ exists; payloads are in-process arguments.
+- `ITypedEventEmitter` → `PluginContext.hook`/`trigger` (the emitter that
+ actually exists).
+- Handlers for the 17 dead names → delete them; they never ran. For plugin
+ phase observation use the boot report (ADR-0084); for per-plugin errors the
+ kernel throws/logs at the failing phase.
+
+Plain deletion rather than `retiredKey()` tombstones, per the #4233
+precedent: these keys were never authorable — they described runtime event
+payload records no config author can write, so the silent-strip class the
+authorable-surface ratchet guards against is vacuous. Its baseline entries
+and the `json-schema.manifest.json` keys are dropped deliberately in this PR.
+No ADR-0087 conversion: no stack metadata names these types; there is nothing
+for `os migrate meta` to rewrite.
diff --git a/content/docs/getting-started/quick-reference.mdx b/content/docs/getting-started/quick-reference.mdx
index ec957745f4..6600eb94a7 100644
--- a/content/docs/getting-started/quick-reference.mdx
+++ b/content/docs/getting-started/quick-reference.mdx
@@ -64,7 +64,6 @@ Plugin architecture, manifests, and kernel runtime.
| **[Context](/docs/references/kernel/context)** | `context.zod.ts` | KernelContext | Runtime execution context |
| **[Plugin](/docs/references/kernel/plugin)** | `plugin.zod.ts` | Plugin, PluginLifecycle | Plugin system interface |
| **[Plugin Capability](/docs/references/kernel/plugin-capability)** | `plugin-capability.zod.ts` | PluginCapability | Plugin capability declarations |
-| **[Plugin Lifecycle](/docs/references/kernel/plugin-lifecycle-events)** | `plugin-lifecycle-events.zod.ts` | PluginEventBase, EventPhase | Plugin lifecycle events |
| **[Plugin Lifecycle Advanced](/docs/references/kernel/plugin-lifecycle-advanced)** | `plugin-lifecycle-advanced.zod.ts` | AdvancedPluginLifecycleConfig, PluginHealthCheck | Advanced lifecycle hooks |
| **[Plugin Loading](/docs/references/kernel/plugin-loading)** | `plugin-loading.zod.ts` | PluginLoadingConfig | Plugin loading and init |
| **[Plugin Security](/docs/references/kernel/plugin-security-advanced)** | `plugin-security-advanced.zod.ts` | KernelSecurityPolicy, PluginPermission | Plugin sandboxing |
diff --git a/content/docs/references/index.mdx b/content/docs/references/index.mdx
index 52e1172934..07c912df63 100644
--- a/content/docs/references/index.mdx
+++ b/content/docs/references/index.mdx
@@ -115,7 +115,6 @@ Defines the plugin architecture and kernel runtime.
| `context.zod.ts` | `KernelContextSchema` | Kernel execution context with user, org, tenant info |
| `plugin.zod.ts` | `PluginSchema` | Plugin lifecycle and interface definitions |
| `plugin-capability.zod.ts` | `PluginCapabilitySchema` | Plugin capability declarations |
-| `plugin-lifecycle-events.zod.ts` | `PluginLifecycleEventsSchema` | Plugin lifecycle event definitions |
| `plugin-lifecycle-advanced.zod.ts` | `PluginLifecycleAdvancedSchema` | Advanced lifecycle hooks |
| `plugin-loading.zod.ts` | `PluginLoadingSchema` | Plugin loading and initialization |
| `plugin-security-advanced.zod.ts` | `PluginSecurityAdvancedSchema` | Plugin security and sandboxing |
diff --git a/content/docs/references/kernel/index.mdx b/content/docs/references/kernel/index.mdx
index a969832035..c4d3f3c9a0 100644
--- a/content/docs/references/kernel/index.mdx
+++ b/content/docs/references/kernel/index.mdx
@@ -22,7 +22,6 @@ This section contains all protocol schemas for the kernel layer of ObjectStack.
-
diff --git a/content/docs/references/kernel/meta.json b/content/docs/references/kernel/meta.json
index 0fd274d631..a44766a69c 100644
--- a/content/docs/references/kernel/meta.json
+++ b/content/docs/references/kernel/meta.json
@@ -4,7 +4,6 @@
"---Plugin Lifecycle---",
"plugin",
"plugin-lifecycle-advanced",
- "plugin-lifecycle-events",
"plugin-loading",
"plugin-registry",
"plugin-runtime",
diff --git a/content/docs/references/kernel/plugin-lifecycle-events.mdx b/content/docs/references/kernel/plugin-lifecycle-events.mdx
deleted file mode 100644
index 4b9f47fd05..0000000000
--- a/content/docs/references/kernel/plugin-lifecycle-events.mdx
+++ /dev/null
@@ -1,220 +0,0 @@
----
-title: Plugin Lifecycle Events
-description: Plugin Lifecycle Events protocol schemas
----
-
-{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}
-
-Plugin Lifecycle Events Protocol
-
-Zod schemas for plugin lifecycle event data structures.
-
-These schemas align with the IPluginLifecycleEvents contract interface.
-
-Following ObjectStack "Zod First" principle - all data structures
-
-must have Zod schemas for runtime validation and JSON Schema generation.
-
-
-**Source:** `packages/spec/src/kernel/plugin-lifecycle-events.zod.ts`
-
-
-## TypeScript Usage
-
-```typescript
-import { EventPhase, HookRegisteredEvent, HookTriggeredEvent, KernelEventBase, KernelReadyEvent, KernelShutdownEvent, PluginErrorEvent, PluginEventBase, PluginLifecycleEventType, PluginLifecyclePhaseEvent, PluginRegisteredEvent, ServiceRegisteredEvent, ServiceUnregisteredEvent } from '@objectstack/spec/kernel';
-import type { EventPhase, HookRegisteredEvent, HookTriggeredEvent, KernelEventBase, KernelReadyEvent, KernelShutdownEvent, PluginErrorEvent, PluginEventBase, PluginLifecycleEventType, PluginLifecyclePhaseEvent, PluginRegisteredEvent, ServiceRegisteredEvent, ServiceUnregisteredEvent } from '@objectstack/spec/kernel';
-
-// Validate data
-const result = EventPhase.parse(data);
-```
-
----
-
-## EventPhase
-
-Plugin lifecycle phase
-
-### Allowed Values
-
-* `init`
-* `start`
-* `destroy`
-
-
----
-
-## HookRegisteredEvent
-
-### Properties
-
-| Property | Type | Required | Description |
-| :--- | :--- | :--- | :--- |
-| **hookName** | `string` | ✅ | Name of the hook |
-| **timestamp** | `integer` | ✅ | Unix timestamp in milliseconds |
-| **handlerCount** | `integer` | ✅ | Number of handlers registered for this hook |
-
-
----
-
-## HookTriggeredEvent
-
-### Properties
-
-| Property | Type | Required | Description |
-| :--- | :--- | :--- | :--- |
-| **hookName** | `string` | ✅ | Name of the hook |
-| **timestamp** | `integer` | ✅ | Unix timestamp in milliseconds |
-| **args** | `any[]` | ✅ | Arguments passed to the hook handlers |
-| **handlerCount** | `integer` | optional | Number of handlers that will handle this event |
-
-
----
-
-## KernelEventBase
-
-### Properties
-
-| Property | Type | Required | Description |
-| :--- | :--- | :--- | :--- |
-| **timestamp** | `integer` | ✅ | Unix timestamp in milliseconds |
-
-
----
-
-## KernelReadyEvent
-
-### Properties
-
-| Property | Type | Required | Description |
-| :--- | :--- | :--- | :--- |
-| **timestamp** | `integer` | ✅ | Unix timestamp in milliseconds |
-| **duration** | `number` | optional | Total initialization duration in milliseconds |
-| **pluginCount** | `integer` | optional | Number of plugins initialized |
-
-
----
-
-## KernelShutdownEvent
-
-### Properties
-
-| Property | Type | Required | Description |
-| :--- | :--- | :--- | :--- |
-| **timestamp** | `integer` | ✅ | Unix timestamp in milliseconds |
-| **reason** | `string` | optional | Reason for kernel shutdown |
-
-
----
-
-## PluginErrorEvent
-
-### Properties
-
-| Property | Type | Required | Description |
-| :--- | :--- | :--- | :--- |
-| **pluginName** | `string` | ✅ | Name of the plugin |
-| **timestamp** | `integer` | ✅ | Unix timestamp in milliseconds when event occurred |
-| **error** | `{ name: string; message: string; stack?: string; code?: string }` | ✅ | Serializable error representation |
-| **phase** | `Enum<'init' \| 'start' \| 'destroy'>` | ✅ | Lifecycle phase where error occurred |
-| **errorMessage** | `string` | optional | Error message |
-| **errorStack** | `string` | optional | Error stack trace |
-
-
----
-
-## PluginEventBase
-
-### Properties
-
-| Property | Type | Required | Description |
-| :--- | :--- | :--- | :--- |
-| **pluginName** | `string` | ✅ | Name of the plugin |
-| **timestamp** | `integer` | ✅ | Unix timestamp in milliseconds when event occurred |
-
-
----
-
-## PluginLifecycleEventType
-
-Plugin lifecycle event type
-
-### Allowed Values
-
-* `kernel:ready`
-* `kernel:bootstrapped`
-* `kernel:listening`
-* `kernel:shutdown`
-* `kernel:before-init`
-* `kernel:after-init`
-* `plugin:registered`
-* `plugin:before-init`
-* `plugin:init`
-* `plugin:after-init`
-* `plugin:before-start`
-* `plugin:started`
-* `plugin:after-start`
-* `plugin:before-destroy`
-* `plugin:destroyed`
-* `plugin:after-destroy`
-* `plugin:error`
-* `service:registered`
-* `service:unregistered`
-* `hook:registered`
-* `hook:triggered`
-
-
----
-
-## PluginLifecyclePhaseEvent
-
-### Properties
-
-| Property | Type | Required | Description |
-| :--- | :--- | :--- | :--- |
-| **pluginName** | `string` | ✅ | Name of the plugin |
-| **timestamp** | `integer` | ✅ | Unix timestamp in milliseconds when event occurred |
-| **duration** | `number` | optional | Duration of the lifecycle phase in milliseconds |
-| **phase** | `Enum<'init' \| 'start' \| 'destroy'>` | optional | Lifecycle phase |
-
-
----
-
-## PluginRegisteredEvent
-
-### Properties
-
-| Property | Type | Required | Description |
-| :--- | :--- | :--- | :--- |
-| **pluginName** | `string` | ✅ | Name of the plugin |
-| **timestamp** | `integer` | ✅ | Unix timestamp in milliseconds when event occurred |
-| **version** | `string` | optional | Plugin version |
-
-
----
-
-## ServiceRegisteredEvent
-
-### Properties
-
-| Property | Type | Required | Description |
-| :--- | :--- | :--- | :--- |
-| **serviceName** | `string` | ✅ | Name of the registered service |
-| **timestamp** | `integer` | ✅ | Unix timestamp in milliseconds |
-| **serviceType** | `string` | optional | Type or interface name of the service |
-
-
----
-
-## ServiceUnregisteredEvent
-
-### Properties
-
-| Property | Type | Required | Description |
-| :--- | :--- | :--- | :--- |
-| **serviceName** | `string` | ✅ | Name of the unregistered service |
-| **timestamp** | `integer` | ✅ | Unix timestamp in milliseconds |
-
-
----
-
diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts
index 902f913874..68238876fd 100644
--- a/packages/core/src/types.ts
+++ b/packages/core/src/types.ts
@@ -1,7 +1,7 @@
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
import { ObjectKernel } from './kernel.js';
-import type { Logger } from '@objectstack/spec/contracts';
+import type { Logger, LifecycleEventName } from '@objectstack/spec/contracts';
/**
* PluginContext - Runtime context available to plugins
@@ -58,18 +58,26 @@ export interface PluginContext {
getServices(): Map;
/**
- * Register a hook handler
+ * Register a hook handler.
+ *
+ * Known lifecycle-bus names (see `IPluginLifecycleEvents` in
+ * `@objectstack/spec`) autocomplete; the bus stays open to custom
+ * cross-plugin event names, so any string remains valid.
+ *
* @param name - Hook name (e.g., 'kernel:ready', 'data:beforeInsert')
* @param handler - Hook handler function
*/
- hook(name: string, handler: (...args: any[]) => void | Promise): void;
+ hook(
+ name: LifecycleEventName | (string & {}),
+ handler: (...args: any[]) => void | Promise,
+ ): void;
/**
* Trigger a hook
- * @param name - Hook name
+ * @param name - Hook name (known lifecycle names autocomplete; custom names stay legal)
* @param args - Arguments to pass to hook handlers
*/
- trigger(name: string, ...args: any[]): Promise;
+ trigger(name: LifecycleEventName | (string & {}), ...args: any[]): Promise;
/**
* Logger instance
diff --git a/packages/spec/PROTOCOL_MAP.md b/packages/spec/PROTOCOL_MAP.md
index 94001cd5c1..6ebe74467c 100644
--- a/packages/spec/PROTOCOL_MAP.md
+++ b/packages/spec/PROTOCOL_MAP.md
@@ -202,7 +202,7 @@ This document serves as the **Grand Map** of the ObjectStack specification. It l
| [`plugin-validator.zod.ts`](src/kernel/plugin-validator.zod.ts) | | **Validation**. Integrity checks for plugins. |
| [`plugin-structure.zod.ts`](src/kernel/plugin-structure.zod.ts) | | **Structure**. Zod rules for folder layout and file naming. |
| [`plugin-capability.zod.ts`](src/kernel/plugin-capability.zod.ts) | | **Capabilities**. What a plugin can do. |
-| [`plugin-lifecycle-events.zod.ts`](src/kernel/plugin-lifecycle-events.zod.ts) | | **Lifecycle Events**. Hooks for plugin state changes. |
+| [`plugin-lifecycle-events.ts`](src/contracts/plugin-lifecycle-events.ts) | | **Lifecycle Events**. Registry of every kernel-bus event that actually fires, with payload tuples; feeds `hook`/`trigger` autocomplete. |
| [`plugin-lifecycle-advanced.zod.ts`](src/kernel/plugin-lifecycle-advanced.zod.ts) | | **Advanced Lifecycle**. Health monitoring, hot reload state management, graceful degradation, and update strategies. |
| [`plugin-security-advanced.zod.ts`](src/kernel/plugin-security-advanced.zod.ts) | | **Advanced Security**. Permission system, sandbox configuration (V8/WASM/container/process), security scanning, and trust levels. |
| [`startup-orchestrator.zod.ts`](src/kernel/startup-orchestrator.zod.ts) | | **Startup**. Boot sequence orchestration. |
diff --git a/packages/spec/api-surface.json b/packages/spec/api-surface.json
index 3c011d5e16..78fd26cf57 100644
--- a/packages/spec/api-surface.json
+++ b/packages/spec/api-surface.json
@@ -1473,8 +1473,6 @@
"EventMetadataSchema (const)",
"EventPersistence (type)",
"EventPersistenceSchema (const)",
- "EventPhase (type)",
- "EventPhaseSchema (const)",
"EventPriority (type)",
"EventQueueConfig (type)",
"EventQueueConfigSchema (const)",
@@ -1506,10 +1504,6 @@
"GracefulDegradationSchema (const)",
"HealthStatus (type)",
"HealthStatusSchema (const)",
- "HookRegisteredEvent (type)",
- "HookRegisteredEventSchema (const)",
- "HookTriggeredEvent (type)",
- "HookTriggeredEventSchema (const)",
"HotReloadConfig (type)",
"HotReloadConfigSchema (const)",
"InstallPackageRequest (type)",
@@ -1520,17 +1514,12 @@
"InstalledPackageSchema (const)",
"KernelContext (type)",
"KernelContextSchema (const)",
- "KernelEventBaseSchema (const)",
- "KernelReadyEvent (type)",
- "KernelReadyEventSchema (const)",
"KernelSecurityPolicy (type)",
"KernelSecurityPolicySchema (const)",
"KernelSecurityScanResult (type)",
"KernelSecurityScanResultSchema (const)",
"KernelSecurityVulnerability (type)",
"KernelSecurityVulnerabilitySchema (const)",
- "KernelShutdownEvent (type)",
- "KernelShutdownEventSchema (const)",
"LintableAuthoringCollection (interface)",
"ListPackagesRequest (type)",
"ListPackagesRequestSchema (const)",
@@ -1676,9 +1665,6 @@
"PluginDynamicImportSchema (const)",
"PluginEngines (type)",
"PluginEnginesSchema (const)",
- "PluginErrorEvent (type)",
- "PluginErrorEventSchema (const)",
- "PluginEventBaseSchema (const)",
"PluginHealthCheck (type)",
"PluginHealthCheckSchema (const)",
"PluginHealthReport (type)",
@@ -1696,9 +1682,6 @@
"PluginIntegritySchema (const)",
"PluginInterface (type)",
"PluginInterfaceSchema (const)",
- "PluginLifecycleEventType (type)",
- "PluginLifecyclePhaseEvent (type)",
- "PluginLifecyclePhaseEventSchema (const)",
"PluginLoadingConfig (type)",
"PluginLoadingConfigSchema (const)",
"PluginLoadingEvent (type)",
@@ -1726,8 +1709,6 @@
"PluginQualityMetrics (type)",
"PluginQualityMetricsInput (type)",
"PluginQualityMetricsSchema (const)",
- "PluginRegisteredEvent (type)",
- "PluginRegisteredEventSchema (const)",
"PluginRegistryEntry (type)",
"PluginRegistryEntryInput (type)",
"PluginRegistryEntrySchema (const)",
@@ -1817,14 +1798,10 @@
"ServiceLeaderStrategySchema (const)",
"ServiceMetadata (type)",
"ServiceMetadataSchema (const)",
- "ServiceRegisteredEvent (type)",
- "ServiceRegisteredEventSchema (const)",
"ServiceRegistryConfig (type)",
"ServiceRegistryConfigInput (type)",
"ServiceRegistryConfigSchema (const)",
"ServiceScopeType (type)",
- "ServiceUnregisteredEvent (type)",
- "ServiceUnregisteredEventSchema (const)",
"StartupOptions (type)",
"StartupOptionsInput (type)",
"StartupOptionsSchema (const)",
@@ -3699,7 +3676,6 @@
"IStorageService (interface)",
"ITeamGraphService (interface)",
"ITenantRouter (interface)",
- "ITypedEventEmitter (interface)",
"IWorkflowService (interface)",
"ImportObjectOpts (interface)",
"ImportObjectResult (interface)",
@@ -3724,6 +3700,7 @@
"KnowledgeReindexResult (interface)",
"KnowledgeSearchOptions (interface)",
"LLMAdapter (interface)",
+ "LifecycleEventName (type)",
"ListExportJobsOptions (interface)",
"ListShareLinksFilter (interface)",
"LockAcquireOptions (interface)",
diff --git a/packages/spec/authorable-surface.json b/packages/spec/authorable-surface.json
index 46fd85831d..9c94819925 100644
--- a/packages/spec/authorable-surface.json
+++ b/packages/spec/authorable-surface.json
@@ -4825,13 +4825,6 @@
"kernel/HealthStatus:healthy",
"kernel/HealthStatus:message",
"kernel/HealthStatus:timestamp",
- "kernel/HookRegisteredEvent:handlerCount",
- "kernel/HookRegisteredEvent:hookName",
- "kernel/HookRegisteredEvent:timestamp",
- "kernel/HookTriggeredEvent:args",
- "kernel/HookTriggeredEvent:handlerCount",
- "kernel/HookTriggeredEvent:hookName",
- "kernel/HookTriggeredEvent:timestamp",
"kernel/HotReloadConfig:afterReload",
"kernel/HotReloadConfig:beforeReload",
"kernel/HotReloadConfig:debounceDelay",
@@ -4869,10 +4862,6 @@
"kernel/KernelContext:startTime",
"kernel/KernelContext:version",
"kernel/KernelContext:workspaceRoot",
- "kernel/KernelEventBase:timestamp",
- "kernel/KernelReadyEvent:duration",
- "kernel/KernelReadyEvent:pluginCount",
- "kernel/KernelReadyEvent:timestamp",
"kernel/KernelSecurityPolicy:auditLog",
"kernel/KernelSecurityPolicy:authentication",
"kernel/KernelSecurityPolicy:cors",
@@ -4904,8 +4893,6 @@
"kernel/KernelSecurityVulnerability:severity",
"kernel/KernelSecurityVulnerability:title",
"kernel/KernelSecurityVulnerability:workaround",
- "kernel/KernelShutdownEvent:reason",
- "kernel/KernelShutdownEvent:timestamp",
"kernel/ListPackagesRequest:enabled",
"kernel/ListPackagesRequest:status",
"kernel/ListPackagesRequest:type",
@@ -5224,14 +5211,6 @@
"kernel/PluginDynamicImport:webpackChunkName",
"kernel/PluginEngines:platform",
"kernel/PluginEngines:protocol",
- "kernel/PluginErrorEvent:error",
- "kernel/PluginErrorEvent:errorMessage",
- "kernel/PluginErrorEvent:errorStack",
- "kernel/PluginErrorEvent:phase",
- "kernel/PluginErrorEvent:pluginName",
- "kernel/PluginErrorEvent:timestamp",
- "kernel/PluginEventBase:pluginName",
- "kernel/PluginEventBase:timestamp",
"kernel/PluginHealthCheck:autoRestart",
"kernel/PluginHealthCheck:checkMethod",
"kernel/PluginHealthCheck:failureThreshold",
@@ -5274,10 +5253,6 @@
"kernel/PluginInterface:name",
"kernel/PluginInterface:stability",
"kernel/PluginInterface:version",
- "kernel/PluginLifecyclePhaseEvent:duration",
- "kernel/PluginLifecyclePhaseEvent:phase",
- "kernel/PluginLifecyclePhaseEvent:pluginName",
- "kernel/PluginLifecyclePhaseEvent:timestamp",
"kernel/PluginLoadingConfig:caching",
"kernel/PluginLoadingConfig:codeSplitting",
"kernel/PluginLoadingConfig:dependencyResolution",
@@ -5344,9 +5319,6 @@
"kernel/PluginQualityMetrics:documentationScore",
"kernel/PluginQualityMetrics:securityScan",
"kernel/PluginQualityMetrics:testCoverage",
- "kernel/PluginRegisteredEvent:pluginName",
- "kernel/PluginRegisteredEvent:timestamp",
- "kernel/PluginRegisteredEvent:version",
"kernel/PluginRegistryEntry:capabilities",
"kernel/PluginRegistryEntry:category",
"kernel/PluginRegistryEntry:compatibility",
@@ -5570,16 +5542,11 @@
"kernel/ServiceMetadata:registeredAt",
"kernel/ServiceMetadata:scope",
"kernel/ServiceMetadata:type",
- "kernel/ServiceRegisteredEvent:serviceName",
- "kernel/ServiceRegisteredEvent:serviceType",
- "kernel/ServiceRegisteredEvent:timestamp",
"kernel/ServiceRegistryConfig:allowOverwrite",
"kernel/ServiceRegistryConfig:enableLogging",
"kernel/ServiceRegistryConfig:maxServices",
"kernel/ServiceRegistryConfig:scopeTypes",
"kernel/ServiceRegistryConfig:strictMode",
- "kernel/ServiceUnregisteredEvent:serviceName",
- "kernel/ServiceUnregisteredEvent:timestamp",
"kernel/StartupOptions:context",
"kernel/StartupOptions:healthCheck",
"kernel/StartupOptions:parallel",
diff --git a/packages/spec/json-schema.manifest.json b/packages/spec/json-schema.manifest.json
index 035ad4188d..266ead7820 100644
--- a/packages/spec/json-schema.manifest.json
+++ b/packages/spec/json-schema.manifest.json
@@ -984,7 +984,6 @@
"kernel/EventMessageQueueConfig",
"kernel/EventMetadata",
"kernel/EventPersistence",
- "kernel/EventPhase",
"kernel/EventPriority",
"kernel/EventQueueConfig",
"kernel/EventReplayConfig",
@@ -1000,19 +999,14 @@
"kernel/GetPackageResponse",
"kernel/GracefulDegradation",
"kernel/HealthStatus",
- "kernel/HookRegisteredEvent",
- "kernel/HookTriggeredEvent",
"kernel/HotReloadConfig",
"kernel/InstallPackageRequest",
"kernel/InstallPackageResponse",
"kernel/InstalledPackage",
"kernel/KernelContext",
- "kernel/KernelEventBase",
- "kernel/KernelReadyEvent",
"kernel/KernelSecurityPolicy",
"kernel/KernelSecurityScanResult",
"kernel/KernelSecurityVulnerability",
- "kernel/KernelShutdownEvent",
"kernel/ListPackagesRequest",
"kernel/ListPackagesResponse",
"kernel/Manifest",
@@ -1077,8 +1071,6 @@
"kernel/PluginDependencyResolutionResult",
"kernel/PluginDynamicImport",
"kernel/PluginEngines",
- "kernel/PluginErrorEvent",
- "kernel/PluginEventBase",
"kernel/PluginHealthCheck",
"kernel/PluginHealthReport",
"kernel/PluginHealthStatus",
@@ -1087,8 +1079,6 @@
"kernel/PluginInstallConfig",
"kernel/PluginIntegrity",
"kernel/PluginInterface",
- "kernel/PluginLifecycleEventType",
- "kernel/PluginLifecyclePhaseEvent",
"kernel/PluginLoadingConfig",
"kernel/PluginLoadingEvent",
"kernel/PluginLoadingState",
@@ -1102,7 +1092,6 @@
"kernel/PluginPreloadConfig",
"kernel/PluginProvenance",
"kernel/PluginQualityMetrics",
- "kernel/PluginRegisteredEvent",
"kernel/PluginRegistryEntry",
"kernel/PluginRuntime",
"kernel/PluginSandboxing",
@@ -1143,10 +1132,8 @@
"kernel/ServiceFactoryRegistration",
"kernel/ServiceLeaderStrategy",
"kernel/ServiceMetadata",
- "kernel/ServiceRegisteredEvent",
"kernel/ServiceRegistryConfig",
"kernel/ServiceScopeType",
- "kernel/ServiceUnregisteredEvent",
"kernel/StartupOptions",
"kernel/StartupOrchestrationResult",
"kernel/TenantRuntimeContext",
diff --git a/packages/spec/scripts/build-skill-references.ts b/packages/spec/scripts/build-skill-references.ts
index 520e90cdfa..ba78360474 100644
--- a/packages/spec/scripts/build-skill-references.ts
+++ b/packages/spec/scripts/build-skill-references.ts
@@ -110,7 +110,9 @@ const SKILL_MAP: Record = {
'kernel/plugin.zod.ts',
'kernel/context.zod.ts',
'kernel/service-registry.zod.ts',
- 'kernel/plugin-lifecycle-events.zod.ts',
+ // Lifecycle events: no Zod surface — the registry is the
+ // IPluginLifecycleEvents interface (src/contracts/plugin-lifecycle-events.ts),
+ // covered by the skill's hand-written references/plugin-hooks.md.
'kernel/plugin-capability.zod.ts',
'kernel/plugin-loading.zod.ts',
'kernel/metadata-plugin.zod.ts',
diff --git a/packages/spec/src/contracts/plugin-lifecycle-events.test.ts b/packages/spec/src/contracts/plugin-lifecycle-events.test.ts
index e2a880b5e6..2b2a1ebb35 100644
--- a/packages/spec/src/contracts/plugin-lifecycle-events.test.ts
+++ b/packages/spec/src/contracts/plugin-lifecycle-events.test.ts
@@ -1,137 +1,90 @@
import { describe, it, expect } from 'vitest';
-import type { IPluginLifecycleEvents, ITypedEventEmitter } from './plugin-lifecycle-events';
-
-describe('Plugin Lifecycle Events Contract', () => {
- describe('IPluginLifecycleEvents interface', () => {
- it('should define all kernel event types', () => {
- // Compile-time check: verify the event map type is correctly shaped
- const events: Record, any> = {
- 'kernel:ready': [],
- 'kernel:bootstrapped': [],
- 'kernel:listening': [],
- 'kernel:shutdown': [],
- 'kernel:before-init': [],
- 'kernel:after-init': [150],
- };
-
- expect(events['kernel:ready']).toEqual([]);
- expect(events['kernel:after-init']).toEqual([150]);
- });
-
- it('should define all plugin event types', () => {
- const events: Record, any> = {
- 'plugin:registered': ['my-plugin'],
- 'plugin:before-init': ['my-plugin'],
- 'plugin:init': ['my-plugin'],
- 'plugin:after-init': ['my-plugin', 50],
- 'plugin:before-start': ['my-plugin'],
- 'plugin:started': ['my-plugin', 100],
- 'plugin:after-start': ['my-plugin', 100],
- 'plugin:before-destroy': ['my-plugin'],
- 'plugin:destroyed': ['my-plugin'],
- 'plugin:after-destroy': ['my-plugin', 25],
- 'plugin:error': ['my-plugin', new Error('fail'), 'init'],
- };
-
- expect(events['plugin:registered']).toEqual(['my-plugin']);
- expect(events['plugin:error'][2]).toBe('init');
+import type { IPluginLifecycleEvents, LifecycleEventName } from './plugin-lifecycle-events';
+
+/**
+ * Pins the lifecycle-event registry to the fire-site inventory.
+ *
+ * Every name below has a real emitter (listed next to it). If you add a key
+ * to `IPluginLifecycleEvents`, add it here WITH its fire site; if you remove
+ * one, remove it here too. A name with no emitter does not belong in the
+ * registry — that is the disease the #4212 retirement cured (seventeen
+ * declared-never-fired names, an unused typed-emitter interface, and a dead
+ * parallel Zod schema file), and this pin is what keeps it cured.
+ */
+const FIRED_EVENTS = [
+ 'kernel:ready', // core/src/kernel.ts, core/src/lite-kernel.ts
+ 'kernel:bootstrapped', // core/src/kernel.ts, core/src/lite-kernel.ts
+ 'kernel:listening', // core/src/kernel.ts, core/src/lite-kernel.ts
+ 'kernel:shutdown', // core/src/kernel.ts, core/src/lite-kernel.ts
+ 'app:seeded', // runtime/src/app-plugin.ts (inline seeder)
+ 'metadata:reloaded', // metadata/src/plugin.ts (artifact watcher)
+ 'external.schema.drift', // runtime/src/external-validation-plugin.ts
+ 'ai:routes', // cloud AI service plugin (out-of-repo); listener in runtime/src/dispatcher-plugin.ts
+ 'auth:configure', // plugins/plugin-auth/src/auth-plugin.ts
+ // The `{service}:ready` convention — payload is the live service instance.
+ 'mcp:ready', // mcp/src/plugin.ts
+ 'automation:ready', // services/service-automation/src/plugin.ts
+ 'analytics:ready', // services/service-analytics/src/plugin.ts
+ 'external-datasource:ready', // services/service-datasource/src/plugin.ts
+ 'datasource-admin:ready', // services/service-datasource/src/datasource-admin-plugin.ts
+] as const;
+
+type FiredEvent = (typeof FIRED_EVENTS)[number];
+type AssertNever = T;
+
+// Two-way exhaustiveness, checked at compile time: the test file fails to
+// build if the registry and this inventory drift apart in either direction.
+type _EveryFiredNameIsDeclared = AssertNever>;
+type _EveryDeclaredNameFires = AssertNever>;
+
+describe('lifecycle-event registry', () => {
+ it('declares exactly the events that fire', () => {
+ expect([...FIRED_EVENTS].sort()).toEqual(
+ [
+ 'ai:routes',
+ 'analytics:ready',
+ 'app:seeded',
+ 'auth:configure',
+ 'automation:ready',
+ 'datasource-admin:ready',
+ 'external-datasource:ready',
+ 'external.schema.drift',
+ 'kernel:bootstrapped',
+ 'kernel:listening',
+ 'kernel:ready',
+ 'kernel:shutdown',
+ 'mcp:ready',
+ 'metadata:reloaded',
+ ].sort(),
+ );
});
- it('should define service and hook event types', () => {
- const events: Record, any> = {
- 'service:registered': ['database'],
- 'service:unregistered': ['cache'],
- 'hook:registered': ['beforeSave', 3],
- 'hook:triggered': ['beforeSave', [{ id: 1 }]],
- };
-
- expect(events['service:registered']).toEqual(['database']);
- expect(events['hook:registered']).toEqual(['beforeSave', 3]);
- });
- });
-
- describe('ITypedEventEmitter interface', () => {
- it('should allow a minimal implementation with on/off/emit', () => {
- const handlers = new Map();
-
- const emitter: ITypedEventEmitter = {
- on(event, handler) {
- const list = handlers.get(event as string) || [];
- list.push(handler as Function);
- handlers.set(event as string, list);
- },
- off(event, handler) {
- const list = handlers.get(event as string) || [];
- const idx = list.indexOf(handler as Function);
- if (idx >= 0) list.splice(idx, 1);
- },
- async emit(event, ...args) {
- const list = handlers.get(event as string) || [];
- for (const h of list) {
- await h(...args);
- }
- },
- };
-
- expect(typeof emitter.on).toBe('function');
- expect(typeof emitter.off).toBe('function');
- expect(typeof emitter.emit).toBe('function');
- });
-
- it('should support registering and emitting typed events', async () => {
- const received: string[] = [];
- const handlers = new Map();
-
- const emitter: ITypedEventEmitter = {
- on(event, handler) {
- const list = handlers.get(event as string) || [];
- list.push(handler as Function);
- handlers.set(event as string, list);
- },
- off(event, handler) {
- const list = handlers.get(event as string) || [];
- const idx = list.indexOf(handler as Function);
- if (idx >= 0) list.splice(idx, 1);
- },
- async emit(event, ...args) {
- const list = handlers.get(event as string) || [];
- for (const h of list) {
- await h(...args);
- }
- },
- };
-
- emitter.on('plugin:registered', (pluginName: string) => {
- received.push(pluginName);
- });
-
- await emitter.emit('plugin:registered', 'auth-plugin');
-
- expect(received).toEqual(['auth-plugin']);
- });
-
- it('should allow optional once, listenerCount, and removeAllListeners', () => {
- const emitter: ITypedEventEmitter = {
- on: () => {},
- off: () => {},
- emit: async () => {},
- once: () => {},
- listenerCount: () => 0,
- removeAllListeners: () => {},
- };
-
- expect(emitter.once).toBeDefined();
- expect(emitter.listenerCount).toBeDefined();
- expect(emitter.removeAllListeners).toBeDefined();
+ it('payload tuples match the fire sites', () => {
+ // Compile-time assignability against the shapes each emitter passes.
+ const kernelReady: IPluginLifecycleEvents['kernel:ready'] = [];
+ const seeded: IPluginLifecycleEvents['app:seeded'] = [
+ { appId: 'crm', overBudget: false },
+ ];
+ // `metadata` is optional: the artifact watcher includes it, but the
+ // documented contract lets announcers omit it.
+ const reloaded: IPluginLifecycleEvents['metadata:reloaded'] = [
+ { changed: ['flow/ticket_closed'] },
+ ];
+ const drift: IPluginLifecycleEvents['external.schema.drift'] = [
+ { datasource: 'erp', object: 'invoice', diffs: [] },
+ ];
+ const authConfigure: IPluginLifecycleEvents['auth:configure'] = [{}, {}];
+ const mcpReady: IPluginLifecycleEvents['mcp:ready'] = [{}];
+ const automationReady: IPluginLifecycleEvents['automation:ready'] = [{}];
+ const aiRoutes: IPluginLifecycleEvents['ai:routes'] = [[]];
+
+ expect(kernelReady).toEqual([]);
+ expect(seeded[0].overBudget).toBe(false);
+ expect(reloaded[0].changed).toEqual(['flow/ticket_closed']);
+ expect(drift[0].object).toBe('invoice');
+ expect(authConfigure).toHaveLength(2);
+ expect(mcpReady).toHaveLength(1);
+ expect(automationReady).toHaveLength(1);
+ expect(aiRoutes[0]).toEqual([]);
});
- });
});
diff --git a/packages/spec/src/contracts/plugin-lifecycle-events.ts b/packages/spec/src/contracts/plugin-lifecycle-events.ts
index f8faee4aeb..b6fa701887 100644
--- a/packages/spec/src/contracts/plugin-lifecycle-events.ts
+++ b/packages/spec/src/contracts/plugin-lifecycle-events.ts
@@ -1,20 +1,45 @@
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
/**
- * IPluginLifecycleEvents - Typed Plugin Lifecycle Events
- *
- * Type-safe event definitions for plugin and kernel lifecycle.
- * Provides strong typing for event emitters and listeners.
- *
- * This replaces the generic Map approach with typed events.
- */
-
-/**
- * Plugin lifecycle event types and their payloads
+ * IPluginLifecycleEvents — the registry of kernel-bus events that actually fire.
+ *
+ * Every key in this interface is an event some shipping code path triggers via
+ * `ctx.trigger(...)` (or `triggerHook` in the lite kernel), with its payload
+ * tuple as observed at the fire site. The registry is *enforced*, not
+ * aspirational, in two ways:
+ *
+ * - {@link LifecycleEventName} feeds `IPluginContext.hook` / `trigger`
+ * autocomplete in `@objectstack/core` (as `LifecycleEventName | (string & {})`,
+ * so custom cross-plugin events remain legal — the bus is open by design).
+ * - `plugin-lifecycle-events.test.ts` pins the key set to the fire-site
+ * inventory. Adding a key here without a real emitter — or removing one that
+ * still fires — fails that test with a pointer back to this doc.
+ *
+ * ## Retired names (ADR-0049 enforce-or-remove)
+ *
+ * An earlier revision of this file also declared a typed-event system that was
+ * never built: `kernel:before-init`, `kernel:after-init`, `plugin:registered`,
+ * `plugin:before-init`, `plugin:init`, `plugin:after-init`,
+ * `plugin:before-start`, `plugin:started`, `plugin:after-start`,
+ * `plugin:before-destroy`, `plugin:destroyed`, `plugin:after-destroy`,
+ * `plugin:error`, `service:registered`, `service:unregistered`,
+ * `hook:registered`, `hook:triggered` — seventeen names with no emitter and no
+ * listener anywhere — plus an `ITypedEventEmitter` interface and a parallel
+ * set of Zod payload schemas (`kernel/plugin-lifecycle-events.zod.ts`) with
+ * zero consumers. All of it is retired, same playbook as the PluginSchema
+ * lifecycle-hook retirement (#4212): a declared-but-dead contract reads as a
+ * promise and silently swallows anyone who codes against it. If per-plugin
+ * phase events become real, re-add each name together with its emitter and a
+ * fire-site pointer, and extend the pinning test.
*/
export interface IPluginLifecycleEvents {
/**
- * Emitted when kernel is ready (all plugins initialized)
+ * Emitted when kernel is ready (all plugins initialized).
+ *
+ * Fired by `ObjectKernel.start()` and `LiteKernel.bootstrap()` after every
+ * plugin's `start()` has completed. Handlers run sequentially in plugin
+ * registration order — see `kernel:bootstrapped` for the ordering caveat.
+ *
* Payload: []
*/
'kernel:ready': [];
@@ -65,6 +90,13 @@ export interface IPluginLifecycleEvents {
*/
'kernel:listening': [];
+ /**
+ * Emitted when kernel is shutting down, before plugins are destroyed.
+ *
+ * Payload: []
+ */
+ 'kernel:shutdown': [];
+
/**
* Emitted by the app plugin when an app's inline seed attempt has settled
* — success, partial (dropped records), or fallback insert. Single-tenant
@@ -87,169 +119,113 @@ export interface IPluginLifecycleEvents {
'app:seeded': [payload: { appId: string; overBudget: boolean }];
/**
- * Emitted when kernel is shutting down
- * Payload: []
- */
- 'kernel:shutdown': [];
-
- /**
- * Emitted before kernel initialization starts
- * Payload: []
- */
- 'kernel:before-init': [];
-
- /**
- * Emitted after kernel initialization completes
- * Payload: [duration: number (milliseconds)]
- */
- 'kernel:after-init': [duration: number];
-
- /**
- * Emitted when a plugin is registered
- * Payload: [pluginName: string]
- */
- 'plugin:registered': [pluginName: string];
-
- /**
- * Emitted before a plugin's init method is called
- * Payload: [pluginName: string]
- */
- 'plugin:before-init': [pluginName: string];
-
- /**
- * Emitted when a plugin has been initialized
- * Payload: [pluginName: string]
- */
- 'plugin:init': [pluginName: string];
-
- /**
- * Emitted after a plugin's init method completes
- * Payload: [pluginName: string, duration: number (milliseconds)]
- */
- 'plugin:after-init': [pluginName: string, duration: number];
-
- /**
- * Emitted before a plugin's start method is called
- * Payload: [pluginName: string]
- */
- 'plugin:before-start': [pluginName: string];
-
- /**
- * Emitted when a plugin has started successfully
- * Payload: [pluginName: string, duration: number (milliseconds)]
- */
- 'plugin:started': [pluginName: string, duration: number];
-
- /**
- * Emitted after a plugin's start method completes
- * Payload: [pluginName: string, duration: number (milliseconds)]
+ * Emitted by the metadata plugin's artifact watcher after a changed
+ * `dist/objectstack.json` has been re-parsed and re-registered mid-run.
+ *
+ * `changed` entries are `'{type}/{name}'` strings (e.g.
+ * `'flow/ticket_closed'`). `metadata`, when present, carries the freshly
+ * parsed artifact collections so subscribers can consume the ones that
+ * never reach the MetadataManager (seed datasets under `data` have no
+ * `name`). The app plugin uses it to load seeds for objects that appear
+ * mid-run. The collections shape is owned by `@objectstack/metadata`; the
+ * spec keeps it opaque.
+ *
+ * Payload: [{ changed, metadata? }]
*/
- 'plugin:after-start': [pluginName: string, duration: number];
-
+ 'metadata:reloaded': [payload: { changed: string[]; metadata?: unknown }];
+
/**
- * Emitted before a plugin's destroy method is called
- * Payload: [pluginName: string]
+ * Emitted by `@objectstack/plugin-auth` while assembling its better-auth
+ * config, BEFORE the auth instance is created. The open extension point
+ * for packages that contribute auth providers (enterprise SSO, hosted
+ * control-plane SSO, …) without forking the auth plugin: handlers mutate
+ * the draft config in place. Both payload types are owned by plugin-auth;
+ * the spec keeps them opaque.
+ *
+ * Payload: [authConfig, ctx]
*/
- 'plugin:before-destroy': [pluginName: string];
-
+ 'auth:configure': [authConfig: unknown, ctx: unknown];
+
+ // ── The `{service}:ready` convention ─────────────────────────────────
+ // Service plugins announce readiness as `{service}:ready`, passing the
+ // live service instance so sibling plugins can extend it. Each instance
+ // type is owned by its package; the spec keeps them opaque. A new service
+ // adopting the convention adds its concrete name here (custom names are
+ // legal without registration — the bus is open — but a registered name
+ // autocompletes and is pinned to its fire site by the registry test).
+
/**
- * Emitted when a plugin has been destroyed
- * Payload: [pluginName: string]
+ * Emitted by `@objectstack/mcp` once its server runtime is constructed
+ * and tools are registered. Payload: the live `MCPServerRuntime`.
+ *
+ * Payload: [runtime]
*/
- 'plugin:destroyed': [pluginName: string];
-
+ 'mcp:ready': [runtime: unknown];
+
/**
- * Emitted after a plugin's destroy method completes
- * Payload: [pluginName: string, duration: number (milliseconds)]
+ * Emitted by `@objectstack/service-automation` at start, once the flow
+ * engine is ready. Payload: the live automation engine.
+ *
+ * Payload: [engine]
*/
- 'plugin:after-destroy': [pluginName: string, duration: number];
-
+ 'automation:ready': [engine: unknown];
+
/**
- * Emitted when a plugin encounters an error
- * Payload: [pluginName: string, error: Error, phase: 'init' | 'start' | 'destroy']
+ * Emitted by `@objectstack/service-analytics` at start. Payload: the live
+ * analytics service.
+ *
+ * Payload: [service]
*/
- 'plugin:error': [pluginName: string, error: Error, phase: 'init' | 'start' | 'destroy'];
-
+ 'analytics:ready': [service: unknown];
+
/**
- * Emitted when a service is registered
- * Payload: [serviceName: string]
+ * Emitted by `@objectstack/service-datasource` at start. Payload: the
+ * live external-datasource service.
+ *
+ * Payload: [service]
*/
- 'service:registered': [serviceName: string];
-
+ 'external-datasource:ready': [service: unknown];
+
/**
- * Emitted when a service is unregistered
- * Payload: [serviceName: string]
+ * Emitted by `@objectstack/service-datasource`'s admin plugin at start.
+ * Payload: the live datasource-admin service.
+ *
+ * Payload: [service]
*/
- 'service:unregistered': [serviceName: string];
-
+ 'datasource-admin:ready': [service: unknown];
+
/**
- * Emitted when a hook is registered
- * Payload: [hookName: string, handlerCount: number]
+ * Emitted by the external-validation plugin's background drift checker
+ * (ADR-0015 §5.2), one event per federated object whose remote schema no
+ * longer matches its declared shape. Consumed by audit / notification
+ * services. `diffs` entries are `SchemaDiffEntry` values owned by
+ * `@objectstack/runtime`; the spec keeps them opaque.
+ *
+ * Payload: [{ datasource, object, diffs }]
*/
- 'hook:registered': [hookName: string, handlerCount: number];
-
+ 'external.schema.drift': [
+ event: { datasource: string; object: string; diffs: unknown[] },
+ ];
+
/**
- * Emitted when a hook is triggered
- * Payload: [hookName: string, args: any[]]
+ * Emitted by the cloud AI service plugin (out-of-repo emitter) with the
+ * dynamic `RouteDefinition[]` it wants mounted; the dispatcher plugin
+ * listens and mounts each route on the HTTP server (environment-scoped
+ * variants included). Because plugin start order is not guaranteed, the
+ * emitter also caches the routes on the kernel as `__aiRoutes` for the
+ * dispatcher to recover when it starts late — see
+ * `DispatcherPlugin` for the recovery half of the protocol.
+ *
+ * Payload: [routes]
*/
- 'hook:triggered': [hookName: string, args: any[]];
+ 'ai:routes': [routes: unknown[]];
}
/**
- * Type-safe event emitter interface
- * Provides compile-time type checking for event names and payloads
+ * Union of every kernel-bus event name that actually fires.
+ *
+ * `IPluginContext.hook` / `trigger` in `@objectstack/core` accept
+ * `LifecycleEventName | (string & {})`: known names autocomplete, custom
+ * cross-plugin event names stay legal.
*/
-export interface ITypedEventEmitter> {
- /**
- * Register an event listener
- * @param event - Event name (type-checked)
- * @param handler - Event handler (type-checked against event payload)
- */
- on(
- event: K,
- handler: (...args: Events[K]) => void | Promise
- ): void;
-
- /**
- * Unregister an event listener
- * @param event - Event name (type-checked)
- * @param handler - Event handler to remove
- */
- off(
- event: K,
- handler: (...args: Events[K]) => void | Promise
- ): void;
-
- /**
- * Emit an event with type-checked payload
- * @param event - Event name (type-checked)
- * @param args - Event payload (type-checked)
- */
- emit(
- event: K,
- ...args: Events[K]
- ): Promise;
-
- /**
- * Register a one-time event listener
- * @param event - Event name (type-checked)
- * @param handler - Event handler (type-checked against event payload)
- */
- once?(
- event: K,
- handler: (...args: Events[K]) => void | Promise
- ): void;
-
- /**
- * Get the number of listeners for an event
- * @param event - Event name
- * @returns Number of registered listeners
- */
- listenerCount?(event: K): number;
-
- /**
- * Remove all listeners for an event (or all events if not specified)
- * @param event - Optional event name
- */
- removeAllListeners?(event?: K): void;
-}
+export type LifecycleEventName = keyof IPluginLifecycleEvents & string;
diff --git a/packages/spec/src/kernel/index.ts b/packages/spec/src/kernel/index.ts
index ac59265dff..06bbf7bbda 100644
--- a/packages/spec/src/kernel/index.ts
+++ b/packages/spec/src/kernel/index.ts
@@ -36,7 +36,6 @@ export * from './package-registry.zod';
export * from './package-upgrade.zod';
export * from './plugin-capability.zod';
export * from './plugin-lifecycle-advanced.zod';
-export * from './plugin-lifecycle-events.zod';
export * from './plugin-loading.zod';
export * from './plugin-runtime.zod';
export * from './plugin-security-advanced.zod';
diff --git a/packages/spec/src/kernel/plugin-lifecycle-events.test.ts b/packages/spec/src/kernel/plugin-lifecycle-events.test.ts
deleted file mode 100644
index a4db596569..0000000000
--- a/packages/spec/src/kernel/plugin-lifecycle-events.test.ts
+++ /dev/null
@@ -1,227 +0,0 @@
-import { describe, it, expect } from 'vitest';
-import {
- EventPhaseSchema,
- PluginRegisteredEventSchema,
- PluginLifecyclePhaseEventSchema,
- PluginErrorEventSchema,
- ServiceRegisteredEventSchema,
- ServiceUnregisteredEventSchema,
- HookRegisteredEventSchema,
- HookTriggeredEventSchema,
- KernelReadyEventSchema,
- KernelShutdownEventSchema,
- PluginLifecycleEventType,
-} from './plugin-lifecycle-events.zod';
-
-describe('Plugin Lifecycle Events Protocol', () => {
- describe('EventPhaseSchema', () => {
- it('should validate valid phases', () => {
- expect(EventPhaseSchema.safeParse('init').success).toBe(true);
- expect(EventPhaseSchema.safeParse('start').success).toBe(true);
- expect(EventPhaseSchema.safeParse('destroy').success).toBe(true);
- });
-
- it('should reject invalid phases', () => {
- expect(EventPhaseSchema.safeParse('unknown').success).toBe(false);
- });
- });
-
- describe('PluginRegisteredEventSchema', () => {
- it('should validate plugin registered event', () => {
- const event = {
- pluginName: 'crm-plugin',
- timestamp: Date.now(),
- version: '1.0.0',
- };
-
- const result = PluginRegisteredEventSchema.safeParse(event);
- expect(result.success).toBe(true);
- });
-
- it('should validate event without version', () => {
- const event = {
- pluginName: 'crm-plugin',
- timestamp: Date.now(),
- };
-
- const result = PluginRegisteredEventSchema.safeParse(event);
- expect(result.success).toBe(true);
- });
- });
-
- describe('PluginLifecyclePhaseEventSchema', () => {
- it('should validate lifecycle phase event', () => {
- const event = {
- pluginName: 'crm-plugin',
- timestamp: Date.now(),
- duration: 1250,
- phase: 'init' as const,
- };
-
- const result = PluginLifecyclePhaseEventSchema.safeParse(event);
- expect(result.success).toBe(true);
- });
- });
-
- describe('PluginErrorEventSchema', () => {
- it('should validate plugin error event', () => {
- const event = {
- pluginName: 'failing-plugin',
- timestamp: Date.now(),
- error: { name: 'Error', message: 'Connection failed' },
- phase: 'start' as const,
- errorMessage: 'Connection failed',
- };
-
- const result = PluginErrorEventSchema.safeParse(event);
- expect(result.success).toBe(true);
- });
-
- it('should require phase field', () => {
- const event = {
- pluginName: 'failing-plugin',
- timestamp: Date.now(),
- error: { name: 'Error', message: 'Connection failed' },
- // missing phase
- };
-
- const result = PluginErrorEventSchema.safeParse(event);
- expect(result.success).toBe(false);
- });
- });
-
- describe('ServiceRegisteredEventSchema', () => {
- it('should validate service registered event', () => {
- const event = {
- serviceName: 'database',
- timestamp: Date.now(),
- serviceType: 'IDataEngine',
- };
-
- const result = ServiceRegisteredEventSchema.safeParse(event);
- expect(result.success).toBe(true);
- });
- });
-
- describe('ServiceUnregisteredEventSchema', () => {
- it('should validate service unregistered event', () => {
- const event = {
- serviceName: 'database',
- timestamp: Date.now(),
- };
-
- const result = ServiceUnregisteredEventSchema.safeParse(event);
- expect(result.success).toBe(true);
- });
- });
-
- describe('HookRegisteredEventSchema', () => {
- it('should validate hook registered event', () => {
- const event = {
- hookName: 'data.beforeInsert',
- timestamp: Date.now(),
- handlerCount: 3,
- };
-
- const result = HookRegisteredEventSchema.safeParse(event);
- expect(result.success).toBe(true);
- });
-
- it('should reject negative handler count', () => {
- const event = {
- hookName: 'data.beforeInsert',
- timestamp: Date.now(),
- handlerCount: -1,
- };
-
- const result = HookRegisteredEventSchema.safeParse(event);
- expect(result.success).toBe(false);
- });
- });
-
- describe('HookTriggeredEventSchema', () => {
- it('should validate hook triggered event', () => {
- const event = {
- hookName: 'data.beforeInsert',
- timestamp: Date.now(),
- args: [{ object: 'customer', data: { name: 'Test' } }],
- handlerCount: 3,
- };
-
- const result = HookTriggeredEventSchema.safeParse(event);
- expect(result.success).toBe(true);
- });
- });
-
- describe('KernelReadyEventSchema', () => {
- it('should validate kernel ready event', () => {
- const event = {
- timestamp: Date.now(),
- duration: 5400,
- pluginCount: 12,
- };
-
- const result = KernelReadyEventSchema.safeParse(event);
- expect(result.success).toBe(true);
- });
-
- it('should validate minimal kernel ready event', () => {
- const event = {
- timestamp: Date.now(),
- };
-
- const result = KernelReadyEventSchema.safeParse(event);
- expect(result.success).toBe(true);
- });
- });
-
- describe('KernelShutdownEventSchema', () => {
- it('should validate kernel shutdown event', () => {
- const event = {
- timestamp: Date.now(),
- reason: 'SIGTERM received',
- };
-
- const result = KernelShutdownEventSchema.safeParse(event);
- expect(result.success).toBe(true);
- });
- });
-
- describe('PluginLifecycleEventType', () => {
- it('should validate all event types', () => {
- const eventTypes = [
- 'kernel:ready',
- 'kernel:bootstrapped',
- 'kernel:listening',
- 'kernel:shutdown',
- 'kernel:before-init',
- 'kernel:after-init',
- 'plugin:registered',
- 'plugin:before-init',
- 'plugin:init',
- 'plugin:after-init',
- 'plugin:before-start',
- 'plugin:started',
- 'plugin:after-start',
- 'plugin:before-destroy',
- 'plugin:destroyed',
- 'plugin:after-destroy',
- 'plugin:error',
- 'service:registered',
- 'service:unregistered',
- 'hook:registered',
- 'hook:triggered',
- ];
-
- eventTypes.forEach(eventType => {
- const result = PluginLifecycleEventType.safeParse(eventType);
- expect(result.success).toBe(true);
- });
- });
-
- it('should reject invalid event type', () => {
- const result = PluginLifecycleEventType.safeParse('invalid:event');
- expect(result.success).toBe(false);
- });
- });
-});
diff --git a/packages/spec/src/kernel/plugin-lifecycle-events.zod.ts b/packages/spec/src/kernel/plugin-lifecycle-events.zod.ts
deleted file mode 100644
index 883732c4cd..0000000000
--- a/packages/spec/src/kernel/plugin-lifecycle-events.zod.ts
+++ /dev/null
@@ -1,346 +0,0 @@
-// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
-
-import { z } from 'zod';
-
-/**
- * Plugin Lifecycle Events Protocol
- *
- * Zod schemas for plugin lifecycle event data structures.
- * These schemas align with the IPluginLifecycleEvents contract interface.
- *
- * Following ObjectStack "Zod First" principle - all data structures
- * must have Zod schemas for runtime validation and JSON Schema generation.
- */
-
-// ============================================================================
-// Event Payload Schemas
-// ============================================================================
-
-/**
- * Event Phase Enum
- * Lifecycle phase where an error occurred
- */
-import { lazySchema } from '../shared/lazy-schema';
-export const EventPhaseSchema = lazySchema(() => z.enum(['init', 'start', 'destroy'])
- .describe('Plugin lifecycle phase'));
-
-export type EventPhase = z.infer;
-
-/**
- * Plugin Event Base Schema
- * Common fields for all plugin events
- */
-export const PluginEventBaseSchema = lazySchema(() => z.object({
- /**
- * Plugin name
- */
- pluginName: z.string().describe('Name of the plugin'),
-
- /**
- * Event timestamp (Unix milliseconds)
- */
- timestamp: z.number().int().describe('Unix timestamp in milliseconds when event occurred'),
-}));
-
-/**
- * Plugin Registered Event Schema
- *
- * @example
- * {
- * "pluginName": "crm-plugin",
- * "timestamp": 1706659200000,
- * "version": "1.0.0"
- * }
- */
-export const PluginRegisteredEventSchema = lazySchema(() => PluginEventBaseSchema.extend({
- /**
- * Plugin version (optional)
- */
- version: z.string().optional().describe('Plugin version'),
-}));
-
-export type PluginRegisteredEvent = z.infer;
-
-/**
- * Plugin Lifecycle Phase Event Schema
- * For init, start, destroy phases
- *
- * @example
- * {
- * "pluginName": "crm-plugin",
- * "timestamp": 1706659200000,
- * "duration": 1250,
- * "phase": "init"
- * }
- */
-export const PluginLifecyclePhaseEventSchema = lazySchema(() => PluginEventBaseSchema.extend({
- /**
- * Duration of the phase (milliseconds)
- */
- duration: z.number().min(0).optional().describe('Duration of the lifecycle phase in milliseconds'),
-
- /**
- * Lifecycle phase
- */
- phase: EventPhaseSchema.optional().describe('Lifecycle phase'),
-}));
-
-export type PluginLifecyclePhaseEvent = z.infer;
-
-/**
- * Plugin Error Event Schema
- * When a plugin encounters an error
- *
- * @example
- * {
- * "pluginName": "crm-plugin",
- * "timestamp": 1706659200000,
- * "error": Error("Connection failed"),
- * "phase": "start",
- * "errorMessage": "Connection failed",
- * "errorStack": "Error: Connection failed\n at ..."
- * }
- */
-export const PluginErrorEventSchema = lazySchema(() => PluginEventBaseSchema.extend({
- /**
- * Error object
- */
- error: z.object({
- name: z.string().describe('Error class name'),
- message: z.string().describe('Error message'),
- stack: z.string().optional().describe('Stack trace'),
- code: z.string().optional().describe('Error code'),
- }).describe('Serializable error representation'),
-
- /**
- * Lifecycle phase where error occurred
- */
- phase: EventPhaseSchema.describe('Lifecycle phase where error occurred'),
-
- /**
- * Error message (for serialization)
- */
- errorMessage: z.string().optional().describe('Error message'),
-
- /**
- * Error stack trace (for debugging)
- */
- errorStack: z.string().optional().describe('Error stack trace'),
-}));
-
-export type PluginErrorEvent = z.infer;
-
-// ============================================================================
-// Service Event Schemas
-// ============================================================================
-
-/**
- * Service Registered Event Schema
- *
- * @example
- * {
- * "serviceName": "database",
- * "timestamp": 1706659200000,
- * "serviceType": "IDataEngine"
- * }
- */
-export const ServiceRegisteredEventSchema = lazySchema(() => z.object({
- /**
- * Service name
- */
- serviceName: z.string().describe('Name of the registered service'),
-
- /**
- * Event timestamp (Unix milliseconds)
- */
- timestamp: z.number().int().describe('Unix timestamp in milliseconds'),
-
- /**
- * Service type (optional)
- */
- serviceType: z.string().optional().describe('Type or interface name of the service'),
-}));
-
-export type ServiceRegisteredEvent = z.infer;
-
-/**
- * Service Unregistered Event Schema
- *
- * @example
- * {
- * "serviceName": "database",
- * "timestamp": 1706659200000
- * }
- */
-export const ServiceUnregisteredEventSchema = lazySchema(() => z.object({
- /**
- * Service name
- */
- serviceName: z.string().describe('Name of the unregistered service'),
-
- /**
- * Event timestamp (Unix milliseconds)
- */
- timestamp: z.number().int().describe('Unix timestamp in milliseconds'),
-}));
-
-export type ServiceUnregisteredEvent = z.infer;
-
-// ============================================================================
-// Hook Event Schemas
-// ============================================================================
-
-/**
- * Hook Registered Event Schema
- *
- * @example
- * {
- * "hookName": "data.beforeInsert",
- * "timestamp": 1706659200000,
- * "handlerCount": 3
- * }
- */
-export const HookRegisteredEventSchema = lazySchema(() => z.object({
- /**
- * Hook name
- */
- hookName: z.string().describe('Name of the hook'),
-
- /**
- * Event timestamp (Unix milliseconds)
- */
- timestamp: z.number().int().describe('Unix timestamp in milliseconds'),
-
- /**
- * Number of handlers registered for this hook
- */
- handlerCount: z.number().int().min(0).describe('Number of handlers registered for this hook'),
-}));
-
-export type HookRegisteredEvent = z.infer;
-
-/**
- * Hook Triggered Event Schema
- *
- * @example
- * {
- * "hookName": "data.beforeInsert",
- * "timestamp": 1706659200000,
- * "args": [{ "object": "customer", "data": {...} }],
- * "handlerCount": 3
- * }
- */
-export const HookTriggeredEventSchema = lazySchema(() => z.object({
- /**
- * Hook name
- */
- hookName: z.string().describe('Name of the hook'),
-
- /**
- * Event timestamp (Unix milliseconds)
- */
- timestamp: z.number().int().describe('Unix timestamp in milliseconds'),
-
- /**
- * Arguments passed to the hook
- */
- args: z.array(z.unknown()).describe('Arguments passed to the hook handlers'),
-
- /**
- * Number of handlers that will handle this event
- */
- handlerCount: z.number().int().min(0).optional().describe('Number of handlers that will handle this event'),
-}));
-
-export type HookTriggeredEvent = z.infer;
-
-// ============================================================================
-// Kernel Event Schemas
-// ============================================================================
-
-/**
- * Kernel Event Base Schema
- * Common fields for kernel events
- */
-export const KernelEventBaseSchema = lazySchema(() => z.object({
- /**
- * Event timestamp (Unix milliseconds)
- */
- timestamp: z.number().int().describe('Unix timestamp in milliseconds'),
-}));
-
-/**
- * Kernel Ready Event Schema
- *
- * @example
- * {
- * "timestamp": 1706659200000,
- * "duration": 5400,
- * "pluginCount": 12
- * }
- */
-export const KernelReadyEventSchema = lazySchema(() => KernelEventBaseSchema.extend({
- /**
- * Total initialization duration (milliseconds)
- */
- duration: z.number().min(0).optional().describe('Total initialization duration in milliseconds'),
-
- /**
- * Number of plugins initialized
- */
- pluginCount: z.number().int().min(0).optional().describe('Number of plugins initialized'),
-}));
-
-export type KernelReadyEvent = z.infer;
-
-/**
- * Kernel Shutdown Event Schema
- *
- * @example
- * {
- * "timestamp": 1706659200000,
- * "reason": "SIGTERM received"
- * }
- */
-export const KernelShutdownEventSchema = lazySchema(() => KernelEventBaseSchema.extend({
- /**
- * Shutdown reason (optional)
- */
- reason: z.string().optional().describe('Reason for kernel shutdown'),
-}));
-
-export type KernelShutdownEvent = z.infer;
-
-// ============================================================================
-// Event Type Registry
-// ============================================================================
-
-/**
- * Plugin Lifecycle Event Type Enum
- * All possible plugin lifecycle event types
- */
-export const PluginLifecycleEventType = z.enum([
- 'kernel:ready',
- 'kernel:bootstrapped',
- 'kernel:listening',
- 'kernel:shutdown',
- 'kernel:before-init',
- 'kernel:after-init',
- 'plugin:registered',
- 'plugin:before-init',
- 'plugin:init',
- 'plugin:after-init',
- 'plugin:before-start',
- 'plugin:started',
- 'plugin:after-start',
- 'plugin:before-destroy',
- 'plugin:destroyed',
- 'plugin:after-destroy',
- 'plugin:error',
- 'service:registered',
- 'service:unregistered',
- 'hook:registered',
- 'hook:triggered',
-]).describe('Plugin lifecycle event type');
-
-export type PluginLifecycleEventType = z.infer;
diff --git a/skills/objectstack-platform/references/_index.md b/skills/objectstack-platform/references/_index.md
index 60a957d474..bd86e85676 100644
--- a/skills/objectstack-platform/references/_index.md
+++ b/skills/objectstack-platform/references/_index.md
@@ -15,7 +15,6 @@ from `node_modules` — there is no local copy in the skill bundle.
- `node_modules/@objectstack/spec/src/kernel/manifest.zod.ts` — Structured permission grants requested by a plugin (ADR-0025 §3.2).
- `node_modules/@objectstack/spec/src/kernel/metadata-plugin.zod.ts` — Metadata Plugin Protocol
- `node_modules/@objectstack/spec/src/kernel/plugin-capability.zod.ts` — Plugin Capability Protocol
-- `node_modules/@objectstack/spec/src/kernel/plugin-lifecycle-events.zod.ts` — Plugin Lifecycle Events Protocol
- `node_modules/@objectstack/spec/src/kernel/plugin-loading.zod.ts` — Plugin Loading Protocol
- `node_modules/@objectstack/spec/src/kernel/plugin.zod.ts` — Shared Plugin Types
- `node_modules/@objectstack/spec/src/kernel/service-registry.zod.ts` — Service Registry Protocol