1use std::path::Path;
19
20use serde::Serialize;
21
22use crate::core::a2a::cost_attribution::CostStore;
23use crate::core::context_overhead::tool_tokens;
24use crate::core::rules_overhead::{RulesFileCost, collect_rules_files, duplicate_clients};
25
26const LOW_USE_TOKEN_FLOOR: usize = 150;
29const LOW_USE_CALL_SHARE: f64 = 0.01;
31const STALE_FACT_DAYS: i64 = 30;
33const CROSS_LAYER_POINTER_MAX: usize = 120;
37const POINTER_TOKENS: usize = 40;
39const DEDICATED_INSTRUCTION_CLIENTS: &[&str] = &["codex", "codebuddy"];
44
45#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)]
46#[serde(rename_all = "snake_case")]
47pub enum ToolStatus {
48 Active,
50 LowUse,
52 Unused,
54 Unknown,
56}
57
58impl ToolStatus {
59 #[must_use]
60 pub fn label(self) -> &'static str {
61 match self {
62 ToolStatus::Active => "active",
63 ToolStatus::LowUse => "low-use",
64 ToolStatus::Unused => "unused",
65 ToolStatus::Unknown => "unknown",
66 }
67 }
68}
69
70#[derive(Debug, Clone, Serialize)]
71pub struct ToolEntry {
72 pub name: String,
73 pub schema_tokens: usize,
74 pub calls: u64,
75 pub last_used: Option<String>,
76 pub status: ToolStatus,
77 pub action: String,
78 pub value_per_1k_tokens: f64,
81}
82
83#[derive(Debug, Clone, Serialize)]
84pub struct RuleEntry {
85 pub path: String,
86 pub file_tokens: usize,
87 pub lean_ctx_tokens: usize,
88 pub carries_full: bool,
89 pub clients: Vec<String>,
90 pub action: String,
91}
92
93#[derive(Debug, Clone, Serialize, Default)]
94pub struct KnowledgeHealth {
95 pub total_facts: usize,
96 pub active_facts: usize,
97 pub stale_facts: usize,
98 pub action: String,
99}
100
101#[derive(Debug, Clone, Serialize, Default)]
104pub struct BehaviorHealth {
105 pub heavy_read_share_pct: Option<u8>,
108 pub heavy_mode_reads: u64,
109 pub total_mode_reads: u64,
110 pub chains_detected: u64,
112 pub compose_calls: u64,
113 pub hints_shown: u64,
114 pub action: String,
115}
116
117const HEAVY_READ_SHARE_WARN_PCT: u8 = 40;
120
121fn is_read_mode(key: &str) -> bool {
125 matches!(
126 key,
127 "full"
128 | "raw"
129 | "auto"
130 | "map"
131 | "signatures"
132 | "task"
133 | "reference"
134 | "aggressive"
135 | "entropy"
136 | "diff"
137 | "delta"
138 | "fresh"
139 | "anchored"
140 ) || key.starts_with("lines:")
141 || key.starts_with("anchored:")
142 || key.starts_with("budget:")
143}
144
145fn behavior_health(
148 modes: &std::collections::HashMap<String, u64>,
149 nudges: &crate::core::behavior_nudge::BehaviorStore,
150 compose_calls: u64,
151) -> BehaviorHealth {
152 let total_mode_reads: u64 = modes
153 .iter()
154 .filter(|(k, _)| is_read_mode(k))
155 .map(|(_, v)| *v)
156 .sum();
157 let heavy_mode_reads: u64 = modes
158 .iter()
159 .filter(|(k, _)| matches!(k.as_str(), "full" | "raw"))
160 .map(|(_, v)| *v)
161 .sum();
162 let heavy_read_share_pct = (total_mode_reads > 0)
163 .then(|| u8::try_from(heavy_mode_reads * 100 / total_mode_reads).unwrap_or(100));
164 let action = match heavy_read_share_pct {
165 Some(pct) if pct >= HEAVY_READ_SHARE_WARN_PCT => format!(
166 "explicit full/raw dominates recorded reads ({pct}%) — signatures/map cover orientation at a fraction of the tokens; in-band nudges are counting (see behavior_nudges)"
167 ),
168 _ => String::new(),
169 };
170 BehaviorHealth {
171 heavy_read_share_pct,
172 heavy_mode_reads,
173 total_mode_reads,
174 chains_detected: nudges.chains_detected,
175 compose_calls,
176 hints_shown: nudges.hints_shown,
177 action,
178 }
179}
180
181#[derive(Debug, Clone, Serialize)]
182pub struct ToolHealthReport {
183 pub tool_profile: String,
184 pub advertised_tools: usize,
185 pub tool_schema_tokens: usize,
186 pub instruction_tokens: usize,
187 pub rules_tokens: usize,
188 pub fixed_total_tokens: usize,
190 pub has_usage_data: bool,
191 pub total_recorded_calls: u64,
192 pub unused_tools: usize,
193 pub unused_tool_tokens: usize,
195 pub disable_candidates: Vec<String>,
197 pub reclaimable_tokens: usize,
199 pub disable_action: String,
201 pub tools: Vec<ToolEntry>,
202 pub rules: Vec<RuleEntry>,
203 pub duplicate_clients: Vec<(String, usize)>,
204 pub knowledge: KnowledgeHealth,
205 pub foreign_servers: Vec<crate::core::foreign_mcp::ForeignServerEntry>,
208 pub behavior: BehaviorHealth,
210}
211
212fn classify(has_usage: bool, calls: u64, schema_tokens: usize, total_calls: u64) -> ToolStatus {
213 if !has_usage {
214 return ToolStatus::Unknown;
215 }
216 if calls == 0 {
217 return ToolStatus::Unused;
218 }
219 if schema_tokens >= LOW_USE_TOKEN_FLOOR
220 && (calls as f64) < (total_calls as f64) * LOW_USE_CALL_SHARE
221 {
222 return ToolStatus::LowUse;
223 }
224 ToolStatus::Active
225}
226
227fn value_per_1k(calls: u64, schema_tokens: usize) -> f64 {
229 if schema_tokens == 0 {
230 0.0
231 } else {
232 calls as f64 / schema_tokens as f64 * 1000.0
233 }
234}
235
236fn action_for(status: ToolStatus, calls: u64, schema_tokens: usize) -> String {
237 match status {
238 ToolStatus::Unused => format!(
239 "never called — trim via a leaner tool profile to reclaim {schema_tokens} tok/session"
240 ),
241 ToolStatus::LowUse => {
242 format!("rarely used ({calls}×) yet costs {schema_tokens} tok/session — review")
243 }
244 ToolStatus::Active | ToolStatus::Unknown => String::new(),
245 }
246}
247
248fn resolve_alias(name: &str) -> Option<&'static str> {
253 match name {
254 "shell" => Some("ctx_shell"),
255 _ => None,
256 }
257}
258
259#[must_use]
262pub(crate) fn build_report(
263 advertised: &[rmcp::model::Tool],
264 usage: &CostStore,
265 rules: &[RulesFileCost],
266 duplicates: Vec<(String, usize)>,
267 instruction_tokens: usize,
268 tool_profile: String,
269 knowledge: KnowledgeHealth,
270) -> ToolHealthReport {
271 let total_recorded_calls: u64 = usage.tools.values().map(|t| t.total_calls).sum();
272 let has_usage_data = total_recorded_calls > 0;
273
274 let mut tools: Vec<ToolEntry> = advertised
275 .iter()
276 .map(|t| {
277 let name = t.name.as_ref().to_string();
278 let schema_tokens = tool_tokens(t);
279 let (calls, last_used) = usage
280 .tools
281 .get(&name)
282 .or_else(|| resolve_alias(&name).and_then(|a| usage.tools.get(a)))
283 .map_or((0, None), |c| (c.total_calls, c.last_used.clone()));
284 let status = classify(has_usage_data, calls, schema_tokens, total_recorded_calls);
285 let action = action_for(status, calls, schema_tokens);
286 ToolEntry {
287 name,
288 schema_tokens,
289 calls,
290 last_used,
291 status,
292 action,
293 value_per_1k_tokens: value_per_1k(calls, schema_tokens),
294 }
295 })
296 .collect();
297 tools.sort_by(|a, b| a.name.cmp(&b.name));
298
299 let tool_schema_tokens: usize = tools.iter().map(|t| t.schema_tokens).sum();
300 let unused_tools = tools
301 .iter()
302 .filter(|t| t.status == ToolStatus::Unused)
303 .count();
304 let unused_tool_tokens = tools
305 .iter()
306 .filter(|t| t.status == ToolStatus::Unused)
307 .map(|t| t.schema_tokens)
308 .sum();
309
310 let low_value = |t: &&ToolEntry| matches!(t.status, ToolStatus::Unused | ToolStatus::LowUse);
313 let disable_candidates: Vec<String> = tools
314 .iter()
315 .filter(low_value)
316 .map(|t| t.name.clone())
317 .collect();
318 let reclaimable_tokens: usize = tools
319 .iter()
320 .filter(low_value)
321 .map(|t| t.schema_tokens)
322 .sum();
323 let disable_action = if disable_candidates.is_empty() {
324 String::new()
325 } else {
326 format!(
327 "consider disabling {} low-value tool(s) to reclaim {reclaimable_tokens} tok/session: {} — apply via `disabled_tools` in config or a leaner `tool_profile`",
328 disable_candidates.len(),
329 disable_candidates.join(", ")
330 )
331 };
332
333 let dup_clients: std::collections::HashSet<&str> =
334 duplicates.iter().map(|(c, _)| c.as_str()).collect();
335 let rules_out: Vec<RuleEntry> = rules
336 .iter()
337 .map(|r| {
338 let is_dup = r.carries_full && r.clients.iter().any(|c| dup_clients.contains(c));
339 let cross_layer = !is_dup
340 && r.lean_ctx_tokens > CROSS_LAYER_POINTER_MAX
341 && r.clients
342 .iter()
343 .any(|c| DEDICATED_INSTRUCTION_CLIENTS.contains(c));
344 let action = if is_dup {
345 "duplicate full lean-ctx source — run `lean-ctx rules dedup --apply`".to_string()
346 } else if cross_layer {
347 format!(
348 "carries {} lean-ctx tokens although MCP instructions already deliver the full mapping on dedicated hosts — thin to a pointer to reclaim ~{} tok/session",
349 r.lean_ctx_tokens,
350 r.lean_ctx_tokens.saturating_sub(POINTER_TOKENS)
351 )
352 } else {
353 String::new()
354 };
355 RuleEntry {
356 path: r.path.clone(),
357 file_tokens: r.file_tokens,
358 lean_ctx_tokens: r.lean_ctx_tokens,
359 carries_full: r.carries_full,
360 clients: r.clients.iter().map(|c| (*c).to_string()).collect(),
361 action,
362 }
363 })
364 .collect();
365
366 let rules_tokens: usize = rules_out.iter().map(|r| r.file_tokens).sum();
367 let fixed_total_tokens = tool_schema_tokens + instruction_tokens + rules_tokens;
368
369 ToolHealthReport {
370 tool_profile,
371 advertised_tools: tools.len(),
372 tool_schema_tokens,
373 instruction_tokens,
374 rules_tokens,
375 fixed_total_tokens,
376 has_usage_data,
377 total_recorded_calls,
378 unused_tools,
379 unused_tool_tokens,
380 disable_candidates,
381 reclaimable_tokens,
382 disable_action,
383 tools,
384 rules: rules_out,
385 duplicate_clients: duplicates,
386 knowledge,
387 foreign_servers: Vec::new(),
388 behavior: BehaviorHealth::default(),
389 }
390}
391
392fn resolve_tool_profile() -> String {
393 let cfg = crate::core::config::Config::load();
394 if crate::core::tool_visibility::explicit_profile(&cfg) {
395 cfg.tool_profile_effective().as_str().to_string()
396 } else {
397 "lean (default)".to_string()
398 }
399}
400
401fn knowledge_health(project: &Path) -> KnowledgeHealth {
402 let Some(knowledge) =
403 crate::core::knowledge::ProjectKnowledge::load(&project.to_string_lossy())
404 else {
405 return KnowledgeHealth::default();
406 };
407 let total = knowledge.facts.len();
408 let current: Vec<_> = knowledge.facts.iter().filter(|f| f.is_current()).collect();
409 let now = chrono::Utc::now();
410 let stale = current
411 .iter()
412 .filter(|f| (now - f.created_at).num_days() > STALE_FACT_DAYS && f.retrieval_count == 0)
413 .count();
414 let action = if stale > 0 {
415 format!(
416 "{stale} stale fact(s) (>{STALE_FACT_DAYS}d, never retrieved) — review with `lean-ctx knowledge`"
417 )
418 } else {
419 String::new()
420 };
421 KnowledgeHealth {
422 total_facts: total,
423 active_facts: current.len(),
424 stale_facts: stale,
425 action,
426 }
427}
428
429#[must_use]
431pub fn compute(home: &Path, project: &Path) -> ToolHealthReport {
432 let advertised = crate::core::tool_visibility::advertised_tool_defs_default();
433 let usage = CostStore::load();
434 let rules = collect_rules_files(home, project);
435 let duplicates = duplicate_clients(&rules);
436 let instructions =
437 crate::instructions::build_instructions(crate::core::protocol::CrpMode::effective());
438 let instruction_tokens = crate::core::tokens::count_tokens(&instructions);
439 let knowledge = knowledge_health(project);
440 let mut report = build_report(
441 &advertised,
442 &usage,
443 &rules,
444 duplicates,
445 instruction_tokens,
446 resolve_tool_profile(),
447 knowledge,
448 );
449 report.foreign_servers = crate::core::foreign_mcp::audit(home, project);
450 report.behavior = behavior_health(
451 &crate::core::stats::load_for_display().cep.modes,
452 &crate::core::behavior_nudge::BehaviorStore::load(),
453 usage.tools.get("ctx_compose").map_or(0, |t| t.total_calls),
454 );
455 report
456}
457
458#[cfg(test)]
459mod tests {
460 use super::*;
461 use crate::core::a2a::cost_attribution::ToolCost;
462
463 fn tool(name: &'static str) -> rmcp::model::Tool {
464 crate::tool_defs::tool_def(
465 name,
466 "a representative description used to give the schema some token weight",
467 serde_json::json!({
468 "type": "object",
469 "properties": { "path": { "type": "string", "description": "file path" } }
470 }),
471 )
472 }
473
474 fn usage_with(calls: &[(&str, u64)]) -> CostStore {
475 let mut store = CostStore::default();
476 for (name, n) in calls {
477 store.tools.insert(
478 (*name).to_string(),
479 ToolCost {
480 tool_name: (*name).to_string(),
481 total_calls: *n,
482 last_used: Some("2026-06-01T00:00:00+00:00".to_string()),
483 ..Default::default()
484 },
485 );
486 }
487 store
488 }
489
490 #[test]
491 fn classify_unknown_without_usage_history() {
492 assert_eq!(classify(false, 0, 500, 0), ToolStatus::Unknown);
493 assert_eq!(classify(false, 9, 500, 0), ToolStatus::Unknown);
494 }
495
496 #[test]
497 fn classify_unused_when_history_exists_but_tool_never_called() {
498 assert_eq!(classify(true, 0, 500, 1000), ToolStatus::Unused);
499 }
500
501 #[test]
502 fn classify_low_use_for_expensive_rarely_called_tool() {
503 assert_eq!(classify(true, 1, 400, 10_000), ToolStatus::LowUse);
505 assert_eq!(classify(true, 1, 50, 10_000), ToolStatus::Active);
507 }
508
509 #[test]
510 fn classify_active_for_well_used_tool() {
511 assert_eq!(classify(true, 500, 400, 1000), ToolStatus::Active);
512 }
513
514 #[test]
515 fn build_report_flags_unused_and_sorts_tools() {
516 let advertised = vec![tool("ctx_search"), tool("ctx_read"), tool("ctx_shell")];
517 let usage = usage_with(&[("ctx_read", 40)]);
519 let report = build_report(
520 &advertised,
521 &usage,
522 &[],
523 Vec::new(),
524 100,
525 "lean (default)".to_string(),
526 KnowledgeHealth::default(),
527 );
528
529 assert!(report.has_usage_data);
530 assert_eq!(report.total_recorded_calls, 40);
531 let names: Vec<&str> = report.tools.iter().map(|t| t.name.as_str()).collect();
533 assert_eq!(names, vec!["ctx_read", "ctx_search", "ctx_shell"]);
534 assert_eq!(report.unused_tools, 2);
536 assert!(report.unused_tool_tokens > 0);
537 let read = report.tools.iter().find(|t| t.name == "ctx_read").unwrap();
538 assert_eq!(read.status, ToolStatus::Active);
539 assert!(read.last_used.is_some());
540 assert_eq!(
542 report.fixed_total_tokens,
543 report.tool_schema_tokens + 100 + report.rules_tokens
544 );
545 }
546
547 #[test]
548 fn value_per_1k_rewards_cheap_well_used_tools() {
549 assert!(value_per_1k(100, 50) > value_per_1k(100, 500));
550 assert_eq!(value_per_1k(0, 100), 0.0);
551 assert_eq!(value_per_1k(10, 0), 0.0, "no schema cost → no division");
552 }
553
554 #[test]
555 fn build_report_recommends_disabling_low_value_tools() {
556 let advertised = vec![tool("ctx_read"), tool("ctx_search"), tool("ctx_shell")];
557 let usage = usage_with(&[("ctx_read", 40)]);
559 let report = build_report(
560 &advertised,
561 &usage,
562 &[],
563 Vec::new(),
564 0,
565 "lean (default)".to_string(),
566 KnowledgeHealth::default(),
567 );
568 assert!(
569 report
570 .disable_candidates
571 .contains(&"ctx_search".to_string())
572 );
573 assert!(report.disable_candidates.contains(&"ctx_shell".to_string()));
574 assert!(
575 !report.disable_candidates.contains(&"ctx_read".to_string()),
576 "an active tool is never a disable candidate"
577 );
578 assert!(report.reclaimable_tokens > 0);
579 assert!(report.disable_action.contains("consider disabling"));
580 let read = report.tools.iter().find(|t| t.name == "ctx_read").unwrap();
581 assert!(read.value_per_1k_tokens > 0.0);
582 }
583
584 #[test]
585 fn build_report_unknown_status_without_history() {
586 let advertised = vec![tool("ctx_read")];
587 let report = build_report(
588 &advertised,
589 &CostStore::default(),
590 &[],
591 Vec::new(),
592 0,
593 "lean (default)".to_string(),
594 KnowledgeHealth::default(),
595 );
596 assert!(!report.has_usage_data);
597 assert_eq!(report.unused_tools, 0, "never flag rot without history");
598 assert_eq!(report.tools[0].status, ToolStatus::Unknown);
599 }
600
601 #[test]
602 fn build_report_marks_duplicate_rules() {
603 let rules = vec![
604 RulesFileCost {
605 path: "a/.cursor/rules/lean-ctx.mdc".into(),
606 file_tokens: 200,
607 lean_ctx_tokens: 200,
608 carries_full: true,
609 clients: vec!["cursor"],
610 },
611 RulesFileCost {
612 path: "a/.cursorrules".into(),
613 file_tokens: 150,
614 lean_ctx_tokens: 150,
615 carries_full: true,
616 clients: vec!["cursor"],
617 },
618 ];
619 let dups = duplicate_clients(&rules);
620 let report = build_report(
621 &[],
622 &CostStore::default(),
623 &rules,
624 dups,
625 0,
626 "lean (default)".to_string(),
627 KnowledgeHealth::default(),
628 );
629 assert_eq!(report.rules.len(), 2);
630 assert!(
631 report.rules.iter().all(|r| r.action.contains("dedup")),
632 "both cursor full sources flagged as duplicates"
633 );
634 assert_eq!(report.rules_tokens, 350);
635 }
636
637 #[test]
641 fn build_report_flags_dedicated_carrier_exceeding_pointer() {
642 let rules = vec![
643 RulesFileCost {
644 path: "/home/u/.codebuddy/CODEBUDDY.md".into(),
645 file_tokens: 700,
646 lean_ctx_tokens: 384,
647 carries_full: false,
648 clients: vec!["codebuddy"],
649 },
650 RulesFileCost {
651 path: "/home/u/.gemini/GEMINI.md".into(),
652 file_tokens: 800,
653 lean_ctx_tokens: 800,
654 carries_full: true,
655 clients: vec!["gemini"],
656 },
657 ];
658 let report = build_report(
659 &[tool("ctx_read")],
660 &usage_with(&[("ctx_read", 5)]),
661 &rules,
662 Vec::new(),
663 0,
664 "lean (default)".to_string(),
665 KnowledgeHealth::default(),
666 );
667 let codebuddy = report
668 .rules
669 .iter()
670 .find(|r| r.path.contains("CODEBUDDY"))
671 .unwrap();
672 assert!(
673 codebuddy.action.contains("thin to a pointer"),
674 "dedicated carrier above pointer size must be flagged: {}",
675 codebuddy.action
676 );
677 assert!(
678 codebuddy.action.contains("~344 tok"),
679 "reclaimable size = lean tokens minus pointer: {}",
680 codebuddy.action
681 );
682 let gemini = report
683 .rules
684 .iter()
685 .find(|r| r.path.contains("GEMINI"))
686 .unwrap();
687 assert!(
688 gemini.action.is_empty(),
689 "non-dedicated client without duplication stays unflagged"
690 );
691 }
692
693 #[test]
696 fn build_report_leaves_pointer_only_dedicated_carrier_alone() {
697 let rules = vec![RulesFileCost {
698 path: "/home/u/.claude/CLAUDE.md".into(),
699 file_tokens: 100,
700 lean_ctx_tokens: 40,
701 carries_full: false,
702 clients: vec!["claude"],
703 }];
704 let report = build_report(
705 &[tool("ctx_read")],
706 &usage_with(&[("ctx_read", 5)]),
707 &rules,
708 Vec::new(),
709 0,
710 "lean (default)".to_string(),
711 KnowledgeHealth::default(),
712 );
713 {
714 let found = &report.rules[0].action;
715 assert!(found.is_empty(), "{found:?}");
716 }
717 assert!(
718 report.foreign_servers.is_empty(),
719 "pure builder adds no foreign servers"
720 );
721 }
722
723 #[test]
727 fn behavior_health_computes_share_from_read_modes_only() {
728 let mut modes = std::collections::HashMap::new();
729 modes.insert("full".to_string(), 50u64);
730 modes.insert("raw".to_string(), 10);
731 modes.insert("signatures".to_string(), 30);
732 modes.insert("lines:1-20".to_string(), 10);
733 modes.insert("register".to_string(), 500);
734 modes.insert("compose".to_string(), 130);
735 let store = crate::core::behavior_nudge::BehaviorStore {
736 chains_detected: 7,
737 hints_shown: 2,
738 ..Default::default()
739 };
740 let b = behavior_health(&modes, &store, 130);
741 assert_eq!(b.total_mode_reads, 100, "register/compose are not reads");
742 assert_eq!(b.heavy_mode_reads, 60);
743 assert_eq!(b.heavy_read_share_pct, Some(60));
744 assert!(b.action.contains("full/raw dominates"));
745 assert_eq!(b.chains_detected, 7);
746 assert_eq!(b.compose_calls, 130);
747 }
748
749 #[test]
750 fn behavior_health_stays_quiet_below_threshold_and_without_data() {
751 let mut modes = std::collections::HashMap::new();
752 modes.insert("full".to_string(), 10u64);
753 modes.insert("signatures".to_string(), 90);
754 let b = behavior_health(&modes, &Default::default(), 0);
755 assert_eq!(b.heavy_read_share_pct, Some(10));
756 assert!(
757 b.action.is_empty(),
758 "10% heavy reads is deliberate edit prep"
759 );
760
761 let empty = behavior_health(&std::collections::HashMap::new(), &Default::default(), 0);
762 assert_eq!(
763 empty.heavy_read_share_pct, None,
764 "no telemetry → no verdict"
765 );
766 assert!(empty.action.is_empty(), "{:?}", empty.action);
767 }
768
769 #[test]
771 fn shell_alias_inherits_ctx_shell_usage_838() {
772 let shell_tool = tool("shell");
773 let mut usage = CostStore::default();
774 let tc = ToolCost {
775 total_calls: 42,
776 last_used: Some("2026-07-15".to_string()),
777 ..ToolCost::default()
778 };
779 usage.tools.insert("ctx_shell".to_string(), tc);
780 let report = build_report(
781 &[shell_tool],
782 &usage,
783 &[],
784 Vec::new(),
785 0,
786 "lean".to_string(),
787 KnowledgeHealth::default(),
788 );
789 assert_eq!(report.tools[0].name, "shell");
790 assert_eq!(
791 report.tools[0].calls, 42,
792 "shell must inherit ctx_shell calls"
793 );
794 assert_ne!(
795 report.tools[0].status,
796 ToolStatus::Unused,
797 "shell must not be flagged unused"
798 );
799 }
800}