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
6 changes: 3 additions & 3 deletions .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@
"name": "TerminallyLazy",
"url": "https://github.com/TerminallyLazy"
},
"description": "Claude Code marketplace for Tree Ring Memory v0.15 verified bootstrap, local-first recall, and receipt-backed harness readiness.",
"version": "0.3.2",
"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",
"plugins": [
{
"name": "tree-ring-memory",
"source": "./plugins/tree-ring-memory",
"displayName": "Tree Ring Memory",
"description": "Local-first memory lifecycle, project bootstrap, and receipt-backed harness guidance for Claude Code using Tree Ring Memory v0.15+.",
"description": "Local-first recall, strict agent-mediated automatic capture, project bootstrap, and receipt-backed harness guidance for Claude Code using Tree Ring Memory v0.15+.",
"author": {
"name": "TerminallyLazy",
"url": "https://github.com/TerminallyLazy"
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.4"
version = "0.15.5"
edition = "2021"
license = "MIT"
authors = ["TerminallyLazy"]
Expand Down
23 changes: 15 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -244,8 +244,9 @@ files in the memory root:
- `.tree-ring/AGENTS.md`: DOX-style Tree Ring Memory guidance and root
`AGENTS.md` merge notes.
- `.tree-ring/SKILL.md`: portable skill instructions for agent runtimes.
- `.tree-ring/CLI.md`: quick command reference for recall, remember, evidence,
DOX/Revolve sync, import/export, audit, maintenance, and TUI usage.
- `.tree-ring/CLI.md`: quick command reference for recall, strict automatic
capture, remember, evidence, DOX/Revolve sync, import/export, audit,
maintenance, and TUI usage.

Existing awareness files are left untouched. Tree Ring Memory does not modify a
project's root `AGENTS.md`; merge the generated guidance manually when you want
Expand Down Expand Up @@ -337,6 +338,7 @@ The `tree-ring` command is the Rust CLI.
```bash
tree-ring init
tree-ring remember "Use protocol-first design." --event-type decision --tag architecture
tree-ring capture "Use receipt-backed lifecycle checkpoints." --event-type decision --ring cambium --project example-service --agent-profile codex --workflow-id workflow-1 --session-id session-1 --operation-id auto-checkpoint-1 --source-ref agent-checkpoint:checkpoint
tree-ring evidence "Snapshot invalidation fixed stale unread chat state." --outcome promoted --evidence-ref evals/chat-state/run-042 --score 0.91
tree-ring recall "protocol design"
tree-ring forget mem_example --mode delete --reason "example cleanup"
Expand All @@ -361,6 +363,9 @@ Command ownership is Rust-native:
`.tree-ring/SKILL.md`, and `.tree-ring/CLI.md` without overwriting existing
files, then attempts create-only project harness configuration.
- `remember`, `recall`, and `forget` cover direct memory capture, retrieval, redaction, and deletion.
- `capture` is the strict automatic lifecycle-write path: agent-scoped only,
normal sensitivity only, bounded cambium/scar/seed classifications, and
required checkpoint identity, idempotency, and provenance.
- `evidence` is the Revolve-inspired improvement-loop entry point for evaluated outcomes.
- `dox sync` and `revolve sync` are read-only source adapters that summarize and point back to authoritative files.
- `integrations scan` discovers nearby agent-framework markers and suggests setup paths without changing their config.
Expand Down Expand Up @@ -784,7 +789,7 @@ The current activation protocol is documented in

## Agent Workflow Integration

- `skills/tree-ring-memory/SKILL.md` gives agents portable guidance for when to recall, remember, redact, forget, or avoid memory capture.
- `skills/tree-ring-memory/SKILL.md` gives agents portable guidance for when to recall, automatically capture, remember, redact, forget, or avoid memory capture.
- `templates/dox/AGENTS.md` is a DOX-style project contract template for repos that want Tree Ring Memory rules alongside source code.
- `docs/integrations/agent-skill.md` explains how to use both without making memory more authoritative than local project docs.
- `tree-ring init` creates canonical guidance and safely configures maintained
Expand Down Expand Up @@ -814,11 +819,13 @@ view to `configured-awaiting-proof` and then receipt-backed `active`. There is
no manual descriptor workflow, and the core/plugin release pair must be
compatible before this path is available.

Memory updates are agent-mediated. Bridge files tell the active agent when to
call `tree-ring recall`, `tree-ring remember`, `tree-ring evidence`,
`tree-ring forget`, `tree-ring consolidate --dry-run`, or `tree-ring maintain`.
Tree Ring does not scrape transcripts, run a hidden recorder, or turn TUI
event-stream pulses into durable memory without an explicit write command.
Memory updates are agent-mediated. Maintained lifecycle bridges perform recall
at session and subagent start, then enforce one automatic checkpoint at stop.
The active agent submits zero to three concise durable normal-sensitivity
candidates through strict `tree-ring capture`; manual `remember` and `evidence`
remain separate surfaces. Zero is correct when nothing reusable occurred.
Tree Ring does not scrape transcripts, run a hidden recorder, or turn TUI event
streams into durable memory without a validated write command.

## Brand Assets

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.4" }
tree-ring-memory-sqlite = { path = "../tree-ring-memory-sqlite", version = "0.15.4" }
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" }

[dev-dependencies]
rusqlite.workspace = true
183 changes: 183 additions & 0 deletions crates/tree-ring-memory-cli/src/actions/remember.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,20 @@ pub struct RememberReport {
pub created: bool,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CaptureRequest {
pub summary: String,
pub event_type: String,
pub ring: String,
pub project: String,
pub agent_profile: String,
pub workflow_id: String,
pub session_id: String,
pub operation_id: String,
pub source_ref: String,
pub tags: Vec<String>,
}

pub fn remember(
store: &mut SQLiteMemoryStore,
request: RememberRequest,
Expand Down Expand Up @@ -71,6 +85,96 @@ pub fn remember(
Ok(RememberReport { memory, created })
}

/// Stores one strict agent-mediated automatic capture candidate.
///
/// Automatic capture deliberately cannot create shared, heartwood, evaluation,
/// or sensitive memory. The lifecycle hook supplies the trusted routing and
/// checkpoint provenance; the active agent supplies only the concise durable
/// candidate and its bounded classification.
pub fn capture(
store: &mut SQLiteMemoryStore,
mut request: CaptureRequest,
) -> ActionResult<RememberReport> {
let allowed = matches!(
(request.event_type.as_str(), request.ring.as_str()),
("preference" | "decision", "cambium")
| ("lesson" | "correction", "cambium" | "scar")
| ("warning", "scar")
| ("seed", "seed")
);
if !allowed {
if !matches!(request.ring.as_str(), "cambium" | "scar" | "seed") {
return Err("automatic capture uses an unsupported ring".to_string());
}
return Err("automatic capture uses an unsupported event type or ring pairing".to_string());
}
let checkpoint_id = request
.source_ref
.strip_prefix("agent-checkpoint:")
.filter(|value| {
!value.is_empty()
&& value.len() <= 128
&& value
.chars()
.all(|character| character.is_ascii_alphanumeric() || "-_.".contains(character))
})
.ok_or_else(|| {
"automatic capture source-ref must contain a safe agent-checkpoint id".to_string()
})?;
let operation_prefix = format!("auto-{checkpoint_id}-");
let slot = request
.operation_id
.strip_prefix(&operation_prefix)
.filter(|slot| matches!(*slot, "1" | "2" | "3"));
if slot.is_none() {
return Err(
"automatic capture operation-id must match its checkpoint and slot 1, 2, or 3"
.to_string(),
);
}

let guard = SensitivityGuard::default();
let values = [
request.summary.as_str(),
request.event_type.as_str(),
request.ring.as_str(),
request.project.as_str(),
request.agent_profile.as_str(),
request.workflow_id.as_str(),
request.session_id.as_str(),
request.operation_id.as_str(),
request.source_ref.as_str(),
]
.into_iter()
.chain(request.tags.iter().map(String::as_str));
let sensitivity = guard
.detect_text_sensitivity(values)
.map_err(|_| "automatic capture accepts only normal-sensitivity candidates".to_string())?;
if sensitivity != "normal" {
return Err("automatic capture accepts only normal-sensitivity candidates".to_string());
}
if !request.tags.iter().any(|tag| tag == "automatic-capture") {
request.tags.push("automatic-capture".to_string());
}

remember(
store,
RememberRequest {
summary: request.summary,
event_type: request.event_type,
ring: request.ring,
scope: "agent".to_string(),
project: Some(request.project),
agent_profile: Some(request.agent_profile),
workflow_id: Some(request.workflow_id),
session_id: Some(request.session_id),
operation_id: Some(request.operation_id),
source_ref: Some(request.source_ref),
tags: request.tags,
},
)
}

pub fn store_event_idempotently(
store: &mut SQLiteMemoryStore,
event: &MemoryEvent,
Expand Down Expand Up @@ -207,6 +311,85 @@ mod tests {
assert_eq!(report.memory.source.ref_, "runs/fanout-42/reviewer-2.json");
}

#[test]
fn automatic_capture_stores_only_agent_scoped_checkpoint_memory() {
let dir = tempdir().unwrap();
let mut store = SQLiteMemoryStore::open(dir.path().join("memory.sqlite")).unwrap();

let report = capture(
&mut store,
CaptureRequest {
summary: "Use receipt-backed lifecycle hooks for automatic recall.".to_string(),
event_type: "decision".to_string(),
ring: "cambium".to_string(),
project: "tree-ring-memory".to_string(),
agent_profile: "codex".to_string(),
workflow_id: "workflow-1".to_string(),
session_id: "session-1".to_string(),
operation_id: "auto-checkpoint-1-1".to_string(),
source_ref: "agent-checkpoint:checkpoint-1".to_string(),
tags: vec!["hooks".to_string()],
},
)
.unwrap();

assert!(report.created);
assert_eq!(report.memory.scope, "agent");
assert_eq!(report.memory.agent_profile.as_deref(), Some("codex"));
assert!(report
.memory
.tags
.iter()
.any(|tag| tag == "automatic-capture"));
}

#[test]
fn automatic_capture_rejects_sensitive_or_unbounded_candidates() {
let dir = tempdir().unwrap();
let mut store = SQLiteMemoryStore::open(dir.path().join("memory.sqlite")).unwrap();
let request = |summary: &str, event_type: &str, ring: &str| CaptureRequest {
summary: summary.to_string(),
event_type: event_type.to_string(),
ring: ring.to_string(),
project: "tree-ring-memory".to_string(),
agent_profile: "codex".to_string(),
workflow_id: "workflow-1".to_string(),
session_id: "session-1".to_string(),
operation_id: "auto-checkpoint-1-1".to_string(),
source_ref: "agent-checkpoint:checkpoint-1".to_string(),
tags: Vec::new(),
};

assert!(capture(
&mut store,
request(
"token = sk-proj-abcdefghijklmnopqrstuvwxyz1234567890",
"lesson",
"cambium"
)
)
.unwrap_err()
.contains("normal-sensitivity"));
assert!(capture(
&mut store,
request("Promote this automatically.", "decision", "heartwood")
)
.unwrap_err()
.contains("unsupported ring"));
assert!(capture(
&mut store,
request("Store a transcript summary.", "transcript", "cambium")
)
.unwrap_err()
.contains("unsupported event type"));
let mut mismatched = request("Store a fourth candidate.", "lesson", "cambium");
mismatched.operation_id = "auto-checkpoint-1-4".to_string();
assert!(capture(&mut store, mismatched)
.unwrap_err()
.contains("slot 1, 2, or 3"));
assert!(store.list_all(false).unwrap().is_empty());
}

#[test]
fn operation_id_replay_is_idempotent_and_conflicts_fail_closed() {
let dir = tempdir().unwrap();
Expand Down
Loading