---
phase: 02-plugin-system
plan: 02
type: execute
wave: 2
depends_on: ["02-01"]
files_modified:
  - summercms/src/plugin/SummerPlugin.scala
  - summercms/src/plugin/PluginState.scala
  - summercms/src/plugin/PluginContext.scala
  - summercms/src/plugin/PluginRegistration.scala
  - summercms/src/plugin/DependencyResolver.scala
  - summercms/src/plugin/PluginManager.scala
autonomous: true

must_haves:
  truths:
    - "Plugins implement SummerPlugin trait with register/boot/shutdown hooks"
    - "Plugin dependencies resolve in topological order (dependencies boot first)"
    - "Circular dependencies are detected at discovery time with clear error"
    - "Plugin state tracks through lifecycle: Discovered -> Registered -> Booting -> Running"
    - "Boot failures disable the failed plugin and its dependents"
  artifacts:
    - path: "summercms/src/plugin/SummerPlugin.scala"
      provides: "Plugin lifecycle trait"
      contains: "trait SummerPlugin"
    - path: "summercms/src/plugin/PluginManager.scala"
      provides: "Manages plugin lifecycle"
      exports: ["PluginManager"]
    - path: "summercms/src/plugin/DependencyResolver.scala"
      provides: "Topological sort for dependency ordering"
      contains: "def resolve"
    - path: "summercms/src/plugin/PluginState.scala"
      provides: "Plugin state machine"
      contains: "enum PluginState"
  key_links:
    - from: "summercms/src/plugin/PluginManager.scala"
      to: "summercms/src/plugin/DependencyResolver.scala"
      via: "dependency resolution before boot"
      pattern: "DependencyResolver\\.resolve"
    - from: "summercms/src/plugin/PluginManager.scala"
      to: "summercms/src/plugin/SummerPlugin.scala"
      via: "calls lifecycle hooks"
      pattern: "plugin\\.(register|boot|shutdown)"
---

<objective>
Implement plugin lifecycle management with dependency resolution for SummerCMS.

Purpose: Plugins need to boot in correct dependency order with proper state tracking. This enables plugins to safely depend on and extend each other. The extension API (02-03) requires running plugins.

Output: SummerPlugin trait, PluginManager service, DependencyResolver, PluginState enum, PluginContext/PluginRegistration types
</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
@summercms/src/plugin/PluginId.scala
@summercms/src/plugin/PluginError.scala
@summercms/src/plugin/PluginManifest.scala
@summercms/src/plugin/PluginDiscovery.scala
</context>

<tasks>

<task type="auto">
  <name>Task 1: Create plugin lifecycle types</name>
  <files>
    summercms/src/plugin/PluginState.scala
    summercms/src/plugin/PluginContext.scala
    summercms/src/plugin/PluginRegistration.scala
    summercms/src/plugin/SummerPlugin.scala
  </files>
  <action>
**PluginState.scala:**
```scala
package plugin

/** Plugin lifecycle state machine */
enum PluginState:
  case Discovered                    // Found on filesystem, manifest parsed
  case Registered                    // register() completed successfully
  case Booting                       // boot() in progress
  case Running                       // boot() succeeded, plugin active
  case Failed(error: PluginError)    // boot() failed
  case Disabled                      // Manually disabled via config
  case ShuttingDown                  // shutdown() in progress
  case Stopped                       // shutdown() completed

  def isActive: Boolean = this == Running
  def canBoot: Boolean = this == Registered
  def canShutdown: Boolean = this == Running || this == Failed(null) // null check is loose, refine if needed
```

**PluginContext.scala:**
```scala
package plugin

import java.nio.file.Path

/** Context provided to plugins during registration and boot */
case class PluginContext(
  manifest: PluginManifest,
  directory: Path,
  config: Map[String, String] = Map.empty  // Plugin-specific config from application.conf
):
  def id: PluginId = manifest.id
  def vendor: String = manifest.vendor
  def name: String = manifest.name
  def version: String = manifest.version
```

