---
phase: 02-plugin-system
plan: 03
type: execute
wave: 3
depends_on: ["02-02"]
files_modified:
  - summercms/src/plugin/EventService.scala
  - summercms/src/plugin/ExtensionRegistry.scala
  - summercms/src/plugin/SummerEvent.scala
  - summercms/src/plugin/package.scala
  - summercms/src/plugin/SummerPlugin.scala
  - summercms/src/Main.scala
autonomous: true

must_haves:
  truths:
    - "Plugins can publish events to a central hub"
    - "Plugins can subscribe to events from other plugins"
    - "Plugins can register type-safe extensions for other plugins"
    - "Extensions are discoverable by target plugin at boot time"
    - "Plugin system integrates with Main.scala and boots on application start"
  artifacts:
    - path: "summercms/src/plugin/EventService.scala"
      provides: "ZIO Hub-based pub/sub event system"
      exports: ["EventService"]
    - path: "summercms/src/plugin/ExtensionRegistry.scala"
      provides: "Type-safe extension registration"
      exports: ["ExtensionRegistry"]
    - path: "summercms/src/plugin/SummerEvent.scala"
      provides: "Base event trait and lifecycle events"
      contains: "sealed trait SummerEvent"
    - path: "summercms/src/plugin/package.scala"
      provides: "Plugin package exports and type aliases"
      contains: "type PluginLayer"
  key_links:
    - from: "summercms/src/Main.scala"
      to: "summercms/src/plugin/PluginManager.scala"
      via: "loads and boots plugins on startup"
      pattern: "PluginManager\\.(loadPlugins|bootAll)"
    - from: "summercms/src/plugin/ExtensionRegistry.scala"
      to: "summercms/src/plugin/PluginManager.scala"
      via: "extensions registered during plugin boot"
      pattern: "ExtensionRegistry\\.register"
---

<objective>
Implement the extension API with event system and type-safe extension registry for SummerCMS.

Purpose: Plugins need to communicate loosely via events and extend each other type-safely. This completes the plugin system, making it ready for components (Phase 3) and themes (Phase 4) to build upon.

Output: EventService (Hub-based), ExtensionRegistry, SummerEvent trait, plugin layer integration with Main.scala
</objective>

<execution_context>
@/home/jin/.claude/get-shit-done/workflows/execute-plan.md
@/home/jin/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/02-plugin-system/02-RESEARCH.md
@.planning/phases/02-plugin-system/02-CONTEXT.md
@.planning/phases/02-plugin-system/02-01-SUMMARY.md
@.planning/phases/02-plugin-system/02-02-SUMMARY.md
@summercms/src/plugin/SummerPlugin.scala
@summercms/src/plugin/PluginManager.scala
@summercms/src/Main.scala
</context>

<tasks>

<task type="auto">
  <name>Task 1: Create event system with ZIO Hub</name>
  <files>
    summercms/src/plugin/SummerEvent.scala
    summercms/src/plugin/EventService.scala
  </files>
  <action>
**SummerEvent.scala:**
```scala
package plugin

import java.time.Instant

/** Base trait for all SummerCMS events */
sealed trait SummerEvent:
  def timestamp: Instant = Instant.now()

object SummerEvent:
  // ===== Plugin Lifecycle Events =====

  /** Emitted when a plugin completes boot successfully */
  case class PluginBooted(id: PluginId) extends SummerEvent

  /** Emitted when a plugin fails to boot */
  case class PluginBootFailed(id: PluginId, error: PluginError) extends SummerEvent

  /** Emitted when a plugin begins shutdown */
  case class PluginShuttingDown(id: PluginId) extends SummerEvent

  /** Emitted when a plugin completes shutdown */
  case class PluginStopped(id: PluginId) extends SummerEvent

  // ===== Domain Events (plugins define their own) =====

  /** Marker trait for domain-specific events from plugins */
  trait DomainEvent extends SummerEvent

  /** Example: User events (will be used by User plugin in Phase 10) */
  trait UserEvent extends DomainEvent
  case class UserCreated(userId: Long, email: String) extends UserEvent
  case class UserLoggedIn(userId: Long) extends UserEvent
  case class UserLoggedOut(userId: Long) extends UserEvent

  /** Example: Content events (will be used by CMS in Phase 9) */
  trait ContentEvent extends DomainEvent
  case class PageCreated(pageId: Long, slug: String) extends ContentEvent
  case class PagePublished(pageId: Long) extends ContentEvent
```

