diff --git a/experiment-baton/AILOG-2026-06-26-002-baton-b1-unit-inventory.md b/experiment-baton/AILOG-2026-06-26-002-baton-b1-unit-inventory.md
new file mode 100644
index 00000000..27bd1614
--- /dev/null
+++ b/experiment-baton/AILOG-2026-06-26-002-baton-b1-unit-inventory.md
@@ -0,0 +1,87 @@
+---
+id: AILOG-2026-06-26-002
+title: Baton Phase 2 B1 — routable-unit inventory
+status: accepted
+created: 2026-06-26
+agent: claude-code-opus-4.8
+confidence: high
+review_required: true
+risk_level: low
+eu_ai_act_risk: not_applicable
+nist_genai_risks: []
+iso_42001_clause: []
+lines_changed: 380
+files_modified: [experiment-baton/src/units.rs, experiment-baton/src/lib.rs, experiment-baton/CHARTER-03-dry-run-router.md, experiment-baton/tests/fixtures/governance-corpus]
+observability_scope: none
+tags: [baton, phase2, dry-run-router, units, inventory, read-only]
+related: [CHARTER-03-dry-run-router, AILOG-2026-06-26-001]
+---
+
+# AILOG: Baton Phase 2 B1 — routable-unit inventory
+
+## Summary
+
+First batch of `CHARTER-03-dry-run-router` (Phase 2). Implements the **routable-unit
+inventory**: read-only enumeration of the work StrayMark already recorded, at the
+four granularities that already exist — **inventing no new vocabulary** (concept
+§4.3 / framing decision #1): Charter, Batch, Follow-up, Task.
+
+`src/units.rs` exposes `RoutableUnit` + `Granularity` and `inventory(root, only)`.
+Each reader harvests only the signals it can read directly; computed signals
+(complexity, arch state, coherence findings) are deferred to B2. A signal a reader
+cannot see stays `None` — never fabricated.
+
+## What changed
+
+- **Charter reader** reuses `straymark_core::charter::{discover_and_parse,
+ display_title}` and `charter_files::parse_files_to_modify` — no second parser.
+ Harvests `effort_estimate` (typed XS..L) and the declared `Files to modify`
+ scope globs.
+- **Batch / Follow-up / Task readers** are tolerant, line-oriented scans (the
+ `scan.rs` philosophy: no regex, char-boundary-safe over accented prose).
+ - Batch: `### Batch N —
` under `## Batch Ledger` in `AILOG-*.md`; id
+ keyed `#batch-N`.
+ - Follow-up: `### FU-NNN — ` under `## Bucket: ` in
+ `.straymark/follow-ups-backlog.md`; harvests bucket + optional `**Severity**`.
+ - Task: `- [ ] T. — ` in `specs/**/tasks.md`; id keyed
+ `:T.`.
+- `--granularity` selection via `Granularity::parse` (`all` → `None`).
+
+## Verification
+
+- `cargo test -p straymark-baton` ✓ — 5 new unit tests over a new fixture corpus
+ (`tests/fixtures/governance-corpus/`) covering all four granularities; full
+ suite green, `cargo clippy` clean.
+- **Sentinel dogfood** (read-only, scratch test, `git status` unchanged): the
+ inventory runs without panic on the real, messier corpus —
+ `charter: 45 · batch: 82 · followup: 135 · task: 500`. It also handled
+ Sentinel's `T001`-style task ids alongside the fixture's `T1.1` form.
+
+## Impact
+
+Read-only and non-breaking. No model/network/agent dependency (NFR2 holds by
+construction — the module has none). Sets up B2 (signal aggregation) and B3
+(classifier).
+
+## EU AI Act Considerations
+
+Not applicable — local developer tooling; no automated decision-making, no
+personal data, no model inference. Read-only over the target tree (NFR1).
+
+## Batch Ledger
+
+### Batch 1 — B1: routable-unit inventory (T1.1–T1.4)
+Completed 2026-06-26 — this AILOG's PR. `src/units.rs` + fixture corpus + 5 tests;
+Sentinel dogfood read-only. Charter → `in-progress`.
+
+### Batch 2 — B2: signal aggregation (T2.1–T2.4)
+Pending.
+
+### Batch 3 — B3: cheap classifier (T3.1–T3.4)
+Pending.
+
+### Batch 4 — B4: tier policy + telemetry + CLI (T4.1–T4.5)
+Pending.
+
+### Batch 5 — B5: Sentinel dogfood + acceptance (T5.1–T5.6)
+Pending.
diff --git a/experiment-baton/CHARTER-03-dry-run-router.md b/experiment-baton/CHARTER-03-dry-run-router.md
index 0c0bf27d..df1ed39d 100644
--- a/experiment-baton/CHARTER-03-dry-run-router.md
+++ b/experiment-baton/CHARTER-03-dry-run-router.md
@@ -1,6 +1,6 @@
---
charter_id: CHARTER-03-dry-run-router
-status: declared
+status: in-progress
effort_estimate: L
trigger: "Baton concepto §7 Fase 2 — no existe ninguna noción de modelo/tier/presupuesto/costo en StrayMark; todo el trabajo corre con el mismo modelo (commit trivial o rediseño). Presión económica §1.1 (fin de subsidios, pago por Mtoken). Tras la Fase 1 (Coherence Bridge), validar empíricamente el principio económico §4.2 sin tocar modelos."
originating_concept: experiment-baton/01-baton-concept.md
@@ -11,7 +11,7 @@ related_issues: [304]
# Charter: Baton Fase 2 — Clasificación + router en seco (dry-run, recomienda no ejecuta)
-> **Status (espejo del frontmatter; la fuente de verdad es el frontmatter):** declared. Effort: L.
+> **Status (espejo del frontmatter; la fuente de verdad es el frontmatter):** in-progress. Effort: L.
> **Origen:** concepto [01-baton-concept.md](01-baton-concept.md) §4.2 + §7 (Fase 2) + §10.4 (unidad enrutable).
> **Encuadre:** segunda fase del experimento Baton. **Recomienda, no ejecuta. Toca modelos solo como telemetría — riesgo cero.** Prototipa en `experiment-baton/`, lógica pura/typed diseñada para graduar a `straymark-core`.
diff --git a/experiment-baton/src/lib.rs b/experiment-baton/src/lib.rs
index 200625bd..807a0628 100644
--- a/experiment-baton/src/lib.rs
+++ b/experiment-baton/src/lib.rs
@@ -19,3 +19,4 @@ pub mod overlay;
pub mod provenance;
mod scan;
pub mod speckit;
+pub mod units;
diff --git a/experiment-baton/src/units.rs b/experiment-baton/src/units.rs
new file mode 100644
index 00000000..0e35b877
--- /dev/null
+++ b/experiment-baton/src/units.rs
@@ -0,0 +1,423 @@
+//! Routable-unit inventory (Baton Phase 2, B1).
+//!
+//! Reads the work StrayMark *already recorded* in a project — at the four
+//! granularities that already exist, **inventing no new vocabulary** (concept
+//! §4.3 / charter framing decision #1): Charter, Batch, Follow-up, Task. The
+//! dry-run router (B2+) classifies these units and recommends a tier; this module
+//! only *enumerates* them and harvests the signals a reader can read directly.
+//!
+//! Read-only. Charter reading reuses `straymark_core::charter`; the other three
+//! are tolerant, line-oriented scans (the `scan.rs` philosophy: no regex,
+//! char-boundary-safe over accented prose). A signal a reader cannot see stays
+//! `None` — never fabricated (honest inputs → honest classification, B3).
+
+use std::path::{Path, PathBuf};
+
+use serde::Serialize;
+use straymark_core::charter::{discover_and_parse, display_title};
+use straymark_core::charter_files::parse_files_to_modify;
+
+use crate::intent::SourceRef;
+
+/// Directories never walked for governance artifacts.
+const SKIP_DIRS: &[&str] = &[
+ ".git",
+ "target",
+ "node_modules",
+ "dist",
+ "build",
+ ".docusaurus",
+];
+
+/// The granularity of a routable unit — the existing artifact it came from.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
+#[serde(rename_all = "kebab-case")]
+pub enum Granularity {
+ Charter,
+ Batch,
+ Followup,
+ Task,
+}
+
+impl Granularity {
+ pub const ALL: [Granularity; 4] = [
+ Granularity::Charter,
+ Granularity::Batch,
+ Granularity::Followup,
+ Granularity::Task,
+ ];
+
+ pub fn as_str(self) -> &'static str {
+ match self {
+ Granularity::Charter => "charter",
+ Granularity::Batch => "batch",
+ Granularity::Followup => "followup",
+ Granularity::Task => "task",
+ }
+ }
+
+ /// Parse a `--granularity` value (`all` → `None`).
+ pub fn parse(s: &str) -> Option