trusty-mpm 0.10.0

trusty-mpm: unified multi-agent orchestration platform (core, daemon, CLI, TUI, Telegram)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
//! Well-known filesystem paths for the trusty-mpm framework installation.
//!
//! Why: the installer, the daemon, and the file watcher all need a single,
//! consistent answer for "where does the framework live?" — hard-coding
//! `~/.trusty-mpm/...` in three places invites drift.
//! What: [`FrameworkPaths`] resolves the framework directory layout rooted at
//! `~/.trusty-mpm`, plus convenience accessors for the two files the daemon
//! reads directly (the optimizer policy and the framework instructions).
//! Test: `cargo test -p trusty-mpm-core paths` asserts the resolved root
//! contains `.trusty-mpm` and that the subdirectories nest correctly.

use std::path::{Path, PathBuf};

/// Directory name (under the user's home) that holds the framework install.
pub const FRAMEWORK_DIR_NAME: &str = ".trusty-mpm";

/// Resolved paths for a trusty-mpm framework installation.
///
/// Why: groups every framework path behind one value so callers pass a single
/// `FrameworkPaths` instead of recomputing joins.
/// What: the install root and each artifact subdirectory; build with
/// [`FrameworkPaths::default`] (home-relative) or [`FrameworkPaths::under`]
/// (for tests against a temp dir).
/// Test: `default_resolves_under_trusty_mpm`, `under_nests_subdirectories`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FrameworkPaths {
    /// `~/.trusty-mpm`
    pub root: PathBuf,
    /// `~/.trusty-mpm/framework`
    pub framework: PathBuf,
    /// `~/.trusty-mpm/framework/agents`
    pub agents: PathBuf,
    /// `~/.trusty-mpm/framework/skills`
    pub skills: PathBuf,
    /// `~/.trusty-mpm/framework/hooks`
    pub hooks: PathBuf,
    /// `~/.trusty-mpm/framework/instructions`
    pub instructions: PathBuf,
    /// `~/.trusty-mpm/registry`
    pub registry: PathBuf,
    /// `~/.claude/agents` — where Claude Code reads composed agent files.
    pub claude_agents: PathBuf,
    /// `~/.claude/skills` — where Claude Code reads skill files.
    pub claude_skills: PathBuf,
    /// The trusty-mpm source checkout root, if one could be located.
    ///
    /// Why: the `agents/` git submodule (`agents/agents/`, `agents/skills/`)
    /// is the preferred distribution source for agents and skills. It only
    /// exists in a source checkout, so this is `None` for a binary-only
    /// install — callers then fall back to the bundled assets.
    /// What: the directory holding `.git` discovered by walking the running
    /// binary's ancestors; `None` when no such directory is found.
    pub trusty_mpm_root: Option<PathBuf>,
}

/// Locate the trusty-mpm source checkout root.
///
/// Why: the `agents/` submodule lives at `<root>/agents/` and is only present
/// in a source checkout; resolving it requires finding that checkout from the
/// running binary's location.
/// What: walks the ancestors of the current executable's directory, returning
/// the first that contains a `.git` entry (the repository root). Returns `None`
/// when the executable path is unresolvable or no ancestor has `.git`.
/// Test: `locate_root_finds_git_ancestor`, `locate_root_none_without_git`.
fn locate_trusty_mpm_root(start: &Path) -> Option<PathBuf> {
    for ancestor in start.ancestors() {
        if ancestor.join(".git").exists() {
            return Some(ancestor.to_path_buf());
        }
    }
    None
}

impl FrameworkPaths {
    /// Resolve the framework layout rooted at the user's home directory.
    ///
    /// Why: production callers want `~/.trusty-mpm` without each one resolving
    /// the home directory itself.
    /// What: locates the home directory via the `dirs` crate, falling back to
    /// the current directory if it cannot be determined (e.g. a stripped CI
    /// environment) so the type is always constructible.
    /// Test: `default_resolves_under_trusty_mpm`.
    #[allow(clippy::should_implement_trait)] // Intentional: no meaningful Default without I/O.
    pub fn default() -> Self {
        let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
        Self::under(home)
    }

