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 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.6"
version = "0.15.7"
edition = "2021"
license = "MIT"
authors = ["TerminallyLazy"]
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -847,3 +847,8 @@ streams into durable memory without a validated write command.
- Sensitive data fails closed.
- Forgetting and supersession are first-class.
- Memory quality should be testable.

Installer onboarding (`welcome --init`) initializes the same project activation
manifest and safe, create-only harness bridges as `init` starting in CLI 0.15.7.
Existing user hooks and memory stores are preserved. Onboarding reports each
harness state; automatic use still requires host trust and a fresh receipt.
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.6" }
tree-ring-memory-sqlite = { path = "../tree-ring-memory-sqlite", version = "0.15.6" }
tree-ring-memory-core = { path = "../tree-ring-memory-core", version = "0.15.7" }
tree-ring-memory-sqlite = { path = "../tree-ring-memory-sqlite", version = "0.15.7" }

[dev-dependencies]
rusqlite.workspace = true
4 changes: 2 additions & 2 deletions crates/tree-ring-memory-cli/src/activation/adapters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +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"),
("3.4.1", "0.15.7", "0.15"),
];
const MAX_AGENT_ZERO_CAPABILITY_BYTES: u64 = 16 * 1024;

Expand Down Expand Up @@ -1123,7 +1123,7 @@ mod tests {
.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}"#,
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.7","minor":"0.15"},"enabled":true}"#,
)
.unwrap();
assert_eq!(
Expand Down
114 changes: 71 additions & 43 deletions crates/tree-ring-memory-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1352,7 +1352,16 @@ fn run(cli: Cli) -> Result<(), String> {
Ok(())
}

fn run_init(root: &Path, dry_run: bool, json_output: bool) -> Result<(), String> {
fn plan_init(
root: &Path,
) -> Result<
(
activation::adapters::ActivationProject,
actions::integrations::IntegrationScanActionReport,
Vec<activation::adapters::AdapterDetection>,
),
String,
> {
let project = activation::adapters::ActivationProject::from_memory_root(root.to_path_buf())?;
let scan = integration_scan_action(IntegrationScanRequest {
source_root: project.project_root.clone(),
Expand All @@ -1376,7 +1385,67 @@ fn run_init(root: &Path, dry_run: bool, json_output: bool) -> Result<(), String>
}
}

Ok((project, scan, candidates))
}

fn initialize_project(
root: &Path,
) -> Result<
(
agent_awareness::AgentAwarenessReport,
IntegrationStatusActionReport,
),
String,
> {
let (project, scan, candidates) = plan_init(root)?;
let awareness = agent_awareness::ensure_agent_awareness(root)?;
let context = write_context(None, "cli:init")?;
let store = SQLiteMemoryStore::open_with_context(root.join("memory.sqlite"), context)
.map_err(|error| error.to_string())?;
drop(store);

let mut manifest = activation::bridge::load_init_manifest_no_follow(&project)?
.unwrap_or_else(|| new_activation_manifest(&project.project_root));
let outcomes = activation::bridge::apply_bridge_plans_create_only(
&project,
&mut manifest,
candidates
.iter()
.map(|detection| detection.plan.clone())
.collect(),
)?;
let mut status = integration_status_action(IntegrationStatusRequest {
source_root: project.project_root,
memory_root: root.to_path_buf(),
verbose: true,
})?;
status.store_id = Some(manifest.store_id);
for outcome in outcomes {
if let Some(entry) = status
.integrations
.iter_mut()
.find(|entry| entry.id == outcome.harness_id)
{
if outcome.result.state == activation::ActivationState::NeedsUserReview {
entry.state = outcome.result.state;
entry.next_step = outcome.result.next_step;
}
}
}
status.integrations.retain(|entry| {
entry.id == "agent-zero"
|| scan
.report
.by_id(&entry.id)
.is_some_and(|item| item.is_candidate())
});

Ok((awareness, status))
}

fn run_init(root: &Path, dry_run: bool, json_output: bool) -> Result<(), String> {
if dry_run {
let (_, _, candidates) = plan_init(root)?;
let reports = candidates
.into_iter()
.map(
Expand Down Expand Up @@ -1420,48 +1489,7 @@ fn run_init(root: &Path, dry_run: bool, json_output: bool) -> Result<(), String>
return Ok(());
}

let awareness = agent_awareness::ensure_agent_awareness(root)?;
let context = write_context(None, "cli:init")?;
let store = SQLiteMemoryStore::open_with_context(root.join("memory.sqlite"), context)
.map_err(|error| error.to_string())?;
drop(store);

let mut manifest = activation::bridge::load_init_manifest_no_follow(&project)?
.unwrap_or_else(|| new_activation_manifest(&project.project_root));
let outcomes = activation::bridge::apply_bridge_plans_create_only(
&project,
&mut manifest,
candidates
.iter()
.map(|detection| detection.plan.clone())
.collect(),
)?;
let mut status = integration_status_action(IntegrationStatusRequest {
source_root: project.project_root,
memory_root: root.to_path_buf(),
verbose: true,
})?;
status.store_id = Some(manifest.store_id);
for outcome in outcomes {
if let Some(entry) = status
.integrations
.iter_mut()
.find(|entry| entry.id == outcome.harness_id)
{
if outcome.result.state == activation::ActivationState::NeedsUserReview {
entry.state = outcome.result.state;
entry.next_step = outcome.result.next_step;
}
}
}
status.integrations.retain(|entry| {
entry.id == "agent-zero"
|| scan
.report
.by_id(&entry.id)
.is_some_and(|item| item.is_candidate())
});

let (awareness, status) = initialize_project(root)?;
if json_output {
println!(
"{}",
Expand Down
39 changes: 32 additions & 7 deletions crates/tree-ring-memory-cli/src/welcome.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@ use std::io::{self, IsTerminal, Write};
use std::path::Path;
use std::thread;
use std::time::Duration;
use tree_ring_memory_sqlite::SQLiteMemoryStore;

use crate::agent_awareness::{ensure_agent_awareness, AgentAwarenessReport};
use crate::agent_awareness::AgentAwarenessReport;
use crate::ring_mark::{
pulse_index, ring_mark_rows_with_activity, RingMarkActivity, RingMarkCell, RingMarkLayer,
};
Expand All @@ -30,12 +29,11 @@ const CORAL_BG: &str = "48;2;255;101;83";

pub fn run(root: &Path, init: bool, no_animation: bool, json_output: bool) -> Result<(), String> {
let db_path = root.join("memory.sqlite");
let (initialized, awareness) = if init {
let awareness = ensure_agent_awareness(root)?;
SQLiteMemoryStore::open(&db_path).map_err(|err| err.to_string())?;
(true, Some(awareness))
let (initialized, awareness, status) = if init {
let (awareness, status) = crate::initialize_project(root)?;

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

1. Custom-root users get partial setup 🐞 Bug ≡ Correctness

welcome::run now sends every requested root into initialize_project, whose activation layer
requires the final directory to be exactly .tree-ring even though the CLI and installer accept any
memory-store root. With --root cache or any other basename, awareness files and memory.sqlite
are created first and manifest loading then errors, so installer onboarding exits unsuccessfully
after leaving a partially initialized store.
Agent Prompt
## Issue description
`welcome --init` now partially initializes custom memory roots before activation rejects them for not being named `.tree-ring`.

## Issue Context
The CLI and installer expose `--root` as a general memory-store directory, but activation enforces a stricter project-local `.tree-ring` shape. Make that contract consistent: either safely support configured roots throughout activation, or reject unsupported roots before creating awareness files or the SQLite database and update the exposed contract accordingly. Add coverage for installer onboarding with a non-default root.

## Fix Focus Areas
- crates/tree-ring-memory-cli/src/welcome.rs[30-36]
- crates/tree-ring-memory-cli/src/main.rs[1391-1416]
- crates/tree-ring-memory-cli/src/activation/bridge.rs[2193-2199]
- install.sh[60-61]

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

2. Nested projects call the wrong store 🐞 Bug ≡ Correctness

initialize_project emits bridge commands that derive project_root from git rev-parse and
hard-code $project_root/.tree-ring, rather than using the memory root whose parent was
initialized. When --root nested/.tree-ring identifies a nested project inside a larger Git
checkout and a native harness is detected there, its new hooks target the outer checkout's store
instead of the initialized nested store.
Agent Prompt
## Issue description
Lifecycle hooks created by onboarding can address the outer Git checkout's `.tree-ring` instead of the configured nested project's store.

## Issue Context
Activation derives the project from the configured memory root, but generated hook commands independently redefine the project as the Git top level. Generate commands that resolve the same project and memory root initialized by onboarding, while retaining safe behavior from nested working directories. Add a test for a nested project within a larger Git checkout.

## Fix Focus Areas
- crates/tree-ring-memory-cli/src/welcome.rs[30-34]
- crates/tree-ring-memory-cli/src/main.rs[1407-1416]
- crates/tree-ring-memory-cli/src/activation/lifecycle.rs[14-20]
- crates/tree-ring-memory-cli/src/activation/bridge.rs[66-102]

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

(true, Some(awareness), Some(status))
} else {
(db_path.exists(), None)
(db_path.exists(), None, None)
};

if json_output {
Expand All @@ -48,6 +46,7 @@ pub fn run(root: &Path, init: bool, no_animation: bool, json_output: bool) -> Re
"initialized": initialized,
"init_requested": init,
"agent_awareness": awareness,
"activation": status,
"next": next_commands(root),
})
);
Expand All @@ -63,6 +62,17 @@ pub fn run(root: &Path, init: bool, no_animation: bool, json_output: bool) -> Re
color,
!no_animation,
)?;
if let Some(status) = status {
println!("\nHarness readiness");
for entry in status.integrations {
println!(
" {}: {}",
entry.name,
crate::activation_state_name(entry.state)
);
println!(" {}", entry.next_step);
}
}
Ok(())
}

Expand Down Expand Up @@ -396,6 +406,17 @@ mod tests {
assert!(contains_active_style(&scar_frame, CORAL, CORAL_FG));
}

#[test]
fn welcome_without_init_does_not_create_project_state() {
let dir = tempdir().unwrap();
let root = dir.path().join(".tree-ring");

run(&root, false, true, true).unwrap();

assert!(!root.exists());
assert!(!dir.path().join(".codex/hooks.json").exists());
}

#[test]
fn no_animation_welcome_can_initialize_store() {
let dir = tempdir().unwrap();
Expand All @@ -404,6 +425,8 @@ mod tests {
run(&root, true, true, false).unwrap();

assert!(root.join("memory.sqlite").exists());
assert!(root.join("activation.json").exists());
assert!(root.join("activation/agent-zero.json").exists());
assert!(root.join("AGENTS.md").exists());
assert!(root.join("SKILL.md").exists());
assert!(root.join("CLI.md").exists());
Expand All @@ -417,6 +440,8 @@ mod tests {
run(&root, true, true, true).unwrap();

assert!(root.join("memory.sqlite").exists());
assert!(root.join("activation.json").exists());
assert!(root.join("activation/agent-zero.json").exists());
assert!(root.join("AGENTS.md").exists());
assert!(root.join("SKILL.md").exists());
assert!(root.join("CLI.md").exists());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,18 @@ fn generated_hooks_capture_and_recall_across_sessions_with_only_a_local_runtime(
.current_dir(&project)
.env("PATH", "/usr/bin:/bin")
.env("HOME", temp.path().join("fixture-home"))
.args(["--json", "init"])
.args(["--json", "welcome", "--init", "--no-animation"])
.output()
.unwrap();
assert_success("local init", &init);
let onboarding: Value = serde_json::from_slice(&init.stdout).unwrap();
assert_eq!(onboarding["initialized"], true);
assert!(onboarding["activation"]["store_id"].is_string());
assert!(onboarding["activation"]["integrations"]
.as_array()
.unwrap()
.iter()
.all(|entry| entry["state"] != "active"));
let run = |command: &str, input: Value| {
let mut child = Command::new("/bin/sh")
.args(["-c", command])
Expand Down