Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 62 additions & 1 deletion crates/tree-ring-memory-cli/src/activation/adapters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ impl ActivationProject {
pub trait HarnessEnvironment {
fn executable_version(&self, command: &str) -> Option<String>;
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<Option<String>, String>;
fn agent_zero_plugin_manifest(&self) -> Option<AgentZeroPluginManifest>;
}
Expand Down Expand Up @@ -364,6 +367,7 @@ struct DeclarativeAdapter {
command: &'static str,
capability: AdapterCapability,
markers: &'static [&'static str],
home_markers: &'static [&'static str],
support: AdapterSupport,
}

Expand All @@ -382,6 +386,7 @@ const ADAPTERS: [DeclarativeAdapter; 7] = [
command: "codex",
capability: AdapterCapability::GuidanceOnly,
markers: &[".codex", "AGENTS.md"],
home_markers: &[".codex"],
support: AdapterSupport::Maintained,
},
DeclarativeAdapter {
Expand All @@ -391,6 +396,7 @@ const ADAPTERS: [DeclarativeAdapter; 7] = [
command: "claude",
capability: AdapterCapability::WrapperPreflight,
markers: &[".claude", "CLAUDE.md"],
home_markers: &[".claude"],
support: AdapterSupport::Maintained,
},
DeclarativeAdapter {
Expand All @@ -400,6 +406,7 @@ const ADAPTERS: [DeclarativeAdapter; 7] = [
command: "pi",
capability: AdapterCapability::NativePreflight,
markers: &[".pi", "pi.toml"],
home_markers: &[".pi"],
support: AdapterSupport::Maintained,
},
DeclarativeAdapter {
Expand All @@ -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 {
Expand All @@ -421,6 +429,7 @@ const ADAPTERS: [DeclarativeAdapter; 7] = [
command: "hermes",
capability: AdapterCapability::GuidanceOnly,
markers: &[".hermes", "hermes.toml"],
home_markers: &[".hermes"],
support: AdapterSupport::Unsupported,
},
DeclarativeAdapter {
Expand All @@ -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 {
Expand All @@ -439,6 +449,7 @@ const ADAPTERS: [DeclarativeAdapter; 7] = [
command: "goose",
capability: AdapterCapability::GuidanceOnly,
markers: &[".goose", "goosehints"],
home_markers: &[".goose"],
support: AdapterSupport::Unsupported,
},
];
Expand Down Expand Up @@ -587,6 +598,18 @@ impl HarnessAdapter for DeclarativeAdapter {
origin: MarkerOrigin::Project,
})
.collect::<Vec<_>>();
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();

Expand Down Expand Up @@ -745,11 +768,15 @@ impl HarnessEnvironment for EmptyHarnessEnvironment {

struct LocalHarnessEnvironment {
project_root: PathBuf,
home_root: Option<PathBuf>,
}

impl LocalHarnessEnvironment {
fn new(project_root: PathBuf) -> Self {
Self { project_root }
Self {
project_root,
home_root: std::env::var_os("HOME").map(PathBuf::from),
}
}
}

Expand All @@ -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<Option<String>, String> {
let path = self.project_root.join(relative);
match std::fs::read_to_string(&path) {
Expand Down Expand Up @@ -799,6 +832,7 @@ mod tests {
struct FakeEnvironment {
executable_versions: BTreeMap<String, String>,
paths: BTreeSet<PathBuf>,
home_paths: BTreeSet<PathBuf>,
files: BTreeMap<PathBuf, String>,
agent_zero: Option<AgentZeroPluginManifest>,
}
Expand All @@ -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<Option<String>, String> {
Ok(self.files.get(relative).cloned())
}
Expand Down Expand Up @@ -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();
Expand Down
11 changes: 6 additions & 5 deletions crates/tree-ring-memory-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
6 changes: 5 additions & 1 deletion crates/tree-ring-memory-cli/src/tui/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"));
}

Expand Down
21 changes: 14 additions & 7 deletions scripts/certify-tree-ring.sh
Original file line number Diff line number Diff line change
Expand Up @@ -347,30 +347,37 @@ 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"
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 \
Expand Down