    /// Resolve the framework layout under an arbitrary base directory.
    ///
    /// Why: tests must exercise install / reload logic without touching the
    /// real `~/.trusty-mpm`; pointing `base` at a `tempfile::TempDir` keeps
    /// them hermetic.
    /// What: joins `<base>/.trusty-mpm` and derives every subdirectory from it.
    /// Test: `under_nests_subdirectories`.
    pub fn under(base: impl AsRef<Path>) -> Self {
        let base = base.as_ref();
        let root = base.join(FRAMEWORK_DIR_NAME);
        let framework = root.join("framework");
        let trusty_mpm_root = std::env::current_exe()
            .ok()
            .and_then(|exe| locate_trusty_mpm_root(&exe));
        Self {
            agents: framework.join("agents"),
            skills: framework.join("skills"),
            hooks: framework.join("hooks"),
            instructions: framework.join("instructions"),
            registry: root.join("registry"),
            claude_agents: base.join(".claude").join("agents"),
            claude_skills: base.join(".claude").join("skills"),
            trusty_mpm_root,
            framework,
            root,
        }
    }

    /// Path of the token-optimizer policy file (`hooks/optimizer.toml`).
    ///
    /// Why: the daemon reads this at startup and on file-change to build its
    /// `OptimizerConfig`.
    /// What: `hooks/optimizer.toml` under the framework root.
    /// Test: `optimizer_config_path_is_under_hooks`.
    pub fn optimizer_config(&self) -> PathBuf {
        self.hooks.join("optimizer.toml")
    }

    /// Path of the session-overseer policy file (`hooks/overseer.toml`).
    ///
    /// Why: the daemon reads this at startup to build its `OverseerConfig`;
    /// keeping the path next to [`optimizer_config`](Self::optimizer_config)
    /// means both framework hook policies resolve consistently.
    /// What: `hooks/overseer.toml` under the framework root.
    /// Test: `overseer_config_path_is_under_hooks`.
    pub fn overseer_config(&self) -> PathBuf {
        self.hooks.join("overseer.toml")
    }

    /// Path of the user-facing configuration file (`config.toml`).
    ///
    /// Why: all code that needs to read `~/.trusty-mpm/config.toml` should
    /// resolve the path through [`FrameworkPaths`] so the location stays
    /// canonical and tests can redirect it to a temp directory.
    /// What: `<root>/config.toml` — the top-level TOML file the user edits.
    /// Test: `config_toml_path_is_under_root`.
    pub fn config_toml(&self) -> PathBuf {
        self.root.join("config.toml")
    }

    /// Path of the framework launch instructions (`instructions/INSTRUCTIONS.md`).
    ///
    /// Why: launchers point new Claude Code sessions at this file; it is the
    /// framework artifact owned and overwritten by trusty-mpm on every install.
    /// What: `instructions/INSTRUCTIONS.md` under the framework root.
    /// Test: `instructions_path_is_under_instructions`.
    pub fn framework_instructions(&self) -> PathBuf {
        self.instructions.join("INSTRUCTIONS.md")
    }

    /// Path of the framework launch instructions — explicit-name alias.
    ///
    /// Why: the instruction merge pipeline refers to this file as
    /// `framework_instructions_path`; providing the alias keeps call sites
    /// readable without renaming the established [`framework_instructions`]
    /// accessor.
    /// What: delegates to [`framework_instructions`](Self::framework_instructions).
    /// Test: `framework_instructions_path_matches_accessor`.
    pub fn framework_instructions_path(&self) -> PathBuf {
        self.framework_instructions()
    }

    /// Path of the user-editable instruction stub (`instructions/CLAUDE.md`).
    ///
    /// Why: the installer seeds this stub once for project-specific notes;
    /// distinguishing it from `framework_instructions()` lets the installer
    /// avoid clobbering user edits on re-install.
    /// What: `instructions/CLAUDE.md` under the framework root.
    /// Test: `claude_stub_path_is_under_instructions`.
    pub fn claude_stub(&self) -> PathBuf {
        self.instructions.join("CLAUDE.md")
    }

    /// Directory holding the trusty-mpm agent *source* files.
    ///
    /// Why: the agent build pipeline reads `extends:`-bearing source agents
    /// from here and composes them before deployment. When the `agents/` git
    /// submodule is populated it is the authoritative, version-controlled
    /// source; only a binary-only install falls back to the bundled assets.
    /// What: prefers `<trusty-mpm-root>/agents/agents/` when that directory
    /// exists, otherwise returns `framework/agents` under the framework root.
    /// Test: `agent_source_dir_is_framework_agents`,
    /// `agent_source_dir_prefers_submodule`.
    pub fn agent_source_dir(&self) -> PathBuf {
        if let Some(root) = &self.trusty_mpm_root {
            let submodule = root.join("agents").join("agents");
            if submodule.is_dir() {
                return submodule;
            }
        }
        self.agents.clone()
    }

