Drop the Cli and ProjectLocal scopes
Neither earned its place. Scope::Cli carried no mounts or env — it
existed only to label -p in `ramekin config` output, so the selection
provenance becomes Option<Scope> with None meaning -p. The
project-local layer duplicated what -p already covers per run; a
committed project config plus a per-run flag is enough until a durable
local-override need actually shows up (noted in the design doc).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ayyqfpg98CeZPmtMPXQEFo
change
commit 0e55315cd014f98d49e8fd329f29a6338efb4d82
author Claude <noreply@anthropic.com>
date
parent 421149a0
diff --git a/.gitignore b/.gitignore
index 633a928..c35a4ed 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,2 @@
 /target
 /work
-.ramekin/config.local.kdl
diff --git a/AGENTS.md b/AGENTS.md
index f62128e..da43c64 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -45,7 +45,7 @@ just           # All four
 
 - The config redesign is documented in `docs/config-redesign.md`; all four steps of its sequencing are implemented.
 - Config files are parsed directly with the `kdl` crate (`parse_config` in `config.rs`), not serde: the grammar is `mounts` blocks, one `env` block syntax (bare child = host passthrough), and `profile` nodes (with children = definition, bare = selection). Unknown nodes fail loudly.
-- Config merges layers, lowest precedence first: binary (staples `~/.config/git`/`~/.config/jj` plus the active agent's config allowlist, mounted read-only, canonicalized, skip-if-missing), profile (the active profile's env/mounts), user (every `*.kdl` in `~/.config/ramekin/`, merged as one layer, duplicate keys within the layer are errors), project (`.ramekin/config.kdl`), and project-local (`.ramekin/config.local.kdl`). A `/dev/null` source masks (removes) a mount inherited from a lower layer. `env` merges per variable; profiles merge by name, last writer takes the whole definition.
+- Config merges layers, lowest precedence first: binary (staples `~/.config/git`/`~/.config/jj` plus the active agent's config allowlist, mounted read-only, canonicalized, skip-if-missing), profile (the active profile's env/mounts), user (every `*.kdl` in `~/.config/ramekin/`, merged as one layer, duplicate keys within the layer are errors), and project (`.ramekin/config.kdl`). A `/dev/null` source masks (removes) a mount inherited from a lower layer. `env` merges per variable; profiles merge by name, last writer takes the whole definition.
 - Profiles subsume agent selection: the binary ships trivial `pi`/`claude` profiles, selection precedence is binary < user < project < project-local < `-p`. `Agent` (in `config.rs`) carries each agent's host config dir, allowlist, and container config dir; `AgentState` (in `main.rs`) carries its persistent host paths and session mounts.
 - Persistence is per-agent, opposite policies: pi is ephemeral-by-default (fresh session dir at `/root/.pi/agent`; allowlisted `auth.json` from `$XDG_DATA_HOME/ramekin/agents/pi/` and per-repo `sessions/` bound on top; teardown logs discarded writes). Claude is persist-by-default (`~/.claude` + `~/.claude.json` from `$XDG_DATA_HOME/ramekin/agents/`, shared across repos; session-scoped dirs bound over the `CLAUDE_EPHEMERAL` denylist).
 - Each workspace mounts at `/workspace/<slug>` (slug is `<dirname>-<hash>`, never a shared `/workspace`) so cwd-keyed agent state stays distinct per repo; compose's `working_dir` puts the agent there on start.
