68136e77d7
First slice of the plan-generation pipeline epic. A pure-Kotlin PlanLinter runs over the compiled freestyle ExecutionPlan (WorkflowGraph) before it is locked, complementing ExecutionPlanCompiler (which throws on unreachable/unknown-kind/bad-edge) with the checks it does NOT make: - HARD unproduced_need — a stage needs an artifact no stage produces. Real gap: only the TOML loader checked this; the freestyle compiler did not, so such plans compiled and failed at runtime. - HARD trap_state — a stage with no path to the terminal (inescapable loop / dead end). Uses terminal-reachability, NOT "any cycle", so legitimate verdict-gated review loops pass. - SOFT stage_count / fan_out / empty_brief / duplicate_brief — scored, never blocking. FreestyleDriver lints after compile and records PlanLintCompletedEvent (pure function of the recorded plan → replay-safe, no env observation in v1); a hard failure rejects the plan (ExecutionPlanRejectedEvent source="lint") before the operator is asked or the plan locks, so a broken plan never executes. Pays off now for single-plan freestyle; becomes the per-candidate filter when multi-candidate generation (Slice 2) lands. Deferred to later slices: file-path grounding + symbol resolution (needs an index), token-budget/ADR-bypass checks, and a dedicated :core:planning module.