    /// Directory holding the trusty-mpm skill *source* files.
    ///
    /// Why: the skill deploy step reads `.md` skill files from here and copies
    /// them into `~/.claude/skills/`. As with agents, the `agents/` submodule
    /// is the authoritative source when present.
    /// What: prefers `<trusty-mpm-root>/agents/skills/` when that directory
    /// exists, otherwise returns `framework/skills` under the framework root.
    /// Test: `skill_source_dir_is_framework_skills`,
    /// `skill_source_dir_prefers_submodule`.
    pub fn skill_source_dir(&self) -> PathBuf {
        if let Some(root) = &self.trusty_mpm_root {
            let submodule = root.join("agents").join("skills");
            if submodule.is_dir() {
                return submodule;
            }
        }
        self.skills.clone()
    }

    /// Directory Claude Code reads composed agent files from (`~/.claude/agents`).
    ///
    /// Why: the deploy step writes inheritance-flattened agents here so Claude
    /// Code sees self-contained files with no `extends:` to interpret.
    /// What: `.claude/agents` under the same base this `FrameworkPaths` was
    /// resolved against (the user's home for [`default`](Self::default), the
    /// temp dir for [`under`](Self::under)).
    /// Test: `claude_agents_dir_is_dotclaude_agents`.
    pub fn claude_agents_dir(&self) -> PathBuf {
        self.claude_agents.clone()
    }

