(dc: &mut DoctorCounters, hooks_path: &Path)
| 750 | } |
| 751 | |
| 752 | fn doctor_check_plugin_hooks(dc: &mut DoctorCounters, hooks_path: &Path) { |
| 753 | if !hooks_path.exists() { |
| 754 | dc.warn(&format!( |
| 755 | "{} not found — run `tracedecay install --agent cursor`", |
| 756 | hooks_path.display() |
| 757 | )); |
| 758 | return; |
| 759 | } |
| 760 | let hooks = load_jsonc_file_strict(hooks_path).unwrap_or_else(|e| { |
| 761 | dc.fail(&format!("{e}")); |
| 762 | json!({}) |
| 763 | }); |
| 764 | let expected = cursor_plugin_hook_expectations(); |
| 765 | let missing: Vec<&str> = expected |
| 766 | .iter() |
| 767 | .filter_map(|(event, command)| { |
| 768 | let has = hooks["hooks"][event.as_str()] |
| 769 | .as_array() |
| 770 | .is_some_and(|entries| { |
| 771 | entries.iter().any(|entry| { |
| 772 | entry["command"] |
| 773 | .as_str() |
| 774 | .is_some_and(|value| value.contains(command)) |
| 775 | }) |
| 776 | }); |
| 777 | (!has).then_some(event.as_str()) |
| 778 | }) |
| 779 | .collect(); |
| 780 | if missing.is_empty() { |
| 781 | dc.pass(&format!( |
| 782 | "All {} Cursor plugin lifecycle hooks registered in {}", |
| 783 | expected.len(), |
| 784 | hooks_path.display() |
| 785 | )); |
| 786 | } else { |
| 787 | dc.fail(&format!( |
| 788 | "Cursor plugin hook(s) missing for {} — run `tracedecay install --agent cursor`", |
| 789 | missing.join(", ") |
| 790 | )); |
| 791 | } |
| 792 | } |
| 793 | |
| 794 | /// Flags a stalled Cursor transcript ingest. The per-turn hooks cap how much |
| 795 | /// transcript tail they read ([`crate::hooks::CURSOR_CATCH_UP_INGEST_MAX_BYTES`]), |
no test coverage detected