**EventService.scala:**
```scala
package plugin

import zio.*
import scala.reflect.ClassTag

/** Pub/sub event service using ZIO Hub */
trait EventService:
  /** Publish an event to all subscribers */
  def publish[E <: SummerEvent](event: E): UIO[Boolean]

  /** Subscribe to all events - returns Dequeue in a Scope */
  def subscribe: ZIO[Scope, Nothing, Dequeue[SummerEvent]]

  /** Subscribe to events of a specific type */
  def subscribeFiltered[E <: SummerEvent: ClassTag]: ZIO[Scope, Nothing, Dequeue[E]]

  /** Subscribe with a handler function that processes events */
  def subscribeWith[E <: SummerEvent: ClassTag](
    handler: E => UIO[Unit]
  ): ZIO[Scope, Nothing, Fiber.Runtime[Nothing, Nothing]]

object EventService:
  /** Publish an event */
  def publish[E <: SummerEvent](event: E): ZIO[EventService, Nothing, Boolean] =
    ZIO.serviceWithZIO[EventService](_.publish(event))

  /** Subscribe to all events */
  def subscribe: ZIO[EventService & Scope, Nothing, Dequeue[SummerEvent]] =
    ZIO.serviceWithZIO[EventService](_.subscribe)

  /** Subscribe to specific event type */
  def subscribeFiltered[E <: SummerEvent: ClassTag]: ZIO[EventService & Scope, Nothing, Dequeue[E]] =
    ZIO.serviceWithZIO[EventService](_.subscribeFiltered[E])

  /** Live implementation with bounded Hub */
  val live: ULayer[EventService] = ZLayer.scoped {
    Hub.bounded[SummerEvent](256).map { hub =>
      new EventService:
        def publish[E <: SummerEvent](event: E): UIO[Boolean] =
          hub.publish(event)

        def subscribe: ZIO[Scope, Nothing, Dequeue[SummerEvent]] =
          hub.subscribe

        def subscribeFiltered[E <: SummerEvent: ClassTag]: ZIO[Scope, Nothing, Dequeue[E]] =
          hub.subscribe.map { dequeue =>
            new Dequeue[E]:
              def take: UIO[E] =
                dequeue.take.flatMap {
                  case e: E => ZIO.succeed(e)
                  case _ => take // Skip non-matching events
                }

              def takeAll: UIO[Chunk[E]] =
                dequeue.takeAll.map(_.collect { case e: E => e })

              def takeUpTo(max: Int): UIO[Chunk[E]] =
                dequeue.takeUpTo(max).map(_.collect { case e: E => e })

              def poll: UIO[Option[E]] =
                dequeue.poll.map(_.collect { case e: E => e })

              def size: UIO[Int] = dequeue.size
              def capacity: Int = dequeue.capacity
              def isShutdown: UIO[Boolean] = dequeue.isShutdown
              def shutdown: UIO[Unit] = dequeue.shutdown
              def awaitShutdown: UIO[Unit] = dequeue.awaitShutdown
          }

        def subscribeWith[E <: SummerEvent: ClassTag](
          handler: E => UIO[Unit]
        ): ZIO[Scope, Nothing, Fiber.Runtime[Nothing, Nothing]] =
          subscribeFiltered[E].flatMap { queue =>
            queue.take.flatMap(handler).forever.fork
          }
    }
  }
```
  </action>
  <verify>Run `./mill summercms.compile` - event system compiles. EventService.live provides a Hub-based pub/sub system.</verify>
  <done>SummerEvent trait with lifecycle and domain events, EventService with publish/subscribe via ZIO Hub</done>
</task>

<task type="auto">
  <name>Task 2: Create extension registry</name>
  <files>summercms/src/plugin/ExtensionRegistry.scala</files>
  <action>
