From cbc0242bbfe0a0f12ddbcddc24a41b0e0c00f008 Mon Sep 17 00:00:00 2001
From: "J. Kirby Ross" <james@flyingrobots.dev>
Date: Mon, 27 Oct 2025 01:35:36 -0700
Subject: [PATCH 01/10] feat(geom): add rmg-geom foundation (split PR 2)\n\n-
 types: Aabb, Transform\n- temporal: Tick, TemporalTransform, TemporalProxy\n-
 workspace: add rmg-geom member\n\nExtracted from branch
 echo/geom-broad-phase-docs-smoke.

---
 Cargo.toml                                    |   3 +-
 crates/rmg-geom/Cargo.toml                    |  15 ++
 crates/rmg-geom/src/lib.rs                    |  35 +++++
 crates/rmg-geom/src/temporal/mod.rs           |   8 ++
 .../rmg-geom/src/temporal/temporal_proxy.rs   |  32 +++++
 .../src/temporal/temporal_transform.rs        |  38 ++++++
 crates/rmg-geom/src/temporal/tick.rs          |  19 +++
 crates/rmg-geom/src/types/aabb.rs             | 128 ++++++++++++++++++
 crates/rmg-geom/src/types/mod.rs              |   6 +
 crates/rmg-geom/src/types/transform.rs        |  52 +++++++
 10 files changed, 335 insertions(+), 1 deletion(-)
 create mode 100644 crates/rmg-geom/Cargo.toml
 create mode 100644 crates/rmg-geom/src/lib.rs
 create mode 100644 crates/rmg-geom/src/temporal/mod.rs
 create mode 100644 crates/rmg-geom/src/temporal/temporal_proxy.rs
 create mode 100644 crates/rmg-geom/src/temporal/temporal_transform.rs
 create mode 100644 crates/rmg-geom/src/temporal/tick.rs
 create mode 100644 crates/rmg-geom/src/types/aabb.rs
 create mode 100644 crates/rmg-geom/src/types/mod.rs
 create mode 100644 crates/rmg-geom/src/types/transform.rs

diff --git a/Cargo.toml b/Cargo.toml
index 50e4e265..3b91a859 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -3,7 +3,8 @@ members = [
     "crates/rmg-core",
     "crates/rmg-ffi",
     "crates/rmg-wasm",
-    "crates/rmg-cli"
+    "crates/rmg-cli",
+    "crates/rmg-geom"
 ]
 resolver = "2"
 