**PluginRegistration.scala:**
```scala
package plugin

/** Data returned from plugin's register() method - declarative, no effects */
case class PluginRegistration(
  // Components provided by this plugin (Phase 3)
  components: List[ComponentDef] = List.empty,
  // Permissions defined by this plugin (Phase 6)
  permissions: List[PermissionDef] = List.empty,
  // Navigation items for admin backend (Phase 8)
  navigation: List[NavigationDef] = List.empty,
  // Settings pages (Phase 8)
  settings: List[SettingDef] = List.empty,
  // Event subscriptions (Phase 2 extension API)
  events: List[EventSubscription] = List.empty,
  // Extensions to other plugins (Phase 2 extension API)
  extensions: List[ExtensionDef] = List.empty,
  // Form field definitions (for YAML-driven forms)
  fields: List[FieldDef] = List.empty
)

object PluginRegistration:
  val empty: PluginRegistration = PluginRegistration()

// Placeholder types - will be fleshed out in later phases
case class ComponentDef(name: String, className: String)
case class PermissionDef(code: String, label: String)
case class NavigationDef(id: String, label: String, url: String, icon: String = "", order: Int = 0)
case class SettingDef(key: String, label: String, icon: String = "")
case class EventSubscription(eventType: String, handler: String)
case class ExtensionDef(target: String, extensionClass: String)
case class FieldDef(name: String, fieldType: String)
```

**SummerPlugin.scala:**
```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.
   *
   * PluginEnv is a placeholder that will be expanded in 02-03 to include
   * EventService and ExtensionRegistry when the extension API is implemented.
   */
  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.
 * Initially just PluginContext; extended in 02-03 to include EventService & ExtensionRegistry.
 */
type PluginEnv = PluginContext
```
  </action>
  <verify>Run `./mill summercms.compile` - all types compile without errors</verify>
  <done>PluginState enum with all lifecycle states, PluginContext, PluginRegistration with placeholder defs including FieldDef, SummerPlugin trait</done>
</task>

<task type="auto">
  <name>Task 2: Create dependency resolver with topological sort</name>
  <files>summercms/src/plugin/DependencyResolver.scala</files>
  <action>
**DependencyResolver.scala:**
```scala
package plugin

import scala.annotation.tailrec

/** Resolves plugin load order using topological sort (Kahn's algorithm) */
object DependencyResolver:

  /**
   * Resolve plugins into dependency order (dependencies first).
   * Returns Left(CyclicDependency) if cycle detected.
   * Returns Right(orderedPlugins) on success.
   */
  def resolve(
    plugins: Map[PluginId, PluginManifest]
  ): Either[PluginError, List[PluginId]] =
    // Build graph: plugin -> plugins that depend on it
    val dependents = buildDependentsGraph(plugins)

    // Calculate in-degrees (number of dependencies each plugin has)
    val inDegree = plugins.map { case (id, manifest) =>
      id -> manifest.dependencies.keys.count { depStr =>
        PluginId.parse(depStr).toOption.exists(plugins.contains)
      }
    }

    // Find plugins with no dependencies (in-degree = 0)
    val roots = inDegree.filter(_._2 == 0).keys.toList
      .sortBy(id => -plugins(id).priority) // Higher priority first

    // Run Kahn's algorithm
    process(roots, List.empty, inDegree, dependents, plugins)

  private def buildDependentsGraph(
    plugins: Map[PluginId, PluginManifest]
  ): Map[PluginId, Set[PluginId]] =
    plugins.values.foldLeft(Map.empty[PluginId, Set[PluginId]]) { (acc, manifest) =>
      manifest.dependencies.keys.foldLeft(acc) { (a, depStr) =>
        PluginId.parse(depStr).toOption match
          case Some(depId) if plugins.contains(depId) =>
            // depId -> manifest.id means "manifest.id depends on depId"
            a.updated(depId, a.getOrElse(depId, Set.empty) + manifest.id)
          case _ => a // Skip invalid or missing dependencies
      }
    }

  @tailrec
  private def process(
    queue: List[PluginId],
    result: List[PluginId],
    degrees: Map[PluginId, Int],
    dependents: Map[PluginId, Set[PluginId]],
    plugins: Map[PluginId, PluginManifest]
  ): Either[PluginError, List[PluginId]] =
    queue match
      case Nil =>
        // If we processed all plugins, success; otherwise cycle detected
        if result.size == plugins.size then Right(result.reverse)
        else
          val remaining = plugins.keySet -- result.toSet
          Left(PluginError.CyclicDependency(remaining))

      case head :: tail =>
        // Process this plugin, decrement in-degrees of its dependents
        val deps = dependents.getOrElse(head, Set.empty)
        val newDegrees = deps.foldLeft(degrees) { (d, dep) =>
          d.updated(dep, d(dep) - 1)
        }

        // Find newly ready plugins (in-degree became 0)
        val newReady = deps
          .filter(d => newDegrees(d) == 0)
          .toList
          .sortBy(id => -plugins(id).priority) // Higher priority first

        process(tail ++ newReady, head :: result, newDegrees, dependents, plugins)

  /**
   * Validate that all dependencies exist and meet version constraints.
   * Call this after resolve() to check version compatibility.
   */
  def validateDependencies(
    plugins: Map[PluginId, PluginManifest]
  ): List[PluginError] =
    plugins.values.flatMap { manifest =>
      manifest.dependencies.flatMap { case (depStr, constraint) =>
        PluginId.parse(depStr).toOption match
          case None =>
            Some(PluginError.InvalidPluginId(depStr, "Invalid format in dependency"))
          case Some(depId) =>
            plugins.get(depId) match
              case None =>
                Some(PluginError.DependencyNotFound(manifest.id, depStr))
              case Some(depManifest) =>
                // TODO: Use semver-parser-scala to validate version constraint
                // For now, just check dependency exists
                None
      }
    }.toList
```
  </action>
  <verify>Run `./mill summercms.compile` - DependencyResolver compiles. Test mentally: A depends on B, B depends on C -> resolve returns [C, B, A]</verify>
  <done>DependencyResolver.resolve() returns topologically sorted plugin IDs, detects cycles</done>