    /// Directory Claude Code reads skill files from (`~/.claude/skills`).
    ///
    /// Why: the skill deploy step writes `.md` skill files here so Claude Code
    /// can resolve them at session start.
    /// What: `.claude/skills` under the same base this `FrameworkPaths` was
    /// resolved against.
    /// Test: `claude_skills_dir_is_dotclaude_skills`.
    pub fn claude_skills_dir(&self) -> PathBuf {
        self.claude_skills.clone()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn default_resolves_under_trusty_mpm() {
        // The home-relative resolver must always land inside a `.trusty-mpm`
        // directory regardless of which home directory the host reports.
        let paths = FrameworkPaths::default();
        assert!(
            paths.root.ends_with(FRAMEWORK_DIR_NAME),
            "root = {}",
            paths.root.display()
        );
        assert!(paths.framework.starts_with(&paths.root));
    }

    #[test]
    fn under_nests_subdirectories() {
        // Given an explicit base, every subdirectory must nest under the
        // framework root with the documented layout.
        let paths = FrameworkPaths::under("/base");
        assert_eq!(paths.root, PathBuf::from("/base/.trusty-mpm"));
        assert_eq!(
            paths.framework,
            PathBuf::from("/base/.trusty-mpm/framework")
        );
        assert_eq!(
            paths.agents,
            PathBuf::from("/base/.trusty-mpm/framework/agents")
        );
        assert_eq!(
            paths.skills,
            PathBuf::from("/base/.trusty-mpm/framework/skills")
        );
        assert_eq!(
            paths.hooks,
            PathBuf::from("/base/.trusty-mpm/framework/hooks")
        );
        assert_eq!(
            paths.instructions,
            PathBuf::from("/base/.trusty-mpm/framework/instructions")
        );
        assert_eq!(paths.registry, PathBuf::from("/base/.trusty-mpm/registry"));
    }

    #[test]
    fn locate_root_finds_git_ancestor() {
        // A `.git` directory in an ancestor must be reported as the root.
        let tmp = tempfile::TempDir::new().unwrap();
        std::fs::create_dir(tmp.path().join(".git")).unwrap();
        let nested = tmp.path().join("crates").join("trusty-mpm-core");
        std::fs::create_dir_all(&nested).unwrap();
        let found = locate_trusty_mpm_root(&nested.join("dummy-exe"));
        assert_eq!(found.as_deref(), Some(tmp.path()));
    }

    #[test]
    fn locate_root_none_without_git() {
        // With no `.git` anywhere above, no root can be located.
        let tmp = tempfile::TempDir::new().unwrap();
        let nested = tmp.path().join("a").join("b");
        std::fs::create_dir_all(&nested).unwrap();
        assert_eq!(locate_trusty_mpm_root(&nested.join("exe")), None);
    }

    #[test]
    fn agent_source_dir_prefers_submodule() {
        // When the `agents/agents/` submodule directory exists under the
        // located root, it must win over the bundled `framework/agents` path.
        let tmp = tempfile::TempDir::new().unwrap();
        let submodule = tmp.path().join("agents").join("agents");
        std::fs::create_dir_all(&submodule).unwrap();
        let mut paths = FrameworkPaths::under("/base");
        paths.trusty_mpm_root = Some(tmp.path().to_path_buf());
        assert_eq!(paths.agent_source_dir(), submodule);
    }

    #[test]
    fn skill_source_dir_prefers_submodule() {
        // When the `agents/skills/` submodule directory exists under the
        // located root, it must win over the bundled `framework/skills` path.
        let tmp = tempfile::TempDir::new().unwrap();
        let submodule = tmp.path().join("agents").join("skills");
        std::fs::create_dir_all(&submodule).unwrap();
        let mut paths = FrameworkPaths::under("/base");
        paths.trusty_mpm_root = Some(tmp.path().to_path_buf());
        assert_eq!(paths.skill_source_dir(), submodule);
    }

    #[test]
    fn skill_source_dir_is_framework_skills() {
        // With no submodule, skill sources fall back to `framework/skills`.
        let mut paths = FrameworkPaths::under("/base");
        paths.trusty_mpm_root = None;
        assert_eq!(
            paths.skill_source_dir(),
            PathBuf::from("/base/.trusty-mpm/framework/skills")
        );
    }

    #[test]
    fn claude_skills_dir_is_dotclaude_skills() {
        // Skills must deploy to `.claude/skills` under the base.
        let paths = FrameworkPaths::under("/base");
        assert_eq!(
            paths.claude_skills_dir(),
            PathBuf::from("/base/.claude/skills")
        );
    }

    #[test]
    fn optimizer_config_path_is_under_hooks() {
        let paths = FrameworkPaths::under("/base");
        assert_eq!(
            paths.optimizer_config(),
            PathBuf::from("/base/.trusty-mpm/framework/hooks/optimizer.toml")
        );
    }

    #[test]
    fn overseer_config_path_is_under_hooks() {
        let paths = FrameworkPaths::under("/base");
        assert_eq!(
            paths.overseer_config(),
            PathBuf::from("/base/.trusty-mpm/framework/hooks/overseer.toml")
        );
    }

    #[test]
    fn instructions_path_is_under_instructions() {
        let paths = FrameworkPaths::under("/base");
        assert_eq!(
            paths.framework_instructions(),
            PathBuf::from("/base/.trusty-mpm/framework/instructions/INSTRUCTIONS.md")
        );
    }

    #[test]
    fn framework_instructions_path_matches_accessor() {
        // The explicit-name alias must resolve identically to the original.
        let paths = FrameworkPaths::under("/base");
        assert_eq!(
            paths.framework_instructions_path(),
            paths.framework_instructions()
        );
    }

    #[test]
    fn claude_stub_path_is_under_instructions() {
        // The user stub lives alongside the framework instructions but under
        // the `CLAUDE.md` name Claude Code reads by convention.
        let paths = FrameworkPaths::under("/base");
        assert_eq!(
            paths.claude_stub(),
            PathBuf::from("/base/.trusty-mpm/framework/instructions/CLAUDE.md")
        );
    }

    #[test]
    fn agent_source_dir_is_framework_agents() {
        // With no submodule, agent sources fall back to `framework/agents`.
        let mut paths = FrameworkPaths::under("/base");
        paths.trusty_mpm_root = None;
        assert_eq!(
            paths.agent_source_dir(),
            PathBuf::from("/base/.trusty-mpm/framework/agents")
        );
    }

    #[test]
    fn claude_agents_dir_is_dotclaude_agents() {
        // Composed agents must deploy to `.claude/agents` under the base —
        // sibling to `.trusty-mpm`, not nested within it.
        let paths = FrameworkPaths::under("/base");
        assert_eq!(
            paths.claude_agents_dir(),
            PathBuf::from("/base/.claude/agents")
        );
    }

    #[test]
    fn framework_instructions_and_stub_are_distinct() {
        // The framework artifact and the user stub must never resolve to the
        // same path, or the installer would overwrite user edits.
        let paths = FrameworkPaths::under("/base");
        assert_ne!(paths.framework_instructions(), paths.claude_stub());
    }

    #[test]
    fn config_toml_path_is_under_root() {
        // The user config file must live directly under the framework root, not
        // nested in a subdirectory, so it is easy to locate and edit.
        let paths = FrameworkPaths::under("/base");
        assert_eq!(
            paths.config_toml(),
            PathBuf::from("/base/.trusty-mpm/config.toml")
        );
    }
}