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
//! The one place trusty-mpm resolves the agent roster and harness docs (#9011).
//!
//! Why: since #9011 the 43 agents and the four harness-understanding docs are
//! instructional content (ADR-0064), not compiled in. Every consumer — `tm
//! install`, the agent source freshness check, pm-guard, the framework
//! manifest, the SM prompt — must resolve them the same way, and a missing
//! source must fail loud in each of them.
//! What: [`agent_roster`] and [`harness_doc`] resolve the dev override from the
//! cwd (a trusty-tools checkout wins), else the bundle installed by `tm content
//! update`. The framework-content resolvers fetch that bundle on first use
//! (#9396) — only on the session-composition and `tm install` paths; every
//! read-only caller (doctor, retirement, savings) resolves with
//! [`Fetch::Never`]. Every failure is an [`AgentContentError`] naming the fix.
//! Functions tests drive hermetically take an [`AgentRoster`] parameter instead.
//! Test: `agent_roster_in_an_empty_cache_is_not_installed`,
//! `the_checkout_roster_resolves_from_inside_the_checkout`.
use std::path::Path;
pub use trusty_agents_common::agent_content::{AgentContentError, AgentRoster, DevOverride};
use trusty_agents_common::agent_content::{
ResolvedContent, checkout_content, resolve_content, resolve_content_in,
};
use trusty_agents_common::harness_doc::HarnessDoc;
use trusty_common::content::find_dev_checkout;
use crate::content::first_use::resolve_or_fetch_at;
/// Whether a resolver may fetch the content release when nothing is
/// installed (#9396).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Fetch {
/// Fetch once on first use: session composition and `tm install`.
OnFirstUse,
/// Never touch the network: `tm doctor` and every read-only caller.
Never,
}
/// The content for work on `dir`, or on the cwd when `dir` is `None`.
///
/// Why: one rule for every caller, so a launch, its roster and the doctor
/// row that predicts the launch cannot resolve different sources (#9396).
/// What: [`resolve_for_in`] with this process's cwd and the default cache.
pub fn resolve_for(dir: Option<&Path>, fetch: Fetch) -> Result<ResolvedContent, AgentContentError> {
let cwd = std::env::current_dir().ok();
let cache = trusty_common::content::default_cache_dir();
resolve_for_in(dir, cwd.as_deref(), cache.as_deref(), fetch)
}
/// [`resolve_for`] with the cwd and the cache named.
///
/// What: the trusted checkout enclosing `dir`, else the one enclosing `cwd`,
/// else the installed bundle in `cache`. A checkout that is found is the
/// source; a failure reading it is returned, never answered by another
/// source. With no checkout, [`Fetch::OnFirstUse`] fetches the release when
/// no lock is installed; [`Fetch::Never`] answers `NotInstalled`.
/// [`AgentContentError::NoCacheDir`] when no checkout serves and `cache` is
/// `None`.
/// Test: `a_managed_workspace_resolves_the_cache_once_a_bundle_lands`,
/// `the_launch_roster_comes_from_the_launch_content`,
/// `the_content_row_never_fetches`.
pub fn resolve_for_in(
dir: Option<&Path>,
cwd: Option<&Path>,
cache: Option<&Path>,
fetch: Fetch,
) -> Result<ResolvedContent, AgentContentError> {
let checkout = dir
.and_then(find_dev_checkout)
.or_else(|| cwd.and_then(find_dev_checkout));
if let Some(root) = checkout {
return checkout_content(&root);
}
let cache = cache.ok_or(AgentContentError::NoCacheDir)?;
match fetch {
Fetch::OnFirstUse => resolve_or_fetch_at(cache, DevOverride::Off),
Fetch::Never => resolve_content_in(cache, DevOverride::Off),
}
}
/// The PM content and the agent roster of one launch, read from one source.
#[derive(Debug)]
pub struct LaunchContent {
/// The skills and instructions.
pub framework: FrameworkContent,
/// The agent roster of the same source, or why it does not load.
pub roster: Result<AgentRoster, AgentContentError>,
}
impl LaunchContent {
/// Loads both from `resolved`; only a framework failure is an error.
///
/// Test: `the_launch_roster_comes_from_the_launch_content`.
pub fn load(resolved: &ResolvedContent) -> Result<Self, AgentContentError> {
Ok(Self {
framework: FrameworkContent::load(resolved)?,
roster: AgentRoster::load(resolved),
})
}
}
/// The content and roster for a launch of `dir` (#9396).
///
/// Why: the roster used to resolve from the process cwd while the content
/// resolved for the project, so one launch could mix two sources.
/// What: [`resolve_for`] `dir` with [`Fetch::OnFirstUse`], then
/// [`LaunchContent::load`].
pub fn launch_content_for(dir: &Path) -> Result<LaunchContent, AgentContentError> {
LaunchContent::load(&resolve_for(Some(dir), Fetch::OnFirstUse)?)
}
/// [`framework_content_for`], or [`framework_content`] for `None`, that never
/// fetches: `tm doctor` and the read-only callers (#9396).
pub fn framework_content_local(dir: Option<&Path>) -> Result<FrameworkContent, AgentContentError> {
FrameworkContent::load(&resolve_for(dir, Fetch::Never)?)
}
pub use crate::core::framework_content::FrameworkContent;
/// `DetectFrom(cwd)`, or `Off` when the cwd cannot be read.
pub fn dev_override() -> DevOverride {
match std::env::current_dir() {
Ok(cwd) => DevOverride::DetectFrom(cwd),
Err(_) => DevOverride::Off,
}
}
/// The agent roster from the checkout enclosing the cwd, else the installed
/// bundle in `~/.trusty-mpm/content`.
pub fn agent_roster() -> Result<AgentRoster, AgentContentError> {
AgentRoster::load(&resolve_content(dev_override())?)
}
/// [`agent_roster`] against an explicit cache directory and override.
pub fn agent_roster_in(
cache_dir: &Path,
dev: DevOverride,
) -> Result<AgentRoster, AgentContentError> {
AgentRoster::load(&resolve_content_in(cache_dir, dev)?)
}
/// The roster for a daemon query about `cwd`, or `None` (#9011).
///
/// Why: the daemon's own cwd says nothing about the project a query names, so
/// a dispatch from inside a trusty-tools checkout must see that checkout's
/// roster, exactly as `tm hook --pm-guard` running there does.
/// What: the checkout enclosing `cwd`, else the installed bundle; failing
/// both, [`agent_roster`] (the checkout enclosing the daemon's own cwd, the
/// resolution this replaced). A content error is logged at WARN and answered
/// `None`, which makes the shared-tree classifiers fail closed.
/// Test: `the_query_roster_resolves_from_the_query_cwd`.
pub fn agent_roster_for_query(cwd: &Path) -> Option<AgentRoster> {
let resolved = resolve_content(DevOverride::DetectFrom(cwd.to_path_buf()))
.and_then(|content| AgentRoster::load(&content))
.or_else(|_| agent_roster());
resolved
.inspect_err(|err| {
tracing::warn!(
cwd = %cwd.display(),
"agent roster unavailable for this query, so every agent counts as a writer: {err}"
);
})
.ok()
}
/// The harness-understanding docs, resolved like [`agent_roster`].
pub fn harness_doc() -> Result<HarnessDoc, AgentContentError> {
HarnessDoc::load(&resolve_content(dev_override())?)
}
/// trusty-mpm's skills and instructions, resolved like [`agent_roster`] (#9012).
///
/// #9396: with nothing installed and no checkout, the content release is
/// fetched once first ([`Fetch::OnFirstUse`]).
pub fn framework_content() -> Result<FrameworkContent, AgentContentError> {
FrameworkContent::load(&resolve_for(None, Fetch::OnFirstUse)?)
}
/// [`agent_roster`], fetching the content release on first use (#9396): the
/// `tm install` gate, which reads the roster before anything else.
pub fn agent_roster_or_fetch() -> Result<AgentRoster, AgentContentError> {
AgentRoster::load(&resolve_for(None, Fetch::OnFirstUse)?)
}
/// [`framework_content`] against an explicit cache directory and override.
pub fn framework_content_in(
cache_dir: &Path,
dev: DevOverride,
) -> Result<FrameworkContent, AgentContentError> {
FrameworkContent::load(&resolve_content_in(cache_dir, dev)?)
}
/// The content for work on `dir` — a project being launched (#9012).
///
/// Why: a daemon-spawned launch runs with the daemon's cwd, which says nothing
/// about the project; a launch of a trusty-tools checkout must read that
/// checkout's content, as `tm launch` run inside it does.
/// What: [`resolve_for`] `dir` — the checkout enclosing `dir`, else the
/// one enclosing this process's cwd, else the installed bundle. #9396: with
/// no checkout and no lock, the content release is fetched once
/// ([`Fetch::OnFirstUse`]).
/// Test: `framework_content_resolves_from_the_named_dir`;
/// `missing_lock_fetches_the_release_once` (the fetch).
pub fn framework_content_for(dir: &Path) -> Result<FrameworkContent, AgentContentError> {
FrameworkContent::load(&resolve_for(Some(dir), Fetch::OnFirstUse)?)
}
/// Test helpers: the repository's own content, through `DevOverride::At`, so
/// no test depends on the cwd, HOME or an installed bundle.
#[cfg(test)]
pub(crate) mod test_support {
use std::path::PathBuf;
use trusty_agents_common::agent_content::checkout_content;
use super::{AgentRoster, HarnessDoc};
/// The trusty-tools checkout this test runs for, resolved at runtime.
///
/// #9298: never the compile-time `CARGO_MANIFEST_DIR`, which names the
/// checkout that BUILT the binary — another worktree under a shared target.
/// Test: `repo_root_follows_the_runtime_checkout_9298`.
pub(crate) fn repo_root() -> PathBuf {
trusty_common::test_harness::test_repo_root()
.expect("resolve the trusty-tools checkout (set TRUSTY_TEST_REPO_ROOT)")
}
/// The checkout's `content/agents` directory.
pub(crate) fn repo_agents_dir() -> PathBuf {
repo_root().join("content/agents")
}
/// The checkout's agent roster.
pub(crate) fn repo_roster() -> AgentRoster {
AgentRoster::load(&checkout_content(&repo_root()).expect("repo content"))
.expect("repo roster")
}
/// The checkout's harness docs.
pub(crate) fn repo_harness_doc() -> HarnessDoc {
HarnessDoc::load(&checkout_content(&repo_root()).expect("repo content"))
.expect("repo harness docs")
}
/// The checkout's skills and instructions (#9012).
pub(crate) fn repo_content() -> super::FrameworkContent {
super::FrameworkContent::load(&checkout_content(&repo_root()).expect("repo content"))
.expect("repo framework content")
}
/// [`repo_content`], loaded once per test binary: the prompt tests compose
/// hundreds of prompts, and each load reads ~200 files.
pub(crate) fn rc() -> &'static super::FrameworkContent {
static CONTENT: std::sync::OnceLock<super::FrameworkContent> = std::sync::OnceLock::new();
CONTENT.get_or_init(repo_content)
}
/// The checkout's bundled PM instruction package, parsed at load (#9012).
pub(crate) fn rc_package() -> &'static crate::core::instruction_package::InstructionPackage {
rc().pm_package()
}
}
#[cfg(test)]
mod tests {
use super::*;
/// #9011: no checkout and no lock is `NotInstalled`, naming the fix.
#[test]
fn agent_roster_in_an_empty_cache_is_not_installed() {
let cache = tempfile::tempdir().expect("tempdir");
let err = agent_roster_in(cache.path(), DevOverride::Off).expect_err("nothing installed");
assert!(
matches!(err, AgentContentError::NotInstalled { .. }),
"got {err:?}"
);
assert!(err.to_string().contains("tm content install"), "{err}");
}
/// #9011: a daemon query resolves from the cwd it names, not the daemon's.
#[test]
fn the_query_roster_resolves_from_the_query_cwd() {
let roster = agent_roster_for_query(&test_support::repo_root().join("crates/trusty-mpm"))
.expect("checkout roster from the query cwd");
assert_eq!(roster.len(), test_support::repo_roster().len());
}
/// #9012: a project inside the checkout reads the checkout's content even
/// with an empty cache.
#[test]
fn framework_content_resolves_from_the_named_dir() {
let content = framework_content_for(&test_support::repo_root().join("crates/trusty-mpm"))
.expect("checkout content from the named dir");
assert!(content.skill("skills/tm.md").is_some());
let cache = tempfile::tempdir().expect("tempdir");
let err = framework_content_in(cache.path(), DevOverride::Off).expect_err("nothing");
assert!(err.is_not_installed(), "got {err:?}");
}
/// From inside the checkout the dev override serves the working tree.
#[test]
fn the_checkout_roster_resolves_from_inside_the_checkout() {
let cache = tempfile::tempdir().expect("tempdir");
let roster = agent_roster_in(
cache.path(),
DevOverride::DetectFrom(test_support::repo_root().join("crates/trusty-mpm")),
)
.expect("checkout roster");
assert!(roster.get("BASE-AGENT.md").is_some());
assert_eq!(roster.len(), test_support::repo_roster().len());
}
}
#[cfg(test)]
#[path = "content_source_root_tests.rs"]
mod content_source_root_tests;
#[cfg(test)]
#[path = "content_source_launch_tests.rs"]
mod content_source_launch_tests;