</task>

<task type="auto">
  <name>Task 3: Create PluginManager service</name>
  <files>summercms/src/plugin/PluginManager.scala</files>
  <action>
**PluginManager.scala:**
```scala
package plugin

import zio.*
import java.nio.file.Path

/** Manages plugin lifecycle: discovery, registration, boot, shutdown */
trait PluginManager:
  /** Discover and register all plugins from directory */
  def loadPlugins(pluginsDir: Path): IO[PluginError, Unit]

  /** Boot all registered plugins in dependency order */
  def bootAll: IO[PluginError, Unit]

  /** Shutdown all running plugins in reverse dependency order */
  def shutdownAll: UIO[Unit]

  /** Get current state of a plugin */
  def getState(id: PluginId): UIO[Option[PluginState]]

  /** Get all registered plugins */
  def listPlugins: UIO[List[(PluginId, PluginState)]]

  /** Get registration data for a plugin */
  def getRegistration(id: PluginId): UIO[Option[PluginRegistration]]

object PluginManager:
  /** Access the manager service */
  def loadPlugins(pluginsDir: Path): ZIO[PluginManager, PluginError, Unit] =
    ZIO.serviceWithZIO[PluginManager](_.loadPlugins(pluginsDir))

  def bootAll: ZIO[PluginManager, PluginError, Unit] =
    ZIO.serviceWithZIO[PluginManager](_.bootAll)

  def shutdownAll: ZIO[PluginManager, Nothing, Unit] =
    ZIO.serviceWithZIO[PluginManager](_.shutdownAll)

  def getState(id: PluginId): ZIO[PluginManager, Nothing, Option[PluginState]] =
    ZIO.serviceWithZIO[PluginManager](_.getState(id))

  def listPlugins: ZIO[PluginManager, Nothing, List[(PluginId, PluginState)]] =
    ZIO.serviceWithZIO[PluginManager](_.listPlugins)

  /** Live implementation */
  val live: ZLayer[PluginDiscovery, Nothing, PluginManager] =
    ZLayer.fromZIO {
      for
        discovery <- ZIO.service[PluginDiscovery]
        pluginsRef <- Ref.make(Map.empty[PluginId, PluginInstance])
        bootOrderRef <- Ref.make(List.empty[PluginId])
      yield LivePluginManager(discovery, pluginsRef, bootOrderRef)
    }

/** Internal: plugin instance with state tracking */
private case class PluginInstance(
  discovered: DiscoveredPlugin,
  plugin: Option[SummerPlugin],        // None until loaded
  registration: Option[PluginRegistration],
  stateRef: Ref[PluginState]
)

private class LivePluginManager(
  discovery: PluginDiscovery,
  plugins: Ref[Map[PluginId, PluginInstance]],
  bootOrder: Ref[List[PluginId]]
) extends PluginManager:

  def loadPlugins(pluginsDir: Path): IO[PluginError, Unit] =
    for
      discovered <- discovery.discover(pluginsDir)
      // Filter enabled plugins only
      enabled = discovered.filter(_.manifest.enabled)
      // Build manifest map for dependency resolution
      manifestMap = enabled.map(d => d.id -> d.manifest).toMap
      // Validate dependencies exist
      errors = DependencyResolver.validateDependencies(manifestMap)
      _ <- ZIO.fail(errors.head).when(errors.nonEmpty)
      // Resolve boot order
      order <- ZIO.fromEither(DependencyResolver.resolve(manifestMap))
      // Create plugin instances
      instances <- ZIO.foreach(enabled) { d =>
        Ref.make(PluginState.Discovered).map { stateRef =>
          d.id -> PluginInstance(d, None, None, stateRef)
        }
      }
      _ <- plugins.set(instances.toMap)
      _ <- bootOrder.set(order)
      // Register all plugins (synchronous phase)
      _ <- ZIO.foreachDiscard(order)(registerPlugin)
    yield ()

  private def registerPlugin(id: PluginId): IO[PluginError, Unit] =
    for
      instance <- plugins.get.map(_.get(id)).someOrFail(
        PluginError.DependencyNotFound(id, id.fullId)
      )
      // TODO: Load actual plugin class from directory
      // For now, plugins are registered via code - manual registration
      // In future: ClassLoader/reflection or compile-time wiring
      ctx = PluginContext(instance.discovered.manifest, instance.discovered.directory)
      // Mark as registered (actual plugin loading deferred)
      _ <- instance.stateRef.set(PluginState.Registered)
    yield ()

  def bootAll: IO[PluginError, Unit] =
    for
      order <- bootOrder.get
      _ <- ZIO.foreachDiscard(order)(bootPlugin)
    yield ()

  private def bootPlugin(id: PluginId): IO[PluginError, Unit] =
    for
      map <- plugins.get
      instance <- ZIO.fromOption(map.get(id)).orElseFail(
        PluginError.DependencyNotFound(id, id.fullId)
      )
      state <- instance.stateRef.get
      _ <- ZIO.when(state.canBoot) {
        for
          _ <- instance.stateRef.set(PluginState.Booting)
          // Boot the plugin if it exists
          result <- instance.plugin match
            case Some(p) =>
              val ctx = PluginContext(instance.discovered.manifest, instance.discovered.directory)
              p.boot.provideEnvironment(ZEnvironment(ctx)).either
            case None =>
              // No plugin class loaded yet - just mark as running
              ZIO.succeed(Right(()))
          _ <- result match
            case Right(_) => instance.stateRef.set(PluginState.Running)
            case Left(err) =>
              instance.stateRef.set(PluginState.Failed(err)) *>
              disableDependents(id)
        yield ()
      }
    yield ()

  private def disableDependents(failedId: PluginId): UIO[Unit] =
    for
      map <- plugins.get
      // Find plugins that depend on failedId
      dependents = map.filter { case (_, inst) =>
        inst.discovered.manifest.dependencies.keys.exists { depStr =>
          PluginId.parse(depStr).toOption.contains(failedId)
        }
      }
      _ <- ZIO.foreachDiscard(dependents.values) { inst =>
        inst.stateRef.set(PluginState.Disabled)
      }
    yield ()

  def shutdownAll: UIO[Unit] =
    for
      order <- bootOrder.get
      map <- plugins.get
      // Shutdown in reverse order
      _ <- ZIO.foreachDiscard(order.reverse) { id =>
        map.get(id) match
          case Some(inst) =>
            for
              state <- inst.stateRef.get
              _ <- ZIO.when(state.isActive) {
                inst.stateRef.set(PluginState.ShuttingDown) *>
                inst.plugin.fold(ZIO.unit)(_.shutdown) *>
                inst.stateRef.set(PluginState.Stopped)
              }
            yield ()
          case None => ZIO.unit
      }
    yield ()

  def getState(id: PluginId): UIO[Option[PluginState]] =
    plugins.get.flatMap { map =>
      map.get(id).fold(ZIO.none)(_.stateRef.get.map(Some(_)))
    }

  def listPlugins: UIO[List[(PluginId, PluginState)]] =
    plugins.get.flatMap { map =>
      ZIO.foreach(map.toList) { case (id, inst) =>
        inst.stateRef.get.map(id -> _)
      }
    }

  def getRegistration(id: PluginId): UIO[Option[PluginRegistration]] =
    plugins.get.map(_.get(id).flatMap(_.registration))
```
  </action>
  <verify>Run `./mill summercms.compile` - PluginManager compiles. The service can load, register, boot, and shutdown plugins.</verify>
  <done>PluginManager.live provides working service with full lifecycle management</done>
</task>

</tasks>

<verification>
1. `./mill summercms.compile` succeeds with all lifecycle types
2. SummerPlugin trait has register/boot/shutdown methods
3. PluginState enum covers full lifecycle
4. DependencyResolver.resolve returns topologically sorted IDs
5. PluginManager.live boots plugins in correct dependency order
</verification>

<success_criteria>
- SummerPlugin trait is implementable by plugins
- DependencyResolver detects cycles (returns Left(CyclicDependency))
- DependencyResolver returns dependency-first order (A depends on B -> [B, A])
- PluginManager tracks state through Discovered -> Registered -> Running
- Boot failures disable dependent plugins
</success_criteria>

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