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
//! trusty-mpm's own instructional content, read at runtime (#9012).
//!
//! Why: ADR-0064 makes skills, PM instruction sections, the instruction
//! package, the supervisor sections, output styles, the SM instructions and the
//! bundled docs runtime-only content. #9012 moved them out of the crate into
//! `content/` and dropped every `include_str!` of them, so each consumer reads
//! them from one loaded value instead of a compiled-in constant.
//! What: [`FrameworkContent::load`] reads every `skills/**` and
//! `instructions/**` file of one resolved source into memory and fails loud,
//! naming `tm content update`, when a file every process needs
//! ([`REQUIRED_INSTRUCTIONS`]) or the whole skills class is absent, or when the
//! PM instruction package does not parse and validate. Accessors borrow from
//! the loaded value; nothing here is global or cached.
//! Test: `the_repository_content_loads`, `a_source_without_skills_is_an_error`,
//! `a_source_missing_a_required_instruction_is_an_error`,
//! `a_source_whose_package_does_not_parse_is_an_error`.
use std::collections::BTreeMap;
use trusty_agents_common::agent_content::describe_source;
pub use trusty_agents_common::agent_content::{AgentContentError, ContentSource, ResolvedContent};
use crate::core::bundled_pm_package::{PM_PACKAGE_PATH, parse_bundled_package};
use crate::core::instruction_package::InstructionPackage;
/// The bundle class holding skills.
pub const SKILLS_CLASS: &str = "skills";
/// The bundle class holding instructions, output styles and SM instructions.
pub const INSTRUCTIONS_CLASS: &str = "instructions";
/// Instruction files (relative to `instructions/`) every process needs.
///
/// Why: a launch, a doctor run and an SM turn each read several of these; one
/// check at load turns a partial bundle into one named error instead of a
/// prompt quietly missing a section.
/// What: the 19 PM sections the package names, the self-improvement section,
/// the package and its schema, the eight supervisor sections, the four output
/// styles and the four SM instruction files. The bundled docs are not here:
/// only `tm install` reads them, and it requires them itself.
/// Test: `the_repository_content_loads`,
/// `a_source_missing_a_required_instruction_is_an_error`.
pub const REQUIRED_INSTRUCTIONS: &[&str] = &[
"pm-instruction-package.json",
"instruction-package.schema.json",
"sections/identity.md",
"sections/core.md",
"sections/pm-allowlist.md",
"sections/delegation-mechanics.md",
"sections/agent-routing.md",
"sections/subagent-re-engagement.md",
"sections/phases.md",
"sections/qa-gate.md",
"sections/git-file-tracking.md",
"sections/tickets-prs-releases.md",
"sections/messages-reports-sessions.md",
"sections/autonomous-execution.md",
"sections/memory.md",
"sections/search.md",
"sections/workflow.md",
"sections/agent-delegation.md",
"sections/enforcement.md",
"sections/non-overridable-rules.md",
"sections/framework-guaranteed-conventions.md",
"sections/prompt-self-improvement.md",
"supervisor/identity.md",
"supervisor/hard-limits.md",
"supervisor/relay-protocol.md",
"supervisor/evidence-labels.md",
"supervisor/decisions.md",
"supervisor/monitoring.md",
"supervisor/tool-priority.md",
"supervisor/prose-style.md",
"output-styles/trusty-mpm.md",
"output-styles/trusty-mpm-teacher.md",
"output-styles/trusty-mpm-research.md",
"output-styles/trusty-mpm-supervisor.md",
"sm_instructions/SM_INSTRUCTIONS.md",
"sm_instructions/SM_WORKFLOW.md",
"sm_instructions/SM_TOOLS.md",
"sm_instructions/BASE_SM.md",
];
/// The skills and instructions of one resolved content source.
///
/// Why: replaces the compiled-in skill table, section constants, package,
/// output styles and SM instructions with the same text, read from content.
/// What: skills keyed by bundle path (`skills/tm.md`), sorted; instructions
/// keyed by path relative to `instructions/` (`sections/core.md`). A loaded
/// value always has at least one skill, every [`REQUIRED_INSTRUCTIONS`] file
/// and a parsed, validated PM instruction package.
/// Test: `the_repository_content_loads`.
#[derive(Debug, Clone)]
pub struct FrameworkContent {
source: ContentSource,
skills: Vec<(String, String)>,
instructions: BTreeMap<String, String>,
pm_package: InstructionPackage,
}
impl FrameworkContent {
/// Reads every `skills/**` and `instructions/**` file of `content`.
///
/// # Errors
/// [`AgentContentError::Missing`] when the source has no skill or lacks a
/// [`REQUIRED_INSTRUCTIONS`] file; [`AgentContentError::Invalid`] when its
/// PM instruction package does not parse or validate (#9012: a content
/// release newer than this binary); [`AgentContentError::Content`] when a
/// listed file cannot be read.
pub fn load(content: &ResolvedContent) -> Result<Self, AgentContentError> {
let origin = describe_source(content.source());
let mut skills = Vec::new();
for key in content.list(SKILLS_CLASS)? {
let body = content.read_to_string(&key)?;
skills.push((key, body));
}
if skills.is_empty() {
return Err(AgentContentError::Missing {
origin,
path: format!("{SKILLS_CLASS}/"),
});
}
let prefix = format!("{INSTRUCTIONS_CLASS}/");
let mut instructions = BTreeMap::new();
for key in content.list(INSTRUCTIONS_CLASS)? {
let body = content.read_to_string(&key)?;
if let Some(rel) = key.strip_prefix(&prefix) {
instructions.insert(rel.to_string(), body);
}
}
if let Some(missing) = REQUIRED_INSTRUCTIONS
.iter()
.find(|rel| !instructions.contains_key(**rel))
{
return Err(AgentContentError::Missing {
origin,
path: format!("{prefix}{missing}"),
});
}
// #9012: validated here, once, so no launch composes from a package it
// cannot parse and no caller needs a degraded fallback for one.
let pm_package =
parse_bundled_package(&instructions).map_err(|reason| AgentContentError::Invalid {
origin,
path: format!("{prefix}{PM_PACKAGE_PATH}"),
reason,
})?;
Ok(Self {
source: content.source().clone(),
skills,
instructions,
pm_package,
})
}
/// Where the content came from.
pub fn source(&self) -> &ContentSource {
&self.source
}
/// The PM instruction package, parsed and validated at load.
pub(crate) fn pm_package(&self) -> &InstructionPackage {
&self.pm_package
}
/// The source named for an error message (`dev checkout <root>`, a tag).
pub fn origin(&self) -> String {
describe_source(&self.source)
}
/// Every skill file as `(bundle path, body)`, sorted by bundle path.
pub fn skills(&self) -> impl Iterator<Item = (&str, &str)> + '_ {
self.skills
.iter()
.map(|(path, body)| (path.as_str(), body.as_str()))
}
/// The body of skill file `path` (`skills/tm.md`), if present.
pub fn skill(&self, path: &str) -> Option<&str> {
self.skills
.iter()
.find(|(p, _)| p == path)
.map(|(_, body)| body.as_str())
}
/// The instruction file at `rel` (`sections/core.md`), if present.
pub fn instruction(&self, rel: &str) -> Option<&str> {
self.instructions.get(rel).map(String::as_str)
}
/// Every instruction path (relative to `instructions/`) under `prefix`
/// (`sections/`), sorted.
pub fn instruction_paths<'a>(&'a self, prefix: &'a str) -> impl Iterator<Item = &'a str> + 'a {
self.instructions
.keys()
.filter(move |k| k.starts_with(prefix))
.map(String::as_str)
}
/// The instruction file at `rel`, or [`AgentContentError::Missing`].
pub fn require_instruction(&self, rel: &str) -> Result<&str, AgentContentError> {
self.instruction(rel)
.ok_or_else(|| AgentContentError::Missing {
origin: self.origin(),
path: format!("{INSTRUCTIONS_CLASS}/{rel}"),
})
}
/// A [`REQUIRED_INSTRUCTIONS`] file. [`FrameworkContent::load`] refused a
/// source without it, so this is total for every listed path.
///
/// # Panics
/// In debug builds, when `rel` is not in [`REQUIRED_INSTRUCTIONS`] — a
/// programming error the test suite reaches on every call site.
pub fn required(&self, rel: &'static str) -> &str {
debug_assert!(
REQUIRED_INSTRUCTIONS.contains(&rel),
"`{rel}` is not a required instruction"
);
self.instruction(rel).unwrap_or_default()
}
}
#[cfg(test)]
#[path = "framework_content_tests.rs"]
pub(crate) mod tests;