**ExtensionRegistry.scala:**
```scala
package plugin

import zio.*
import scala.reflect.ClassTag

/**
 * Registry for type-safe plugin extensions.
 *
 * Plugins can register extensions for other plugins:
 * - SEO plugin registers BlogExtension for Blog plugin
 * - Blog plugin queries registry for all BlogExtension implementations
 *
 * Extensions are keyed by trait type (ClassTag) and registered by plugin ID.
 */
trait ExtensionRegistry:
  /** Register an extension from a plugin */
  def register[E: ClassTag](pluginId: PluginId, extension: E): UIO[Unit]

  /** Get all extensions of a type */
  def getExtensions[E: ClassTag]: UIO[List[(PluginId, E)]]

  /** Get extension from a specific plugin */
  def getExtension[E: ClassTag](pluginId: PluginId): UIO[Option[E]]

  /** Check if any extensions exist for a type */
  def hasExtensions[E: ClassTag]: UIO[Boolean]

  /** Remove all extensions from a plugin (used during unload/reload) */
  def removePlugin(pluginId: PluginId): UIO[Unit]

object ExtensionRegistry:
  /** Register an extension */
  def register[E: ClassTag](pluginId: PluginId, extension: E): ZIO[ExtensionRegistry, Nothing, Unit] =
    ZIO.serviceWithZIO[ExtensionRegistry](_.register(pluginId, extension))

  /** Get all extensions of a type */
  def getExtensions[E: ClassTag]: ZIO[ExtensionRegistry, Nothing, List[(PluginId, E)]] =
    ZIO.serviceWithZIO[ExtensionRegistry](_.getExtensions[E])

  /** Get extension from a specific plugin */
  def getExtension[E: ClassTag](pluginId: PluginId): ZIO[ExtensionRegistry, Nothing, Option[E]] =
    ZIO.serviceWithZIO[ExtensionRegistry](_.getExtension[E](pluginId))

  /** Live implementation using Ref */
  val live: ULayer[ExtensionRegistry] = ZLayer.fromZIO {
    // Map from extension type key -> Map from plugin ID -> extension instance
    Ref.make(Map.empty[String, Map[PluginId, Any]]).map { registry =>
      new ExtensionRegistry:
        private def typeKey[E: ClassTag]: String =
          summon[ClassTag[E]].runtimeClass.getName

        def register[E: ClassTag](pluginId: PluginId, extension: E): UIO[Unit] =
          registry.update { map =>
            val key = typeKey[E]
            val existing = map.getOrElse(key, Map.empty)
            map.updated(key, existing + (pluginId -> extension))
          }

        def getExtensions[E: ClassTag]: UIO[List[(PluginId, E)]] =
          registry.get.map { map =>
            map.getOrElse(typeKey[E], Map.empty)
              .toList
              .map { case (id, ext) => (id, ext.asInstanceOf[E]) }
          }

        def getExtension[E: ClassTag](pluginId: PluginId): UIO[Option[E]] =
          registry.get.map { map =>
            map.getOrElse(typeKey[E], Map.empty)
              .get(pluginId)
              .map(_.asInstanceOf[E])
          }

        def hasExtensions[E: ClassTag]: UIO[Boolean] =
          registry.get.map { map =>
            map.getOrElse(typeKey[E], Map.empty).nonEmpty
          }

        def removePlugin(pluginId: PluginId): UIO[Unit] =
          registry.update { map =>
            map.view.mapValues(_ - pluginId).toMap
          }
    }
  }

/**
 * Example extension trait that plugins can define.
 * Other plugins implement this to extend the defining plugin.
 *
 * Example: Blog plugin defines BlogExtension, SEO plugin implements it.
 */
trait BlogExtension:
  /** Additional fields to add to post forms */
  def extendPostFields: List[FieldDef]

  /** Transform post data before save */
  def beforeSave(data: Map[String, Any]): Map[String, Any] = data

  /** Validate post data */
  def validate(data: Map[String, Any]): Either[String, Unit] = Right(())

// FieldDef is defined in PluginRegistration.scala
```
  </action>
  <verify>Run `./mill summercms.compile` - ExtensionRegistry compiles. It provides type-safe extension registration keyed by ClassTag.</verify>
  <done>ExtensionRegistry.live provides type-safe extension storage with register/get/remove operations</done>
</task>

<task type="auto">
  <name>Task 3: Create plugin package exports, update PluginEnv, and integrate with Main</name>
  <files>
    summercms/src/plugin/package.scala
    summercms/src/plugin/SummerPlugin.scala
    summercms/src/Main.scala
  </files>
  <action>
**package.scala:**
```scala
package object plugin:
  /** Combined layer for all plugin services */
  type PluginServices = PluginManager & PluginDiscovery & EventService & ExtensionRegistry

  /** Create all plugin service layers */
  val pluginLayer: zio.ZLayer[Any, Nothing, PluginServices] =
    PluginDiscovery.live >>>
      (PluginManager.live ++ EventService.live ++ ExtensionRegistry.live)

  /** Environment provided to plugins during boot - includes all services */
  type PluginBootEnv = PluginContext & EventService & ExtensionRegistry
```

**Update SummerPlugin.scala** to use the expanded PluginEnv:

Read the existing file from 02-02 and update ONLY the PluginEnv type alias at the bottom:

```scala
package plugin

import zio.*

/** Base trait for all SummerCMS plugins */
trait SummerPlugin:
  /** Plugin identifier - must match manifest */
  def id: PluginId

  /**
   * Synchronous registration phase - pure data, no effects.
   * Called during discovery to collect plugin declarations.
   */
  def register(ctx: PluginContext): PluginRegistration = PluginRegistration.empty

  /**
   * Async boot phase - can perform effects.
   * Called after all dependencies have booted.
   * Use for: database setup, event subscriptions, service initialization.
   */
  def boot: ZIO[PluginEnv, PluginError, Unit] = ZIO.unit

  /**
   * Async shutdown phase - cleanup resources.
   * Called in reverse dependency order during application shutdown.
   */
  def shutdown: ZIO[Any, Nothing, Unit] = ZIO.unit

/**
 * Environment available to plugins during boot.
 * Includes context, events, and extension registry for full plugin capabilities.
 */
type PluginEnv = PluginContext & EventService & ExtensionRegistry
```

**Main.scala** (complete replacement):
```scala
import zio.*
import zio.http.*
import zio.config.typesafe.TypesafeConfigProvider

import api.Routes
import _root_.config.{AppConfig as SummerConfig}
import db.QuillContext
import plugin.{PluginManager, PluginDiscovery, pluginLayer}

object Main extends ZIOAppDefault {

  private val banner: String =
    """
      |
      |             |   .
      |      `.  *  |     .'
      |        `. ._|_* .'  .
      |      . * .'   `.  *
      |   -------|     |-------
      |      .  *`.___.' *  .
      |         .'  |* `.  *
      |       .' *  |  . `.
      |           . |
      |
      |     S U M M E R C M S
      |""".stripMargin

  override val bootstrap: ZLayer[ZIOAppArgs, Any, Any] =
    Runtime.setConfigProvider(TypesafeConfigProvider.fromResourcePath())

  private val startupLogic: ZIO[PluginManager & PluginDiscovery, Any, Nothing] =
    for {
      cfg <- ZIO.config[SummerConfig](SummerConfig.config)
      _   <- Console.printLine(banner)
      _   <- Console.printLine(s"Starting on port ${cfg.server.port}...")
      _   <- Console.printLine("")
      // Initialize plugin system
      _   <- Console.printLine("Loading plugins...")
      _   <- PluginManager.loadPlugins(PluginDiscovery.defaultPluginsDir)
        .catchAll(e => Console.printLine(s"Plugin loading warning: ${e.message}").as(()))
      _   <- PluginManager.bootAll
        .catchAll(e => Console.printLine(s"Plugin boot warning: ${e.message}").as(()))
      plugins <- PluginManager.listPlugins
      _   <- Console.printLine(s"Loaded ${plugins.count(_._2.isActive)} plugin(s)")
      _   <- Console.printLine("")
      // Note: Migrations are NOT auto-run. Use CLI to run migrations (Phase 5).
      res <- Server.serve(Routes.routes).provide(
               Server.defaultWithPort(cfg.server.port),
               QuillContext.dataSourceLayer
             )
    } yield res

  override def run: ZIO[Any, Any, Any] =
    startupLogic.provide(pluginLayer)
}
```
  </action>
  <verify>
Run `./mill summercms.compile` - Main.scala compiles with plugin integration.
Run `./mill summercms.run` - Server starts, logs "Loading plugins..." and "Loaded 0 plugin(s)" (since no plugins exist yet).
  </verify>
  <done>Plugin package exports combined layer, PluginEnv updated to include EventService and ExtensionRegistry, Main.scala loads and boots plugins on startup</done>
</task>

</tasks>

<verification>
1. `./mill summercms.compile` succeeds
2. EventService.live provides Hub-based pub/sub
3. ExtensionRegistry.live provides type-safe extension storage
4. pluginLayer combines all services
5. Main.scala boots the application with plugin system initialization
6. Running `./mill summercms.run` shows "Loaded 0 plugin(s)" (no plugins installed)
</verification>

<success_criteria>
- Events can be published and subscribed to via EventService
- Extensions can be registered and retrieved by type via ExtensionRegistry
- Application startup loads and boots plugins automatically
- System works with zero plugins (graceful empty state)
- Plugin system is ready for Component System (Phase 3) to build upon
</success_criteria>

<output>
After completion, create `.planning/phases/02-plugin-system/02-03-SUMMARY.md`
</output>
