diff --git a/docs/development/current/main/10-Now.md b/docs/development/current/main/10-Now.md
index f4008b1d..f1060acb 100644
--- a/docs/development/current/main/10-Now.md
+++ b/docs/development/current/main/10-Now.md
@@ -3,7 +3,7 @@
## Current Focus
- Phase: `docs/development/current/main/phases/phase-29ap/README.md`
-- Next: Phase 29ap P12 (planned; see `docs/development/current/main/phases/phase-29ap/README.md`)
+- Next: Phase 29ap P13 (planned; see `docs/development/current/main/phases/phase-29ap/README.md`)
## Gate (SSOT)
diff --git a/docs/development/current/main/30-Backlog.md b/docs/development/current/main/30-Backlog.md
index f4a14ace..9a24536e 100644
--- a/docs/development/current/main/30-Backlog.md
+++ b/docs/development/current/main/30-Backlog.md
@@ -5,7 +5,7 @@ Scope: 「次にやる候補」を短く列挙するメモ。入口は `docs/dev
## Active
-- Phase 29ap: `docs/development/current/main/phases/phase-29ap/README.md` (Next: P12 planned)
+- Phase 29ap: `docs/development/current/main/phases/phase-29ap/README.md` (Next: P13 planned)
- JoinIR regression gate SSOT: `docs/development/current/main/phases/phase-29ae/README.md`
- CorePlan hardening (docs-first): `docs/development/current/main/phases/phase-29al/README.md`
diff --git a/docs/development/current/main/design/coreplan-migration-done-criteria-ssot.md b/docs/development/current/main/design/coreplan-migration-done-criteria-ssot.md
index 314f5b9e..3497cd84 100644
--- a/docs/development/current/main/design/coreplan-migration-done-criteria-ssot.md
+++ b/docs/development/current/main/design/coreplan-migration-done-criteria-ssot.md
@@ -6,7 +6,7 @@ This document defines when the CorePlan migration can be considered done.
- Release adopt gate patterns execute via CorePlan without strict/dev flags.
- Gate smokes cover the release paths and remain green.
-- The legacy path remains only as a fallback for non-gate cases.
+- JoinIR legacy loop table is removed; routing is plan/composer only.
## Stage-3 Done (composer v0/v1)
@@ -17,5 +17,5 @@ This document defines when the CorePlan migration can be considered done.
## Still allowed to remain
- `DomainPlan` can remain as an intent layer for normalization/compatibility.
-- Legacy extractors and router branches remain until Phase 29ap removes them.
-
+- Legacy extractors may remain in the plan layer as long as they do not reintroduce
+ JoinIR-side routing tables.
diff --git a/docs/development/current/main/design/coreplan-migration-roadmap-ssot.md b/docs/development/current/main/design/coreplan-migration-roadmap-ssot.md
index a34d0075..b32b31fe 100644
--- a/docs/development/current/main/design/coreplan-migration-roadmap-ssot.md
+++ b/docs/development/current/main/design/coreplan-migration-roadmap-ssot.md
@@ -34,7 +34,7 @@ Related:
## 1.1 Current (active)
- Active phase: `docs/development/current/main/phases/phase-29ap/README.md`
-- Next step: Phase 29ap P12 (planned)
+- Next step: Phase 29ap P13 (planned)
## 2. すでに固めた SSOT(再発防止の土台)
diff --git a/docs/development/current/main/phases/phase-29ae/README.md b/docs/development/current/main/phases/phase-29ae/README.md
index c1c08395..bb5bfb56 100644
--- a/docs/development/current/main/phases/phase-29ae/README.md
+++ b/docs/development/current/main/phases/phase-29ae/README.md
@@ -27,6 +27,7 @@ Goal: JoinIR の最小回帰セットを SSOT として固定する。
- Pattern7 (release adopt, VM): `phase29ao_pattern7_release_adopt_vm`
- Pattern7: `phase29ab_pattern7_*`
- この pack が JoinIR 回帰の唯一の integration gate(phase143_* は対象外)
+- JoinIR routing is plan/composer SSOT only (legacy loop table removed in Phase 29ap P12)
- phase143_* は LoopBuilder 撤去 / plugin disable 固定 / LLVM exe 期待が古いので除外
- phase286_pattern9_* は plugins disabled 経路の mismatch があるため legacy pack 側で SKIP(phase29ae pack には含めない)
- shadow adopt tag(`[coreplan/shadow_adopt:*]`)は `filter_noise` で除去される
diff --git a/docs/development/current/main/phases/phase-29ap/README.md b/docs/development/current/main/phases/phase-29ap/README.md
index c1c7a948..b549fc32 100644
--- a/docs/development/current/main/phases/phase-29ap/README.md
+++ b/docs/development/current/main/phases/phase-29ap/README.md
@@ -128,6 +128,15 @@ Gate (SSOT):
- No new logs/tags in release.
- Gate stays green.
+## P12: Remove JoinIR legacy loop table ✅
+
+- Scope:
+ - Delete legacy loop table routing from JoinIR router.
+ - Remove legacy-only modules now that plan/composer is SSOT.
+- Guardrails:
+ - No change to logs or error strings.
+ - Gate stays green.
+
## Next (planned)
-- P12: Decide final removal of legacy nested loop entry (gate-first)
+- P13: Dead-code cleanup (warnings-only, no behavior change)
diff --git a/src/mir/builder/control_flow/joinir/patterns/mod.rs b/src/mir/builder/control_flow/joinir/patterns/mod.rs
index ad2e6fe4..7c2ad26c 100644
--- a/src/mir/builder/control_flow/joinir/patterns/mod.rs
+++ b/src/mir/builder/control_flow/joinir/patterns/mod.rs
@@ -5,10 +5,9 @@
//! - Pattern 3: Loop with If-Else PHI (pattern3_with_if_phi.rs)
//! - Pattern 4: Loop with Continue (migrated to plan routing in Phase 29ap P8)
//!
-//! Phase 194: Table-driven router for pattern dispatch
-//! - Router module provides table-driven pattern matching
-//! - Each pattern exports can_lower() and lower() functions
-//! - See router.rs for how to add new patterns
+//! Phase 29ap P12: Router delegates to plan/composer SSOT (legacy table removed)
+//! - Router only coordinates planner/composer adoption
+//! - Pattern-specific logic lives in plan layer
//!
//! Phase 193: AST Feature Extraction Modularization
//! - ast_feature_extractor.rs: Pure function module for analyzing loop AST
@@ -77,7 +76,6 @@ pub(in crate::mir::builder) mod exit_binding_validator; // Phase 222.5-C
pub(in crate::mir::builder) mod loop_scope_shape_builder;
pub(in crate::mir::builder) mod pattern1_minimal;
pub(in crate::mir::builder) mod pattern3_with_if_phi;
-pub(in crate::mir::builder) mod pattern6_nested_minimal; // Phase 188.3: 1-level nested loop (Pattern1 outer + Pattern1 inner)
pub(in crate::mir::builder) mod pattern8_scan_bool_predicate; // Phase 259 P0: boolean predicate scan (is_integer/is_valid)
pub(in crate::mir::builder) mod pattern_pipeline;
pub(in crate::mir::builder) mod router;
diff --git a/src/mir/builder/control_flow/joinir/patterns/pattern6_nested_minimal.rs b/src/mir/builder/control_flow/joinir/patterns/pattern6_nested_minimal.rs
deleted file mode 100644
index 9b82f410..00000000
--- a/src/mir/builder/control_flow/joinir/patterns/pattern6_nested_minimal.rs
+++ /dev/null
@@ -1,211 +0,0 @@
-//! Phase 188.3: Pattern6 NestedLoopMinimal - 1-level nested loop lowering
-//!
-//! Handles loops of the form:
-//! ```nyash
-//! loop(outer_cond) {
-//! loop(inner_cond) {
-//! // inner body
-//! }
-//! // outer body (after inner loop)
-//! }
-//! ```
-//!
-//! Requirements (Pattern1 for both):
-//! - Outer loop: no break, no continue (Pattern1)
-//! - Inner loop: no break, no continue (Pattern1)
-//! - Exactly 1 inner loop
-//! - max_loop_depth == 2
-//!
-//! Strategy:
-//! - Generate outer_step continuation (contains inner loop call)
-//! - Generate inner_step continuation (tail recursion)
-//! - Generate k_inner_exit (bridges to outer continuation)
-//! - Wire continuations
-
-use crate::ast::ASTNode;
-use crate::mir::builder::control_flow::joinir::patterns::LoopPatternContext;
-use crate::mir::builder::MirBuilder;
-use crate::mir::loop_pattern_detection::LoopPatternKind;
-use crate::mir::ValueId;
-
-/// Phase 188.3: Validate strict mode constraints
-///
-/// Requirements (most already checked in is_pattern6_lowerable):
-/// - Inner loop has no break (checked in routing.rs)
-/// - Inner loop has no continue (checked in routing.rs)
-/// - Outer loop has no break (checked in routing.rs)
-/// - Outer loop has no continue (checked in routing.rs)
-///
-/// Additional checks (Phase 188.4+):
-/// - Outer variable write-back detection (TODO: add to is_pattern6_lowerable)
-///
-/// This function serves as final safety check before lowering.
-/// If validation fails → Fail-Fast (not silent fallback).
-fn validate_strict_mode(_inner_ast: &ASTNode, _ctx: &LoopPatternContext) -> Result<(), String> {
- // Phase 188.3: Most validation already done in is_pattern6_lowerable()
- // This is a placeholder for future strict mode checks
-
- // Future Phase 188.4+ checks:
- // - Detect outer variable write-back in inner loop body
- // - Validate carrier compatibility
- // - Check for unsupported AST patterns
-
- Ok(())
-}
-
-/// Phase 188.3: Extract inner loop AST from outer loop body
-///
-/// Requirements:
-/// - Outer body must contain exactly 1 Loop node
-/// - 0 loops → Error (shouldn't happen after Pattern6 selection)
-/// - 2+ loops → Error (multiple inner loops not supported)
-///
-/// Returns reference to inner loop ASTNode
-fn extract_inner_loop_ast<'a>(ctx: &'a LoopPatternContext) -> Result<&'a ASTNode, String> {
- // Find all Loop nodes in outer body
- let mut inner_loop: Option<&ASTNode> = None;
- let mut loop_count = 0;
-
- for stmt in ctx.body.iter() {
- if matches!(stmt, ASTNode::Loop { .. }) {
- loop_count += 1;
- if inner_loop.is_none() {
- inner_loop = Some(stmt);
- }
- }
- }
-
- // Validate exactly 1 inner loop
- match loop_count {
- 0 => Err(
- "[Pattern6/extract] No inner loop found (should not happen after Pattern6 selection)"
- .to_string(),
- ),
- 1 => Ok(inner_loop.unwrap()), // Safe: loop_count == 1 guarantees Some
- _ => Err(format!(
- "[Pattern6/extract] Multiple inner loops ({}) not supported. \
- Phase 188.3 supports exactly 1 inner loop only.",
- loop_count
- )),
- }
-}
-
-/// Detect if this context can be lowered as Pattern6 (NestedLoopMinimal)
-///
-/// Pattern selection happens in choose_pattern_kind() (SSOT).
-/// This function just verifies ctx.pattern_kind matches.
-pub(crate) fn can_lower(_builder: &MirBuilder, ctx: &LoopPatternContext) -> bool {
- ctx.pattern_kind == LoopPatternKind::Pattern6NestedLoopMinimal
-}
-
-/// Lower Pattern6 (NestedLoopMinimal) to MIR
-///
-/// Phase 188.3 P1: Full implementation with JoinIR pipeline
-pub(crate) fn lower(
- builder: &mut MirBuilder,
- ctx: &LoopPatternContext,
-) -> Result