oh-my-codex 0.17.2 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. package/Cargo.lock +13 -5
  2. package/Cargo.toml +2 -1
  3. package/README.md +1 -0
  4. package/crates/omx-api/Cargo.toml +19 -0
  5. package/crates/omx-api/src/lib.rs +2940 -0
  6. package/crates/omx-api/src/main.rs +10 -0
  7. package/crates/omx-api/tests/cli.rs +558 -0
  8. package/crates/omx-explore/src/main.rs +4 -0
  9. package/crates/omx-sparkshell/src/codex_bridge.rs +437 -123
  10. package/crates/omx-sparkshell/src/exec.rs +4 -0
  11. package/crates/omx-sparkshell/src/main.rs +738 -29
  12. package/crates/omx-sparkshell/src/prompt.rs +25 -3
  13. package/crates/omx-sparkshell/src/redaction.rs +241 -0
  14. package/crates/omx-sparkshell/tests/execution.rs +479 -238
  15. package/dist/cli/__tests__/api.test.d.ts +2 -0
  16. package/dist/cli/__tests__/api.test.d.ts.map +1 -0
  17. package/dist/cli/__tests__/api.test.js +175 -0
  18. package/dist/cli/__tests__/api.test.js.map +1 -0
  19. package/dist/cli/__tests__/ask.test.js +72 -5
  20. package/dist/cli/__tests__/ask.test.js.map +1 -1
  21. package/dist/cli/__tests__/autoresearch-goal.test.js +14 -1
  22. package/dist/cli/__tests__/autoresearch-goal.test.js.map +1 -1
  23. package/dist/cli/__tests__/doctor-warning-copy.test.js +51 -0
  24. package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
  25. package/dist/cli/__tests__/explore.test.js +23 -0
  26. package/dist/cli/__tests__/explore.test.js.map +1 -1
  27. package/dist/cli/__tests__/index.test.js +123 -5
  28. package/dist/cli/__tests__/index.test.js.map +1 -1
  29. package/dist/cli/__tests__/launch-fallback.test.js +76 -0
  30. package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
  31. package/dist/cli/__tests__/package-bin-contract.test.js +4 -3
  32. package/dist/cli/__tests__/package-bin-contract.test.js.map +1 -1
  33. package/dist/cli/__tests__/question.test.js +45 -22
  34. package/dist/cli/__tests__/question.test.js.map +1 -1
  35. package/dist/cli/__tests__/setup-agents-overwrite.test.js +2 -0
  36. package/dist/cli/__tests__/setup-agents-overwrite.test.js.map +1 -1
  37. package/dist/cli/__tests__/setup-install-mode.test.js +138 -0
  38. package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
  39. package/dist/cli/__tests__/setup-scope.test.js +8 -2
  40. package/dist/cli/__tests__/setup-scope.test.js.map +1 -1
  41. package/dist/cli/__tests__/sparkshell-cli.test.js +5 -0
  42. package/dist/cli/__tests__/sparkshell-cli.test.js.map +1 -1
  43. package/dist/cli/__tests__/version-sync-contract.test.js +4 -0
  44. package/dist/cli/__tests__/version-sync-contract.test.js.map +1 -1
  45. package/dist/cli/__tests__/windows-popup-loop-contract.test.js +1 -1
  46. package/dist/cli/__tests__/windows-popup-loop-contract.test.js.map +1 -1
  47. package/dist/cli/api.d.ts +26 -0
  48. package/dist/cli/api.d.ts.map +1 -0
  49. package/dist/cli/api.js +153 -0
  50. package/dist/cli/api.js.map +1 -0
  51. package/dist/cli/doctor.d.ts.map +1 -1
  52. package/dist/cli/doctor.js +39 -4
  53. package/dist/cli/doctor.js.map +1 -1
  54. package/dist/cli/explore.d.ts +2 -0
  55. package/dist/cli/explore.d.ts.map +1 -1
  56. package/dist/cli/explore.js +43 -1
  57. package/dist/cli/explore.js.map +1 -1
  58. package/dist/cli/index.d.ts +10 -4
  59. package/dist/cli/index.d.ts.map +1 -1
  60. package/dist/cli/index.js +128 -10
  61. package/dist/cli/index.js.map +1 -1
  62. package/dist/cli/native-assets.d.ts +2 -1
  63. package/dist/cli/native-assets.d.ts.map +1 -1
  64. package/dist/cli/native-assets.js +1 -0
  65. package/dist/cli/native-assets.js.map +1 -1
  66. package/dist/cli/setup.d.ts.map +1 -1
  67. package/dist/cli/setup.js +6 -1
  68. package/dist/cli/setup.js.map +1 -1
  69. package/dist/cli/sparkshell.d.ts.map +1 -1
  70. package/dist/cli/sparkshell.js +20 -3
  71. package/dist/cli/sparkshell.js.map +1 -1
  72. package/dist/config/generator.d.ts.map +1 -1
  73. package/dist/config/generator.js +90 -0
  74. package/dist/config/generator.js.map +1 -1
  75. package/dist/hooks/__tests__/best-practice-research-skill.test.d.ts +2 -0
  76. package/dist/hooks/__tests__/best-practice-research-skill.test.d.ts.map +1 -0
  77. package/dist/hooks/__tests__/best-practice-research-skill.test.js +27 -0
  78. package/dist/hooks/__tests__/best-practice-research-skill.test.js.map +1 -0
  79. package/dist/hooks/__tests__/keyword-detector.test.js +11 -0
  80. package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
  81. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js +6 -0
  82. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js.map +1 -1
  83. package/dist/hooks/__tests__/prompt-guidance-wave-two.test.js +4 -0
  84. package/dist/hooks/__tests__/prompt-guidance-wave-two.test.js.map +1 -1
  85. package/dist/hooks/keyword-registry.d.ts.map +1 -1
  86. package/dist/hooks/keyword-registry.js +1 -0
  87. package/dist/hooks/keyword-registry.js.map +1 -1
  88. package/dist/hud/__tests__/reconcile.test.js +2 -2
  89. package/dist/hud/__tests__/reconcile.test.js.map +1 -1
  90. package/dist/hud/__tests__/tmux.test.js +23 -18
  91. package/dist/hud/__tests__/tmux.test.js.map +1 -1
  92. package/dist/hud/tmux.d.ts.map +1 -1
  93. package/dist/hud/tmux.js +7 -6
  94. package/dist/hud/tmux.js.map +1 -1
  95. package/dist/mcp/__tests__/bootstrap.test.js +75 -1
  96. package/dist/mcp/__tests__/bootstrap.test.js.map +1 -1
  97. package/dist/mcp/bootstrap.d.ts +3 -1
  98. package/dist/mcp/bootstrap.d.ts.map +1 -1
  99. package/dist/mcp/bootstrap.js +71 -2
  100. package/dist/mcp/bootstrap.js.map +1 -1
  101. package/dist/scripts/__tests__/codex-native-hook.test.js +737 -26
  102. package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
  103. package/dist/scripts/__tests__/notify-dispatcher.test.js +183 -1
  104. package/dist/scripts/__tests__/notify-dispatcher.test.js.map +1 -1
  105. package/dist/scripts/__tests__/smoke-packed-install.test.js +4 -1
  106. package/dist/scripts/__tests__/smoke-packed-install.test.js.map +1 -1
  107. package/dist/scripts/build-api.d.ts +2 -0
  108. package/dist/scripts/build-api.d.ts.map +1 -0
  109. package/dist/scripts/build-api.js +44 -0
  110. package/dist/scripts/build-api.js.map +1 -0
  111. package/dist/scripts/codex-native-hook.d.ts.map +1 -1
  112. package/dist/scripts/codex-native-hook.js +208 -8
  113. package/dist/scripts/codex-native-hook.js.map +1 -1
  114. package/dist/scripts/codex-native-pre-post.d.ts.map +1 -1
  115. package/dist/scripts/codex-native-pre-post.js +89 -24
  116. package/dist/scripts/codex-native-pre-post.js.map +1 -1
  117. package/dist/scripts/notify-dispatcher.js +88 -0
  118. package/dist/scripts/notify-dispatcher.js.map +1 -1
  119. package/dist/scripts/notify-hook/team-dispatch.d.ts.map +1 -1
  120. package/dist/scripts/notify-hook/team-dispatch.js +27 -9
  121. package/dist/scripts/notify-hook/team-dispatch.js.map +1 -1
  122. package/dist/scripts/notify-hook/team-leader-nudge.d.ts.map +1 -1
  123. package/dist/scripts/notify-hook/team-leader-nudge.js +26 -11
  124. package/dist/scripts/notify-hook/team-leader-nudge.js.map +1 -1
  125. package/dist/scripts/notify-hook/team-tmux-guard.d.ts +1 -0
  126. package/dist/scripts/notify-hook/team-tmux-guard.d.ts.map +1 -1
  127. package/dist/scripts/notify-hook/team-tmux-guard.js +38 -0
  128. package/dist/scripts/notify-hook/team-tmux-guard.js.map +1 -1
  129. package/dist/scripts/notify-hook/team-worker-stop.d.ts.map +1 -1
  130. package/dist/scripts/notify-hook/team-worker-stop.js +27 -14
  131. package/dist/scripts/notify-hook/team-worker-stop.js.map +1 -1
  132. package/dist/scripts/run-provider-advisor.js +9 -3
  133. package/dist/scripts/run-provider-advisor.js.map +1 -1
  134. package/dist/scripts/smoke-packed-install.d.ts +1 -1
  135. package/dist/scripts/smoke-packed-install.d.ts.map +1 -1
  136. package/dist/scripts/smoke-packed-install.js +2 -0
  137. package/dist/scripts/smoke-packed-install.js.map +1 -1
  138. package/dist/team/__tests__/runtime.test.js +2 -2
  139. package/dist/team/__tests__/runtime.test.js.map +1 -1
  140. package/dist/team/__tests__/tmux-session.test.js +153 -25
  141. package/dist/team/__tests__/tmux-session.test.js.map +1 -1
  142. package/dist/team/tmux-session.d.ts +1 -0
  143. package/dist/team/tmux-session.d.ts.map +1 -1
  144. package/dist/team/tmux-session.js +55 -10
  145. package/dist/team/tmux-session.js.map +1 -1
  146. package/dist/utils/__tests__/agents-md.test.js +45 -1
  147. package/dist/utils/__tests__/agents-md.test.js.map +1 -1
  148. package/dist/utils/agents-md.d.ts +2 -0
  149. package/dist/utils/agents-md.d.ts.map +1 -1
  150. package/dist/utils/agents-md.js +19 -0
  151. package/dist/utils/agents-md.js.map +1 -1
  152. package/dist/verification/__tests__/ci-rust-gates.test.js +85 -10
  153. package/dist/verification/__tests__/ci-rust-gates.test.js.map +1 -1
  154. package/dist/verification/__tests__/explore-harness-release-workflow.test.js +1 -0
  155. package/dist/verification/__tests__/explore-harness-release-workflow.test.js.map +1 -1
  156. package/package.json +4 -3
  157. package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
  158. package/plugins/oh-my-codex/skills/best-practice-research/SKILL.md +83 -0
  159. package/plugins/oh-my-codex/skills/deep-interview/SKILL.md +1 -0
  160. package/plugins/oh-my-codex/skills/ralplan/SKILL.md +1 -1
  161. package/prompts/researcher.md +15 -10
  162. package/skills/best-practice-research/SKILL.md +83 -0
  163. package/skills/deep-interview/SKILL.md +1 -0
  164. package/skills/ralplan/SKILL.md +1 -1
  165. package/src/scripts/__tests__/codex-native-hook.test.ts +810 -4
  166. package/src/scripts/__tests__/notify-dispatcher.test.ts +223 -1
  167. package/src/scripts/__tests__/smoke-packed-install.test.ts +8 -2
  168. package/src/scripts/build-api.ts +48 -0
  169. package/src/scripts/codex-native-hook.ts +262 -10
  170. package/src/scripts/codex-native-pre-post.ts +103 -24
  171. package/src/scripts/notify-dispatcher.ts +97 -0
  172. package/src/scripts/notify-hook/team-dispatch.ts +27 -8
  173. package/src/scripts/notify-hook/team-leader-nudge.ts +25 -11
  174. package/src/scripts/notify-hook/team-tmux-guard.ts +42 -0
  175. package/src/scripts/notify-hook/team-worker-stop.ts +24 -13
  176. package/src/scripts/run-provider-advisor.ts +11 -3
  177. package/src/scripts/smoke-packed-install.ts +2 -0
  178. package/templates/catalog-manifest.json +7 -0
