diff --git a/crates/tree-ring-memory-cli/src/activation/adapters.rs b/crates/tree-ring-memory-cli/src/activation/adapters.rs index 5b7422d..bba9c2c 100644 --- a/crates/tree-ring-memory-cli/src/activation/adapters.rs +++ b/crates/tree-ring-memory-cli/src/activation/adapters.rs @@ -72,6 +72,9 @@ impl ActivationProject { pub trait HarnessEnvironment { fn executable_version(&self, command: &str) -> Option; fn project_path_exists(&self, relative: &Path) -> bool; + fn home_path_exists(&self, _relative: &Path) -> bool { + false + } fn read_project_file(&self, relative: &Path) -> Result, String>; fn agent_zero_plugin_manifest(&self) -> Option; } @@ -364,6 +367,7 @@ struct DeclarativeAdapter { command: &'static str, capability: AdapterCapability, markers: &'static [&'static str], + home_markers: &'static [&'static str], support: AdapterSupport, } @@ -382,6 +386,7 @@ const ADAPTERS: [DeclarativeAdapter; 7] = [ command: "codex", capability: AdapterCapability::GuidanceOnly, markers: &[".codex", "AGENTS.md"], + home_markers: &[".codex"], support: AdapterSupport::Maintained, }, DeclarativeAdapter { @@ -391,6 +396,7 @@ const ADAPTERS: [DeclarativeAdapter; 7] = [ command: "claude", capability: AdapterCapability::WrapperPreflight, markers: &[".claude", "CLAUDE.md"], + home_markers: &[".claude"], support: AdapterSupport::Maintained, }, DeclarativeAdapter { @@ -400,6 +406,7 @@ const ADAPTERS: [DeclarativeAdapter; 7] = [ command: "pi", capability: AdapterCapability::NativePreflight, markers: &[".pi", "pi.toml"], + home_markers: &[".pi"], support: AdapterSupport::Maintained, }, DeclarativeAdapter { @@ -412,6 +419,7 @@ const ADAPTERS: [DeclarativeAdapter; 7] = [ // tree_ring_memory plugin is installed or enabled. The plugin may // prove its capability only through the explicit descriptor below. markers: &[], + home_markers: &[], support: AdapterSupport::AgentZero, }, DeclarativeAdapter { @@ -421,6 +429,7 @@ const ADAPTERS: [DeclarativeAdapter; 7] = [ command: "hermes", capability: AdapterCapability::GuidanceOnly, markers: &[".hermes", "hermes.toml"], + home_markers: &[".hermes"], support: AdapterSupport::Unsupported, }, DeclarativeAdapter { @@ -430,6 +439,7 @@ const ADAPTERS: [DeclarativeAdapter; 7] = [ command: "opencode", capability: AdapterCapability::GuidanceOnly, markers: &[".opencode", "opencode.json", "opencode.toml"], + home_markers: &[".opencode"], support: AdapterSupport::Unsupported, }, DeclarativeAdapter { @@ -439,6 +449,7 @@ const ADAPTERS: [DeclarativeAdapter; 7] = [ command: "goose", capability: AdapterCapability::GuidanceOnly, markers: &[".goose", "goosehints"], + home_markers: &[".goose"], support: AdapterSupport::Unsupported, }, ]; @@ -587,6 +598,18 @@ impl HarnessAdapter for DeclarativeAdapter { origin: MarkerOrigin::Project, }) .collect::>(); + markers.extend( + self.home_markers + .iter() + .filter(|marker| env.home_path_exists(Path::new(marker))) + .map(|marker| IntegrationMarker { + path: normalized_relative_path(marker) + .expect("static home marker paths are normalized") + .display() + .to_string(), + origin: MarkerOrigin::Home, + }), + ); markers.sort(); markers.dedup(); @@ -745,11 +768,15 @@ impl HarnessEnvironment for EmptyHarnessEnvironment { struct LocalHarnessEnvironment { project_root: PathBuf, + home_root: Option, } impl LocalHarnessEnvironment { fn new(project_root: PathBuf) -> Self { - Self { project_root } + Self { + project_root, + home_root: std::env::var_os("HOME").map(PathBuf::from), + } } } @@ -769,6 +796,12 @@ impl HarnessEnvironment for LocalHarnessEnvironment { self.project_root.join(relative).exists() } + fn home_path_exists(&self, relative: &Path) -> bool { + self.home_root + .as_ref() + .is_some_and(|home| home.join(relative).exists()) + } + fn read_project_file(&self, relative: &Path) -> Result, String> { let path = self.project_root.join(relative); match std::fs::read_to_string(&path) { @@ -799,6 +832,7 @@ mod tests { struct FakeEnvironment { executable_versions: BTreeMap, paths: BTreeSet, + home_paths: BTreeSet, files: BTreeMap, agent_zero: Option, } @@ -812,6 +846,10 @@ mod tests { self.paths.contains(relative) } + fn home_path_exists(&self, relative: &Path) -> bool { + self.home_paths.contains(relative) + } + fn read_project_file(&self, relative: &Path) -> Result, String> { Ok(self.files.get(relative).cloned()) } @@ -980,6 +1018,29 @@ mod tests { assert!(!Path::new(&marker.path).is_absolute()); } + #[test] + fn detection_distinguishes_project_and_home_markers_without_exposing_home_paths() { + let mut env = FakeEnvironment::default(); + env.paths.insert(PathBuf::from("CLAUDE.md")); + env.home_paths.insert(PathBuf::from(".claude")); + + let report = detect_adapters(&project(), &env); + let claude = report.by_id("claude-code").unwrap(); + + assert!(claude.markers.contains(&IntegrationMarker { + path: "CLAUDE.md".to_string(), + origin: MarkerOrigin::Project, + })); + assert!(claude.markers.contains(&IntegrationMarker { + path: ".claude".to_string(), + origin: MarkerOrigin::Home, + })); + assert!(claude + .markers + .iter() + .all(|marker| !Path::new(&marker.path).is_absolute())); + } + #[test] fn deactivation_retains_managed_block_ownership() { let plan = plan_deactivation("codex", &project()).unwrap(); diff --git a/crates/tree-ring-memory-cli/src/main.rs b/crates/tree-ring-memory-cli/src/main.rs index 914e159..0efec11 100644 --- a/crates/tree-ring-memory-cli/src/main.rs +++ b/crates/tree-ring-memory-cli/src/main.rs @@ -3063,11 +3063,12 @@ mod tests { assert!(!root.exists()); let report = activation::adapters::scan_integrations(dir.path()); - assert_eq!(report.detected_count, 1); - assert_ne!( - report.by_id("codex").unwrap().state, - activation::ActivationState::Active - ); + assert!(report.detected_count >= 1); + let codex = report.by_id("codex").unwrap(); + assert!(codex.markers.iter().any(|marker| { + marker.origin == activation::adapters::MarkerOrigin::Project && marker.path == ".codex" + })); + assert_ne!(codex.state, activation::ActivationState::Active); } #[test] diff --git a/crates/tree-ring-memory-cli/src/tui/app.rs b/crates/tree-ring-memory-cli/src/tui/app.rs index 0faf99e..a68bf16 100644 --- a/crates/tree-ring-memory-cli/src/tui/app.rs +++ b/crates/tree-ring-memory-cli/src/tui/app.rs @@ -939,7 +939,11 @@ mod tests { assert_eq!(app.mode, AppMode::Integrations); let report = app.integration_report.as_ref().unwrap(); - assert_eq!(report.detected_count, 1); + assert!(report.detected_count >= 1); + assert!(report.by_id("codex").unwrap().markers.iter().any(|marker| { + marker.origin == crate::activation::adapters::MarkerOrigin::Project + && marker.path == ".codex" + })); assert!(app.status.contains("integration scan")); } diff --git a/scripts/certify-tree-ring.sh b/scripts/certify-tree-ring.sh index 328257b..a5e000a 100755 --- a/scripts/certify-tree-ring.sh +++ b/scripts/certify-tree-ring.sh @@ -347,13 +347,15 @@ Generated: $created_at - 50k extended metrics: $([ "$EXTENDED" = "1" ] && printf 'recorded in `performance-50000.out`' || printf 'skipped') - memory quality scenarios: passed - memory quality summary: \`quality/quality-summary.md\` +- harness activation fixture: 6 explicit skips without fresh-session receipts - Agent Zero plugin smoke: $(printf '%s' "$agent_zero_status" | tr -d '"') Machine-readable metrics: \`metrics.json\` EOF mv -f "$SUMMARY_TMP" "$SUMMARY" -HOME="$scan_home" "$BIN" --json integrations certify --source-root "$scan_root" --out-dir "$OUT_DIR" \ +HOME="$scan_home" "$BIN" --root "$scan_root/.tree-ring" --json integrations certify \ + --source-root "$scan_root" --out-dir "$OUT_DIR" \ > "$OUT_DIR/harness-certification.json" require_file "$OUT_DIR/harness/codex.json" require_file "$OUT_DIR/harness/claude-code.json" @@ -361,16 +363,21 @@ require_file "$OUT_DIR/harness/opencode.json" require_file "$OUT_DIR/harness/goose.json" require_file "$OUT_DIR/harness/pi.json" require_file "$OUT_DIR/harness/agent-zero.json" -grep -E '"pass_count"[[:space:]]*:[[:space:]]*5' "$OUT_DIR/harness-certification.json" > /dev/null \ - || fail "harness certification did not report pass_count 5" +# This fixture deliberately provides markers and generated guidance without +# fresh-session preflight receipts. Certification must preserve that boundary: +# every harness is skipped, and none may be promoted to pass from markers alone. +grep -E '"pass_count"[[:space:]]*:[[:space:]]*0' "$OUT_DIR/harness-certification.json" > /dev/null \ + || fail "harness certification did not report pass_count 0" grep -E '"fail_count"[[:space:]]*:[[:space:]]*0' "$OUT_DIR/harness-certification.json" > /dev/null \ || fail "harness certification did not report fail_count 0" -grep -E '"skip_count"[[:space:]]*:[[:space:]]*1' "$OUT_DIR/harness-certification.json" > /dev/null \ - || fail "harness certification did not report skip_count 1" -grep -E '"status"[[:space:]]*:[[:space:]]*"pass"' "$OUT_DIR/harness/codex.json" > /dev/null \ - || fail "codex harness did not report pass status" +grep -E '"skip_count"[[:space:]]*:[[:space:]]*6' "$OUT_DIR/harness-certification.json" > /dev/null \ + || fail "harness certification did not report skip_count 6" +grep -E '"status"[[:space:]]*:[[:space:]]*"skip"' "$OUT_DIR/harness/codex.json" > /dev/null \ + || fail "codex harness did not report skip status" grep -E '"status"[[:space:]]*:[[:space:]]*"skip"' "$OUT_DIR/harness/pi.json" > /dev/null \ || fail "pi harness did not report skip status" +grep -E '"status"[[:space:]]*:[[:space:]]*"skip"' "$OUT_DIR/harness/agent-zero.json" > /dev/null \ + || fail "agent-zero harness did not report skip status" grep -F '"harness"' "$INDEX" > /dev/null \ || fail "evidence index did not include harness records" grep -F '"codex"' "$INDEX" > /dev/null \