Skip to main content

qcode/profile/
guidance.rs

1//! The instruction files of a QCode high profile: the file each harness reads its standing
2//! instructions from in the workspace, and QCode's own section in it.
3//!
4//! Two parties write into these files, and the person is a third. graphify writes its own section
5//! and hooks itself, from inside the profile's container (`graphify <platform> install`); QCode
6//! writes only the section between [`BEGIN`] and [`END`], which tells the agent that it is one of
7//! several tabs of a workspace and how to reach the others. Everything outside the markers is left
8//! byte for byte as it was, and a file that already says what QCode would write is not written.
9//!
10//! The merging is plain text in, plain text out, so it is tested here without a disk; [`write()`]
11//! is the one function that touches the workspace folder.
12
13use std::io::ErrorKind;
14use std::path::Path;
15use std::sync::{Mutex, MutexGuard, PoisonError};
16
17use crate::profile::HarnessKind;
18
19/// The heading QCode's section stands under. It comes before [`BEGIN`], not after it, because of
20/// how graphify finds its own section again: measured in graphify 0.9.65, its section runs from
21/// its `## graphify` heading to the next line that starts with `## `, and a second install writes
22/// that whole stretch anew. An opening line right after graphify's section and before any heading
23/// of ours was taken as graphify's and dropped; a heading of ours ends graphify's section first.
24pub const HEADING: &str = "## QCode workspace";
25
26/// The line QCode's own words start after. An HTML comment, so a Markdown reader shows nothing.
27pub const BEGIN: &str = "<!-- qcode:begin -->";
28
29/// The line QCode's section ends with.
30pub const END: &str = "<!-- qcode:end -->";
31
32/// What QCode tells every agent of a workspace, in English, the language of the agents and of the
33/// bridge server's own words. The same text for every harness, because two of them read the same
34/// file (`AGENTS.md`) and Antigravity IDE reads the others' files as well as its own; so every
35/// sentence has to be true whichever agent reads it.
36pub const SECTION: &str = "\
37You are one of several coding agents open as tabs of one QCode workspace, and all of them work in the same folder, `/work`.
38- `list_tabs`, a tool of the MCP server `qcode`, says which tab you are (`you`: your id, title and workspace; its first line says it too) and names the other agent tabs of this workspace: the id to send to, the title, the coding tool, the profile, whether it reaches the network, and how many messages still wait to go into it. Ids count within this workspace; only `list_tabs` tells you yours.
39- `send_message` with `tab` (that id, or `all` for every other agent tab), `text` and `kind` hands them a message. `kind` is `info` (for their information, no answer needed; the default), `question` (you wait for their answer) or `report` (a task: you wait for a report of the result when they finish). If the person turned asking on, the first message from this tab to another waits until they allow it. A message is typed into that tab's prompt once nobody is typing there and its tool has stopped writing. The answer says whether it went in, still waits, or was refused, and why, tab by tab for `all`; `list_tabs` says later whether it arrived.
40- Write the message for an agent that has not seen your conversation. Share files through `/work`: put anything longer than 16000 characters in a file there and send its path.
41- A message from another tab arrives in your own prompt under two lines, in English \"Through QCode, from tab <id> «<title>» (<coding tool>) of this workspace: <kind>\" and how to answer. Answer a `question` with `send_message` to that tab with kind `info`; when you finish a `report` task, send its result the same way.
42- A window tab (Antigravity IDE) has no prompt. A message sent to it waits until its agent calls `check_inbox`, another tool of `qcode`, which hands over every message waiting for that tab with its sender's id and kind. If you are the agent in a window, call `check_inbox` at the start of every task and again when you finish one, and answer each message with `send_message`. Any agent may call it to take messages that wait for its own tab.
43- QCode refuses a message from a tab without the network to a tab with it, ends an exchange between tabs after 6 messages, and lets a tab send at most 5 messages a minute; a message to `all` counts once.
44";
45
46/// What opens Antigravity IDE's rule file when QCode creates it: a rule is loaded into every
47/// conversation only when it says `always_on`; without it the model decides whether to read it.
48const ANTIGRAVITY_RULE: &str = "\
49---
50trigger: always_on
51description: How to reach the other agent tabs of this QCode workspace.
52---
53
54";
55
56/// The file `harness` reads its standing instructions from, relative to the workspace folder.
57///
58/// Antigravity IDE reads every Markdown file in `.agents/rules/` (its own guide, inside the
59/// application, says so), so it gets a file of QCode's own there, beside graphify's.
60#[must_use]
61pub fn file(harness: HarnessKind) -> &'static str {
62    match harness {
63        HarnessKind::ClaudeCode => "CLAUDE.md",
64        HarnessKind::OpenCode | HarnessKind::Codex | HarnessKind::KimiCode | HarnessKind::QwenCode => "AGENTS.md",
65        HarnessKind::GeminiCli => "GEMINI.md",
66        HarnessKind::AntigravityIde => ".agents/rules/qcode.md",
67    }
68}
69
70/// The word graphify's own installers know `harness` by: `graphify <word> install` writes its
71/// section and hooks into the workspace, `graphify install --platform <word>` its skill into the
72/// home directory.
73#[must_use]
74pub fn platform(harness: HarnessKind) -> &'static str {
75    match harness {
76        HarnessKind::ClaudeCode => "claude",
77        HarnessKind::OpenCode => "opencode",
78        HarnessKind::GeminiCli => "gemini",
79        HarnessKind::Codex => "codex",
80        // graphify has no installer named after either; its `agents` installer writes its section
81        // into `AGENTS.md`, which both read, and its skill into `~/.agents/skills`, where both look
82        // for skills shared between tools (measured with graphify 0.9.66). Codex's installer
83        // would write the same section and leave a `.codex/hooks.json` in the workspace besides.
84        // Its `install --platform kimi` puts the skill in `~/.kimi/skills`, the folder of the older
85        // Python CLI of that name, which Kimi Code CLI does not read, and there is no `graphify
86        // kimi install` for the workspace at all.
87        HarnessKind::KimiCode | HarnessKind::QwenCode => "agents",
88        HarnessKind::AntigravityIde => "antigravity",
89    }
90}
91
92/// Where graphify's skill for `harness` lands, relative to the home directory, as measured: the
93/// image build checks that it is there.
94#[must_use]
95pub fn skill(harness: HarnessKind) -> &'static str {
96    match harness {
97        HarnessKind::ClaudeCode => ".claude/skills/graphify/SKILL.md",
98        HarnessKind::OpenCode => ".config/opencode/skills/graphify/SKILL.md",
99        HarnessKind::GeminiCli => ".gemini/skills/graphify/SKILL.md",
100        HarnessKind::Codex => ".codex/skills/graphify/SKILL.md",
101        HarnessKind::KimiCode | HarnessKind::QwenCode => ".agents/skills/graphify/SKILL.md",
102        HarnessKind::AntigravityIde => ".gemini/config/skills/graphify/SKILL.md",
103    }
104}
105
106/// The file graphify's workspace installer (`graphify <platform> install`) writes its section into,
107/// relative to the folder it runs in, as measured with graphify 0.9.67.
108#[must_use]
109pub fn graphify_file(harness: HarnessKind) -> &'static str {
110    match harness {
111        HarnessKind::ClaudeCode => "CLAUDE.md",
112        HarnessKind::GeminiCli => "GEMINI.md",
113        HarnessKind::OpenCode | HarnessKind::Codex | HarnessKind::KimiCode | HarnessKind::QwenCode => "AGENTS.md",
114        HarnessKind::AntigravityIde => ".agents/rules/graphify.md",
115    }
116}
117
118/// The file `harness` reads standing instructions from for every folder it works in, relative to
119/// the home directory: where QCode basic puts graphify's section, so that nothing is written into
120/// the person's own files.
121///
122/// Each was proven in a container with the harness's own package, apart from Antigravity IDE: a
123/// line only this file held was found in the request the harness sent to a server standing in
124/// for its provider (Claude Code 2.1.281, opencode 1.18.32, Gemini CLI 0.61.0, Kimi Code CLI
125/// 2.1.1, Qwen Code 0.24.4), and in the prompt `codex debug prompt-input` prints (Codex 0.156.1).
126/// Antigravity IDE 2.5.5's own guide, inside its language server, names `~/.gemini/config/` as
127/// the root of its global customizations and `rules/` under a root as where rules are read,
128/// with the frontmatter graphify writes; it was not run, since it needs a sign-in and a display.
129#[must_use]
130pub fn user_file(harness: HarnessKind) -> &'static str {
131    match harness {
132        HarnessKind::ClaudeCode => ".claude/CLAUDE.md",
133        HarnessKind::OpenCode => ".config/opencode/AGENTS.md",
134        HarnessKind::GeminiCli => ".gemini/GEMINI.md",
135        HarnessKind::Codex => ".codex/AGENTS.md",
136        HarnessKind::KimiCode => ".kimi-code/AGENTS.md",
137        HarnessKind::QwenCode => ".qwen/QWEN.md",
138        HarnessKind::AntigravityIde => ".gemini/config/rules/graphify.md",
139    }
140}
141
142/// What else graphify's workspace installer leaves in the folder it runs in, run in the home
143/// directory, and where each belongs so that the harness reads it for every folder: `(from, to)`,
144/// both relative to the home directory, with `to` empty for what has no place there and goes.
145///
146/// Claude Code's and Gemini CLI's hooks need nothing: the installer writes them into
147/// `.claude/settings.json` and `.gemini/settings.json` of the folder it runs in, which in the
148/// home directory are the harness's own settings, merged with what is there already (measured:
149/// the template's keys stay). Codex's `.codex/hooks.json` is the same. opencode's plugin reads
150/// the map of whichever folder opencode works in, and opencode 1.18.32 loads every file in
151/// `~/.config/opencode/plugins/` (its `debug config` lists it with the scope "global"); its
152/// `.opencode/opencode.json` only names the plugin by the folder's own path and goes.
153/// Antigravity's workflow is left out, with the folder its rule was in: `/graphify` is its
154/// skill's, which is in the home already.
155#[must_use]
156pub fn graphify_leftovers(harness: HarnessKind) -> &'static [(&'static str, &'static str)] {
157    match harness {
158        HarnessKind::OpenCode => {
159            &[(".opencode/plugins/graphify.js", ".config/opencode/plugins/graphify.js"), (".opencode", "")]
160        }
161        // Nothing else of the image lives under `~/.agents` of Antigravity IDE's home.
162        HarnessKind::AntigravityIde => &[(".agents", "")],
163        HarnessKind::ClaudeCode
164        | HarnessKind::GeminiCli
165        | HarnessKind::Codex
166        | HarnessKind::KimiCode
167        | HarnessKind::QwenCode => &[],
168    }
169}
170
171/// Why the instruction file of a harness could not be brought up to date. The tab starts all the
172/// same; the person is told.
173#[derive(Debug, Clone, PartialEq, Eq)]
174pub enum Unguided {
175    /// graphify could not set itself up in the workspace: what it, or the engine, said.
176    Graphify(String),
177    /// The file could not be read or written: the file, relative to the workspace, and the
178    /// system's words.
179    Unwritten(String, String),
180    /// The file has only one of QCode's two marker lines, so where QCode's section starts or ends
181    /// cannot be told from the person's text beside it. The file, relative to the workspace.
182    Broken(String),
183}
184
185/// QCode's whole section as it stands in a file: [`HEADING`], then [`SECTION`] between the markers.
186#[must_use]
187pub fn block() -> String {
188    format!("{HEADING}\n{BEGIN}\n{SECTION}{END}")
189}
190
191/// `existing` with QCode's section in it, for `harness`; `None` when it says so already and
192/// nothing is to be written.
193///
194/// A file that is not there is made with the section alone (for Antigravity IDE, with the lines
195/// that make it a rule loaded every time). A file without the section gets it at its end, after a
196/// blank line. A file with it has only what lies between the markers replaced, where it stands;
197/// and [`HEADING`] put back right before the opening line when it is not there, since without it
198/// graphify's next install would take the opening line for part of its own section.
199///
200/// # Errors
201///
202/// [`Unguided::Broken`] when the file has one marker line without the other.
203pub fn merge(existing: Option<&str>, harness: HarnessKind) -> Result<Option<String>, Unguided> {
204    let inner = format!("{BEGIN}\n{SECTION}{END}");
205    let Some(text) = existing else {
206        let opening = if harness == HarnessKind::AntigravityIde { ANTIGRAVITY_RULE } else { "" };
207        return Ok(Some(format!("{opening}{}\n", block())));
208    };
209    let broken = || Unguided::Broken(file(harness).to_owned());
210    let merged = match text.find(BEGIN) {
211        Some(start) => {
212            let length = text[start..].find(END).ok_or_else(broken)?;
213            let end = start + length + END.len();
214            let before = &text[..start];
215            let ours = if before.ends_with(&format!("{HEADING}\n")) { inner } else { block() };
216            format!("{before}{ours}{}", &text[end..])
217        }
218        None if text.contains(END) => return Err(broken()),
219        None => {
220            let gap = if text.is_empty() || text.ends_with("\n\n") {
221                ""
222            } else if text.ends_with('\n') {
223                "\n"
224            } else {
225                "\n\n"
226            };
227            format!("{text}{gap}{}\n", block())
228        }
229    };
230    Ok((merged != text).then_some(merged))
231}
232
233/// Brings QCode's section of `harness`'s instruction file in the workspace folder `code` up to
234/// date, and says whether anything was written.
235///
236/// The new text is written beside the file and moved over it, with the old file's permissions, so
237/// an agent reading at that moment finds the old file or the new one and never half of one. Runs
238/// on the disk, so it belongs on a background thread.
239///
240/// # Errors
241///
242/// [`Unguided`] when the file cannot be read or written, or its section cannot be found.
243pub fn write(code: &Path, harness: HarnessKind) -> Result<bool, Unguided> {
244    let name = file(harness);
245    let path = code.join(name);
246    let unwritten = |error: std::io::Error| Unguided::Unwritten(name.to_owned(), error.to_string());
247    let existing = match std::fs::read_to_string(&path) {
248        Ok(text) => Some(text),
249        Err(error) if error.kind() == ErrorKind::NotFound => None,
250        Err(error) => return Err(unwritten(error)),
251    };
252    let Some(text) = merge(existing.as_deref(), harness)? else { return Ok(false) };
253    if let Some(parent) = path.parent() {
254        std::fs::create_dir_all(parent).map_err(unwritten)?;
255    }
256    let file_name = path.file_name().map(|name| name.to_string_lossy().into_owned()).unwrap_or_default();
257    let beside = path.with_file_name(format!(".{file_name}.qcode-{}", std::process::id()));
258    let written = std::fs::write(&beside, &text)
259        .and_then(|()| match std::fs::metadata(&path) {
260            Ok(old) => std::fs::set_permissions(&beside, old.permissions()),
261            Err(error) if error.kind() == ErrorKind::NotFound => Ok(()),
262            Err(error) => Err(error),
263        })
264        .and_then(|()| std::fs::rename(&beside, &path));
265    if let Err(error) = written {
266        let _ = std::fs::remove_file(&beside);
267        return Err(unwritten(error));
268    }
269    Ok(true)
270}
271
272/// Held while one tab's graphify and QCode write a workspace's instruction files.
273///
274/// Two tabs coming up at once in one workspace would otherwise interleave: one reads the file,
275/// the other's graphify writes its section, the first writes the file back without it. Every tab
276/// of every workspace shares the one lock; what it guards takes well under a second.
277pub fn lock() -> MutexGuard<'static, ()> {
278    static WRITING: Mutex<()> = Mutex::new(());
279    WRITING.lock().unwrap_or_else(PoisonError::into_inner)
280}
281
282#[cfg(test)]
283mod tests {
284    use super::*;
285
286    const HARNESSES: [HarnessKind; 7] = HarnessKind::ALL;
287
288    /// What graphify wrote into a `CLAUDE.md` in the measured container, word for word.
289    const GRAPHIFY: &str = "## graphify\n\nThis project has a knowledge graph at graphify-out/ with god nodes, \
290                            community structure, and cross-file relationships.\n\nRules:\n- After modifying \
291                            code, run `graphify update .` to keep the graph current (AST-only, no API cost).\n";
292
293    fn ours(text: &str) -> &str {
294        let start = text.find(BEGIN).expect("the section is there");
295        let end = text.find(END).expect("and ends") + END.len();
296        &text[start..end]
297    }
298
299    #[test]
300    fn each_harness_gets_the_file_it_reads() {
301        assert_eq!(file(HarnessKind::ClaudeCode), "CLAUDE.md");
302        assert_eq!(file(HarnessKind::OpenCode), "AGENTS.md");
303        assert_eq!(file(HarnessKind::Codex), "AGENTS.md");
304        assert_eq!(file(HarnessKind::GeminiCli), "GEMINI.md");
305        assert_eq!(file(HarnessKind::KimiCode), "AGENTS.md");
306        assert_eq!(file(HarnessKind::QwenCode), "AGENTS.md");
307        assert_eq!(file(HarnessKind::AntigravityIde), ".agents/rules/qcode.md");
308        // Beside graphify's own rule, never in place of it.
309        assert_ne!(file(HarnessKind::AntigravityIde), ".agents/rules/graphify.md");
310    }
311
312    #[test]
313    fn each_harness_is_known_to_graphify_by_its_own_word() {
314        let words: Vec<&str> = HARNESSES.iter().map(|harness| platform(*harness)).collect();
315        assert_eq!(words, ["claude", "opencode", "gemini", "codex", "agents", "agents", "antigravity"]);
316    }
317
318    #[test]
319    fn a_missing_file_is_made_with_the_section_alone() {
320        let made = merge(None, HarnessKind::ClaudeCode).expect("mergeable").expect("written");
321        assert_eq!(made, format!("{HEADING}\n{BEGIN}\n{SECTION}{END}\n"));
322        assert_eq!(made, format!("{}\n", block()));
323    }
324
325    #[test]
326    fn antigravitys_file_is_made_as_a_rule_it_always_loads() {
327        let made = merge(None, HarnessKind::AntigravityIde).expect("mergeable").expect("written");
328        assert!(made.starts_with("---\ntrigger: always_on\n"), "{made}");
329        assert!(made.ends_with(&format!("{}\n", block())), "{made}");
330    }
331
332    #[test]
333    fn the_persons_text_and_graphifys_section_are_kept_byte_for_byte() {
334        let before = format!("# My project\n\nKeep tabs, not spaces.\n\n{GRAPHIFY}");
335        let after = merge(Some(&before), HarnessKind::ClaudeCode).expect("mergeable").expect("written");
336        assert!(after.starts_with(&before), "{after}");
337        assert_eq!(after, format!("{before}\n{}\n", block()));
338    }
339
340    #[test]
341    fn a_file_without_a_final_line_break_is_not_run_into() {
342        let after = merge(Some("# Mine"), HarnessKind::OpenCode).expect("mergeable").expect("written");
343        assert!(after.starts_with(&format!("# Mine\n\n{HEADING}\n{BEGIN}\n")), "{after}");
344    }
345
346    #[test]
347    fn an_older_section_is_replaced_where_it_stands() {
348        let before = format!("# Mine\n\n{HEADING}\n{BEGIN}\nwhat an older QCode said\n{END}\n\n{GRAPHIFY}");
349        let after = merge(Some(&before), HarnessKind::ClaudeCode).expect("mergeable").expect("written");
350        assert_eq!(after, format!("# Mine\n\n{}\n\n{GRAPHIFY}", block()), "only between the markers");
351        assert!(!after.contains("what an older QCode said"), "{after}");
352        assert_eq!(ours(&after), format!("{BEGIN}\n{SECTION}{END}"));
353    }
354
355    #[test]
356    fn a_section_that_lost_its_heading_gets_it_back_right_before_the_opening_line() {
357        let before = format!("{GRAPHIFY}\n{BEGIN}\n{SECTION}{END}\n");
358        let after = merge(Some(&before), HarnessKind::ClaudeCode).expect("mergeable").expect("written");
359        assert_eq!(after, format!("{GRAPHIFY}\n{}\n", block()));
360    }
361
362    #[test]
363    fn the_same_section_is_not_written_again() {
364        let once = merge(Some(&format!("# Mine\n\n{GRAPHIFY}")), HarnessKind::ClaudeCode)
365            .expect("mergeable")
366            .expect("written");
367        assert_eq!(merge(Some(&once), HarnessKind::ClaudeCode), Ok(None));
368    }
369
370    #[test]
371    fn the_markers_are_never_doubled() {
372        let mut text = merge(None, HarnessKind::GeminiCli).expect("mergeable").expect("written");
373        text.push_str("\nThe person's line after it.\n");
374        let again = merge(Some(&text), HarnessKind::GeminiCli).expect("mergeable");
375        assert_eq!(again, None, "nothing to change");
376        let older = text.replace(SECTION, "older\n");
377        let merged = merge(Some(&older), HarnessKind::GeminiCli).expect("mergeable").expect("written");
378        for line in [HEADING, BEGIN, END] {
379            assert_eq!(merged.matches(line).count(), 1, "{line}: {merged}");
380        }
381        assert!(merged.ends_with("\nThe person's line after it.\n"), "{merged}");
382    }
383
384    #[test]
385    fn one_marker_line_without_the_other_leaves_the_file_alone() {
386        let opening = format!("# Mine\n\n{BEGIN}\nhalf of it\n\nThe person's text.\n");
387        assert_eq!(merge(Some(&opening), HarnessKind::Codex), Err(Unguided::Broken("AGENTS.md".to_owned())));
388        let closing = format!("# Mine\n\nhalf of it\n{END}\n\nThe person's text.\n");
389        assert_eq!(merge(Some(&closing), HarnessKind::Codex), Err(Unguided::Broken("AGENTS.md".to_owned())));
390    }
391
392    /// What graphify 0.9.65 does to a file when it installs, as read from its own source
393    /// (`_replace_or_append_section`) and seen in a container: no `## graphify` line, and its
394    /// section is appended after a blank line; one, and the stretch from it to the next line
395    /// starting with `## ` is written anew, with one blank line on either side.
396    fn graphify_installs(text: &str) -> String {
397        let lines: Vec<&str> = text.split('\n').collect();
398        let section = GRAPHIFY.trim();
399        let Some(start) = lines.iter().rposition(|line| line.trim() == "## graphify") else {
400            return if text.trim().is_empty() {
401                format!("{section}\n")
402            } else {
403                format!("{}\n\n{section}\n", text.trim_end())
404            };
405        };
406        let end = (start + 1..lines.len()).find(|&j| lines[j].starts_with("## ")).unwrap_or(lines.len());
407        let head = lines[..start].join("\n");
408        let tail = lines[end..].join("\n");
409        let parts: Vec<&str> =
410            [head.trim_end(), section, tail.trim_start()].into_iter().filter(|part| !part.is_empty()).collect();
411        let out = parts.join("\n\n");
412        if out.ends_with('\n') { out } else { format!("{out}\n") }
413    }
414
415    #[test]
416    fn graphify_installing_again_after_qcode_keeps_qcodes_section_whole() {
417        // The order a tab comes up in, twice over: graphify, then QCode; graphify, then QCode.
418        for start in [None, Some("# Firefly\n\nThe person's own line.\n")] {
419            let first = graphify_installs(start.unwrap_or(""));
420            let ours = merge(Some(&first), HarnessKind::ClaudeCode).expect("mergeable").expect("written");
421            let second = graphify_installs(&ours);
422            assert!(second.contains(&block()), "graphify kept QCode's section whole:\n{second}");
423            assert_eq!(merge(Some(&second), HarnessKind::ClaudeCode), Ok(None), "and QCode has nothing to add");
424            assert_eq!(second, ours, "nothing moved at all");
425        }
426    }
427
428    #[test]
429    fn the_section_says_nothing_graphify_says() {
430        assert!(!SECTION.contains("graphify"), "graphify writes its own section");
431        assert!(SECTION.lines().count() < 15, "short: {} lines", SECTION.lines().count());
432    }
433
434    #[test]
435    fn the_section_names_the_bridges_tools_their_arguments_and_limits_as_the_code_has_them() {
436        use crate::bridge::protocol::MOST_TEXT;
437        use crate::bridge::rules::{MOST_HOPS, MOST_PER_WINDOW, RATE_WINDOW};
438        for word in [
439            "`list_tabs`",
440            "`send_message`",
441            "`check_inbox`",
442            "`tab`",
443            "`text`",
444            "`kind`",
445            "`info`",
446            "`question`",
447            "`report`",
448            "`all`",
449            "`you`",
450            "`qcode`",
451            "`/work`",
452        ] {
453            assert!(SECTION.contains(word), "{word}");
454        }
455        assert!(SECTION.contains(&format!("longer than {MOST_TEXT} characters")), "{SECTION}");
456        assert!(SECTION.contains(&format!("after {MOST_HOPS} messages")), "{SECTION}");
457        assert_eq!(RATE_WINDOW.as_secs(), 60, "the section says a minute");
458        assert!(SECTION.contains(&format!("at most {MOST_PER_WINDOW} messages a minute")), "{SECTION}");
459        assert!(SECTION.contains(&format!("MCP server `{}`", crate::bridge::SERVER_NAME)), "{SECTION}");
460    }
461
462    /// The line a delivered message starts with, as QCode types it in English, is the one the
463    /// section shows the agent.
464    #[test]
465    fn the_section_shows_the_line_a_delivered_message_starts_with() {
466        let english = include_str!("../../assets/locales/en.toml");
467        let handed =
468            english.lines().find_map(|line| line.strip_prefix("handed = \"")).expect("how a message is handed");
469        assert!(handed.starts_with("{header}\\n"), "the header comes first: {handed}");
470        let header = english
471            .lines()
472            .find_map(|line| line.strip_prefix("header = \""))
473            .and_then(|line| line.strip_suffix('"'))
474            .expect("the English line a delivered message starts with");
475        let shown = header
476            .replace("{tab}", "<id>")
477            .replace("{title}", "<title>")
478            .replace("{harness}", "<coding tool>")
479            .replace("{kind}", "<kind>");
480        assert!(SECTION.contains(&shown), "{shown}");
481    }
482
483    #[test]
484    fn writing_makes_keeps_and_leaves_the_file_on_disk() {
485        let folder = std::env::temp_dir().join(format!("qcode-guidance-{}", std::process::id()));
486        let _ = std::fs::remove_dir_all(&folder);
487        std::fs::create_dir_all(&folder).expect("a folder");
488        assert_eq!(write(&folder, HarnessKind::AntigravityIde), Ok(true));
489        let rule = std::fs::read_to_string(folder.join(".agents/rules/qcode.md")).expect("made");
490        assert!(rule.contains(SECTION));
491        std::fs::write(folder.join("CLAUDE.md"), "# Mine\n").expect("the person's file");
492        assert_eq!(write(&folder, HarnessKind::ClaudeCode), Ok(true));
493        let modified = std::fs::metadata(folder.join("CLAUDE.md")).and_then(|meta| meta.modified()).expect("a time");
494        assert_eq!(write(&folder, HarnessKind::ClaudeCode), Ok(false), "the same content is not written");
495        let again = std::fs::metadata(folder.join("CLAUDE.md")).and_then(|meta| meta.modified()).expect("a time");
496        assert_eq!(modified, again);
497        let text = std::fs::read_to_string(folder.join("CLAUDE.md")).expect("read");
498        assert!(text.starts_with("# Mine\n\n"), "{text}");
499        let leftovers: Vec<_> = std::fs::read_dir(&folder)
500            .expect("listed")
501            .filter_map(Result::ok)
502            .filter(|entry| entry.file_name().to_string_lossy().contains(".qcode-"))
503            .collect();
504        assert!(leftovers.is_empty(), "nothing is left beside the file");
505        let _ = std::fs::remove_dir_all(&folder);
506    }
507}