@@ -1,4 +1,5 @@
1
1
  use crate::exec::CommandOutput;
2
+ use crate::redaction::redact_output;
2
3
  use std::borrow::Cow;
3
4
  use std::env;
4
5
  use std::path::Path;
@@ -111,8 +112,10 @@ fn command_basename(command: &str) -> Cow<'_, str> {
111
112
  pub fn build_summary_prompt(command: &[String], output: &CommandOutput) -> String {
112
113
  let executable = command.first().map(String::as_str).unwrap_or("unknown");
113
114
  let family = select_command_family(executable);
114
- let stdout_text = output.stdout_text();
115
- let stderr_text = output.stderr_text();
115
+ let redacted = redact_output(output);
116
+ let safe_output = &redacted.output;
117
+ let stdout_text = safe_output.stdout_text();
118
+ let stderr_text = safe_output.stderr_text();
116
119
  let stdout_lines = count_lines(&stdout_text);
117
120
  let stderr_lines = count_lines(&stderr_text);
118
121
  let stdout_excerpt = truncate_for_prompt(&stdout_text, "stdout");
@@ -141,7 +144,7 @@ pub fn build_summary_prompt(command: &[String], output: &CommandOutput) -> Strin
141
144
  family_pattern = family.pattern,
142
145
  family_description = family.description,
143
146
  family_what_it_does = family.what_it_does,
144
- exit_code = output.exit_code(),
147
+ exit_code = safe_output.exit_code(),
145
148
  stdout_lines = stdout_lines,
146
149
  stdout_bytes = stdout_text.len(),
147
150
  stderr_lines = stderr_lines,
@@ -308,6 +311,25 @@ mod tests {
308
311
  assert!(prompt.contains("<<<STDERR"));
309
312
  }
310
313
 
314
+ #[test]
315
+ fn prompt_redacts_secret_like_stdout_and_stderr_before_embedding() {
316
+ let output = CommandOutput {
317
+ status: ok_status(),
318
+ stdout: b"API_TOKEN=super-secret\nsk-prod-secret\nvisible\n".to_vec(),
319
+ stderr: b"Authorization: Bearer bearer-secret\nghp_secret\n".to_vec(),
320
+ };
321
+
322
+ let prompt = build_summary_prompt(&["env".into()], &output);
323
+
324
+ assert!(prompt.contains("API_TOKEN=[REDACTED]"));
325
+ assert!(prompt.contains("Authorization: Bearer [REDACTED]"));
326
+ assert!(prompt.contains("visible"));
327
+ assert!(!prompt.contains("super-secret"));
328
+ assert!(!prompt.contains("sk-prod-secret"));
329
+ assert!(!prompt.contains("bearer-secret"));
330
+ assert!(!prompt.contains("ghp_secret"));
331
+ }
332
+
311
333
  #[test]
312
334
  fn prompt_truncates_large_streams_before_embedding_them() {
313
335
  let _guard = env_lock();
@@ -0,0 +1,241 @@
1
+ use crate::exec::CommandOutput;
2
+
3
+ #[derive(Debug, Clone)]
4
+ pub(crate) struct RedactedOutput {
5
+ pub(crate) output: CommandOutput,
6
+ pub(crate) count: usize,
7
+ }
8
+
9
+ pub(crate) fn redact_output(output: &CommandOutput) -> RedactedOutput {
10
+ let (stdout, stdout_count) = redact_bytes(&output.stdout);
11
+ let (stderr, stderr_count) = redact_bytes(&output.stderr);
12
+ RedactedOutput {
13
+ output: CommandOutput {
14
+ status: output.status,
15
+ stdout,
16
+ stderr,
17
+ },
18
+ count: stdout_count + stderr_count,
19
+ }
20
+ }
21
+
22
+ fn redact_bytes(bytes: &[u8]) -> (Vec<u8>, usize) {
23
+ let (text, count) = redact_text(&String::from_utf8_lossy(bytes));
24
+ (text.into_bytes(), count)
25
+ }
26
+
27
+ fn redact_text(text: &str) -> (String, usize) {
28
+ let mut count = 0;
29
+ let mut lines = Vec::new();
30
+ for line in text.lines() {
31
+ let mut redacted = line.to_string();
32
+ count += redact_authorization_headers(&mut redacted);
33
+ count += redact_key_value_secrets(&mut redacted);
34
+ count += redact_secret_markers(&mut redacted);
35
+ lines.push(redacted);
36
+ }
37
+ let mut rendered = lines.join("\n");
38
+ if text.ends_with('\n') {
39
+ rendered.push('\n');
40
+ }
41
+ (rendered, count)
42
+ }
43
+
44
+ fn redact_authorization_headers(text: &mut String) -> usize {
45
+ let mut count = 0;
46
+ let mut search_start = 0;
47
+ while search_start < text.len() {
48
+ let Some(relative_start) = text[search_start..]
49
+ .to_ascii_lowercase()
50
+ .find("authorization: bearer ")
51
+ else {
52
+ break;
53
+ };
54
+ let start = search_start + relative_start;
55
+ let value_start = start + "authorization: bearer ".len();
56
+ let end = find_secret_end(text, value_start);
57
+ if text.get(value_start..end) == Some("[REDACTED]") {
58
+ search_start = end;
59
+ continue;
60
+ }
61
+ text.replace_range(value_start..end, "[REDACTED]");
62
+ count += 1;
63
+ search_start = value_start + "[REDACTED]".len();
64
+ }
65
+ count
66
+ }
67
+
68
+ fn redact_key_value_secrets(text: &mut String) -> usize {
69
+ let mut count = 0;
70
+ let secret_keys = [
71
+ "access_token",
72
+ "api_key",
73
+ "apikey",
74
+ "auth_token",
75
+ "password",
76
+ "secret",
77
+ "token",
78
+ ];
79
+ let mut search_start = 0;
80
+ loop {
81
+ if search_start >= text.len() {
82
+ break;
83
+ }
84
+ let lower = text[search_start..].to_ascii_lowercase();
85
+ let Some((relative_key_start, key)) = secret_keys
86
+ .iter()
87
+ .filter_map(|key| lower.find(key).map(|index| (index, *key)))
88
+ .min_by_key(|(index, _)| *index)
89
+ else {
90
+ break;
91
+ };
92
+ let key_start = search_start + relative_key_start;
93
+ let key_end = key_start + key.len();
94
+ let after_key = &text[key_end..];
95
+ let Some(delimiter_offset) = after_key.char_indices().find_map(|(offset, ch)| match ch {
96
+ '=' | ':' => Some(offset),
97
+ '"' | '\'' | ' ' | '\t' => None,
98
+ _ => Some(usize::MAX),
99
+ }) else {
100
+ break;
101
+ };
102
+ if delimiter_offset == usize::MAX {
103
+ search_start = key_end;
104
+ continue;
105
+ }
106
+ let delimiter = key_end + delimiter_offset;
107
+ let mut value_start = delimiter + 1;
108
+ while text
109
+ .as_bytes()
110
+ .get(value_start)
111
+ .is_some_and(u8::is_ascii_whitespace)
112
+ {
113
+ value_start += 1;
114
+ }
115
+ let quote = text
116
+ .as_bytes()
117
+ .get(value_start)
118
+ .copied()
119
+ .filter(|byte| *byte == b'"' || *byte == b'\'');
120
+ if quote.is_some() {
121
+ value_start += 1;
122
+ }
123
+ let mut value_end = if let Some(quote) = quote {
124
+ text.as_bytes()
125
+ .get(value_start..)
126
+ .and_then(|tail| tail.iter().position(|byte| *byte == quote))
127
+ .map(|offset| value_start + offset)
128
+ .unwrap_or_else(|| find_secret_end(text, value_start))
129
+ } else {
130
+ find_secret_end(text, value_start)
131
+ };
132
+ if value_end <= value_start {
133
+ search_start = key_end;
134
+ continue;
135
+ }
136
+ if quote.is_none() {
137
+ value_end = value_end
138
+ .min(
139
+ text[value_start..]
140
+ .find(',')
141
+ .map(|offset| value_start + offset)
142
+ .unwrap_or(text.len()),
143
+ )
144
+ .min(
145
+ text[value_start..]
146
+ .find('}')
147
+ .map(|offset| value_start + offset)
148
+ .unwrap_or(text.len()),
149
+ );
150
+ }
151
+ text.replace_range(value_start..value_end, "[REDACTED]");
152
+ count += 1;
153
+ search_start = value_start + "[REDACTED]".len();
154
+ }
155
+ count
156
+ }
157
+
158
+ fn redact_secret_markers(text: &mut String) -> usize {
159
+ let mut count = 0;
160
+ for marker in ["sk-", "ghp_", "xoxb-"] {
161
+ while let Some(start) = text.find(marker) {
162
+ let end = find_secret_end(text, start);
163
+ text.replace_range(start..end, "[REDACTED]");
164
+ count += 1;
165
+ }
166
+ }
167
+ count
168
+ }
169
+
170
+ fn find_secret_end(text: &str, start: usize) -> usize {
171
+ text[start..]
172
+ .char_indices()
173
+ .find_map(|(offset, ch)| {
174
+ if ch.is_whitespace() || matches!(ch, ',' | ';' | ')' | ']' | '}') {
175
+ Some(start + offset)
176
+ } else {
177
+ None
178
+ }
179
+ })
180
+ .unwrap_or(text.len())
181
+ }
182
+
183
+ #[cfg(test)]
184
+ mod tests {
185
+ use super::redact_output;
186
+ use crate::exec::CommandOutput;
187
+ use std::process::Command;
188
+
189
+ fn ok_status() -> std::process::ExitStatus {
190
+ Command::new("sh")
191
+ .arg("-c")
192
+ .arg("exit 0")
193
+ .status()
194
+ .expect("status")
195
+ }
196
+
197
+ #[test]
198
+ fn redacts_secret_like_stdout_and_stderr() {
199
+ let output = CommandOutput {
200
+ status: ok_status(),
201
+ stdout: b"API_TOKEN=secret-value\nplain\nsk-live123\n".to_vec(),
202
+ stderr: b"Authorization: Bearer bearer-secret\nghp_secret123\n".to_vec(),
203
+ };
204
+
205
+ let redacted = redact_output(&output);
206
+ let stdout = String::from_utf8(redacted.output.stdout).expect("stdout utf8");
207
+ let stderr = String::from_utf8(redacted.output.stderr).expect("stderr utf8");
208
+
209
+ assert_eq!(redacted.count, 4);
210
+ assert!(stdout.contains("API_TOKEN=[REDACTED]"));
211
+ assert!(stdout.contains("[REDACTED]"));
212
+ assert!(stderr.contains("Authorization: Bearer [REDACTED]"));
213
+ assert!(!stdout.contains("secret-value"));
214
+ assert!(!stdout.contains("sk-live123"));
215
+ assert!(!stderr.contains("bearer-secret"));
216
+ assert!(!stderr.contains("ghp_secret123"));
217
+ }
218
+
219
+ #[test]
220
+ fn redacts_json_colon_and_repeated_secret_forms() {
221
+ let output = CommandOutput {
222
+ status: ok_status(),
223
+ stdout:
224
+ br#"{"access_token":"tok-1","api_key":"sk-json"} password: hunter2 sk-one sk-two"#
225
+ .to_vec(),
226
+ stderr: b"Authorization: Bearer first Authorization: Bearer second\n".to_vec(),
227
+ };
228
+
229
+ let redacted = redact_output(&output);
230
+ let stdout = String::from_utf8(redacted.output.stdout).expect("stdout utf8");
231
+ let stderr = String::from_utf8(redacted.output.stderr).expect("stderr utf8");
232
+
233
+ for secret in [
234
+ "tok-1", "sk-json", "hunter2", "sk-one", "sk-two", "first", "second",
235
+ ] {
236
+ assert!(!stdout.contains(secret), "stdout leaked {secret}: {stdout}");
237
+ assert!(!stderr.contains(secret), "stderr leaked {secret}: {stderr}");
238
+ }
239
+ assert!(redacted.count >= 7);
240
+ }
241
+ }