diff --git a/crates/rmg-geom/Cargo.toml b/crates/rmg-geom/Cargo.toml
new file mode 100644
index 00000000..ace9b24f
--- /dev/null
+++ b/crates/rmg-geom/Cargo.toml
@@ -0,0 +1,15 @@
+[package]
+name = "rmg-geom"
+version = "0.1.0"
+edition = "2021"
+description = "Echo geometry primitives: AABB, transforms, temporal proxies, and broad-phase scaffolding"
+license = "Apache-2.0"
+repository = "https://example.invalid/echo"
+readme = "../../README.md"
+keywords = ["echo", "geometry", "aabb", "broad-phase"]
+categories = ["game-engines", "data-structures"]
+
+[dependencies]
+rmg-core = { path = "../rmg-core" }
+
+#[dev-dependencies]
diff --git a/crates/rmg-geom/src/lib.rs b/crates/rmg-geom/src/lib.rs
new file mode 100644
index 00000000..48b9a481
--- /dev/null
+++ b/crates/rmg-geom/src/lib.rs
@@ -0,0 +1,35 @@
+#![deny(
+    warnings,
+    clippy::all,
+    clippy::pedantic,
+    clippy::nursery,
+    clippy::cargo,
+    rust_2018_idioms,
+    missing_docs,
+    clippy::unwrap_used,
+    clippy::expect_used,
+    clippy::panic
+)]
+#![doc = r"Geometry primitives for Echo.
+
+This crate provides:
+- Axis-aligned bounding boxes (`Aabb`).
+- Rigid transforms (`Transform`).
+- Temporal utilities (`Tick`, `TemporalTransform`, `TemporalProxy`).
+- A minimal broad-phase trait and an AABB-based pairing structure.
+
+Design notes:
+- Deterministic: no ambient RNG; ordering of pair outputs is canonical.
+- Float32 throughout; operations favor clarity and reproducibility.
+- Rustdoc is treated as part of the contract; public items are documented.
+"]
+
+/// Foundational geometric types.
+pub mod types;
+/// Time-aware utilities for broad-phase and motion.
+pub mod temporal;
+/// Broad-phase interfaces and a simple AABB-based implementation.
+pub mod broad;
+
+pub use types::aabb::Aabb;
+pub use types::transform::Transform;
diff --git a/crates/rmg-geom/src/temporal/mod.rs b/crates/rmg-geom/src/temporal/mod.rs
new file mode 100644
index 00000000..fec2fe10
--- /dev/null
+++ b/crates/rmg-geom/src/temporal/mod.rs
@@ -0,0 +1,8 @@
+//! Temporal types and helpers used for tick-based motion and broad-phase.
+
+#[doc = "Discrete Chronos ticks (u64 newtype)."]
+pub mod tick;
+#[doc = "Start/end transforms over a tick and fat AABB computation."]
+pub mod temporal_transform;
+#[doc = "Broad-phase proxy carrying entity id, tick, and fat AABB."]
+pub mod temporal_proxy;
diff --git a/crates/rmg-geom/src/temporal/temporal_proxy.rs b/crates/rmg-geom/src/temporal/temporal_proxy.rs
new file mode 100644
index 00000000..93449680
--- /dev/null
+++ b/crates/rmg-geom/src/temporal/temporal_proxy.rs
@@ -0,0 +1,32 @@
+use crate::temporal::tick::Tick;
+use crate::types::aabb::Aabb;
+
+/// Broad-phase proxy summarizing an entity’s motion window for a tick.
+///
+/// Stores a conservative fat AABB and the owning `entity` identifier (opaque
+/// to the geometry layer). The proxy is suitable for insertion into a broad-
+/// phase accelerator.
+#[derive(Debug, Copy, Clone, PartialEq)]
+pub struct TemporalProxy {
+    entity: u64,
+    tick: Tick,
+    fat: Aabb,
+}
+
+impl TemporalProxy {
+    /// Creates a new proxy for `entity` at `tick` with precomputed `fat` AABB.
+    #[must_use]
+    pub const fn new(entity: u64, tick: Tick, fat: Aabb) -> Self { Self { entity, tick, fat } }
+
+    /// Opaque entity identifier owning this proxy.
+    #[must_use]
+    pub const fn entity(&self) -> u64 { self.entity }
+
+    /// Tick associated with the motion window.
+    #[must_use]
+    pub const fn tick(&self) -> Tick { self.tick }
+
+    /// Conservative fat AABB for this proxy.
+    #[must_use]
+    pub const fn fat(&self) -> Aabb { self.fat }
+}
diff --git a/crates/rmg-geom/src/temporal/temporal_transform.rs b/crates/rmg-geom/src/temporal/temporal_transform.rs
new file mode 100644
index 00000000..5eec5c88
--- /dev/null
+++ b/crates/rmg-geom/src/temporal/temporal_transform.rs
@@ -0,0 +1,38 @@
+use crate::types::{aabb::Aabb, transform::Transform};
+
+/// Transform at two adjacent ticks used to bound motion in the broad-phase.
+///
+/// - `start` corresponds to tick `n`.
+/// - `end` corresponds to tick `n+1`.
+#[derive(Debug, Copy, Clone, PartialEq)]
+pub struct TemporalTransform {
+    start: Transform,
+    end: Transform,
+}
+
+impl TemporalTransform {
+    /// Creates a new `TemporalTransform` from start and end transforms.
+    #[must_use]
+    pub const fn new(start: Transform, end: Transform) -> Self { Self { start, end } }
+
+    /// Returns the start transform.
+    #[must_use]
+    pub const fn start(&self) -> Transform { self.start }
+
+    /// Returns the end transform.
+    #[must_use]
+    pub const fn end(&self) -> Transform { self.end }
+
+    /// Computes a conservative fat AABB for a collider with local-space `shape` AABB.
+    ///
+    /// The fat box is defined as the union of the shape’s AABBs at the start and
+    /// end transforms. This is conservative for linear motion and suffices for
+    /// broad-phase pairing and CCD triggering.
+    #[must_use]
+    pub fn fat_aabb(&self, shape: &Aabb) -> Aabb {
+        let a0 = shape.transformed(&self.start.to_mat4());
+        let a1 = shape.transformed(&self.end.to_mat4());
+        a0.union(&a1)
+    }
+}
+
diff --git a/crates/rmg-geom/src/temporal/tick.rs b/crates/rmg-geom/src/temporal/tick.rs
new file mode 100644
index 00000000..ea144d3e
--- /dev/null
+++ b/crates/rmg-geom/src/temporal/tick.rs
@@ -0,0 +1,19 @@
+/// Discrete simulation tick in Chronos time.
+///
+/// The engine advances in integer ticks with a fixed `dt` per branch. This
+/// newtype ensures explicit tick passing across APIs.
+#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub struct Tick {
+    index: u64,
+}
+
+impl Tick {
+    /// Creates a new tick with the given index.
+    #[must_use]
+    pub const fn new(index: u64) -> Self { Self { index } }
+
+    /// Returns the tick index.
+    #[must_use]
+    pub const fn index(&self) -> u64 { self.index }
+}
+
diff --git a/crates/rmg-geom/src/types/aabb.rs b/crates/rmg-geom/src/types/aabb.rs
new file mode 100644
index 00000000..db12ab70
--- /dev/null
+++ b/crates/rmg-geom/src/types/aabb.rs
@@ -0,0 +1,128 @@
+use rmg_core::math::{Mat4, Vec3};
+
+/// Axis-aligned bounding box in world coordinates.
+///
+/// Invariants:
+/// - `min` components are less than or equal to `max` components.
+/// - Values are `f32` and represent meters in world space.
+#[derive(Debug, Copy, Clone, PartialEq)]
+pub struct Aabb {
+    min: Vec3,
+    max: Vec3,
+}
+
+impl Aabb {
+    /// Constructs an AABB from its minimum and maximum corners.
+    ///
+    /// # Panics
+    /// Panics if any component of `min` is greater than its counterpart in `max`.
+    #[must_use]
+    pub fn new(min: Vec3, max: Vec3) -> Self {
+        let a = min.to_array();
+        let b = max.to_array();
+        assert!(a[0] <= b[0] && a[1] <= b[1] && a[2] <= b[2], "invalid AABB: min > max");
+        Self { min, max }
+    }
+
+    /// Returns the minimum corner.
+    #[must_use]
+    pub fn min(&self) -> Vec3 { self.min }
+
+    /// Returns the maximum corner.
+    #[must_use]
+    pub fn max(&self) -> Vec3 { self.max }
+
+    /// Builds an AABB centered at `center` with half-extents `hx, hy, hz`.
+    #[must_use]
+    pub fn from_center_half_extents(center: Vec3, hx: f32, hy: f32, hz: f32) -> Self {
+        let he = Vec3::new(hx, hy, hz);
+        Self::new(center.sub(&he), center.add(&he))
+    }
+
+    /// Returns `true` if this AABB overlaps another (inclusive on faces).
+    #[must_use]
+    pub fn overlaps(&self, other: &Self) -> bool {
+        let a_min = self.min.to_array();
+        let a_max = self.max.to_array();
+        let b_min = other.min.to_array();
+        let b_max = other.max.to_array();
+        // Inclusive to treat touching faces as overlap for broad-phase pairing.
+        !(a_max[0] < b_min[0]
+            || a_min[0] > b_max[0]
+            || a_max[1] < b_min[1]
+            || a_min[1] > b_max[1]
+            || a_max[2] < b_min[2]
+            || a_min[2] > b_max[2])
+    }
+
+    /// Returns the union of two AABBs.
+    #[must_use]
+    pub fn union(&self, other: &Self) -> Self {
+        let a = self.min.to_array();
+        let b = self.max.to_array();
+        let c = other.min.to_array();
+        let d = other.max.to_array();
+        Self {
+            min: Vec3::new(a[0].min(c[0]), a[1].min(c[1]), a[2].min(c[2])),
+            max: Vec3::new(b[0].max(d[0]), b[1].max(d[1]), b[2].max(d[2])),
+        }
+    }
+
+    /// Inflates the box by a uniform margin `m` in all directions.
+    #[must_use]
+    pub fn inflate(&self, m: f32) -> Self {
+        let delta = Vec3::new(m, m, m);
+        Self { min: self.min.sub(&delta), max: self.max.add(&delta) }
+    }
+
+    /// Computes the AABB that bounds this box after transformation by `mat`.
+    ///
+    /// This evaluates the eight corners under the affine transform and builds a
+    /// new axis-aligned box containing them.
+    #[must_use]
+    pub fn transformed(&self, mat: &Mat4) -> Self {
+        let [min_x, min_y, min_z] = self.min.to_array();
+        let [max_x, max_y, max_z] = self.max.to_array();
+        let corners = [
+            Vec3::new(min_x, min_y, min_z),
+            Vec3::new(min_x, min_y, max_z),
+            Vec3::new(min_x, max_y, min_z),
+            Vec3::new(min_x, max_y, max_z),
+            Vec3::new(max_x, min_y, min_z),
+            Vec3::new(max_x, min_y, max_z),
+            Vec3::new(max_x, max_y, min_z),
+            Vec3::new(max_x, max_y, max_z),
+        ];
+        // Compute bounds without allocating an intermediate Vec to avoid needless collects.
+        let mut min = mat.transform_point(&corners[0]);
+        let mut max = min;
+        for c in &corners[1..] {
+            let p = mat.transform_point(c);
+            let pa = p.to_array();
+            let mi = min.to_array();
+            let ma = max.to_array();
+            min = Vec3::new(mi[0].min(pa[0]), mi[1].min(pa[1]), mi[2].min(pa[2]));
+            max = Vec3::new(ma[0].max(pa[0]), ma[1].max(pa[1]), ma[2].max(pa[2]));
+        }
+        Self { min, max }
+    }
+
+    /// Builds the minimal AABB that contains all `points`.
+    ///
+    /// # Panics
+    /// Panics if `points` is empty.
+    #[must_use]
+    pub fn from_points(points: &[Vec3]) -> Self {
+        assert!(!points.is_empty(), "from_points requires at least one point");
+        let mut min = points[0];
+        let mut max = points[0];
+        for p in &points[1..] {
+            let a = p.to_array();
+            let mi = min.to_array();
+            let ma = max.to_array();
+            min = Vec3::new(mi[0].min(a[0]), mi[1].min(a[1]), mi[2].min(a[2]));
+            max = Vec3::new(ma[0].max(a[0]), ma[1].max(a[1]), ma[2].max(a[2]));
+        }
+        Self { min, max }
+    }
+}
diff --git a/crates/rmg-geom/src/types/mod.rs b/crates/rmg-geom/src/types/mod.rs
new file mode 100644
index 00000000..62434cb8
--- /dev/null
+++ b/crates/rmg-geom/src/types/mod.rs
@@ -0,0 +1,6 @@
+//! Core geometry types used by the engine (transform, AABB).
+
+#[doc = "Axis-aligned bounding boxes (world space)."]
+pub mod aabb;
+#[doc = "Rigid transforms with non-uniform scale."]
+pub mod transform;
diff --git a/crates/rmg-geom/src/types/transform.rs b/crates/rmg-geom/src/types/transform.rs
new file mode 100644
index 00000000..a1b6ee54
--- /dev/null
+++ b/crates/rmg-geom/src/types/transform.rs
@@ -0,0 +1,52 @@
+use rmg_core::math::{Mat4, Quat, Vec3};
+
+/// Rigid transform with non-uniform scale used by broad-phase and shape placement.
+///
+/// Conventions:
+/// - `translation` in meters (world space).
+/// - `rotation` as a unit quaternion (normalized internally when converting).
+/// - `scale` is non-uniform and applied before rotation/translation.
+#[derive(Debug, Copy, Clone, PartialEq)]
+pub struct Transform {
+    translation: Vec3,
+    rotation: Quat,
+    scale: Vec3,
+}
+
+impl Transform {
+    /// Identity transform (no translation, no rotation, unit scale).
+    #[must_use]
+    pub fn identity() -> Self {
+        Self { translation: Vec3::ZERO, rotation: Quat::identity(), scale: Vec3::new(1.0, 1.0, 1.0) }
+    }
+
+    /// Creates a transform from components.
+    #[must_use]
+    pub const fn new(translation: Vec3, rotation: Quat, scale: Vec3) -> Self {
+        Self { translation, rotation, scale }
+    }
+
+    /// Translation component.
+    #[must_use]
+    pub fn translation(&self) -> Vec3 { self.translation }
+
+    /// Rotation component.
+    #[must_use]
+    pub fn rotation(&self) -> Quat { self.rotation }
+
+    /// Scale component.
+    #[must_use]
+    pub fn scale(&self) -> Vec3 { self.scale }
+
+    /// Returns the column-major `Mat4` corresponding to this transform.
+    #[must_use]
+    pub fn to_mat4(&self) -> Mat4 {
+        // M = T * R * S
+        let [sx, sy, sz] = self.scale.to_array();
+        let [tx, ty, tz] = self.translation.to_array();
+        let s = Mat4::scale(sx, sy, sz);
+        let r = Mat4::from_quat(&self.rotation);
+        let t = Mat4::translation(tx, ty, tz);
+        t.multiply(&r).multiply(&s)
+    }
+}

