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
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"url": "https://github.com/TerminallyLazy"
},
"description": "Claude Code marketplace for Tree Ring Memory v0.15 verified bootstrap, lifecycle recall, strict automatic capture, and receipt-backed harness readiness.",
"version": "0.3.3",
"version": "0.3.4",
"plugins": [
{
"name": "tree-ring-memory",
Expand Down
6 changes: 3 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ members = [
resolver = "2"

[workspace.package]
version = "0.15.5"
version = "0.15.6"
edition = "2021"
license = "MIT"
authors = ["TerminallyLazy"]
Expand Down
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,13 @@ tree-ring init
tree-ring integrations status
```

If memory seems silent, run `tree-ring integrations status --verbose`. The last
recall result count distinguishes a successful empty recall from a hook that
has never run. CLI 0.15.6 repairs cross-session startup recall and generated
hooks for project-local installs; see the
[recall visibility and startup brief contract](docs/protocol/harness-activation.md#recall-visibility-and-startup-briefs).
The skills-only public-directory plugin does not install lifecycle hooks.

Default `init` creates the canonical project-local store and configures
maintained adapters where new project-local bridge and manifest entries can be
created safely. It does not require copying a skill or manually running
Expand Down
4 changes: 2 additions & 2 deletions crates/tree-ring-memory-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ semver.workspace = true
sha2.workspace = true
tempfile.workspace = true
uuid.workspace = true
tree-ring-memory-core = { path = "../tree-ring-memory-core", version = "0.15.5" }
tree-ring-memory-sqlite = { path = "../tree-ring-memory-sqlite", version = "0.15.5" }
tree-ring-memory-core = { path = "../tree-ring-memory-core", version = "0.15.6" }
tree-ring-memory-sqlite = { path = "../tree-ring-memory-sqlite", version = "0.15.6" }

[dev-dependencies]
rusqlite.workspace = true
102 changes: 98 additions & 4 deletions crates/tree-ring-memory-cli/src/actions/integrations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ pub struct IntegrationStatusEntry {
pub managed_paths: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub receipt_age_seconds: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_recall_result_count: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_recall_query_class: Option<String>,
pub next_step: String,
}

Expand Down Expand Up @@ -117,6 +121,12 @@ pub fn status(request: IntegrationStatusRequest) -> Result<IntegrationStatusActi
let activation = manifest
.as_ref()
.and_then(|manifest| manifest.harnesses.get(&detected.id));
let stale_adapter = activation.is_some_and(|harness| {
activation::adapters::adapter_version(&detected.id)
!= Some(harness.adapter_version.as_str())
|| activation::adapters::adapter_capability(&detected.id)
!= Some(harness.adapter_capability)
});
let receipt = manifest
.as_ref()
.and_then(|manifest| {
Expand Down Expand Up @@ -156,11 +166,16 @@ pub fn status(request: IntegrationStatusRequest) -> Result<IntegrationStatusActi
.map(|receipt| receipt.state)
.unwrap_or(detected.state)
}
} else if stale_adapter {
ActivationState::NeedsUserReview
} else {
receipt
.as_ref()
.map(|receipt| receipt.state)
.or_else(|| activation.map(|activation| activation.state))
.or_else(|| activation.map(|activation| match activation.state {
ActivationState::Active | ActivationState::ActiveIsolated => ActivationState::ConfiguredAwaitingProof,
state => state,
}))
.unwrap_or(detected.state)
};
let managed_paths = if request.verbose {
Expand All @@ -187,21 +202,34 @@ pub fn status(request: IntegrationStatusRequest) -> Result<IntegrationStatusActi
let receipt_age_seconds = request
.verbose
.then(|| {
receipt.map(|receipt| {
receipt.as_ref().map(|receipt| {
now.signed_duration_since(receipt.recorded_at)
.num_seconds()
.max(0)
})
})
.flatten();
let next_step = if stale_adapter && detected.id != "agent-zero" {
"The installed adapter definition is out of date. Review the managed hook files and activation manifest, then reconfigure them with this CLI; preserve the memory database.".to_string()
} else {
next_step_for_state(state, &detected.next_step)
};
IntegrationStatusEntry {
id: detected.id,
name: detected.name,
state,
capability: detected.capability,
managed_paths,
receipt_age_seconds,
next_step: next_step_for_state(state, &detected.next_step),
last_recall_result_count: receipt
.as_ref()
.filter(|_| request.verbose)
.map(|receipt| receipt.result_count),
last_recall_query_class: receipt
.as_ref()
.filter(|_| request.verbose)
.map(|receipt| receipt.query_class.clone()),
next_step,
}
})
.collect();
Expand Down Expand Up @@ -549,6 +577,11 @@ fn verify_activation_receipts_at(
harness: &activation::HarnessActivation,
now: chrono::DateTime<Utc>,
) -> ReceiptVerification {
if activation::adapters::adapter_version(harness_id) != Some(harness.adapter_version.as_str())
|| activation::adapters::adapter_capability(harness_id) != Some(harness.adapter_capability)
{
return invalid_receipt(None, "installed adapter definition is out of date");
}
if harness.bridge_fingerprint != bridge_fingerprint(harness_id, harness) {
return invalid_receipt(None, "bridge fingerprint does not match adapter contract");
}
Expand Down Expand Up @@ -744,7 +777,10 @@ mod tests {
adapter_version: "1".to_string(),
bridge_fingerprint: String::new(),
bridge_path: Some(".agents/skills/tree-ring-memory/SKILL.md".to_string()),
owned_files: Vec::new(),
owned_files: vec![activation::manifest::OwnedBridgeFile {
path: ".agents/skills/tree-ring-memory/SKILL.md".to_string(),
sha256: "c".repeat(64),
}],
managed_blocks: Vec::new(),
};
harness.bridge_fingerprint = bridge_fingerprint("pi", &harness);
Expand Down Expand Up @@ -789,6 +825,64 @@ mod tests {
assert!(report.report.detected_count > 0);
}

#[test]
fn status_rejects_a_matching_old_receipt_when_the_adapter_is_outdated() {
let (_temp, project, mut manifest, mut harness, mut receipt) = receipt_fixture();
fs::create_dir_all(project.project_root.join(".pi")).unwrap();
harness.adapter_version = "0".to_string();
harness.bridge_fingerprint = bridge_fingerprint("pi", &harness);
receipt.adapter_version = harness.adapter_version.clone();
receipt.bridge_fingerprint = harness.bridge_fingerprint.clone();
manifest.harnesses.insert("pi".to_string(), harness.clone());
fs::write(
project.memory_root.join("activation.json"),
serde_json::to_vec_pretty(&manifest).unwrap(),
)
.unwrap();
activation::manifest::write_receipt(&project.memory_root, &receipt).unwrap();
let report = status(IntegrationStatusRequest {
source_root: project.project_root,
memory_root: project.memory_root,
verbose: true,
})
.unwrap();
let entry = report
.integrations
.iter()
.find(|entry| entry.id == "pi")
.unwrap();
assert_eq!(entry.state, ActivationState::NeedsUserReview);
assert!(entry.next_step.contains("out of date"));
assert_eq!(entry.last_recall_result_count, None);
}

#[test]
fn persisted_active_state_without_a_receipt_does_not_claim_activation() {
let (_temp, project, mut manifest, mut harness, _receipt) = receipt_fixture();
fs::create_dir_all(project.project_root.join(".pi")).unwrap();
harness.state = ActivationState::Active;
harness.bridge_fingerprint = bridge_fingerprint("pi", &harness);
manifest.harnesses.insert("pi".to_string(), harness);
fs::write(
project.memory_root.join("activation.json"),
serde_json::to_vec_pretty(&manifest).unwrap(),
)
.unwrap();
let report = status(IntegrationStatusRequest {
source_root: project.project_root,
memory_root: project.memory_root,
verbose: true,
})
.unwrap();
let entry = report
.integrations
.iter()
.find(|entry| entry.id == "pi")
.unwrap();
assert_eq!(entry.state, ActivationState::ConfiguredAwaitingProof);
assert_eq!(entry.last_recall_result_count, None);
}

#[test]
fn agent_zero_stdin_rejects_task_hints_and_capabilities() {
let dir = tempdir().unwrap();
Expand Down
26 changes: 21 additions & 5 deletions crates/tree-ring-memory-cli/src/activation/adapters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const AGENT_ZERO_CAPABILITY_CONTRACTS: &[(&str, &str, &str)] = &[
("3.3.0", "0.15.3", "0.15"),
("3.3.1", "0.15.3", "0.15"),
("3.4.0", "0.15.5", "0.15"),
("3.4.1", "0.15.6", "0.15"),
];
const MAX_AGENT_ZERO_CAPABILITY_BYTES: u64 = 16 * 1024;

Expand Down Expand Up @@ -393,7 +394,7 @@ enum AdapterSupport {
const ADAPTERS: [DeclarativeAdapter; 7] = [
DeclarativeAdapter {
id: "codex",
version: "3",
version: "4",
display_name: "Codex",
command: "codex",
capability: AdapterCapability::NativePreflight,
Expand All @@ -403,7 +404,7 @@ const ADAPTERS: [DeclarativeAdapter; 7] = [
},
DeclarativeAdapter {
id: "claude-code",
version: "3",
version: "4",
display_name: "Claude Code",
command: "claude",
capability: AdapterCapability::NativePreflight,
Expand Down Expand Up @@ -483,7 +484,7 @@ pub fn adapter_version(id: &str) -> Option<&'static str> {
}

/// Returns the activation capability registered for a harness.
pub(crate) fn adapter_capability(id: &str) -> Option<AdapterCapability> {
pub fn adapter_capability(id: &str) -> Option<AdapterCapability> {
registered_adapters()
.find(|adapter| adapter.id == id)
.map(|adapter| adapter.capability)
Expand Down Expand Up @@ -954,8 +955,8 @@ mod tests {
.collect::<Vec<_>>(),
vec!["codex", "claude-code", "pi", "agent-zero"]
);
assert_eq!(adapter_version("codex"), Some("3"));
assert_eq!(adapter_version("claude-code"), Some("3"));
assert_eq!(adapter_version("codex"), Some("4"));
assert_eq!(adapter_version("claude-code"), Some("4"));
assert_eq!(adapter_version("pi"), Some("1"));
assert_eq!(adapter_version("agent-zero"), Some("1"));
for id in ["hermes", "opencode", "goose"] {
Expand Down Expand Up @@ -1115,6 +1116,21 @@ mod tests {
Some(AgentZeroPluginManifest::compatible())
);

fs::write(
plugin.join("plugin.yaml"),
"name: tree_ring_memory\nversion: 3.4.1\n",
)
.unwrap();
fs::write(
&descriptor,
r#"{"schema_version":1,"kind":"tree-ring-agent-zero-plugin-capability","plugin_id":"tree_ring_memory","plugin_version":"3.4.1","activation_protocol_version":1,"tree_ring_version":{"min":"0.15.6","minor":"0.15"},"enabled":true}"#,
)
.unwrap();
assert_eq!(
read_agent_zero_plugin_manifest(&project, &descriptor),
Some(AgentZeroPluginManifest::compatible())
);

fs::write(
plugin.join("plugin.yaml"),
"name: tree_ring_memory\nversion: 3.4.0\n",
Expand Down
Loading