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.5",
"version": "0.3.6",
"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.10"
version = "0.15.11"
edition = "2021"
license = "MIT"
authors = ["TerminallyLazy"]
Expand Down
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -678,7 +678,32 @@ an agent working in that project.
local instruction files that may not be tracked in Git. Review the source paths,
project, destination store and candidates before confirming. Confirmation saves
that preview and refreshes the dashboard; cancellation leaves memories unchanged.
Files containing detected secret content are skipped entirely. With a custom
memory-store `--root`, the TUI scans the project from which it was launched.
Repeated sync updates the same source-linked records rather than duplicating them.
DOX records retain their stable IDs and include a source-root fingerprint. If a
shared store has the same ID from another project or root, sync rejects the whole
batch; use separate project stores. Matching legacy records without a fingerprint
can be updated in the source project's `.tree-ring` store; their earlier location
cannot be verified. Shared legacy records and fingerprinted records copied from
a different root require provenance review before reuse. Sync does not
automatically rebind a recorded fingerprint or migrate IDs.

If a shared legacy store rejects a sync, preserve that store and select an empty,
dedicated store for this source project. For example, when the project's
`.tree-ring` store is unused:

```bash
tree-ring --root /path/to/project/.tree-ring dox sync --source-root /path/to/project --project project-name --dry-run
# After reviewing the candidates, repeat without --dry-run to save them.
```

If that destination already contains conflicting records, choose a new dedicated
directory with `--root` instead. Existing memories remain in the original store;
this creates reviewed summaries from authoritative source files and does not
transfer or relabel old memories. There is no automatic legacy-provenance
migration command.

In Coordinated mode, saving requires the coordinator capability in the terminal
environment when the TUI starts; preview remains available without it.

Expand Down
93 changes: 92 additions & 1 deletion crates/tree-ring-memory-cli/src/actions/adapters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,31 @@ pub fn apply_dox_preview(
store: &mut SQLiteMemoryStore,
report: &DoxSyncReport,
) -> ActionResult<()> {
// Older DOX records have no root provenance. Only a source project's own
// .tree-ring store can establish that association without guessing which
// project originally wrote a shared legacy record.
// Resolve a bare file spelling such as AGENTS.md before taking its parent;
// its lexical parent is empty, not a canonicalizable project directory.
let source_root = std::fs::canonicalize(&report.root).ok().and_then(|source| {
if source.is_file() {
source.parent().map(|parent| parent.to_path_buf())
} else {
Some(source)
}
});
let local_store_project = store.database_path().ok().and_then(|database| {
// A local-looking directory or database symlink does not establish
// ownership of an external legacy store. Inspect the resolved target.
let database = std::fs::canonicalize(database).ok()?;
let memory_root = database.parent()?;
if memory_root.file_name()? != ".tree-ring" {
return None;
}
Some(memory_root.parent()?.to_path_buf())
});
let allow_legacy_sources = source_root.is_some() && source_root == local_store_project;
store
.put_many(&report.events)
.put_dox_many(&report.events, allow_legacy_sources)
Comment on lines +84 to +86

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

3. Agent zero legacy syncs fail 🔗 Cross-repo conflict ≡ Correctness

apply_dox_preview sets allow_legacy_sources only for a source-local .tree-ring database,
leaving no compatibility route for Agent Zero’s global default store. After upgrading, any
non-dry-run Agent Zero DOX sync that encounters an existing matching record without dox-root
provenance reaches the new guard and is rejected.
Agent Prompt
## Issue description
Agent Zero stores memories in a global root and exposes write-capable DOX sync, but existing DOX records there have no root fingerprint. The new guard rejects those records without providing Agent Zero a safe upgrade or reconciliation path.

## Fix Focus Areas
- crates/tree-ring-memory-cli/src/actions/adapters.rs[62-80]
- crates/tree-ring-memory-sqlite/src/lib.rs[1816-1824]
- /cross_repos/tree-ring-memory-agent-zero/helpers/upgrade.py[25-64]

## Recommended Fix
Provide an explicit, reviewed CLI migration or provenance-reconciliation operation for matching legacy DOX records, preserving collision checks. Coordinate Agent Zero’s upgrade flow to invoke that operation for its verified project and configured store before allowing write-capable DOX sync.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

.map_err(|err| err.to_string())
}