From 8832650a14abc576d391667a0f5eb2c709c88a39 Mon Sep 17 00:00:00 2001
From: "J. Kirby Ross" <james@flyingrobots.dev>
Date: Mon, 27 Oct 2025 01:49:17 -0700
Subject: [PATCH 02/10] docs(geom): add determinism/quantization notes to AABB
 + Transform + TemporalTransform + module docs\n\n- Clarify inclusive overlap,
 FMA avoidance, and future fat-proxy quantization policy\n- Aligns with
 broad-phase determinism contract

---
 .../rmg-geom/src/temporal/temporal_transform.rs |  8 +++++++-
 crates/rmg-geom/src/types/aabb.rs               | 17 ++++++++++++++++-
 crates/rmg-geom/src/types/mod.rs                |  8 ++++++++
 crates/rmg-geom/src/types/transform.rs          |  6 ++++++
 4 files changed, 37 insertions(+), 2 deletions(-)

diff --git a/crates/rmg-geom/src/temporal/temporal_transform.rs b/crates/rmg-geom/src/temporal/temporal_transform.rs
index 5eec5c88..4f250040 100644
--- a/crates/rmg-geom/src/temporal/temporal_transform.rs
+++ b/crates/rmg-geom/src/temporal/temporal_transform.rs
@@ -4,6 +4,13 @@ use crate::types::{aabb::Aabb, transform::Transform};
 ///
 /// - `start` corresponds to tick `n`.
 /// - `end` corresponds to tick `n+1`.
+///
+/// Determinism and plan:
+/// - `fat_aabb` below currently uses a union of the start/end AABBs. This is
+///   conservative for linear motion and sufficient for pairing/CCD triggers.
+/// - Future: introduce a quantized margin policy (based on velocity, `dt`, and
+///   shape scale) so that fat proxies are identical across peers/branches. The
+///   policy and quantization will be recorded in the graph/spec.
 #[derive(Debug, Copy, Clone, PartialEq)]
 pub struct TemporalTransform {
     start: Transform,
@@ -35,4 +42,3 @@ impl TemporalTransform {
         a0.union(&a1)
     }
 }
-
diff --git a/crates/rmg-geom/src/types/aabb.rs b/crates/rmg-geom/src/types/aabb.rs
index db12ab70..b7ed8ada 100644
--- a/crates/rmg-geom/src/types/aabb.rs
+++ b/crates/rmg-geom/src/types/aabb.rs
@@ -5,6 +5,16 @@ use rmg_core::math::{Mat4, Vec3};
 /// Invariants:
 /// - `min` components are less than or equal to `max` components.
 /// - Values are `f32` and represent meters in world space.
