supercode_harness/
context_injection.rs1use crate::config::{Config, ContextInjectionBlock};
30use crate::modules::ModuleId;
31
32pub fn builtin_blocks(config: &Config) -> Vec<ContextInjectionBlock> {
39 let mut blocks = Vec::new();
40 let active = |id: ModuleId| config.module_registry && config.module_activation.is_active(id);
41
42 if active(ModuleId::Todos) {
43 blocks.push(ContextInjectionBlock::new(
44 "Task list",
45 "A persistent task list is available through the plan/todo tool. Keep it current: \
46 write the plan out before starting multi-step work, mark each step completed as \
47 you finish it, and add work you discover along the way. The list survives \
48 compaction, so it is the durable record of where this session is.",
49 ));
50 }
51 if active(ModuleId::PlanMode) {
52 blocks.push(ContextInjectionBlock::new(
53 "Plan mode",
54 "This session can enter a read-only planning mode. While it is active, do not edit \
55 files, write files, or run state-changing commands — investigate, then present the \
56 plan and wait for it to be accepted.",
57 ));
58 }
59 if !config.permissions_protected_paths.is_empty() {
60 blocks.push(ContextInjectionBlock::new(
61 "Protected paths",
62 format!(
63 "Writes to these paths are never auto-approved and will stop for the user's \
64 decision: {}. Prefer a route that doesn't touch them.",
65 config.permissions_protected_paths.join(", ")
66 ),
67 ));
68 }
69 if active(ModuleId::ToolsBackground) {
70 blocks.push(ContextInjectionBlock::new(
71 "Background work",
72 "Long-running commands can be started in the background instead of blocking the \
73 turn. Start them detached, keep working, and read their output when it matters — \
74 never sit on a foreground command waiting for it to finish.",
75 ));
76 }
77 blocks
78}
79
80pub fn blocks(config: &Config, spliced: &[ContextInjectionBlock]) -> Vec<ContextInjectionBlock> {
84 if !config.context_injections {
85 return Vec::new();
86 }
87 let mut out = builtin_blocks(config);
88 out.extend(config.context_injection_blocks.iter().cloned());
89 out.extend(spliced.iter().cloned());
90 out
91}
92
93pub fn render(blocks: &[ContextInjectionBlock]) -> String {
97 let mut out = String::new();
98 for block in blocks {
99 out.push_str(&format!("\n\n# {}\n{}", block.name, block.content));
100 }
101 out
102}
103
104pub fn assemble(config: &Config, spliced: &[ContextInjectionBlock]) -> String {
107 render(&blocks(config, spliced))
108}
109
110#[cfg(test)]
111mod tests {
112 use super::*;
113 use crate::configfile::{resolve, ResolveOptions};
114
115 fn resolved(preset: &str) -> Config {
116 let toml = crate::presets::lookup(preset).unwrap();
117 resolve(toml, None, &ResolveOptions { strict: true })
118 .unwrap_or_else(|e| panic!("{preset} resolves: {e}"))
119 .config
120 }
121
122 #[test]
124 fn the_gate_off_contributes_nothing() {
125 let mut config = resolved("cc-parity");
126 config.context_injections = false;
127 assert!(assemble(&config, &[]).is_empty());
128 }
129
130 #[test]
133 fn both_presets_arm_real_builtin_blocks() {
134 for preset in ["cc-parity", "cx-parity"] {
135 let config = resolved(preset);
136 assert!(
137 config.context_injections,
138 "{preset} must set core.context_injections"
139 );
140 let blocks = blocks(&config, &[]);
141 assert!(
142 !blocks.is_empty(),
143 "{preset}: the registry produced no blocks"
144 );
145 let names: Vec<&str> = blocks.iter().map(|b| b.name.as_str()).collect();
147 assert!(names.contains(&"Task list"), "{preset}: {names:?}");
148 assert!(names.contains(&"Protected paths"), "{preset}: {names:?}");
149 }
150 }
151
152 #[test]
155 fn builtin_blocks_track_the_module_set() {
156 let cc: Vec<String> = blocks(&resolved("cc-parity"), &[])
157 .into_iter()
158 .map(|b| b.name)
159 .collect();
160 let cx: Vec<String> = blocks(&resolved("cx-parity"), &[])
161 .into_iter()
162 .map(|b| b.name)
163 .collect();
164 assert!(cc.iter().any(|n| n == "Plan mode"));
165 assert!(
166 !cx.iter().any(|n| n == "Plan mode"),
167 "cx-parity has plan_mode off"
168 );
169 }
170
171 #[test]
173 fn three_sources_splice_in_order() {
174 let mut config = resolved("cc-parity");
175 config.context_injection_blocks = vec![ContextInjectionBlock::new("User", "user body")];
176 let spliced = [ContextInjectionBlock::new("Spliced", "spliced body")];
177 let text = assemble(&config, &spliced);
178 let builtin_at = text.find("# Task list").unwrap();
179 let user_at = text.find("# User").unwrap();
180 let spliced_at = text.find("# Spliced").unwrap();
181 assert!(builtin_at < user_at && user_at < spliced_at);
182 assert!(text.contains("spliced body"));
183 }
184}