diff --git a/README.md b/README.md
index d152dd7..5edffb8 100644
--- a/README.md
+++ b/README.md
@@ -65,7 +65,7 @@ profile "pi-glm" {
 }
 ```
 
-A bare `profile "name"` node (no block) *selects* a profile. Selection precedence, lowest to highest: binary default (`pi`) → user KDL (the per-machine default) → project KDL → project-local KDL → `-p` on the command line. Profile selection subsumes agent selection; there is no separate `--agent`. Model choice within a provider stays out of profiles — that's per-run agent args after `--`.
+A bare `profile "name"` node (no block) *selects* a profile. Selection precedence, lowest to highest: binary default (`pi`) → user KDL (the per-machine default) → project KDL → `-p` on the command line. Profile selection subsumes agent selection; there is no separate `--agent`. Model choice within a provider stays out of profiles — that's per-run agent args after `--`.
 
 Profiles merge by name across layers and the last writer takes the whole definition. Fine-grained tweaks (one env var) go through the ordinary layered `env`, which overlays the active profile's env per variable.
 
@@ -103,7 +103,6 @@ Mount configuration merges across layers, lowest to highest precedence:
 2. **Profile** — the active profile's `mounts`
 3. **User** — every `*.kdl` in `$XDG_CONFIG_HOME/ramekin/`, merged as one layer (defining the same key twice within the layer is an error; per-file symlinking into dotfiles is a dotfiles decision)
 4. **Project** — `<workspace>/.ramekin/config.kdl`, committed
-5. **Project-local** — `<workspace>/.ramekin/config.local.kdl`, gitignored
 
 Additional host paths can be mounted into the container via the KDL layers. Directories, files, and devices (such as `/dev/null`) all work. Mounts whose source doesn't exist on the host are silently skipped.
 
diff --git a/docs/config-redesign.md b/docs/config-redesign.md
index 4fa265e..8c07251 100644
--- a/docs/config-redesign.md
+++ b/docs/config-redesign.md
@@ -171,8 +171,10 @@ Layers, lowest to highest precedence:
    `profile` selection and machine-only mounts (ranger). Symlink
    granularity is a dotfiles decision, not a ramekin one.
 3. **project** — `<workspace>/.ramekin/config.kdl`, committed
-4. **project-local** — `<workspace>/.ramekin/config.local.kdl`, gitignored
-5. **CLI** — `-p`; `--mount`/`--env` when they earn their keep
+4. **CLI** — `-p`; `--mount`/`--env` when they earn their keep. (A
+   gitignored `config.local.kdl` layer was tried and dropped as
+   unnecessary — `-p` covers the per-run case; revisit if a durable
+   local-override need shows up.)
 
 Merging: `env` merges per variable, overlaying the active profile's env, so
 any layer can adjust one variable without redefining the profile; mounts
diff --git a/src/config.rs b/src/config.rs
index f6c70cc..5b802a3 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -17,10 +17,6 @@ pub enum Scope {
     User,
     /// Project-level `<workspace>/.ramekin/config.kdl`, committed.
     Project,
-    /// Project-local `<workspace>/.ramekin/config.local.kdl`, gitignored.
-    ProjectLocal,
-    /// Command-line arguments (currently just `-p`).
-    Cli,
 }
 
 impl fmt::Display for Scope {
@@ -30,8 +26,6 @@ impl fmt::Display for Scope {
             Self::Profile => write!(f, "profile"),
             Self::User => write!(f, "user"),
             Self::Project => write!(f, "project"),
-            Self::ProjectLocal => write!(f, "project-local"),
-            Self::Cli => write!(f, "cli"),
         }
     }
 }
@@ -186,8 +180,8 @@ pub struct ScopedConfig {
     /// Every known profile (builtin trivial ones plus KDL definitions),
     /// merged by name — later layers take the whole definition.
     pub profiles: BTreeMap<String, ScopedValue<Profile>>,
-    /// The active profile's name and the scope that selected it.
-    pub selection: ScopedValue<String>,
+    /// The layer that selected the active profile, or `None` for `-p`.
+    pub selected_by: Option<Scope>,
     /// The active profile.
     pub profile: Profile,
 }
@@ -215,7 +209,6 @@ impl ScopedConfig {
     /// 2. Profile (the active profile's env and mounts)
     /// 3. User (every `*.kdl` in `~/.config/ramekin/`, merged as one layer)
     /// 4. Project (`<workspace>/.ramekin/config.kdl`)
-    /// 5. Project-local (`<workspace>/.ramekin/config.local.kdl`)
     ///
     /// `cli_profile` is the `-p` selection, which beats any layer's.
     ///
@@ -249,17 +242,12 @@ impl ScopedConfig {
             }
         }
 
-        // Project layers
-        for (scope, name) in [
-            (Scope::Project, "config.kdl"),
-            (Scope::ProjectLocal, "config.local.kdl"),
-        ] {
-            let path = workspace.join(".ramekin").join(name);
-            if path.exists() {
-                let mut builder = LayerBuilder::new(scope, Some(path.clone()));
-                builder.add_file(&path, workspace_target)?;
-                builders.push(builder);
-            }
+        // Project layer
+        let project_path = workspace.join(".ramekin/config.kdl");
+        if project_path.exists() {
+            let mut builder = LayerBuilder::new(Scope::Project, Some(project_path.clone()));
+            builder.add_file(&project_path, workspace_target)?;
+            builders.push(builder);
         }
 
         // Profiles merge by name across layers, last writer takes the whole
@@ -277,10 +265,7 @@ impl ScopedConfig {
                 )
             })
             .collect();
-        let mut selection = ScopedValue {
-            scope: Scope::Binary,
-            value: DEFAULT_PROFILE.to_string(),
-        };
+        let mut selection = (DEFAULT_PROFILE.to_string(), Some(Scope::Binary));
         for builder in &builders {
             for profile in &builder.profiles {
                 profiles.insert(
@@ -292,28 +277,23 @@ impl ScopedConfig {
                 );
             }
             if let Some(name) = &builder.selection {
-                selection = ScopedValue {
-                    scope: builder.scope,
-                    value: name.clone(),
-                };
+                selection = (name.clone(), Some(builder.scope));
             }
         }
         if let Some(name) = cli_profile {
-            selection = ScopedValue {
-                scope: Scope::Cli,
-                value: name.to_string(),
-            };
+            selection = (name.to_string(), None);
         }
+        let (selected_name, selected_by) = selection;
 
         let profile = profiles
-            .get(&selection.value)
+            .get(&selected_name)
             .map(|sv| sv.value.clone())
             .ok_or_else(|| {
                 let known = profiles.keys().cloned().collect::<Vec<_>>().join(", ");
+                let by = selected_by.map_or("-p".to_string(), |s| s.to_string());
                 miette!(
-                    "profile `{}` (selected by {}) is not defined; known profiles: {known}",
-                    selection.value,
-                    selection.scope,
+                    "profile `{selected_name}` (selected by {by}) is not defined; \
+                     known profiles: {known}",
                 )
             })?;
 
@@ -340,7 +320,7 @@ impl ScopedConfig {
         Ok(Self {
             layers,
             profiles,
-            selection,
+            selected_by,
             profile,
         })
     }
@@ -768,10 +748,7 @@ mod tests {
         ScopedConfig {
             layers,
             profiles: BTreeMap::new(),
-            selection: ScopedValue {
-                scope: Scope::Binary,
-                value: DEFAULT_PROFILE.into(),
-            },
+            selected_by: Some(Scope::Binary),
             profile: Profile {
                 name: DEFAULT_PROFILE.into(),
                 agent: Agent::Pi,
@@ -977,7 +954,6 @@ mod tests {
         assert_eq!(Scope::Binary.to_string(), "binary");
         assert_eq!(Scope::User.to_string(), "user");
         assert_eq!(Scope::Project.to_string(), "project");
-        assert_eq!(Scope::ProjectLocal.to_string(), "project-local");
     }
 
     fn layer(scope: Scope, mounts: Vec<ResolvedMount>) -> ConfigLayer {
@@ -1113,9 +1089,8 @@ mod tests {
         let config = ScopedConfig::load(Path::new("/tmp"), WS, None).unwrap();
         // Binary layer is always first (lowest precedence)
         assert_eq!(config.layers.first().unwrap().scope, Scope::Binary);
-        // No project layers
+        // No project layer
         assert!(!config.layers.iter().any(|l| l.scope == Scope::Project));
-        assert!(!config.layers.iter().any(|l| l.scope == Scope::ProjectLocal));
     }
 
     #[test]
@@ -1125,12 +1100,7 @@ mod tests {
         fs_err::create_dir_all(&ramekin_dir).unwrap();
         fs_err::write(
             ramekin_dir.join("config.kdl"),
-            "mounts {\nsource \"/tmp\"\ntarget \"/container/tmp\"\n}\n",
-        )
-        .unwrap();
-        fs_err::write(
-            ramekin_dir.join("config.local.kdl"),
-            "env {\nFOO \"bar\"\n}\n",
+            "mounts {\nsource \"/tmp\"\ntarget \"/container/tmp\"\n}\nenv {\nFOO \"bar\"\n}\n",
         )
         .unwrap();
 
@@ -1143,14 +1113,8 @@ mod tests {
             .expect("project layer missing");
         assert_eq!(project.mounts.len(), 1);
         assert_eq!(project.mounts[0].target, "/container/tmp");
-
-        let local = config
-            .layers
-            .iter()
-            .find(|l| l.scope == Scope::ProjectLocal)
-            .expect("project-local layer missing");
-        assert_eq!(local.env.len(), 1);
-        assert_eq!(local.env[0].name, "FOO");
+        assert_eq!(project.env.len(), 1);
+        assert_eq!(project.env[0].name, "FOO");
     }
 
     #[test]
@@ -1239,8 +1203,8 @@ mod tests {
     fn builtin_trivial_profile_is_the_default() {
         let dir = tempfile::tempdir().unwrap();
         let config = ScopedConfig::load(dir.path(), WS, None).unwrap();
-        assert_eq!(config.selection.value, "pi");
-        assert_eq!(config.selection.scope, Scope::Binary);
+        assert_eq!(config.profile.name, "pi");
+        assert_eq!(config.selected_by, Some(Scope::Binary));
         assert_eq!(config.agent(), Agent::Pi);
     }
 
@@ -1252,8 +1216,8 @@ mod tests {
         fs_err::write(ramekin_dir.join("config.kdl"), "profile \"claude\"\n").unwrap();
 
         let config = ScopedConfig::load(dir.path(), WS, None).unwrap();
-        assert_eq!(config.selection.value, "claude");
-        assert_eq!(config.selection.scope, Scope::Project);
+        assert_eq!(config.profile.name, "claude");
+        assert_eq!(config.selected_by, Some(Scope::Project));
         assert_eq!(config.agent(), Agent::Claude);
     }
 
@@ -1265,8 +1229,9 @@ mod tests {
         fs_err::write(ramekin_dir.join("config.kdl"), "profile \"claude\"\n").unwrap();
 
         let config = ScopedConfig::load(dir.path(), WS, Some("pi")).unwrap();
-        assert_eq!(config.selection.value, "pi");
-        assert_eq!(config.selection.scope, Scope::Cli);
+        assert_eq!(config.profile.name, "pi");
+        // `-p` isn't a layer; selected_by is None.
+        assert_eq!(config.selected_by, None);
         assert_eq!(config.agent(), Agent::Pi);
     }
 
@@ -1339,21 +1304,11 @@ mod tests {
         let dir = tempfile::tempdir().unwrap();
         let ramekin_dir = dir.path().join(".ramekin");
         fs_err::create_dir_all(&ramekin_dir).unwrap();
+        // Redefine the builtin trivial `pi` profile from the project layer.
         fs_err::write(
             ramekin_dir.join("config.kdl"),
             r#"
-            profile "custom" {
-                agent "pi"
-                env { FOO "project" }
-            }
-            profile "custom"
-            "#,
-        )
-        .unwrap();
-        fs_err::write(
-            ramekin_dir.join("config.local.kdl"),
-            r#"
-            profile "custom" {
+            profile "pi" {
                 agent "claude"
             }
             "#,
@@ -1361,9 +1316,12 @@ mod tests {
         .unwrap();
 
         let config = ScopedConfig::load(dir.path(), WS, None).unwrap();
-        // Local layer's definition wins wholesale: agent changes, env gone.
+        // The default selection still names `pi`, but the project layer's
+        // definition wins wholesale: it now runs claude.
+        assert_eq!(config.profile.name, "pi");
+        assert_eq!(config.selected_by, Some(Scope::Binary));
         assert_eq!(config.agent(), Agent::Claude);
-        assert!(config.profile.env.is_empty());
+        assert_eq!(config.profiles.get("pi").unwrap().scope, Scope::Project);
     }
 
     #[test]
diff --git a/src/main.rs b/src/main.rs
index 0fceaac..8be7372 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -552,11 +552,14 @@ impl Ramekin {
 
         println!();
         println!("Profile");
+        let selected_by = self
+            .config
+            .selected_by
+            .map_or("-p".to_string(), |scope| scope.to_string());
         println!(
-            "  {} (agent {}, selected by {})",
+            "  {} (agent {}, selected by {selected_by})",
             self.config.profile.name,
             self.config.agent(),
-            self.config.selection.scope,
         );
         for (name, sv) in &self.config.profiles {
             let marker = if *name == self.config.profile.name {