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
//! The floor appended to a project output style (#8533, owner ruling 2026-09-25).
//!
//! Why: a project output style replaces the bundled style's voice, and with it
//! the PRIMARY DIRECTIVE and the "Communication — Write Plainly" rules. The PM
//! prompt's "Prose Style — Write Plainly" section points at that output-style
//! section, so under a project style the pointer named nothing. A project style
//! keeps its prose as authored; the floor is appended to it.
//! What: [`style_floor`] cuts the floor sections out of the bundled
//! `trusty-mpm` style — the one source, never a copy. [`delivered_style_text`]
//! is a style's body as delivered: a bundled style unchanged (each carries its
//! own floor), a project style folded on its own and followed by the floor.
//! For the native path, where Claude Code reads the style file itself,
//! [`native_style_id`] writes a composite style — the project prose, then the
//! floor — and names it in `outputStyle`, so a bare `claude` launch gets the
//! floor too; [`floor_for`] is the floor alone, for a tm launch whose composite
//! is not in place.
//! Test: `output_style_floor_tests.rs`.
use std::path::{Path, PathBuf};
use super::{ActiveStyle, PROJECT_STYLES_DIR, strip_frontmatter};
use crate::core::agent_manifest::{ManifestError, atomic_write};
use crate::core::claude_config::ClaudeConfigReader;
use crate::core::framework_content::FrameworkContent;
use crate::core::instruction_fold::{fold_block, step_fence};
use crate::core::instruction_pipeline::SECTION_SEPARATOR;
/// Headings of the bundled `trusty-mpm` style sections that form the floor.
pub const FLOOR_SECTIONS: [&str; 2] = [
"## 🔴 PRIMARY DIRECTIVE — MANDATORY DELEGATION",
"## Communication — Write Plainly",
];
/// Heading of the floor block appended to a project output style.
pub const STYLE_FLOOR_HEADING: &str =
"# trusty-mpm Floor (appended to the project output style; not overridable)";
/// Suffix of a generated composite style's id: `<id>.tm-floor`.
pub const COMPOSITE_STYLE_SUFFIX: &str = ".tm-floor";
/// The floor block: [`STYLE_FLOOR_HEADING`], then each [`FLOOR_SECTIONS`]
/// section as the bundled `trusty-mpm` style carries it.
///
/// Why: one source for the floor text — an edit to the bundled style reaches
/// every project style on the next launch.
/// What: each section runs from its heading to the next `#` or `##` heading,
/// cut from the `trusty-mpm` style in `content` (#9012: runtime content).
/// Test: `the_floor_is_cut_from_the_bundled_style`.
pub fn style_floor(content: &FrameworkContent) -> String {
let professional = crate::core::bundle::OUTPUT_STYLES[0].content(content);
let sections: Vec<&str> = FLOOR_SECTIONS
.iter()
.filter_map(|heading| section(professional, heading))
.collect();
format!("{STYLE_FLOOR_HEADING}\n\n{}", sections.join("\n\n"))
}
/// The `heading` section of `doc`, heading included, trimmed.
///
/// What: starts at a line equal to `heading` and ends before the next `#` or
/// `##` heading line. #8533: a line inside a fenced code block is neither, so a
/// `# comment` in a shell example does not cut the section short.
/// Test: `a_heading_like_line_inside_a_fence_does_not_end_the_section`.
fn section<'a>(doc: &'a str, heading: &str) -> Option<&'a str> {
let mut fence = None;
let mut start = None;
let mut offset = 0;
for line in doc.split_inclusive('\n') {
let text = line.trim_end_matches(['\n', '\r']);
let outside = !step_fence(&mut fence, text) && fence.is_none();
match start {
None if outside && text == heading => start = Some(offset),
Some(from) if outside && (text.starts_with("# ") || text.starts_with("## ")) => {
return Some(doc[from..offset].trim());
}
_ => {}
}
offset += line.len();
}
start.map(|from| doc[from..].trim())
}
/// The floor `style` needs appended: `Some` for a project style, `None` for a
/// bundled one, which carries its own PRIMARY DIRECTIVE and Write Plainly
/// sections.
///
/// Test: `the_bundled_styles_get_no_appended_floor`.
pub fn floor_for(content: &FrameworkContent, style: &ActiveStyle) -> Option<String> {
match style {
ActiveStyle::Bundled(_) => None,
ActiveStyle::Project { .. } => Some(style_floor(content)),
}
}
/// `style`'s body as delivered: frontmatter stripped, the floor appended to a
/// project style.
///
/// Why: the project prose must not be able to hide the floor. It is folded on
/// its own, as an override body is (#8533 finding 1), so a trailing unclosed
/// `<!--` ends with the prose; an open fence of any kind is closed.
/// What: a bundled style → its body. A project style → the folded prose, the
/// section separator, then [`style_floor`]; the floor alone when the fold
/// leaves no prose. The floor is appended whatever the prose claims, so a
/// heading spoofing the floor adds nothing and removes nothing.
/// Test: `a_project_style_is_delivered_as_its_prose_plus_the_floor_once`,
/// `the_floor_survives_an_unclosed_comment_or_fence_and_a_spoofed_heading`.
pub fn delivered_style_text(content: &FrameworkContent, style: &ActiveStyle) -> String {
let body = strip_frontmatter(style.text(content)).trim();
let Some(floor) = floor_for(content, style) else {
return body.to_string();
};
let prose = fold_block(body);
if prose.is_empty() {
floor
} else {
format!("{prose}{SECTION_SEPARATOR}{floor}")
}
}
/// The composite style id for project style `id`.
pub fn composite_style_id(id: &str) -> String {
format!("{id}{COMPOSITE_STYLE_SUFFIX}")
}
/// Whether `id` names a generated composite, which is never selectable itself.
pub fn is_composite_style_id(id: &str) -> bool {
id.ends_with(COMPOSITE_STYLE_SUFFIX)
}
/// Where project style `id`'s composite lives.
pub fn composite_path(project_dir: &Path, id: &str) -> PathBuf {
project_dir
.join(PROJECT_STYLES_DIR)
.join(format!("{}.md", composite_style_id(id)))
}
/// The frontmatter lines of `content`, without the `---` delimiters.
fn frontmatter(content: &str) -> &str {
let Some(rest) = content.trim_start().strip_prefix("---") else {
return "";
};
rest.find("\n---").map_or("", |end| rest[..end].trim())
}
/// The composite style file for `style`: `None` for a bundled style.
///
/// Why (#8533): a bare `claude` launch loads the style `outputStyle` names and
/// no appended prompt, so a project style file alone lost the floor there.
/// What: frontmatter naming [`composite_style_id`] (the project's other
/// frontmatter lines kept, its `name:` replaced), then [`delivered_style_text`]
/// — the project prose, then the floor.
/// Test: `a_bare_claude_launch_loads_the_project_prose_then_the_floor`.
pub fn composite_style_text(framework: &FrameworkContent, style: &ActiveStyle) -> Option<String> {
let ActiveStyle::Project { id, content, .. } = style else {
return None;
};
let mut front = vec![
format!("name: {}", composite_style_id(id)),
format!("# Generated by tm from {id}.md with the trusty-mpm floor; edit {id}.md."),
];
front.extend(
frontmatter(content)
.lines()
.filter(|line| !line.starts_with("name:"))
.map(str::to_string),
);
Some(format!(
"---\n{}\n---\n\n{}\n",
front.join("\n"),
delivered_style_text(framework, style)
))
}
/// Write `style`'s composite into the project and return the id the
/// `outputStyle` settings key names.
///
/// Why (#8533): Claude Code reads the named style file itself, on a tm launch
/// and on a bare `claude` launch alike. Naming the composite makes the floor
/// part of what it reads.
/// What: a bundled style → its own id, nothing written. A project style → its
/// [`composite_style_text`] written to
/// `<project>/.claude/output-styles/<id>.tm-floor.md` when the file differs,
/// and the composite id. A symlink at that path is an error, never followed.
/// #8533: the file is published by a temp file renamed over the path, so a
/// link planted after the symlink check is replaced, never written through,
/// and a reader never sees a half-written composite.
/// Test: `a_bare_claude_launch_loads_the_project_prose_then_the_floor`,
/// `a_symlinked_composite_is_refused`,
/// `the_composite_is_replaced_by_rename_never_written_through`.
pub fn native_style_id(
content: &FrameworkContent,
project_dir: &Path,
style: &ActiveStyle,
) -> std::io::Result<String> {
let Some(text) = composite_style_text(content, style) else {
return Ok(style.id().to_string());
};
let path = composite_path(project_dir, style.id());
if std::fs::symlink_metadata(&path).is_ok_and(|m| m.file_type().is_symlink()) {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("{} is a symlink; not writing through it", path.display()),
));
}
if std::fs::read_to_string(&path).ok().as_deref() != Some(text.as_str()) {
atomic_write(&path, &text).map_err(|err| match err {
ManifestError::Io(io) => io,
other => std::io::Error::other(other.to_string()),
})?;
}
Ok(composite_style_id(style.id()))
}
/// The `outputStyle` Claude Code resolves from the project's own settings.
///
/// Why (#8533): Claude Code applies `.claude/settings.local.json` ahead of
/// `.claude/settings.json`. Reading only the plain file trusted a composite
/// that a local setting had displaced, and the floor went undelivered.
/// What: `settings.local.json`, then `settings.json`; the first that sets a
/// string `outputStyle` wins, as `daemon::doctor_output_style` resolves the
/// project scope. `None` when neither sets it, or when a layer is unreadable
/// or not JSON — an unknown value is never taken for the composite.
/// Test: `a_local_setting_naming_the_raw_style_keeps_the_floor_in_the_prompt`.
fn project_output_style(project_dir: &Path) -> Option<String> {
let paths = ClaudeConfigReader::paths_for_project(project_dir);
for path in [paths.project_local_settings, paths.project_settings] {
let raw = match std::fs::read_to_string(&path) {
Ok(raw) => raw,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => continue,
Err(_) => return None,
};
let value = serde_json::from_str::<serde_json::Value>(&raw).ok()?;
if let Some(id) = value.get("outputStyle").and_then(serde_json::Value::as_str) {
return Some(id.to_string());
}
}
None
}
/// Whether the project's effective `outputStyle` names `style`'s composite
/// and the composite on disk is current, so Claude Code delivers the floor
/// itself.
///
/// What: [`project_output_style`] equals [`composite_style_id`], and the file
/// at [`composite_path`] reads as [`composite_style_text`].
/// Test: `a_project_style_keeps_the_floor_with_and_without_native_support`,
/// `a_local_setting_naming_the_raw_style_keeps_the_floor_in_the_prompt`.
pub fn composite_is_active(
content: &FrameworkContent,
project_dir: &Path,
style: &ActiveStyle,
) -> bool {
let Some(text) = composite_style_text(content, style) else {
return false;
};
// #8533: the effective value across both project layers, not settings.json alone.
project_output_style(project_dir).as_deref() == Some(composite_style_id(style.id()).as_str())
&& std::fs::read_to_string(composite_path(project_dir, style.id()))
.ok()
.as_deref()
== Some(text.as_str())
}
#[cfg(test)]
#[path = "output_style_floor_tests.rs"]
mod tests;