Expand Down Expand Up @@ -110,4 +133,72 @@ mod tests {
assert_eq!(report.report.memory_count, 1);
assert!(!dir.path().join("memory.sqlite").exists());
}

#[test]
fn legacy_dox_updates_only_in_its_source_projects_local_store() {
let dir = tempdir().unwrap();
fs::write(dir.path().join("AGENTS.md"), "# Rules\n\nAlways run tests.").unwrap();
let preview = sync_dox(
None,
DoxSyncActionRequest {
source_root: dir.path().to_path_buf(),
project: Some("project".to_string()),
dry_run: true,
},
)
.unwrap()
.report;
let mut legacy = preview.events[0].clone();
legacy.links.retain(|link| link.link_type != "dox-root");

for (location, allowed) in [(".tree-ring", true), ("shared-store", false)] {
let root = dir.path().join(location);
fs::create_dir(&root).unwrap();
let mut store = SQLiteMemoryStore::open(root.join("memory.sqlite")).unwrap();
store.put(&legacy).unwrap();
let result = apply_dox_preview(&mut store, &preview);
assert_eq!(result.is_ok(), allowed, "{location}: {result:?}");
let saved = store.list_all(true).unwrap();
assert_eq!(saved.len(), 1);
assert_eq!(
saved[0],
if allowed {
preview.events[0].clone()
} else {
legacy.clone()
}
);
}
}

