From d6826b0f00830a215a4009154e93ba21b698abf8 Mon Sep 17 00:00:00 2001 From: lazy Date: Tue, 8 Sep 2026 16:05:46 -0400 Subject: [PATCH] fix: initialize lifecycle bridges during installer onboarding --- Cargo.lock | 6 +- Cargo.toml | 2 +- README.md | 5 + crates/tree-ring-memory-cli/Cargo.toml | 4 +- .../src/activation/adapters.rs | 4 +- crates/tree-ring-memory-cli/src/main.rs | 114 +++++++++++------- crates/tree-ring-memory-cli/src/welcome.rs | 39 ++++-- .../tests/harness_activation_acceptance.rs | 10 +- 8 files changed, 125 insertions(+), 59 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6f36b28..8b45f7c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1690,7 +1690,7 @@ checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" [[package]] name = "tree-ring-memory-cli" -version = "0.15.6" +version = "0.15.7" dependencies = [ "chrono", "clap", @@ -1709,7 +1709,7 @@ dependencies = [ [[package]] name = "tree-ring-memory-core" -version = "0.15.6" +version = "0.15.7" dependencies = [ "chrono", "libc", @@ -1725,7 +1725,7 @@ dependencies = [ [[package]] name = "tree-ring-memory-sqlite" -version = "0.15.6" +version = "0.15.7" dependencies = [ "rusqlite", "serde", diff --git a/Cargo.toml b/Cargo.toml index 4a57f22..faf7243 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ members = [ resolver = "2" [workspace.package] -version = "0.15.6" +version = "0.15.7" edition = "2021" license = "MIT" authors = ["TerminallyLazy"] diff --git a/README.md b/README.md index 22d2c75..2786d4a 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/crates/tree-ring-memory-cli/Cargo.toml b/crates/tree-ring-memory-cli/Cargo.toml index fc488e3..8d49814 100644 --- a/crates/tree-ring-memory-cli/Cargo.toml +++ b/crates/tree-ring-memory-cli/Cargo.toml @@ -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 diff --git a/crates/tree-ring-memory-cli/src/activation/adapters.rs b/crates/tree-ring-memory-cli/src/activation/adapters.rs index c7cbbee..894d8a5 100644 --- a/crates/tree-ring-memory-cli/src/activation/adapters.rs +++ b/crates/tree-ring-memory-cli/src/activation/adapters.rs @@ -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; @@ -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!( diff --git a/crates/tree-ring-memory-cli/src/main.rs b/crates/tree-ring-memory-cli/src/main.rs index 2835de7..66987b3 100644 --- a/crates/tree-ring-memory-cli/src/main.rs +++ b/crates/tree-ring-memory-cli/src/main.rs @@ -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, + ), + 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(), @@ -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( @@ -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!( "{}", diff --git a/crates/tree-ring-memory-cli/src/welcome.rs b/crates/tree-ring-memory-cli/src/welcome.rs index b2ea0a6..4f3de5c 100644 --- a/crates/tree-ring-memory-cli/src/welcome.rs +++ b/crates/tree-ring-memory-cli/src/welcome.rs @@ -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, }; @@ -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)?; + (true, Some(awareness), Some(status)) } else { - (db_path.exists(), None) + (db_path.exists(), None, None) }; if json_output { @@ -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), }) ); @@ -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(()) } @@ -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(); @@ -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()); @@ -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()); diff --git a/crates/tree-ring-memory-cli/tests/harness_activation_acceptance.rs b/crates/tree-ring-memory-cli/tests/harness_activation_acceptance.rs index c23af2e..f7e3eda 100644 --- a/crates/tree-ring-memory-cli/tests/harness_activation_acceptance.rs +++ b/crates/tree-ring-memory-cli/tests/harness_activation_acceptance.rs @@ -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])