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
26 changes: 23 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,9 @@ tree-ring integrations scan --source-root .

It looks for local markers for DOX, Revolve, Codex, Claude Code, Agent Zero/A0,
Goose, OpenCode, Hermes, and Pi, then suggests next steps without editing those
tools' configuration.
tools' configuration. JSON output records whether each marker came from the
project or from the user's home configuration, so harness readiness is not
overstated when only global config exists.

Agent-mediated bridge linking is the planned next step after read-only
discovery. The design keeps `.tree-ring` as the canonical memory root while
Expand All @@ -276,8 +278,8 @@ OpenCode, or other agent configuration.
`tree_ring_memory_export` header with schema and plugin version metadata; each
remaining line is a `memory_event` envelope. The command excludes sensitive and
superseded memories unless `--include-sensitive` or `--include-superseded` is
set. Import validates all events, skips duplicate ids by default, and replaces
existing ids only with `--replace-existing`.
set. Import validates all events, batches writes through SQLite, skips duplicate
ids by default, and replaces existing ids only with `--replace-existing`.

`tree-ring audit` is non-mutating. It reports deterministic local findings for
stale expiry, sensitive retention, low-confidence durable memory, supersession
Expand Down Expand Up @@ -362,6 +364,7 @@ cargo run -p tree-ring-memory-cli -- dox sync --help
cargo run -p tree-ring-memory-cli -- revolve sync --help
cargo run -p tree-ring-memory-cli -- integrations scan --help
cargo run --release -p tree-ring-memory-sqlite --example performance_smoke -- 1000
sh scripts/certify-tree-ring.sh
sh scripts/package-release.sh
```

Expand All @@ -370,6 +373,23 @@ asserts nonempty recalls, emits a `METRICS_JSON=` line, and uses conservative
local thresholds of at least 500 inserts/sec and max recall latency of 250 ms
for the synthetic workload.

`scripts/certify-tree-ring.sh` runs the fuller local certification suite:
formatting, tests, Clippy, release build, isolated project/global installs, CLI
JSON smokes, DOX/Revolve adapter smokes, integration-scan origin checks, import
throughput, and 10k/30k recall timing. It writes
`target/tree-ring-certification/summary.md` and `metrics.json`.

Latest local certification run, generated at `2026-07-09T02:42:24Z` with
Agent Zero plugin smoke enabled:

- Release binary: 6,104,272 bytes.
- Project install with init: 6,032 KB.
- Global install: 5,988 KB.
- CLI import: 10,000 memories in 5 seconds, about 2,000/sec.
- 10k recall max: 6.740 ms.
- 30k recall max: 14.705 ms.
- Agent Zero plugin smoke: passed.

`scripts/package-release.sh` builds the Rust CLI in release mode, creates a
platform tarball under `dist/`, and writes a SHA-256 checksum file. Tag pushes
run the release artifact workflow for Linux and macOS.
Expand Down
103 changes: 91 additions & 12 deletions crates/tree-ring-memory-cli/src/integrations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,23 @@ pub struct AgentIntegration {
pub name: &'static str,
pub status: IntegrationStatus,
pub confidence: f64,
pub markers: Vec<String>,
pub markers: Vec<IntegrationMarker>,
pub next_step: &'static str,
}

#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
pub struct IntegrationMarker {
pub path: String,
pub origin: MarkerOrigin,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum MarkerOrigin {
Home,
Project,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum IntegrationStatus {
Expand All @@ -28,12 +41,24 @@ pub enum IntegrationStatus {

pub fn scan_integrations(root: &Path) -> IntegrationScanReport {
let home = env::var_os("HOME").map(PathBuf::from);
scan_integrations_with_home(root, home.as_deref())
}

pub fn format_markers(markers: &[IntegrationMarker]) -> String {
markers
.iter()
.map(|marker| format!("{}:{}", marker.origin.as_str(), marker.path))
.collect::<Vec<_>>()
.join(", ")
}

fn scan_integrations_with_home(root: &Path, home: Option<&Path>) -> IntegrationScanReport {
let mut integrations = vec![
detect(
"dox",
"DOX / AGENTS.md",
root,
home.as_deref(),
home,
&["AGENTS.md"],
&[],
"Run `tree-ring dox sync --source-root . --dry-run`, then sync when the preview looks right.",
Expand All @@ -42,7 +67,7 @@ pub fn scan_integrations(root: &Path) -> IntegrationScanReport {
"revolve",
"Revolve",
root,
home.as_deref(),
home,
&["revolve", ".revolve"],
&[],
"Run `tree-ring revolve sync --source-root revolve --dry-run` to import promoted lessons, scars, and seeds.",
Expand All @@ -51,7 +76,7 @@ pub fn scan_integrations(root: &Path) -> IntegrationScanReport {
"codex",
"Codex",
root,
home.as_deref(),
home,
&[".codex", "AGENTS.md"],
&[".codex"],
"Reference `.tree-ring/SKILL.md` and `.tree-ring/CLI.md` from project guidance.",
Expand All @@ -60,7 +85,7 @@ pub fn scan_integrations(root: &Path) -> IntegrationScanReport {
"claude-code",
"Claude Code",
root,
home.as_deref(),
home,
&[".claude", "CLAUDE.md"],
&[".claude"],
"Reference `.tree-ring/SKILL.md` from `CLAUDE.md` or `.claude` project instructions.",
Expand All @@ -69,7 +94,7 @@ pub fn scan_integrations(root: &Path) -> IntegrationScanReport {
"agent-zero",
"Agent Zero / A0",
root,
home.as_deref(),
home,
&["usr/plugins", "a0", "agent-zero", ".a0"],
&[".a0"],
"Use the generated skill/CLI guidance, or bridge via an Agent Zero plugin without modifying core code.",
Expand All @@ -78,7 +103,7 @@ pub fn scan_integrations(root: &Path) -> IntegrationScanReport {
"goose",
"Goose",
root,
home.as_deref(),
home,
&[".goose", "goosehints"],
&[".goose"],
"Add Tree Ring Memory recall and remember commands to Goose project instructions.",
Expand All @@ -87,7 +112,7 @@ pub fn scan_integrations(root: &Path) -> IntegrationScanReport {
"opencode",
"OpenCode",
root,
home.as_deref(),
home,
&[".opencode", "opencode.json", "opencode.toml"],
&[".opencode"],
"Reference the Tree Ring CLI from OpenCode project configuration or instructions.",
Expand All @@ -96,7 +121,7 @@ pub fn scan_integrations(root: &Path) -> IntegrationScanReport {
"hermes",
"Hermes",
root,
home.as_deref(),
home,
&[".hermes", "hermes.toml"],
&[".hermes"],
"Reference Tree Ring Memory as a local CLI memory lifecycle layer.",
Expand All @@ -105,7 +130,7 @@ pub fn scan_integrations(root: &Path) -> IntegrationScanReport {
"pi",
"Pi",
root,
home.as_deref(),
home,
&[".pi", "pi.toml"],
&[".pi"],
"Use the generated portable skill and CLI reference as project instructions.",
Expand All @@ -129,6 +154,15 @@ pub fn scan_integrations(root: &Path) -> IntegrationScanReport {
}
}

impl MarkerOrigin {
pub fn as_str(self) -> &'static str {
match self {
Self::Project => "project",
Self::Home => "home",
}
}
}

fn detect(
id: &'static str,
name: &'static str,
Expand All @@ -142,14 +176,20 @@ fn detect(
for marker in project_markers {
let path = root.join(marker);
if path.exists() {
markers.push(path.display().to_string());
markers.push(IntegrationMarker {
path: path.display().to_string(),
origin: MarkerOrigin::Project,
});
}
}
if let Some(home) = home {
for marker in home_markers {
let path = home.join(marker);
if path.exists() {
markers.push(path.display().to_string());
markers.push(IntegrationMarker {
path: path.display().to_string(),
origin: MarkerOrigin::Home,
});
}
}
}
Expand Down Expand Up @@ -197,6 +237,37 @@ mod tests {
assert!(detected(&report, "claude-code"));
}

#[test]
fn marks_project_and_home_marker_origins() {
let project = tempdir().unwrap();
let home = tempdir().unwrap();
fs::write(project.path().join("CLAUDE.md"), "# Claude instructions").unwrap();
fs::create_dir_all(project.path().join(".codex")).unwrap();
fs::create_dir_all(home.path().join(".claude")).unwrap();

let report = scan_integrations_with_home(project.path(), Some(home.path()));

let claude = integration(&report, "claude-code");
assert!(claude
.markers
.iter()
.any(|marker| marker.origin == MarkerOrigin::Project
&& marker.path.ends_with("CLAUDE.md")));
assert!(claude
.markers
.iter()
.any(|marker| marker.origin == MarkerOrigin::Home && marker.path.ends_with(".claude")));

let codex = integration(&report, "codex");
assert!(codex.markers.iter().any(
|marker| marker.origin == MarkerOrigin::Project && marker.path.ends_with(".codex")
));
assert!(!codex
.markers
.iter()
.any(|marker| marker.origin == MarkerOrigin::Home));
}

#[test]
fn unavailable_integrations_still_include_onboarding_next_steps() {
let dir = tempdir().unwrap();
Expand All @@ -217,4 +288,12 @@ mod tests {
integration.id == id && integration.status == IntegrationStatus::Detected
})
}

fn integration<'a>(report: &'a IntegrationScanReport, id: &str) -> &'a AgentIntegration {
report
.integrations
.iter()
.find(|integration| integration.id == id)
.unwrap()
}
}
5 changes: 4 additions & 1 deletion crates/tree-ring-memory-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1095,7 +1095,10 @@ fn print_integration_report(
integration.name, integration.status, integration.confidence
);
if !integration.markers.is_empty() {
println!(" markers: {}", integration.markers.join(", "));
println!(
" markers: {}",
integrations::format_markers(&integration.markers)
);
}
println!(" next: {}", integration.next_step);
}
Expand Down
Loading
Loading