#[test]
fn shared_store_rejects_distinct_roots_with_the_same_project_name() {
let dir = tempdir().unwrap();
let mut store = SQLiteMemoryStore::open(dir.path().join("shared.sqlite")).unwrap();
for (index, parent) in ["first", "second"].into_iter().enumerate() {
let root = dir.path().join(parent).join("project");
fs::create_dir_all(&root).unwrap();
fs::write(root.join("AGENTS.md"), "# Rules\n\nAlways run tests.").unwrap();
let preview = sync_dox(
None,
DoxSyncActionRequest {
source_root: root,
project: Some("project".to_string()),
dry_run: true,
},
)
.unwrap()
.report;
let before = store.list_all(true).unwrap();
let result = apply_dox_preview(&mut store, &preview);
if index == 0 {
result.unwrap();
apply_dox_preview(&mut store, &preview).unwrap();
assert_eq!(store.list_all(true).unwrap().len(), 1);
} else {
assert!(result.is_err());
assert_eq!(store.list_all(true).unwrap(), before);
}
}
}
}
45 changes: 45 additions & 0 deletions crates/tree-ring-memory-cli/src/agent_awareness.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ const AGENT_RUNTIME_HEADING: &str = "## Local Runtime Bootstrap And Updates";
const AGENT_RUNTIME_ANCHOR: &str = "## Harness Bridges";
const SKILL_RUNTIME_HEADING: &str = "## Runtime Bootstrap And Updates";
const SKILL_RUNTIME_ANCHOR: &str = "## When To Recall";
const SKILL_DOX_COMPAT_HEADING: &str = "## DOX Persistence Compatibility";
const SKILL_DOX_COMPAT_ANCHOR: &str = "## Harness Activation";
const CLI_RUNTIME_HEADING: &str = "Runtime bootstrap and updates:";
const CLI_RUNTIME_ANCHOR: &str = "Core commands:";
const PREFLIGHT_HEADING: &str = "## Harness Preflight";
Expand Down Expand Up @@ -218,6 +220,17 @@ pub fn ensure_agent_awareness(root: &Path) -> Result<AgentAwarenessReport, Strin
SKILL_RUNTIME_ANCHOR,
extract_section(SKILL_TEMPLATE, SKILL_RUNTIME_HEADING, SKILL_RUNTIME_ANCHOR),
)?;
maybe_backfill_generated_file(
&root.join("SKILL.md"),
is_generated_skill_file,
SKILL_DOX_COMPAT_HEADING,
SKILL_DOX_COMPAT_ANCHOR,
extract_section(
SKILL_TEMPLATE,
SKILL_DOX_COMPAT_HEADING,
SKILL_DOX_COMPAT_ANCHOR,
),
)?;
maybe_backfill_generated_file(
&root.join("CLI.md"),
is_generated_cli_file,
Expand Down Expand Up @@ -943,6 +956,38 @@ mod tests {
assert!(skill.contains("which -a tree-ring"));
}

#[test]
fn generated_backfills_dox_compatibility_without_replacing_owner_guidance() {
let dir = tempdir().unwrap();
let root = dir.path().join(".tree-ring");
fs::create_dir_all(&root).unwrap();
let old_skill = remove_managed_section(
SKILL_TEMPLATE,
SKILL_DOX_COMPAT_HEADING,
SKILL_DOX_COMPAT_ANCHOR,
)
.unwrap();
let owner_note = "\n## Owner notes\n\nKeep the project instructions separate.\n";
fs::write(root.join("SKILL.md"), format!("{old_skill}{owner_note}")).unwrap();
fs::write(root.join("AGENTS.md"), "Owner's memory contract.\n").unwrap();
ensure_agent_awareness(&root).unwrap();
let updated = fs::read_to_string(root.join("SKILL.md")).unwrap();
assert_eq!(updated.matches(SKILL_DOX_COMPAT_HEADING).count(), 1);
assert!(updated.contains("DOX persistence requires Tree Ring CLI 0.15.11 or newer"));
assert!(updated.ends_with(owner_note));
assert_eq!(
fs::read_to_string(root.join("AGENTS.md")).unwrap(),
"Owner's memory contract.\n"
);
ensure_agent_awareness(&root).unwrap();
assert_eq!(fs::read_to_string(root.join("SKILL.md")).unwrap(), updated);

let custom = "# Custom skill\n\nPreserve all owner instructions.\n";
fs::write(root.join("SKILL.md"), custom).unwrap();
ensure_agent_awareness(&root).unwrap();
assert_eq!(fs::read_to_string(root.join("SKILL.md")).unwrap(), custom);
}

#[test]
fn generated_backfills_quality_gates_into_recognized_stale_generated_files() {
let dir = tempdir().unwrap();
Expand Down
31 changes: 30 additions & 1 deletion crates/tree-ring-memory-cli/src/tui/actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ impl PendingAction {
.filter(|event| event.sensitivity != "normal")
.count();
let summary = format!(
"Import or update {} DOX summaries from {} AGENTS.md files.\nSource: {}\nStore: {}\nProject: {}\nIncludes {} sensitive; skips {} secret sections; {} warnings.\nStable IDs update existing summaries; source files stay authoritative.",
"Import or update {} DOX summaries from {} AGENTS.md files.\nSource: {}\nStore: {}\nProject: {}\nIncludes {} sensitive; secret source files skipped: {}; {} warnings.\nStable IDs update existing summaries; source files stay authoritative.",
preview.memory_count,
preview.source_count,
preview.root.display(),
Expand Down Expand Up @@ -163,6 +163,35 @@ impl PendingAction {
mod tests {
use super::*;

#[test]
fn dox_confirmation_counts_secret_source_files_not_sections() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(
dir.path().join("AGENTS.md"),
"# Rules\nReview source contracts.\n",
)
.unwrap();
std::fs::create_dir(dir.path().join("nested")).unwrap();
std::fs::write(
dir.path().join("nested/AGENTS.md"),
"# Earlier rules\nUse reviewed configuration.\n# Credentials\nUse key sk-proj-abcdefghijklmnopqrstuvwxyz1234567890\n# Later rules\nRead the source.\n",
)
.unwrap();
let preview = tree_ring_memory_core::collect_dox_memories(
&tree_ring_memory_core::DoxSyncRequest::new(dir.path()),
)
.unwrap();
assert_eq!(preview.source_count, 2);
assert_eq!(preview.skipped_secret_count, 1);
assert!(preview
.events
.iter()
.all(|event| !event.source.ref_.starts_with("nested/")));
let pending = PendingAction::sync_dox(preview, "fixture", &dir.path().join(".tree-ring"));
assert!(pending.summary.contains("secret source files skipped: 1"));
assert!(!pending.summary.contains("secret sections"));
}

#[test]
fn dangerous_actions_are_explicit_pending_values() {
let pending = PendingAction::delete("mem_1".to_string(), "Bad memory".to_string());
Expand Down
Loading