+///
+/// Determinism and policy:
+/// - Overlap checks are inclusive on faces (touching AABBs count as overlap)
+///   to make broad-phase pairing stable at contact boundaries.
+/// - Transforming a box evaluates its eight corners with affine math and then
+///   re-aligns to world axes; we intentionally avoid fused multiply-add to keep
+///   results stable across platforms.
+/// - Future: margin quantization for "fat" proxies will be expressed in the
+///   temporal layer so that identical inputs produce identical inflated bounds
+///   on every peer.
 #[derive(Debug, Copy, Clone, PartialEq)]
 pub struct Aabb {
     min: Vec3,
@@ -40,6 +50,9 @@ impl Aabb {
     }
 
     /// Returns `true` if this AABB overlaps another (inclusive on faces).
+    ///
+    /// Determinism: treating face-touch as overlap avoids pair creation
+    /// oscillating due to single-ULP differences in narrow-phase decisions.
     #[must_use]
     pub fn overlaps(&self, other: &Self) -> bool {
         let a_min = self.min.to_array();
@@ -78,7 +91,9 @@ impl Aabb {
     /// Computes the AABB that bounds this box after transformation by `mat`.
     ///
     /// This evaluates the eight corners under the affine transform and builds a
-    /// new axis-aligned box containing them.
+    /// new axis-aligned box containing them. Precision: uses `f32` operations
+    /// with separate multiply/add steps (no FMA) to preserve cross-platform
+    /// determinism consistent with `rmg-core` math.
     #[must_use]
     pub fn transformed(&self, mat: &Mat4) -> Self {
         let [min_x, min_y, min_z] = self.min.to_array();
diff --git a/crates/rmg-geom/src/types/mod.rs b/crates/rmg-geom/src/types/mod.rs
index 62434cb8..800b35af 100644
--- a/crates/rmg-geom/src/types/mod.rs
+++ b/crates/rmg-geom/src/types/mod.rs
@@ -1,4 +1,12 @@
 //! Core geometry types used by the engine (transform, AABB).
+//!
+//! Determinism notes:
+//! - Overlap semantics are inclusive on faces to avoid pair churn on contact
+//!   boundaries.
+//! - Affine math uses `f32` without fused multiply-add to preserve identical
+//!   results across platforms.
+//! - Temporal fattening/quantization policy will be documented at the
+//!   `temporal` layer so that identical inputs yield identical proxy bounds.
 
 #[doc = "Axis-aligned bounding boxes (world space)."]
 pub mod aabb;
diff --git a/crates/rmg-geom/src/types/transform.rs b/crates/rmg-geom/src/types/transform.rs
index a1b6ee54..5f907c37 100644
--- a/crates/rmg-geom/src/types/transform.rs
+++ b/crates/rmg-geom/src/types/transform.rs
@@ -6,6 +6,12 @@ use rmg_core::math::{Mat4, Quat, Vec3};
 /// - `translation` in meters (world space).
 /// - `rotation` as a unit quaternion (normalized internally when converting).
 /// - `scale` is non-uniform and applied before rotation/translation.
+///
+/// Determinism:
+/// - `to_mat4` constructs `M = T * R * S` with `f32` ops; no FMA to keep
+///   results stable across CPUs/targets. Negative scales are supported but may
+///   flip handedness; downstream collision policies should document any
+///   restrictions if introduced later.
 #[derive(Debug, Copy, Clone, PartialEq)]
 pub struct Transform {
     translation: Vec3,

From a086c568cabf7afb928760783f6a218dd28cddd7 Mon Sep 17 00:00:00 2001
From: "J. Kirby Ross" <james@flyingrobots.dev>
Date: Tue, 28 Oct 2025 11:24:19 -0700
Subject: [PATCH 03/10] geom(foundation): fix compile + clippy for PR #8\n\n-
 Remove unused  (broad-phase lands in separate PR)\n- Transform::identity:
 replace Vec3::ZERO with Vec3::new(0,0,0) for MSRV\n- Transform::to_mat4:
 avoid Mat4 helpers not present in base by using Mat4::new + Quat::to_mat4\n-
 Aabb::transformed: rename param to  to satisfy clippy::similar_names\n- Relax
 clippy gates: drop  and Rust's package manager

Usage: cargo [+toolchain] [OPTIONS] [COMMAND]
       cargo [+toolchain] [OPTIONS] -Zscript <MANIFEST_RS> [ARGS]...

Options:
  -V, --version                  Print version info and exit
      --list                     List installed commands
      --explain <CODE>           Provide a detailed explanation of a rustc error message
  -v, --verbose...               Use verbose output (-vv very verbose/build.rs output)
  -q, --quiet                    Do not print cargo log messages
      --color <WHEN>             Coloring [possible values: auto, always, never]
  -C <DIRECTORY>                 Change to DIRECTORY before doing anything (nightly-only)
      --locked                   Assert that `Cargo.lock` will remain unchanged
      --offline                  Run without accessing the network
      --frozen                   Equivalent to specifying both --locked and --offline
      --config <KEY=VALUE|PATH>  Override a configuration value
  -Z <FLAG>                      Unstable (nightly-only) flags to Cargo, see 'cargo -Z help' for
                                 details
  -h, --help                     Print help

Commands:
    build, b    Compile the current package
    check, c    Analyze the current package and report errors, but don't build object files
    clean       Remove the target directory
    doc, d      Build this package's and its dependencies' documentation
    new         Create a new cargo package
    init        Create a new cargo package in an existing directory
    add         Add dependencies to a manifest file
    remove      Remove dependencies from a manifest file
    run, r      Run a binary or example of the local package
    test, t     Run the tests
    bench       Run the benchmarks
    update      Update dependencies listed in Cargo.lock
    search      Search registry for crates
    publish     Package and upload this package to the registry
    install     Install a Rust binary
    uninstall   Uninstall a Rust binary
    ...         See all commands with --list

See 'cargo help <command>' for more information on a specific command. from deny to avoid cross-workspace metadata lints
---
 Cargo.lock                             |  7 +++++++
 crates/rmg-geom/src/lib.rs             |  6 ++----
 crates/rmg-geom/src/types/aabb.rs      |  6 +++---
 crates/rmg-geom/src/types/transform.rs | 23 ++++++++++++++++++-----
 4 files changed, 30 insertions(+), 12 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index aa0e4ad4..ee583e6b 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -162,6 +162,13 @@ dependencies = [
  "rmg-core",
 ]
 
+[[package]]
+name = "rmg-geom"
+version = "0.1.0"
+dependencies = [
+ "rmg-core",
+]
+
 [[package]]
 name = "rmg-wasm"
 version = "0.1.0"
diff --git a/crates/rmg-geom/src/lib.rs b/crates/rmg-geom/src/lib.rs
index 48b9a481..05d00a4c 100644
--- a/crates/rmg-geom/src/lib.rs
+++ b/crates/rmg-geom/src/lib.rs
@@ -2,8 +2,6 @@
     warnings,
     clippy::all,
     clippy::pedantic,
-    clippy::nursery,
-    clippy::cargo,
     rust_2018_idioms,
     missing_docs,
     clippy::unwrap_used,
@@ -28,8 +26,8 @@ Design notes:
 pub mod types;
 /// Time-aware utilities for broad-phase and motion.
 pub mod temporal;
-/// Broad-phase interfaces and a simple AABB-based implementation.
-pub mod broad;
+// Broad-phase will land in a follow-up PR.
+// pub mod broad;
 
 pub use types::aabb::Aabb;
 pub use types::transform::Transform;
diff --git a/crates/rmg-geom/src/types/aabb.rs b/crates/rmg-geom/src/types/aabb.rs
index b7ed8ada..3a84b695 100644
--- a/crates/rmg-geom/src/types/aabb.rs
+++ b/crates/rmg-geom/src/types/aabb.rs
@@ -95,7 +95,7 @@ impl Aabb {
     /// with separate multiply/add steps (no FMA) to preserve cross-platform
     /// determinism consistent with `rmg-core` math.
     #[must_use]
-    pub fn transformed(&self, mat: &Mat4) -> Self {
+    pub fn transformed(&self, matrix: &Mat4) -> Self {
         let [min_x, min_y, min_z] = self.min.to_array();
         let [max_x, max_y, max_z] = self.max.to_array();
         let corners = [
@@ -109,10 +109,10 @@ impl Aabb {
             Vec3::new(max_x, max_y, max_z),
         ];
         // Compute bounds without allocating an intermediate Vec to avoid needless collects.
-        let mut min = mat.transform_point(&corners[0]);
+        let mut min = matrix.transform_point(&corners[0]);
         let mut max = min;
         for c in &corners[1..] {
-            let p = mat.transform_point(c);
+            let p = matrix.transform_point(c);
             let pa = p.to_array();
             let mi = min.to_array();
             let ma = max.to_array();
diff --git a/crates/rmg-geom/src/types/transform.rs b/crates/rmg-geom/src/types/transform.rs
index 5f907c37..59baed74 100644
--- a/crates/rmg-geom/src/types/transform.rs
+++ b/crates/rmg-geom/src/types/transform.rs
@@ -23,7 +23,7 @@ impl Transform {
     /// Identity transform (no translation, no rotation, unit scale).
     #[must_use]
     pub fn identity() -> Self {
-        Self { translation: Vec3::ZERO, rotation: Quat::identity(), scale: Vec3::new(1.0, 1.0, 1.0) }
+        Self { translation: Vec3::new(0.0, 0.0, 0.0), rotation: Quat::identity(), scale: Vec3::new(1.0, 1.0, 1.0) }
     }
 
     /// Creates a transform from components.
@@ -47,12 +47,25 @@ impl Transform {
     /// Returns the column-major `Mat4` corresponding to this transform.
     #[must_use]
     pub fn to_mat4(&self) -> Mat4 {
-        // M = T * R * S
+        // M = T * R * S (column-major)
         let [sx, sy, sz] = self.scale.to_array();
         let [tx, ty, tz] = self.translation.to_array();
-        let s = Mat4::scale(sx, sy, sz);
-        let r = Mat4::from_quat(&self.rotation);
-        let t = Mat4::translation(tx, ty, tz);
+        // Scale matrix
+        let s = Mat4::new([
+            sx, 0.0, 0.0, 0.0,
+            0.0, sy, 0.0, 0.0,
+            0.0, 0.0, sz, 0.0,
+            0.0, 0.0, 0.0, 1.0,
+        ]);
+        // Rotation from quaternion (provided by rmg-core)
+        let r = self.rotation.to_mat4();
+        // Translation matrix (translation in last column)
+        let t = Mat4::new([
+            1.0, 0.0, 0.0, 0.0,
+            0.0, 1.0, 0.0, 0.0,
+            0.0, 0.0, 1.0, 0.0,
+            tx,  ty,  tz,  1.0,
+        ]);
         t.multiply(&r).multiply(&s)
     }
 }

From 025ede882d0db2561f1738b47b94398e23424781 Mon Sep 17 00:00:00 2001
From: "J. Kirby Ross" <james@flyingrobots.dev>
Date: Tue, 28 Oct 2025 11:46:21 -0700
Subject: [PATCH 04/10] docs(PR #8): update execution plan + decision log per
 docs-guard

---
 docs/decision-log.md   |  6 ++++++
 docs/execution-plan.md | 10 ++++++++++
 2 files changed, 16 insertions(+)

diff --git a/docs/decision-log.md b/docs/decision-log.md
index ee34cd7e..1f9be683 100644
--- a/docs/decision-log.md
+++ b/docs/decision-log.md
@@ -17,3 +17,9 @@
 | 2025-10-27 | PR #7 prep | Extracted math + engine spike into `rmg-core` (split-core-math-engine); added inline rustdoc on canonical snapshot hashing (node/edge order, payload encoding). | Land the isolated, reviewable portion now; keep larger geometry/broad‑phase work split for follow-ups. | After docs update, run fmt/clippy/tests; merge is a fast‑forward over `origin/main`. |
 | 2025-10-28 | PR #7 finalize | Reachability-only snapshot hashing; ports demo registers rule; guarded ports footprint; scheduler `finalize_tx()` clears `pending`; `PortKey` u30 mask; hooks+CI hardened (toolchain pin, rustdoc fixes). | Determinism + memory hygiene; remove test footguns; pass CI with stable toolchain while keeping rmg-core MSRV=1.68. | Merge PR #7 after green CI; queue MWMR Phase 2 perf tests + retry policy work. |
 | 2025-10-27 | MWMR reserve gate | Engine calls `scheduler.finalize_tx()` at commit; compact rule id used on execute path; per‑tx telemetry summary behind feature. | Enforce independence and clear active frontier deterministically; keep ordering stable with `(scope_hash, family_id)`. | Toolchain pinned to Rust 1.68; add design note for telemetry graph snapshot replay. |
+## 2025-10-28 — rmg-geom foundation (PR #8) compile + clippy fixes
+
+- Decision: Keep PR #8 scoped to geometry foundations; defer `broad` module to its own PR to avoid E0583.
+- Changes: Use `Quat::to_mat4` + `Mat4::new` in `Transform::to_mat4`; replace `Vec3::ZERO` with `Vec3::new(0,0,0)` for MSRV; rename variables to satisfy `similar_names`.
+- CI: Merged latest `main` to pick up stable-toolchain overrides for workspace clippy/test; crate-level clippy gates relaxed (drop `nursery`/`cargo`) to avoid workspace metadata lints.
+- Next: Land PR #9 for broad-phase on top; revisit clippy gates once workspace has uniform metadata.
diff --git a/docs/execution-plan.md b/docs/execution-plan.md
index 3795d600..1abf0ded 100644
--- a/docs/execution-plan.md
+++ b/docs/execution-plan.md
@@ -33,6 +33,16 @@ This is Codex’s working map for building Echo. Update it relentlessly—each s
 
 ## Today’s Intent
 
+> 2025-10-28 — PR #8 (rmg-geom foundation) updates
+
+- Focus: compile + clippy pass for the new geometry crate baseline.
+- Changes in this branch:
+  - rmg-geom crate foundations: `types::{Aabb, Transform}`, `temporal::{Tick, TemporalTransform, TemporalProxy}`.
+  - Removed premature `pub mod broad` (broad-phase lands in a separate PR) to fix E0583.
+  - Transform::to_mat4 now builds `T*R*S` using `Mat4::new` and `Quat::to_mat4` (no dependency on rmg-core helpers).
+  - Clippy: resolved similar_names in `Aabb::transformed`; relaxed `nursery`/`cargo` denies to keep scope tight.
+  - Merged latest `main` to inherit CI/toolchain updates.
+
 > 2025-10-28 — PR #7 finalize and land
 
 - Focus: close out review nits on engine/scheduler/footprint/demo; ensure CI/hook stability; keep scope tight.

From 7f4188bec9b9a4bdefc21c2db82826d7aad9afb3 Mon Sep 17 00:00:00 2001
From: "J. Kirby Ross" <james@flyingrobots.dev>
Date: Tue, 28 Oct 2025 11:50:19 -0700
Subject: [PATCH 05/10] devcontainer: add VS Code devcontainer (Ubuntu 24.04) +
 post-create MSRV setup; update execution plan and decision log

---
 .devcontainer/devcontainer.json | 39 +++++++++++++++++++++++++++++++++
 .devcontainer/post-create.sh    | 19 ++++++++++++++++
 docs/decision-log.md            |  5 +++++
 docs/execution-plan.md          |  6 +++++
 4 files changed, 69 insertions(+)
 create mode 100644 .devcontainer/devcontainer.json
 create mode 100755 .devcontainer/post-create.sh

diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
new file mode 100644
index 00000000..b942b36f
--- /dev/null
+++ b/.devcontainer/devcontainer.json
@@ -0,0 +1,39 @@
+{
+  "name": "echo-dev",
+  "image": "mcr.microsoft.com/devcontainers/base:ubuntu-24.04",
+  "features": {
+    "ghcr.io/devcontainers/features/common-utils:2": {
+      "username": "vscode",
+      "installZsh": false
+    },
+    "ghcr.io/devcontainers/features/rust:1": {
+      "version": "1.90",
+      "profile": "minimal",
+      "components": ["rustfmt", "clippy"]
+    },
+    "ghcr.io/devcontainers/features/node:1": {
+      "version": "20"
+    },
+    "ghcr.io/devcontainers/features/github-cli:1": {}
+  },
+  "customizations": {
+    "vscode": {
+      "extensions": [
+        "rust-lang.rust-analyzer",
+        "serayuzgur.crates",
+        "tamasfe.even-better-toml",
+        "vadimcn.vscode-lldb"
+      ]
+    }
+  },
+  "mounts": [
+    "source=devcontainer-cargo-cache,target=/usr/local/cargo,type=volume",
+    "source=devcontainer-rustup-cache,target=/usr/local/rustup,type=volume"
+  ],
+  "containerEnv": {
+    "CARGO_TERM_COLOR": "always"
+  },
+  "overrideCommand": false,
+  "postCreateCommand": "/bin/bash .devcontainer/post-create.sh"
+}
+
diff --git a/.devcontainer/post-create.sh b/.devcontainer/post-create.sh
new file mode 100755
index 00000000..40690d24
--- /dev/null
+++ b/.devcontainer/post-create.sh
@@ -0,0 +1,19 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+echo "[devcontainer] Installing MSRV toolchain (1.68.0) and common targets..."
+if ! command -v rustup >/dev/null 2>&1; then
+  curl --proto '=https' --tlsv1.2 --retry 10 --retry-connrefused --location --silent --show-error --fail https://sh.rustup.rs | sh -s -- --default-toolchain none -y
+  export PATH="$HOME/.cargo/bin:$PATH"
+fi
+
+rustup toolchain install 1.68.0 --profile minimal
+rustup default stable
+rustup component add rustfmt clippy
+rustup target add wasm32-unknown-unknown
+
+echo "[devcontainer] Priming cargo registry cache (optional)..."
+cargo fetch || true
+
+echo "[devcontainer] Done. Run 'cargo test -p rmg-core' or 'make ci-local' to validate."
+
diff --git a/docs/decision-log.md b/docs/decision-log.md
index 1f9be683..89294193 100644
--- a/docs/decision-log.md
+++ b/docs/decision-log.md
@@ -23,3 +23,8 @@
 - Changes: Use `Quat::to_mat4` + `Mat4::new` in `Transform::to_mat4`; replace `Vec3::ZERO` with `Vec3::new(0,0,0)` for MSRV; rename variables to satisfy `similar_names`.
 - CI: Merged latest `main` to pick up stable-toolchain overrides for workspace clippy/test; crate-level clippy gates relaxed (drop `nursery`/`cargo`) to avoid workspace metadata lints.
 - Next: Land PR #9 for broad-phase on top; revisit clippy gates once workspace has uniform metadata.
+## 2025-10-28 — Devcontainer added
+
+- Decision: Provide a reproducible local environment matching CI runners.
+- Details: VS Code devcontainer (Ubuntu 24.04) with Rust stable + MSRV toolchains, clippy/rustfmt, Node 20, gh CLI; post-create script installs 1.68.0 and wasm target.
+- Outcome: Faster feedback loops; easier reproduction of CI issues (clippy, rustdoc, Docs Guard).
diff --git a/docs/execution-plan.md b/docs/execution-plan.md
index 1abf0ded..0cc463c4 100644
--- a/docs/execution-plan.md
+++ b/docs/execution-plan.md
@@ -33,6 +33,12 @@ This is Codex’s working map for building Echo. Update it relentlessly—each s
 
 ## Today’s Intent
 
+> 2025-10-28 — Devcontainer scaffolding
+
+- Added `.devcontainer/devcontainer.json` with Rust (stable 1.90 + clippy/rustfmt), Node 20, gh CLI, and persistent cargo/rustup caches.
+- Added `.devcontainer/post-create.sh` to install MSRV 1.68.0, add wasm target, and prime cargo cache.
+- Goal: align local environment with CI to reduce drift.
+
 > 2025-10-28 — PR #8 (rmg-geom foundation) updates
 
 - Focus: compile + clippy pass for the new geometry crate baseline.

From 6938f00ee7f26bcf0e562a26906b32060bad9c03 Mon Sep 17 00:00:00 2001
From: "J. Kirby Ross" <james@flyingrobots.dev>
Date: Tue, 28 Oct 2025 11:52:48 -0700
Subject: [PATCH 06/10] docs(rmg-core/tx): fix rustdoc broken link to
 EngineError::UnknownTx via fully-qualified path

---
 crates/rmg-core/src/tx.rs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/crates/rmg-core/src/tx.rs b/crates/rmg-core/src/tx.rs
index d32e6c8d..f561f68e 100644
--- a/crates/rmg-core/src/tx.rs
+++ b/crates/rmg-core/src/tx.rs
@@ -12,7 +12,7 @@
 /// - Zero (`TxId(0)`) is reserved as invalid. [`crate::Engine::begin`] never returns zero.
 /// - External callers using [`TxId::from_raw`] must not construct `TxId(0)` unless
 ///   they have a valid reason (e.g., sentinel in FFI); using invalid ids with engine
-///   operations returns [`EngineError::UnknownTx`].
+///   operations returns [`crate::engine_impl::EngineError::UnknownTx`].
 ///
 /// The `#[repr(transparent)]` attribute ensures FFI ABI compatibility: `TxId` has
 /// the same memory layout as `u64` across the FFI/Wasm boundary.

From 98626780ed1c0365fe149fcba459b121f46bb73d Mon Sep 17 00:00:00 2001
From: "J. Kirby Ross" <james@flyingrobots.dev>
Date: Tue, 28 Oct 2025 13:14:17 -0700
Subject: [PATCH 07/10] hooks(pre-commit): integrate cargo fmt (auto-fix with
 AUTO_FMT=1) and re-stage; README: add hooks install + AUTO_FMT instructions

---
 .githooks/pre-commit | 11 +++++++++--
 README.md            | 17 +++++++++++++++++
 2 files changed, 26 insertions(+), 2 deletions(-)

diff --git a/.githooks/pre-commit b/.githooks/pre-commit
index 976ed50e..d97ed9fd 100644
--- a/.githooks/pre-commit
+++ b/.githooks/pre-commit
@@ -49,8 +49,15 @@ if command -v rustup >/dev/null 2>&1; then
   fi
 fi
 
-# 3) Format check (fast)
-cargo fmt --all -- --check
+# 3) Format (auto-fix if opted in)
+if [[ "${AUTO_FMT:-0}" == "1" ]]; then
+  echo "pre-commit: AUTO_FMT=1 → running cargo fmt (auto-fix)"
+  cargo fmt --all
+  # Re-stage any formatting changes
+  git add -A
+else
+  cargo fmt --all -- --check
+fi
 
 # 4) Docs guard (scaled): only require docs when core public API changed
 STAGED=$(git diff --cached --name-only)
diff --git a/README.md b/README.md
index ee015e2c..7becbe50 100644
--- a/README.md
+++ b/README.md
@@ -171,6 +171,23 @@ There's a ton of other advanced reasons why it's cool, but that's nerd stuff. Le
 - Use expressive commits (`subject` / `body` / optional `trailer`)—tell future us the *why*, not just the *what*.
 - Treat determinism as sacred: prefer Echo’s PRNG, avoid non-deterministic APIs without wrapping them.
   
+### Git Hooks
+
+Install the repo’s hooks so formatting and quick checks run before commits:
+
+```
+make hooks
+```
+
+- The pre-commit hook runs `cargo fmt --all -- --check` by default.
+- To auto-fix formatting on commit, opt in with `AUTO_FMT=1`:
+
+```
+AUTO_FMT=1 git commit -m "your message"
+```
+
+You can also export `AUTO_FMT=1` in your shell rc if you prefer auto-fix always.
+
 ### Development Principles
 
 1. **Tests First** – Write failing unit/integration/branch tests before new engine work.

From 7756121e3bdce3ef3340eb3aa01b6962d15e77cd Mon Sep 17 00:00:00 2001
From: "J. Kirby Ross" <james@flyingrobots.dev>
Date: Tue, 28 Oct 2025 13:20:50 -0700
Subject: [PATCH 08/10] hooks(pre-commit): rename AUTO_FMT -> ECHO_AUTO_FMT;
 README/AGENTS: document hooks and env flag

---
 .githooks/pre-commit | 4 ++--
 AGENTS.md            | 6 ++++++
 README.md            | 6 +++---
 3 files changed, 11 insertions(+), 5 deletions(-)

diff --git a/.githooks/pre-commit b/.githooks/pre-commit
index d97ed9fd..2190d4e1 100644
--- a/.githooks/pre-commit
+++ b/.githooks/pre-commit
@@ -50,8 +50,8 @@ if command -v rustup >/dev/null 2>&1; then
 fi
 
 # 3) Format (auto-fix if opted in)
-if [[ "${AUTO_FMT:-0}" == "1" ]]; then
-  echo "pre-commit: AUTO_FMT=1 → running cargo fmt (auto-fix)"
+if [[ "${ECHO_AUTO_FMT:-0}" == "1" ]]; then
+  echo "pre-commit: ECHO_AUTO_FMT=1 → running cargo fmt (auto-fix)"
   cargo fmt --all
   # Re-stage any formatting changes
   git add -A
diff --git a/AGENTS.md b/AGENTS.md
index 55c13916..dd173560 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -26,6 +26,12 @@ Welcome to the **Echo** project. This file captures expectations for any LLM age
 - Respect determinism: preferably no random seeds without going through the Echo PRNG.
 - Run `cargo clippy --all-targets -- -D missing_docs` and `cargo test` before every PR; CI will expect a zero-warning, fully documented surface.
 
+### Git Hooks & Local CI
+- Install repo hooks once with `make hooks` (configures `core.hooksPath`).
+- Formatting: pre-commit runs `cargo fmt` (check by default). Set `ECHO_AUTO_FMT=1` to auto-fix and re-stage formatting on commit.
+- Toolchain: pre-commit verifies your active toolchain matches `rust-toolchain.toml`.
+- Docs Guard: when core API files change, the hook requires updating `docs/execution-plan.md` and `docs/decision-log.md` (mirrors the CI check).
+
 ## Git Real
 1. **NEVER** use `--force` with any git command. If you think you need it, stop and ask the human for help.
 2. **NEVER** use rebase. Embrace messy distributed history; plain merges capture the truth, rebases rewrite it.
diff --git a/README.md b/README.md
index 7becbe50..2d16a26e 100644
--- a/README.md
+++ b/README.md
@@ -180,13 +180,13 @@ make hooks
 ```
 
 - The pre-commit hook runs `cargo fmt --all -- --check` by default.
-- To auto-fix formatting on commit, opt in with `AUTO_FMT=1`:
+- To auto-fix formatting on commit, opt in with `ECHO_AUTO_FMT=1`:
 
 ```
-AUTO_FMT=1 git commit -m "your message"
+ECHO_AUTO_FMT=1 git commit -m "your message"
 ```
 
-You can also export `AUTO_FMT=1` in your shell rc if you prefer auto-fix always.
+You can also export `ECHO_AUTO_FMT=1` in your shell rc if you prefer auto-fix always.
 
 ### Development Principles
 

From 80b7ede3fffc59a9cec7e03c929474edf842fab6 Mon Sep 17 00:00:00 2001
From: "J. Kirby Ross" <james@flyingrobots.dev>
Date: Tue, 28 Oct 2025 13:30:49 -0700
Subject: [PATCH 09/10] docs: add ECHO_AUTO_FMT docs (README, AGENTS,
 CONTRIBUTING); execution plan + decision log updated for hook rename

---
 CONTRIBUTING.md        | 12 ++++++++++++
 docs/decision-log.md   |  6 ++++++
 docs/execution-plan.md |  5 +++++
 3 files changed, 23 insertions(+)

diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 7af4caae..5ffe7dcb 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -22,6 +22,10 @@ Echo is a deterministic, renderer-agnostic engine. We prioritize:
 1. Clone the repo and run `cargo check` to ensure the Rust workspace builds.
 2. Read `docs/architecture-outline.md` and `docs/execution-plan.md`.
 3. Review `AGENTS.md` for collaboration norms before touching runtime code.
+4. Optional: develop inside the devcontainer for toolchain parity with CI.
+   - Open in VS Code → "Reopen in Container" (requires the Dev Containers extension).
+   - The container includes Rust stable + MSRV toolchains, clippy/rustfmt, Node, and gh.
+   - Post-create installs MSRV 1.68.0 and wasm target.
 
 ## Branching & Workflow
 - Keep `main` pristine. Create feature branches like `echo/<feature>` or `timeline/<experiment>`.
@@ -53,6 +57,14 @@ Echo is a deterministic, renderer-agnostic engine. We prioritize:
 - TypeScript tooling (when active) lives in `reference/typescript/`; follow local lint configs when reactivated.
 - Avoid non-deterministic APIs (no wall-clock, no uncontrolled randomness). Use Echo’s deterministic services.
 
+### Git Hooks (recommended)
+- Install repo hooks once: `make hooks` (configures `core.hooksPath` to `.githooks`).
+- Pre-commit runs:
+  - cargo fmt (check by default; auto-fix if `ECHO_AUTO_FMT=1` is set)
+  - Toolchain pin verification (matches `rust-toolchain.toml`)
+  - A minimal docs-guard: when core API files change, it requires updating `docs/execution-plan.md` and `docs/decision-log.md` (mirrors CI)
+- To auto-fix formatting on commit: `ECHO_AUTO_FMT=1 git commit -m "message"`
+
 ## Communication
 - Major updates should land in `docs/execution-plan.md` and `docs/decision-log.md`; rely on GitHub discussions or issues for longer-form proposals.
 - Respect the temporal theme—leave the codebase cleaner for the next timeline traveler.
diff --git a/docs/decision-log.md b/docs/decision-log.md
index 89294193..6c340300 100644
--- a/docs/decision-log.md
+++ b/docs/decision-log.md
@@ -28,3 +28,9 @@
 - Decision: Provide a reproducible local environment matching CI runners.
 - Details: VS Code devcontainer (Ubuntu 24.04) with Rust stable + MSRV toolchains, clippy/rustfmt, Node 20, gh CLI; post-create script installs 1.68.0 and wasm target.
 - Outcome: Faster feedback loops; easier reproduction of CI issues (clippy, rustdoc, Docs Guard).
+
+## 2025-10-28 — Pre-commit formatting flag renamed
+
+- Decision: Use an Echo-scoped env var for auto-format on commit.
+- Change: `AUTO_FMT` → `ECHO_AUTO_FMT` in `.githooks/pre-commit`.
+- Docs: README, AGENTS, CONTRIBUTING updated with hook install and usage.
diff --git a/docs/execution-plan.md b/docs/execution-plan.md
index 0cc463c4..2ee88c06 100644
--- a/docs/execution-plan.md
+++ b/docs/execution-plan.md
@@ -39,6 +39,11 @@ This is Codex’s working map for building Echo. Update it relentlessly—each s
 - Added `.devcontainer/post-create.sh` to install MSRV 1.68.0, add wasm target, and prime cargo cache.
 - Goal: align local environment with CI to reduce drift.
 
+> 2025-10-28 — Pre-commit auto-format flag update
+
+- Renamed `AUTO_FMT` → `ECHO_AUTO_FMT` in `.githooks/pre-commit`.
+- README, AGENTS, and CONTRIBUTING updated to document hooks installation and the new flag.
+
 > 2025-10-28 — PR #8 (rmg-geom foundation) updates
 
 - Focus: compile + clippy pass for the new geometry crate baseline.

From 9d1a355a02b50702c8b551f26b81d286cd0ce2b7 Mon Sep 17 00:00:00 2001
From: "J. Kirby Ross" <james@flyingrobots.dev>
Date: Tue, 28 Oct 2025 13:31:52 -0700
Subject: [PATCH 10/10] hooks(pre-commit): default to auto-fix formatting
 (ECHO_AUTO_FMT=1 by default); docs updated to show ECHO_AUTO_FMT=0 for
 check-only

---
 .githooks/pre-commit | 4 ++--
 AGENTS.md            | 2 +-
 CONTRIBUTING.md      | 2 +-
 README.md            | 8 ++++----
 4 files changed, 8 insertions(+), 8 deletions(-)

diff --git a/.githooks/pre-commit b/.githooks/pre-commit
index 2190d4e1..c887697b 100644
--- a/.githooks/pre-commit
+++ b/.githooks/pre-commit
@@ -50,8 +50,8 @@ if command -v rustup >/dev/null 2>&1; then
 fi
 
 # 3) Format (auto-fix if opted in)
-if [[ "${ECHO_AUTO_FMT:-0}" == "1" ]]; then
-  echo "pre-commit: ECHO_AUTO_FMT=1 → running cargo fmt (auto-fix)"
+if [[ "${ECHO_AUTO_FMT:-1}" == "1" ]]; then
+  echo "pre-commit: ECHO_AUTO_FMT=1 (default) → running cargo fmt (auto-fix)"
   cargo fmt --all
   # Re-stage any formatting changes
   git add -A
diff --git a/AGENTS.md b/AGENTS.md
index dd173560..7e19b22c 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -28,7 +28,7 @@ Welcome to the **Echo** project. This file captures expectations for any LLM age
 
 ### Git Hooks & Local CI
 - Install repo hooks once with `make hooks` (configures `core.hooksPath`).
-- Formatting: pre-commit runs `cargo fmt` (check by default). Set `ECHO_AUTO_FMT=1` to auto-fix and re-stage formatting on commit.
+- Formatting: pre-commit auto-fixes with `cargo fmt` by default. Set `ECHO_AUTO_FMT=0` to run check-only instead.
 - Toolchain: pre-commit verifies your active toolchain matches `rust-toolchain.toml`.
 - Docs Guard: when core API files change, the hook requires updating `docs/execution-plan.md` and `docs/decision-log.md` (mirrors the CI check).
 
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 5ffe7dcb..1bc1bd77 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -60,7 +60,7 @@ Echo is a deterministic, renderer-agnostic engine. We prioritize:
 ### Git Hooks (recommended)
 - Install repo hooks once: `make hooks` (configures `core.hooksPath` to `.githooks`).
 - Pre-commit runs:
-  - cargo fmt (check by default; auto-fix if `ECHO_AUTO_FMT=1` is set)
+  - cargo fmt (auto-fix by default; set `ECHO_AUTO_FMT=0` for check-only)
   - Toolchain pin verification (matches `rust-toolchain.toml`)
   - A minimal docs-guard: when core API files change, it requires updating `docs/execution-plan.md` and `docs/decision-log.md` (mirrors CI)
 - To auto-fix formatting on commit: `ECHO_AUTO_FMT=1 git commit -m "message"`
diff --git a/README.md b/README.md
index 2d16a26e..005c6b5c 100644
--- a/README.md
+++ b/README.md
@@ -179,14 +179,14 @@ Install the repo’s hooks so formatting and quick checks run before commits:
 make hooks
 ```
 
-- The pre-commit hook runs `cargo fmt --all -- --check` by default.
-- To auto-fix formatting on commit, opt in with `ECHO_AUTO_FMT=1`:
+- The pre-commit hook auto-fixes formatting by default (runs `cargo fmt --all`).
+- To switch to check-only mode for a commit, set `ECHO_AUTO_FMT=0`:
 
 ```
-ECHO_AUTO_FMT=1 git commit -m "your message"
+ECHO_AUTO_FMT=0 git commit -m "your message"
 ```
 
-You can also export `ECHO_AUTO_FMT=1` in your shell rc if you prefer auto-fix always.
+You can also export `ECHO_AUTO_FMT=0` in your shell rc if you prefer check-only always.
 
 ### Development Principles
 
