diff --git a/cli/migrations/agent-trace-repository/002_repository_source_instance_id.sql b/cli/migrations/agent-trace-repository/002_repository_source_instance_id.sql new file mode 100644 index 00000000..6c65598e --- /dev/null +++ b/cli/migrations/agent-trace-repository/002_repository_source_instance_id.sql @@ -0,0 +1,7 @@ +-- Adds the stable per-database-lineage source-instance identity alongside the +-- logical repository_id already stored in repository_metadata. This column is +-- additive to the 001 baseline: existing rows get an empty placeholder here, +-- and initialization code (RepositoryAgentTraceDb::verify_or_initialize_repository_metadata) +-- atomically fills it in exactly once. A migration can only add a fixed +-- default, so it must never invent a pseudo-UUID itself. +ALTER TABLE repository_metadata ADD COLUMN source_instance_id TEXT NOT NULL DEFAULT ''; diff --git a/cli/src/services/agent_trace_db/lifecycle.rs b/cli/src/services/agent_trace_db/lifecycle.rs index c5787a52..cad20f2d 100644 --- a/cli/src/services/agent_trace_db/lifecycle.rs +++ b/cli/src/services/agent_trace_db/lifecycle.rs @@ -77,6 +77,7 @@ impl ServiceLifecycle for AgentTraceDbLifecycle { #[derive(Clone, Debug, Eq, PartialEq)] struct RepositoryDatabaseSetup { repository_id: String, + source_instance_id: String, canonical_identity: String, identity_source: String, configured_remote: Option, @@ -103,6 +104,7 @@ fn initialize_repository_agent_trace_db(repo_root: &Path) -> Result S .map(|remote| format!("\nAgent Trace configured remote: {remote}")) .unwrap_or_default(); format!( - "Agent Trace repository ID: {}\nAgent Trace identity source: {}\nAgent Trace canonical identity: {}{}\nAgent Trace checkout identity: {}\nAgent Trace repository-scoped database initialized at '{}'.", + "Agent Trace repository ID: {}\nAgent Trace source-instance identity: {}\nAgent Trace identity source: {}\nAgent Trace canonical identity: {}{}\nAgent Trace checkout identity: {}\nAgent Trace repository-scoped database initialized at '{}'.", setup.repository_id, + setup.source_instance_id, setup.identity_source, setup.canonical_identity, remote_line, diff --git a/cli/src/services/agent_trace_db/repository.rs b/cli/src/services/agent_trace_db/repository.rs index 5e469efe..3e0638ec 100644 --- a/cli/src/services/agent_trace_db/repository.rs +++ b/cli/src/services/agent_trace_db/repository.rs @@ -1,15 +1,17 @@ //! Repository-scoped Agent Trace database adapter. //! //! One logical Git repository maps to one database at -//! `/sce/repos//agent-trace.db`. The schema -//! baseline is one fresh schema SQL file (`agent-trace-repository` -//! migrations), because repository-scoped databases are always created new; -//! there is no incremental chain and no migration path from legacy -//! checkout-scoped databases. Trace tables carry no `checkout_id` columns. +//! `/sce/repos//agent-trace.db`. The schema starts +//! from one fresh baseline SQL file (`agent-trace-repository` migrations, +//! `001_repository_schema`) with later migrations layered on top of it, such +//! as `002_repository_source_instance_id`; there is still no migration path +//! from legacy checkout-scoped databases. Trace tables carry no `checkout_id` +//! columns. use std::path::PathBuf; use anyhow::Result; +use uuid::Uuid; use crate::{ generated_migrations, @@ -26,7 +28,9 @@ use super::{ const REPOSITORY_AGENT_TRACE_SCHEMA_SETUP_GUIDANCE: &str = "Run 'sce setup'."; const SELECT_REPOSITORY_METADATA_SQL: &str = - "SELECT repository_id FROM repository_metadata WHERE id = 1"; + "SELECT repository_id, source_instance_id FROM repository_metadata WHERE id = 1"; +const SELECT_SOURCE_INSTANCE_ID_SQL: &str = + "SELECT source_instance_id FROM repository_metadata WHERE id = 1"; const SELECT_SQLITE_OBJECT_SQL: &str = "SELECT name FROM sqlite_master WHERE type = ?1 AND name = ?2 LIMIT 1"; const RECORD_REPOSITORY_SCHEMA_MIGRATION_SQL: &str = @@ -47,6 +51,49 @@ const INSERT_REPOSITORY_METADATA_SQL: &str = "INSERT INTO repository_metadata (id, repository_id) VALUES (1, ?1) ON CONFLICT (id) DO NOTHING"; +/// Atomically claims the missing source-instance identity: the conditional +/// `WHERE` clause means only one concurrent caller's `UPDATE` actually +/// changes a row, so every caller can safely re-read the stored value +/// afterwards and observe the same winner. +const UPDATE_MISSING_SOURCE_INSTANCE_ID_SQL: &str = + "UPDATE repository_metadata SET source_instance_id = ?1 +WHERE id = 1 AND source_instance_id = ''"; + +/// Typed repository Agent Trace metadata. +/// +/// `repository_id` is the logical Git repository identity shared by every +/// database file resolved for that repository. `source_instance_id` is the +/// stable identity of this particular database *lineage*: it is generated +/// once per independently created database file and is preserved across +/// reopen, setup, and migration, but it is never derived from +/// `repository_id`, checkout identity, remote, hostname, or path. Two +/// independently created database files for the same repository have +/// different `source_instance_id` values; callers that resolve the same +/// physical repository-scoped file always observe the same one. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RepositoryMetadata { + pub repository_id: String, + pub source_instance_id: String, +} + +/// Generate a new UUIDv4-style source-instance identity. +/// +/// Never derive a source-instance identity from repository ID, checkout ID, +/// remote, hostname, or path; this is the only supported way to mint one. +pub fn generate_source_instance_id() -> String { + Uuid::new_v4().to_string() +} + +/// Validate that a value is a well-formed, non-empty UUID-style source +/// instance identity. +/// +/// The `002_repository_source_instance_id` migration seeds existing rows with +/// an empty placeholder, which this rejects so callers can distinguish +/// "not yet initialized" from a valid stored identity. +pub fn is_valid_source_instance_id(value: &str) -> bool { + Uuid::parse_str(value).is_ok() +} + /// Repository-scoped Agent Trace database configuration. pub struct RepositoryAgentTraceDbSpec; @@ -113,28 +160,76 @@ impl RepositoryAgentTraceDb { Ok(!rows.is_empty()) } - /// Seed repository metadata on first initialization and validate it on - /// every open. + /// Seed repository metadata on first initialization, atomically fill in a + /// missing source-instance identity exactly once, and validate the result + /// on every open. /// /// The stored `repository_id` must match the resolved repository ID for /// this database path; a mismatch means the file does not belong to the - /// resolved repository and is an error rather than a write target. - pub fn verify_or_initialize_repository_metadata(&self, repository_id: &str) -> Result<()> { + /// resolved repository and is an error rather than a write target. An + /// existing valid `source_instance_id` is returned unchanged; concurrent + /// callers filling in a missing one always converge on the same stored + /// winner via a conditional `UPDATE`. + pub fn verify_or_initialize_repository_metadata( + &self, + repository_id: &str, + ) -> Result { self.execute(INSERT_REPOSITORY_METADATA_SQL, (repository_id,))?; let stored = self.query_map(SELECT_REPOSITORY_METADATA_SQL, (), |row| { - row.get::(0).map_err(Into::into) + let repository_id = row.get::(0).map_err(anyhow::Error::from)?; + let source_instance_id = row.get::(1).map_err(anyhow::Error::from)?; + Ok((repository_id, source_instance_id)) })?; - match stored.first() { - Some(stored_repository_id) if stored_repository_id == repository_id => Ok(()), - Some(stored_repository_id) => anyhow::bail!( - "repository Agent Trace DB metadata mismatch: stored repository ID \ - {stored_repository_id} does not match resolved repository ID {repository_id}" - ), - None => anyhow::bail!( + let Some((stored_repository_id, stored_source_instance_id)) = stored.into_iter().next() + else { + anyhow::bail!( "repository Agent Trace DB metadata is missing its repository ID row. \ {REPOSITORY_AGENT_TRACE_SCHEMA_SETUP_GUIDANCE}" + ) + }; + + if stored_repository_id != repository_id { + anyhow::bail!( + "repository Agent Trace DB metadata mismatch: stored repository ID \ + {stored_repository_id} does not match resolved repository ID {repository_id}" + ); + } + + let source_instance_id = if is_valid_source_instance_id(&stored_source_instance_id) { + stored_source_instance_id + } else { + self.initialize_missing_source_instance_id()? + }; + + Ok(RepositoryMetadata { + repository_id: stored_repository_id, + source_instance_id, + }) + } + + /// Atomically claim the missing source-instance identity for this + /// database and return the stored winner. + /// + /// Never replaces an existing valid identity: the conditional `UPDATE` + /// only ever changes the placeholder row, so a racing caller that loses + /// the update simply re-reads the value the winner stored. + fn initialize_missing_source_instance_id(&self) -> Result { + let candidate = generate_source_instance_id(); + self.execute(UPDATE_MISSING_SOURCE_INSTANCE_ID_SQL, (candidate,))?; + + let stored = self.query_map(SELECT_SOURCE_INSTANCE_ID_SQL, (), |row| { + row.get::(0).map_err(Into::into) + })?; + + match stored.into_iter().next() { + Some(source_instance_id) if is_valid_source_instance_id(&source_instance_id) => { + Ok(source_instance_id) + } + other => anyhow::bail!( + "repository Agent Trace DB source-instance identity initialization failed to \ + converge on a valid value: {other:?}" ), } } @@ -256,7 +351,7 @@ mod tests { } #[test] - fn open_at_initializes_the_full_schema_from_one_migration() { + fn open_at_initializes_the_full_schema_from_all_migrations() { let db_path = unique_test_db_path("baseline"); let db = RepositoryAgentTraceDb::new_at(&db_path).expect("repository DB should open"); @@ -302,8 +397,11 @@ mod tests { .expect("migration metadata query should succeed"); assert_eq!( applied_ids, - vec![String::from("001_repository_schema")], - "repository DBs should be initialized from exactly one schema file" + vec![ + String::from("001_repository_schema"), + String::from("002_repository_source_instance_id"), + ], + "repository DBs should be initialized from every embedded migration in order" ); db.ensure_schema_ready_for_hooks() @@ -312,6 +410,96 @@ mod tests { remove_test_db(&db_path); } + #[test] + fn fresh_repository_database_receives_a_valid_source_instance_id_once() { + let db_path = unique_test_db_path("source-instance-init"); + let repository_id = "a".repeat(64); + let db = RepositoryAgentTraceDb::new_at(&db_path).expect("repository DB should open"); + + let first = db + .verify_or_initialize_repository_metadata(&repository_id) + .expect("repository metadata initialization should succeed"); + assert!( + is_valid_source_instance_id(&first.source_instance_id), + "a fresh database should receive a valid source-instance identity" + ); + + let second = db + .verify_or_initialize_repository_metadata(&repository_id) + .expect("repeated initialization should succeed"); + assert_eq!( + first.source_instance_id, second.source_instance_id, + "repeated setup must never replace an already-stored source-instance identity" + ); + + remove_test_db(&db_path); + } + + #[test] + fn independently_created_databases_for_the_same_repository_receive_different_source_instance_ids( + ) { + let first_db_path = unique_test_db_path("independent-a"); + let second_db_path = unique_test_db_path("independent-b"); + let repository_id = "a".repeat(64); + + let first_db = RepositoryAgentTraceDb::new_at(&first_db_path) + .expect("first repository DB should open"); + let second_db = RepositoryAgentTraceDb::new_at(&second_db_path) + .expect("second repository DB should open"); + + let first = first_db + .verify_or_initialize_repository_metadata(&repository_id) + .expect("first metadata initialization should succeed"); + let second = second_db + .verify_or_initialize_repository_metadata(&repository_id) + .expect("second metadata initialization should succeed"); + + assert_eq!(first.repository_id, second.repository_id); + assert_ne!( + first.source_instance_id, second.source_instance_id, + "independently created database files must not share a source-instance identity" + ); + + remove_test_db(&first_db_path); + remove_test_db(&second_db_path); + } + + #[test] + fn baseline_only_fixture_gains_a_stable_source_instance_id_through_setup_migration() { + let db_path = unique_test_db_path("baseline-upgrade"); + let repository_id = "a".repeat(64); + + let db = RepositoryAgentTraceDb::open_without_migrations_at(&db_path) + .expect("repository DB should open without migrations"); + db.run_migrations_up_to(1) + .expect("baseline migration should apply"); + db.execute(INSERT_REPOSITORY_METADATA_SQL, (repository_id.as_str(),)) + .expect("baseline metadata row should seed"); + + db.run_migrations() + .expect("setup should apply the remaining migration"); + let first = db + .verify_or_initialize_repository_metadata(&repository_id) + .expect("setup initialization should succeed"); + assert!( + is_valid_source_instance_id(&first.source_instance_id), + "an upgraded baseline database should receive a valid source-instance identity" + ); + drop(db); + + let reopened = RepositoryAgentTraceDb::open_without_migrations_at(&db_path) + .expect("repository DB should reopen"); + let second = reopened + .verify_or_initialize_repository_metadata(&repository_id) + .expect("reopen validation should succeed"); + assert_eq!( + first.source_instance_id, second.source_instance_id, + "an upgraded source-instance identity must remain stable across reopen" + ); + + remove_test_db(&db_path); + } + #[test] fn trace_tables_have_no_checkout_id_columns() { let db_path = unique_test_db_path("no-checkout-id"); @@ -592,6 +780,33 @@ mod tests { remove_test_db(&second_db_path); } + #[test] + fn generated_source_instance_ids_are_valid_and_unique() { + let first = generate_source_instance_id(); + let second = generate_source_instance_id(); + + assert!(is_valid_source_instance_id(&first)); + assert!(is_valid_source_instance_id(&second)); + assert_ne!( + first, second, + "independently generated source-instance identities must differ" + ); + } + + #[test] + fn empty_placeholder_source_instance_id_is_invalid() { + assert!( + !is_valid_source_instance_id(""), + "the migration's empty placeholder must not validate as a stored identity" + ); + } + + #[test] + fn non_uuid_source_instance_id_is_invalid() { + assert!(!is_valid_source_instance_id("not-a-uuid")); + assert!(!is_valid_source_instance_id("a".repeat(64).as_str())); + } + #[test] fn spec_path_constructor_is_rejected() { let error = RepositoryAgentTraceDbSpec::db_path() diff --git a/cli/src/services/agent_trace_storage/mod.rs b/cli/src/services/agent_trace_storage/mod.rs index a5375ea1..9fe1ad97 100644 --- a/cli/src/services/agent_trace_storage/mod.rs +++ b/cli/src/services/agent_trace_storage/mod.rs @@ -14,7 +14,7 @@ use std::time::Duration; use anyhow::{Context, Result}; -use crate::services::agent_trace_db::repository::RepositoryAgentTraceDb; +use crate::services::agent_trace_db::repository::{RepositoryAgentTraceDb, RepositoryMetadata}; use crate::services::checkout::{get_or_create_checkout_id, resolve_git_dir}; use crate::services::default_paths::{ agent_trace_db_path_for_repository, agent_trace_db_path_for_repository_at, @@ -54,16 +54,28 @@ pub struct ResolvedAgentTraceStorage { pub db_path: PathBuf, /// Open repository-scoped Agent Trace database. pub db: RepositoryAgentTraceDb, + /// Typed repository metadata (`repository_id` and `source_instance_id`) + /// read or initialized while opening `db`. + pub metadata: RepositoryMetadata, } /// Resolves the repository-scoped Agent Trace storage for a checkout using /// the canonical state root from the default-path catalog. +/// +/// May create the database and run migrations when it does not yet exist or +/// predates a later migration; intended for setup/lifecycle and diagnostic +/// callers, not high-frequency hook runtime callers. pub fn resolve_agent_trace_storage( context: &AgentTraceStorageContext<'_>, ) -> Result { let repository_identity = resolve_identity(context)?; let db_path = agent_trace_db_path_for_repository(&repository_identity.identity.repository_id)?; - open_storage(context, repository_identity, db_path) + open_storage( + context, + repository_identity, + db_path, + open_repository_db_concurrently_safe, + ) } /// Resolution core against an explicit state root, so tests can exercise the @@ -77,7 +89,53 @@ pub fn resolve_agent_trace_storage_at_state_root( state_root, &repository_identity.identity.repository_id, )?; - open_storage(context, repository_identity, db_path) + open_storage( + context, + repository_identity, + db_path, + open_repository_db_concurrently_safe, + ) +} + +/// Resolves the repository-scoped Agent Trace storage for a checkout without +/// creating, migrating, or repairing the database schema. +/// +/// Intended for high-frequency hook runtime callers: a missing or +/// migration-incomplete database fails readiness with actionable `sce setup` +/// guidance instead of silently creating or migrating one. Source-instance +/// initialization is still applied when the schema is already ready, since +/// it only fills in an existing column and never mutates schema/migrations. +pub fn resolve_agent_trace_storage_for_hook_runtime( + context: &AgentTraceStorageContext<'_>, +) -> Result { + let repository_identity = resolve_identity(context)?; + let db_path = agent_trace_db_path_for_repository(&repository_identity.identity.repository_id)?; + open_storage( + context, + repository_identity, + db_path, + open_repository_db_for_hook_runtime, + ) +} + +/// Resolution core of [`resolve_agent_trace_storage_for_hook_runtime`] against +/// an explicit state root, so tests can exercise the no-migration hook path +/// without touching the real user state directory. +pub fn resolve_agent_trace_storage_for_hook_runtime_at_state_root( + context: &AgentTraceStorageContext<'_>, + state_root: &Path, +) -> Result { + let repository_identity = resolve_identity(context)?; + let db_path = agent_trace_db_path_for_repository_at( + state_root, + &repository_identity.identity.repository_id, + )?; + open_storage( + context, + repository_identity, + db_path, + open_repository_db_for_hook_runtime, + ) } fn resolve_identity(context: &AgentTraceStorageContext<'_>) -> Result { @@ -93,6 +151,7 @@ fn open_storage( context: &AgentTraceStorageContext<'_>, repository_identity: ResolvedRepositoryIdentity, db_path: PathBuf, + open_db: impl FnOnce(&Path, &str) -> Result<(RepositoryAgentTraceDb, RepositoryMetadata)>, ) -> Result { let git_dir = resolve_git_dir(context.repository_root).with_context(|| { format!( @@ -107,25 +166,30 @@ fn open_storage( ) })?; - // Opening the database creates `repos//` when missing; - // directory creation is idempotent and first-time schema initialization may - // briefly race on SQLite metadata locks, so retry the fast-path/migrate - // sequence a small bounded number of times. let repository_id = &repository_identity.identity.repository_id; - let db = open_repository_db_concurrently_safe(&db_path, repository_id)?; + let (db, metadata) = open_db(&db_path, repository_id)?; Ok(ResolvedAgentTraceStorage { repository_identity, checkout_id, db_path, db, + metadata, }) } +/// Open the repository-scoped Agent Trace database for setup/lifecycle and +/// diagnostic callers, tolerating and repairing the narrow first-open races +/// documented on [`RepositoryAgentTraceDb::repair_missing_repository_schema_migration_metadata`]. +/// +/// Opening the database creates `repos//` when missing; +/// directory creation is idempotent and first-time schema initialization may +/// briefly race on `SQLite` metadata locks, so retry the fast-path/migrate +/// sequence a small bounded number of times. fn open_repository_db_concurrently_safe( db_path: &Path, repository_id: &str, -) -> Result { +) -> Result<(RepositoryAgentTraceDb, RepositoryMetadata)> { let mut last_error = None; for attempt in 1..=REPOSITORY_DB_INITIALIZATION_ATTEMPTS { @@ -134,16 +198,16 @@ fn open_repository_db_concurrently_safe( if db.ensure_schema_ready_for_hooks().is_err() { db.repair_missing_repository_schema_migration_metadata()?; } - db.verify_or_initialize_repository_metadata(repository_id)?; - Ok(db) + let metadata = db.verify_or_initialize_repository_metadata(repository_id)?; + Ok((db, metadata)) }); match fast_open { - Ok(db) => return Ok(db), + Ok(result) => return Ok(result), Err(fast_error) => match RepositoryAgentTraceDb::new_at(db_path) { Ok(db) => { - db.verify_or_initialize_repository_metadata(repository_id)?; - return Ok(db); + let metadata = db.verify_or_initialize_repository_metadata(repository_id)?; + return Ok((db, metadata)); } Err(init_error) => { last_error = Some(anyhow::anyhow!( @@ -163,6 +227,25 @@ fn open_repository_db_concurrently_safe( Err(last_error.expect("repository DB initialization should record an error")) } +/// Open the repository-scoped Agent Trace database for high-frequency hook +/// runtime callers. +/// +/// Never creates, migrates, or repairs schema/migration metadata: a missing +/// or migration-incomplete database fails with the existing `sce setup` +/// guidance from [`RepositoryAgentTraceDb::ensure_schema_ready_for_hooks`]. +/// Source-instance initialization still runs when the schema is ready, since +/// it only fills in an existing column via an atomic, race-safe `UPDATE` and +/// never applies a schema migration. +fn open_repository_db_for_hook_runtime( + db_path: &Path, + repository_id: &str, +) -> Result<(RepositoryAgentTraceDb, RepositoryMetadata)> { + let db = RepositoryAgentTraceDb::open_without_migrations_at(db_path)?; + db.ensure_schema_ready_for_hooks()?; + let metadata = db.verify_or_initialize_repository_metadata(repository_id)?; + Ok((db, metadata)) +} + #[cfg(test)] mod tests { use super::*; @@ -519,4 +602,100 @@ mod tests { ); } } + + #[test] + fn hook_runtime_resolution_fails_with_setup_guidance_before_setup_ran() { + let state_root = unique_temp_dir("state-hook-before-setup"); + let repo = + init_git_repo_with_remote("hook-before-setup", "git@github.com:acme/widgets.git"); + + let Err(error) = resolve_agent_trace_storage_for_hook_runtime_at_state_root( + &context_for(&repo), + &state_root, + ) else { + panic!("hook runtime resolution must not create or migrate a missing database") + }; + assert!( + error.to_string().contains("sce setup"), + "unexpected error: {error}" + ); + + std::fs::remove_dir_all(&state_root).expect("clean up state root"); + std::fs::remove_dir_all(&repo).expect("clean up repo"); + } + + fn build_baseline_only_fixture(db_path: &Path, repository_id: &str) { + let db = RepositoryAgentTraceDb::open_without_migrations_at(db_path) + .expect("fixture DB should open without migrations"); + db.run_migrations_up_to(1) + .expect("baseline migration should apply"); + db.execute( + "INSERT INTO repository_metadata (id, repository_id) VALUES (1, ?1)", + (repository_id,), + ) + .expect("baseline metadata row should seed"); + } + + #[test] + fn hook_runtime_resolution_fails_with_setup_guidance_on_a_baseline_only_schema_without_mutating_it( + ) { + let state_root = unique_temp_dir("state-hook-baseline-only"); + let repo = + init_git_repo_with_remote("hook-baseline-only", "git@github.com:acme/widgets.git"); + + let identity = resolve_repository_identity(&repo, None, "origin") + .expect("repository identity should resolve"); + let db_path = + agent_trace_db_path_for_repository_at(&state_root, &identity.identity.repository_id) + .expect("db path should resolve"); + build_baseline_only_fixture(&db_path, &identity.identity.repository_id); + + let Err(error) = resolve_agent_trace_storage_for_hook_runtime_at_state_root( + &context_for(&repo), + &state_root, + ) else { + panic!("hook runtime resolution must not silently migrate an old-schema database") + }; + assert!( + error.to_string().contains("sce setup"), + "unexpected error: {error}" + ); + + let applied_ids = RepositoryAgentTraceDb::open_without_migrations_at(&db_path) + .expect("fixture DB should reopen") + .query_map( + "SELECT id FROM __sce_migrations ORDER BY id ASC", + (), + |row| row.get::(0).map_err(Into::into), + ) + .expect("migration metadata query should succeed"); + assert_eq!( + applied_ids, + vec![String::from("001_repository_schema")], + "hook runtime resolution must not apply the missing migration" + ); + + std::fs::remove_dir_all(&state_root).expect("clean up state root"); + std::fs::remove_dir_all(&repo).expect("clean up repo"); + } + + #[test] + fn hook_runtime_resolution_succeeds_and_reuses_metadata_after_setup() { + let state_root = unique_temp_dir("state-hook-after-setup"); + let repo = init_git_repo_with_remote("hook-after-setup", "git@github.com:acme/widgets.git"); + + let setup = resolve_agent_trace_storage_at_state_root(&context_for(&repo), &state_root) + .expect("setup-safe resolution should create the database"); + + let hook_storage = resolve_agent_trace_storage_for_hook_runtime_at_state_root( + &context_for(&repo), + &state_root, + ) + .expect("hook runtime resolution should succeed once the database is set up"); + + assert_eq!(hook_storage.metadata, setup.metadata); + + std::fs::remove_dir_all(&state_root).expect("clean up state root"); + std::fs::remove_dir_all(&repo).expect("clean up repo"); + } } diff --git a/cli/src/services/db/mod.rs b/cli/src/services/db/mod.rs index 6561629f..0b2c8d01 100644 --- a/cli/src/services/db/mod.rs +++ b/cli/src/services/db/mod.rs @@ -598,6 +598,21 @@ impl TursoDb { self.core.run_migrations() } + /// Run only the first `count` embedded migrations, in order. + /// + /// Testing seam for building a fixture database that predates the + /// migrations after `count`, without exposing raw batch-SQL execution. + #[cfg(test)] + pub fn run_migrations_up_to(&self, count: usize) -> Result<()> { + let migrations = &M::migrations()[..count]; + run_embedded_migrations( + &self.core.conn, + &self.core.runtime, + M::db_name(), + migrations, + ) + } + /// Check migration metadata for problems that would prevent safe hook /// runtime access. /// diff --git a/cli/src/services/hooks/mod.rs b/cli/src/services/hooks/mod.rs index 7a6d44fd..eb2914e0 100644 --- a/cli/src/services/hooks/mod.rs +++ b/cli/src/services/hooks/mod.rs @@ -19,7 +19,9 @@ use crate::services::agent_trace_db::{ PartType, PostCommitPatchIntersectionInsert, RecentDiffTracePatches, PAYLOAD_TYPE_PATCH, PAYLOAD_TYPE_STRUCTURED, }; -use crate::services::agent_trace_storage::{resolve_agent_trace_storage, AgentTraceStorageContext}; +use crate::services::agent_trace_storage::{ + resolve_agent_trace_storage_for_hook_runtime, AgentTraceStorageContext, +}; use crate::services::config; use crate::services::observability::traits::Logger; use crate::services::patch::{ @@ -337,7 +339,7 @@ fn open_agent_trace_db_for_hook_runtime( repository_remote: &storage_config.repository_remote, }; - resolve_agent_trace_storage(&storage_context) + resolve_agent_trace_storage_for_hook_runtime(&storage_context) .map(|storage| storage.db) .context(context_message) } diff --git a/context/architecture.md b/context/architecture.md index 2277fec6..f42f4eed 100644 --- a/context/architecture.md +++ b/context/architecture.md @@ -117,12 +117,12 @@ The repository includes a new placeholder Rust binary crate at `cli/`. - `cli/src/services/config/types.rs` is the canonical owner for the shared runtime/config primitive seam used by the CLI: `LogLevel`, `LogFormat`, `SCE_LOG_LEVEL`, `SCE_LOG_FORMAT`, `SCE_LOG_DIR`, `DEFAULT_LOG_FILE_RETENTION_LIMIT`, and the shared bool parsing helpers used by both config resolution and observability bootstrap; `cli/src/services/config/mod.rs` re-exports those primitives through the facade. - `cli/src/services/capabilities.rs` defines the current broad CLI capability traits consumed by the borrowed, compile-time-typed `AppContext`: `FsOps` with `StdFsOps` for filesystem operations and `GitOps` with `ProcessGitOps` for git command execution plus repository-root/hooks-directory resolution. Existing service internals do not consume these traits directly yet; command execution uses narrow accessors and repo-root-scoped context derivation. - `cli/src/services/lifecycle.rs` defines the current compile-safe lifecycle seam. `ServiceLifecycle` has default no-op generic `diagnose`, `fix`, and `setup` methods over `C: HasRepoRoot`, with lifecycle-owned health, fix, and setup result types so the trait contract is not publicly anchored to doctor/setup module types or the full `AppContext` shape. The same module owns the static `LifecycleProvider` enum and shared `lifecycle_providers(include_hooks)` catalog/factory, returning providers in deterministic order (config → local_db → auth_db → agent_trace_db → hooks when requested); enum dispatch calls each concrete provider through generic context methods without boxed lifecycle-provider allocation or repo-root trait-object context erasure. Hooks exposes a `HooksLifecycle` provider in `cli/src/services/hooks/lifecycle.rs` for hook rollout diagnosis/fix/setup using lifecycle-owned health records plus the canonical required-hook installer. Config exposes a `ConfigLifecycle` provider in `cli/src/services/config/lifecycle.rs` for global/repo-local config validation and repo-local `.sce/config.json` bootstrap. local_db exposes a `LocalDbLifecycle` provider in `cli/src/services/local_db/lifecycle.rs` for canonical local DB path health, parent-directory readiness/bootstrap, and `LocalDb::new()` setup. auth_db exposes an `AuthDbLifecycle` provider in `cli/src/services/auth_db/lifecycle.rs` for canonical auth DB path health, parent-directory readiness/bootstrap, and `AuthDb::new()` setup. agent_trace_db exposes an `AgentTraceDbLifecycle` provider in `cli/src/services/agent_trace_db/lifecycle.rs` for setup-time repository-scoped Agent Trace storage initialization when a repo root is available and repository Agent Trace DB path health/fix from resolved repository identity, returning an actionable "requires a Git repository" diagnostic outside repository context (no global/checkout fallback path; the former fallback was removed by the `retire-legacy-agent-trace-db` plan). Doctor runtime aggregates the full provider catalog for `diagnose` and `fix` and adapts lifecycle records into doctor report/fix records at the orchestration boundary; setup command aggregates the shared catalog for `setup` with hooks included only when requested and adapts hook setup outcomes before rendering setup-owned messages. -- Agent Trace lifecycle setup resolves `agent_trace.repository_id` / `agent_trace.repository_remote`, creates/reuses checkout identity for diagnostics, and initializes the repository-scoped DB through `agent_trace_storage::resolve_agent_trace_storage(...)`; hook runtime uses the same storage resolver and `RepositoryAgentTraceDb` lazy fast-path-then-migrate open with bounded retry plus narrow migration-metadata repair for concurrent first-open races. +- Agent Trace lifecycle setup resolves `agent_trace.repository_id` / `agent_trace.repository_remote`, creates/reuses checkout identity for diagnostics, and initializes the repository-scoped DB through `agent_trace_storage::resolve_agent_trace_storage(...)`, a lazy fast-path-then-migrate open with bounded retry plus narrow migration-metadata repair for concurrent first-open races that also atomically seeds/reads typed `RepositoryMetadata { repository_id, source_instance_id }`. Hook runtime instead resolves through `agent_trace_storage::resolve_agent_trace_storage_for_hook_runtime(...)`, a separate no-migration path that never creates, migrates, or repairs schema/migration metadata: a missing or migration-incomplete database fails readiness with the existing `sce setup` guidance instead of silently initializing one, while an already-ready database still has its missing `source_instance_id` atomically filled in (a metadata-only, non-schema-mutating operation). - `cli/src/services/auth_command/mod.rs` defines the implemented auth command surface for `sce auth login|renew|logout|status`, including device-flow login, stored-token renewal (`--force` supported for renew), logout, and status rendering in text/JSON formats; `cli/src/services/auth_command/command.rs` owns the `AuthCommand` payload used by the static `RuntimeCommand` enum. - `cli/src/services/db/mod.rs` provides the shared generic Turso infrastructure seam: `DbSpec` supplies a service-specific name, path, ordered embedded migrations, and config-file lookup key (`db_config_key()`), while `TursoDb` owns parent-directory creation, `Builder::new_local(...)` initialization (with `experimental_multiprocess_wal(true)` for safe concurrent access), Turso connection setup, tokio current-thread runtime bridging, retry-backed blocking `execute`/`query`/`query_values`/`query_map` wrappers, and generic migration execution with per-database `__sce_migrations` metadata. `TursoDb::new()` and `EncryptedTursoDb::new()` wrap only their local open/connect block in `run_with_retry_sync` using a config-driven connection-open policy resolved from the `DATABASE_RETRY_CONFIG` `OnceLock` with fallback to hardcoded defaults, while operation methods use a config-driven operation policy from the same source. `query_values()` returns fully fetched column names plus raw `turso::Value` rows for deterministic operator-facing rendering; `query_map()` retries the initial query and row-fetch loop, then applies caller row mapping after retry completion. Migration execution is not retried and uses batch execution so one migration file may contain multiple SQL statements while still recording one migration ID. The same module also provides `EncryptedTursoDb`, a structurally parallel encrypted adapter that resolves the encryption key through `encryption_key::get_or_create_encryption_key()`, enables Turso local encryption with strict `aegis256` cipher selection, and exposes retry-backed synchronous wrappers plus migration execution. `cli/src/services/db/encryption_key.rs` first derives a Turso-compatible 64-character hex key from non-empty `SCE_AUTH_DB_ENCRYPTION_KEY` env-secret text when present, otherwise falls back to keyring-backed credential-store get-or-create behavior; no plaintext auth DB fallback exists. - `cli/src/services/local_db/mod.rs` provides the concrete local DB spec and `LocalDb` type alias over the shared generic `TursoDb` adapter. `LocalDbSpec` resolves the deterministic persistent runtime DB target through the shared default-path seam and declares no local migrations; `TursoDb` supplies retry-backed blocking `execute`/`query`, parent-directory creation, Turso connection setup, tokio current-thread runtime bridging, and generic migration execution. - `cli/src/services/auth_db/mod.rs` provides the encrypted auth DB spec and `AuthDb` type alias over `EncryptedTursoDb`. `AuthDbSpec` resolves `/sce/auth.db` through the shared default-path seam and embeds ordered auth migrations. Auth DB lifecycle setup/doctor integration is wired through `AuthDbLifecycle`; auth command/token-storage reads/writes are directed through `token_storage.rs`. -- `cli/src/services/agent_trace_db/mod.rs` owns the shared Agent Trace insert payloads, SQL constants, and typed row helpers (diff-trace/intersection/Agent Trace/message/part) plus `ensure_schema_ready_for_hooks()` consumed by the repository adapter. `cli/src/services/agent_trace_db/repository.rs` defines the sole `RepositoryAgentTraceDb` adapter over `TursoDb` with one fresh `agent-trace-repository/001_repository_schema.sql` baseline for `diff_traces` (including `payload_type`), `post_commit_patch_intersections`, `agent_traces`, `messages`, `parts`, indexes, and triggers, `repository_metadata` validation, no trace-table `checkout_id` columns, `agent_traces.agent_trace_id NOT NULL UNIQUE`, and `recent_diff_trace_patches(cutoff_time_ms, end_time_ms)` using the inclusive chronological parser without checkout filtering. Active hook runtime, setup/lifecycle storage, and trace status/list/shell flows resolve through `agent_trace_storage` and use `RepositoryAgentTraceDb`. The checkout-scoped `AgentTraceDb`/`AgentTraceDbSpec` adapter, its `agent_trace_db_path()`/`agent_trace_db_path_for_checkout()` helpers, the 15-file `cli/migrations/agent-trace/` chain, and the `sce trace --legacy` surface were removed by the `retire-legacy-agent-trace-db` plan. +- `cli/src/services/agent_trace_db/mod.rs` owns the shared Agent Trace insert payloads, SQL constants, and typed row helpers (diff-trace/intersection/Agent Trace/message/part) plus `ensure_schema_ready_for_hooks()` consumed by the repository adapter. `cli/src/services/agent_trace_db/repository.rs` defines the sole `RepositoryAgentTraceDb` adapter over `TursoDb` with a fresh `agent-trace-repository/001_repository_schema.sql` baseline for `diff_traces` (including `payload_type`), `post_commit_patch_intersections`, `agent_traces`, `messages`, `parts`, indexes, and triggers, plus the additive `002_repository_source_instance_id.sql` migration adding `repository_metadata.source_instance_id`, `repository_metadata` validation via typed `RepositoryMetadata { repository_id, source_instance_id }`, no trace-table `checkout_id` columns, `agent_traces.agent_trace_id NOT NULL UNIQUE`, and `recent_diff_trace_patches(cutoff_time_ms, end_time_ms)` using the inclusive chronological parser without checkout filtering. Active hook runtime, setup/lifecycle storage, and trace status/list/shell flows resolve through `agent_trace_storage` and use `RepositoryAgentTraceDb`. The checkout-scoped `AgentTraceDb`/`AgentTraceDbSpec` adapter, its `agent_trace_db_path()`/`agent_trace_db_path_for_checkout()` helpers, the 15-file `cli/migrations/agent-trace/` chain, and the `sce trace --legacy` surface were removed by the `retire-legacy-agent-trace-db` plan. - `cli/src/services/setup/mod.rs` defines the setup command contract (`SetupMode`, `SetupTarget`, `SetupRequest`, CLI flag parser/validator), an `inquire`-backed interactive target prompter (`InquireSetupTargetPrompter`), setup dispatch outcomes (proceed/cancelled), additive durable-context bootstrap (`bootstrap_context_baseline` for standalone `--bootstrap-context` and every normal successful setup path), and compile-time embedded asset access (`EmbeddedAsset`, target-scoped iterators, required-hook asset iterators/lookups). It also owns the install-time optional-workflow seam: the `OptionalWorkflow` type plus the build-generated `OPTIONAL_WORKFLOWS` catalog, a per-target `WorkflowAssetLayout` built from the existing `default_paths` command/skill directory constants (`command`/`commands`/`prompts` plus `skills`), and `iter_embedded_assets_for_setup_target_with_selection(target, selection)`, which yields every embedded asset except the `{command_dir}/{command_slug}.md` file and `{skills_dir}/{skill_slug}/` subtree of each optional workflow the selection omits. Membership is derived from the catalog's slugs rather than an enumerated file list, so a new optional workflow needs no Rust change. This filtered iterator is the only way embedded assets are enumerated; setup installs through it and doctor inspects through it, so there is no unfiltered enumeration path that could reintroduce an unselected workflow. The non-interactive selection flows through the repeatable `--workflow ` flag into `SetupRequest.optional_workflows: Option>` (`None` meaning the flag was absent); `validate_optional_workflow_slugs` checks each slug against `OPTIONAL_WORKFLOWS` during request resolution, before any write, and `run_setup_for_mode` resolves `None` to the persisted `integrations.optional_workflows` (exported as `persisted_optional_workflows`) before installing through the filtered iterator and persisting the resolved selection. The interactive selection flows through the same seam: `SetupTargetPrompter` carries `prompt_target` plus `prompt_optional_workflows(defaults)` (returning `None` for a cancelled prompt), `SetupDispatch::Proceed { mode, optional_workflows }` carries a prompted selection alongside the resolved mode, and `resolve_setup_dispatch(mode, prompter, defaults)` runs the workflow prompt only after an interactive target prompt, mapping either cancellation to `SetupDispatch::Cancelled`. The prompt module builds its `inquire::MultiSelect` from `optional_workflow_prompt_inputs(catalog, defaults)`, which returns `None` for an empty catalog (skipping the prompt) and otherwise catalog-ordered rows plus the indices to pre-check, ignoring ids absent from the catalog. `setup/command.rs` therefore resolves the repository root before dispatch, so a non-git directory fails before any prompt. For repository builds, `cli/build.rs` validates the `SCE_CLI_GENERATED_INPUT_DIR` payload and canonical-input inventories, copies the payload into Cargo `OUT_DIR/pkl-generated`, stages `cli/assets/hooks/**` under `OUT_DIR/static`, requires the staged `config/optional-workflows.json`, and generates both the setup manifest and the optional-workflow catalog (`optional_workflows.rs`, rejecting a manifest whose `schemaVersion` is not 1 or whose entries lack a non-empty `id`/`title`/`description`/`commandSlug`/`skillSlug`) in `OUT_DIR`; focused internal seams separate install-flow from prompt-flow logic; `cli/src/services/setup/command.rs` owns the `SetupCommand` payload used by the static `RuntimeCommand` enum and executes against any context implementing repo-root scoping. Its install engine/orchestrator stages embedded files and uses a unified remove-and-replace policy (removing existing targets before swapping staged content, with deterministic recovery guidance on swap failure and no backup artifact creation), and formats deterministic completion messaging; required-hook install orchestration (`install_required_git_hooks`) follows the same remove-and-replace policy (removing existing hooks before swapping staged content, with deterministic recovery guidance on swap failure). After the Git gate, setup always ensures the context baseline; context-only requests return there, while normal modes derive a repo-root-scoped context before aggregating static lifecycle provider `setup` dispatch across providers (config → local_db → auth_db → agent_trace_db → hooks when requested), so setup providers consume only repo-root access from the scoped context. - `cli/src/services/setup/mod.rs` keeps those responsibilities inside one file for now, but the current ownership split is explicit: the inline `install` module owns repository-path normalization, staging/swap install behavior, required-hook installation, and filesystem safety guards, while the inline `prompt` module owns interactive target selection and prompt styling. - `cli/src/services/security.rs` provides shared security utilities for deterministic secret redaction (`redact_sensitive_text`) and directory write-permission probes (`ensure_directory_is_writable`) used by app/setup/observability surfaces. diff --git a/context/cli/agent-trace-storage.md b/context/cli/agent-trace-storage.md index caf2d4c0..42c802ef 100644 --- a/context/cli/agent-trace-storage.md +++ b/context/cli/agent-trace-storage.md @@ -5,23 +5,27 @@ Module at `cli/src/services/agent_trace_storage/` (T04 of the `repository-scoped ## Public API - `AgentTraceStorageContext { repository_root, explicit_repository_id, repository_remote }` — borrowed inputs mirroring the `agent_trace.repository_id` / `agent_trace.repository_remote` config keys; callers pass already-resolved configuration values. Active hook runtime and Agent Trace lifecycle setup/health resolve these values through the config service before constructing the storage context. -- `ResolvedAgentTraceStorage { repository_identity, checkout_id, db_path, db }` — resolved repository identity (`ResolvedRepositoryIdentity` including source provenance), the checkout ID for diagnostics (never persisted on Agent Trace rows), the repository-scoped DB path, and the open `RepositoryAgentTraceDb`. -- `resolve_agent_trace_storage(context)` — production entrypoint using the canonical state root from the default-path catalog. -- `resolve_agent_trace_storage_at_state_root(context, state_root)` — resolution core against an explicit state root; used by tests to exercise the full path without touching the real user state directory. +- `ResolvedAgentTraceStorage { repository_identity, checkout_id, db_path, db, metadata }` — resolved repository identity (`ResolvedRepositoryIdentity` including source provenance), the checkout ID for diagnostics (never persisted on Agent Trace rows), the repository-scoped DB path, the open `RepositoryAgentTraceDb`, and the typed `RepositoryMetadata { repository_id, source_instance_id }` (see [../sce/agent-trace-db.md](../sce/agent-trace-db.md)) read or initialized while opening `db`. +- `resolve_agent_trace_storage(context)` / `resolve_agent_trace_storage_at_state_root(context, state_root)` — setup/lifecycle and diagnostic entrypoint; may create the database and run migrations. Used by Agent Trace lifecycle setup and `sce trace status`. The `_at_state_root` variant is the resolution core against an explicit state root, used by tests to exercise the full path without touching the real user state directory. +- `resolve_agent_trace_storage_for_hook_runtime(context)` / `resolve_agent_trace_storage_for_hook_runtime_at_state_root(context, state_root)` — hook runtime entrypoint; never creates, migrates, or repairs schema/migration metadata. Used only by `hooks/mod.rs::open_agent_trace_db_for_hook_runtime`, the single call site for every hook subcommand (`diff-trace`, `post-commit`, `conversation-trace`, and the commit-msg staged-diff AI-overlap preflight). ## Resolution flow +Both entrypoint families share one internal `open_storage` core for identity/checkout resolution and differ only in how they open the database: + 1. Repository identity via `repository_identity::resolve` precedence (explicit config ID → configured remote URL, default `origin`); resolution errors carry `.sce/config.json` guidance and never echo URLs. A failed identity resolution creates no state directories. 2. Checkout identity reuse via `checkout::resolve_git_dir` + `get_or_create_checkout_id` (`/sce/checkout-id`). 3. DB path from `default_paths::agent_trace_db_path_for_repository{,_at}`, which rejects empty or path-unsafe repository IDs (separators, `.`, `..`). -4. DB open through `agent_trace_db::repository::RepositoryAgentTraceDb` with a bounded concurrent-safe fast-path-then-migrate sequence: `open_without_migrations_at` + `ensure_schema_ready_for_hooks()` + `verify_or_initialize_repository_metadata(repository_id)`, falling back to migration-running `new_at` plus the same metadata validation. Directory creation rides on `TursoDb` parent-dir `create_dir_all`. If concurrent first open leaves all repository schema tables present but the one-file baseline migration record missing, the repository adapter records that missing metadata before rechecking readiness; otherwise the resolver retries the open/migrate sequence for a bounded window while SQLite/Turso locks clear. +4. DB open, one of: + - **Setup-safe** (`resolve_agent_trace_storage*`): `agent_trace_db::repository::RepositoryAgentTraceDb` opens through a bounded concurrent-safe fast-path-then-migrate sequence — `open_without_migrations_at` + `ensure_schema_ready_for_hooks()` + `verify_or_initialize_repository_metadata(repository_id)`, falling back to migration-running `new_at` plus the same metadata call. Directory creation rides on `TursoDb` parent-dir `create_dir_all`. If concurrent first open leaves all repository schema tables present but the one-file baseline migration record missing, the repository adapter records that missing metadata before rechecking readiness; otherwise the resolver retries the open/migrate sequence for a bounded window while SQLite/Turso locks clear. + - **Hook-runtime-safe** (`resolve_agent_trace_storage_for_hook_runtime*`): opens only `open_without_migrations_at` + `ensure_schema_ready_for_hooks()` + `verify_or_initialize_repository_metadata(repository_id)`, with no fallback, no bounded retry, and no migration-metadata repair. A missing or migration-incomplete database fails immediately with the existing `sce setup` guidance from `ensure_schema_ready_for_hooks()`. `verify_or_initialize_repository_metadata` still runs here because it only fills a missing `source_instance_id` through an atomic, already-existing-column `UPDATE` — never a schema migration. ## Never-touch on-disk boundary -The resolver never selects, creates, or touches pre-migration checkout-scoped `/sce/agent-trace-.db` files or the pre-migration global `/sce/agent-trace.db`; tests assert neither appears after resolution. Active hook/runtime, lifecycle setup call sites, and `sce trace` status/shell resolution use this resolver. There is no longer a checkout-scoped resolver: the `retire-legacy-agent-trace-db` plan removed `checkout::resolve_or_create_agent_trace_db_for_checkout` and the `sce trace --legacy` inspection surface. Any pre-migration checkout/global DB files left on disk are never migrated, imported, renamed, or deleted, and are no longer inspectable through the CLI. +Neither resolver family selects, creates, or touches pre-migration checkout-scoped `/sce/agent-trace-.db` files or the pre-migration global `/sce/agent-trace.db`; tests assert neither appears after resolution. There is no longer a checkout-scoped resolver: the `retire-legacy-agent-trace-db` plan removed `checkout::resolve_or_create_agent_trace_db_for_checkout` and the `sce trace --legacy` inspection surface. Any pre-migration checkout/global DB files left on disk are never migrated, imported, renamed, or deleted, and are no longer inspectable through the CLI. ## Status -Registered in `cli/src/services/mod.rs` and consumed by hook runtime, Agent Trace lifecycle setup, and current-repository trace status/shell flows. T05 changed the resolved DB handle to the repository-scoped adapter and validates the stored `repository_metadata.repository_id` before returning storage; T08 wired hooks/lifecycle to pass resolved config values into this context; T09 wired trace UX to repository-scoped storage (the checkout-scoped `sce trace --legacy` surface was later removed by the `retire-legacy-agent-trace-db` plan). Covered by in-module tests: repository separation, SSH/HTTPS clone consolidation, linked-worktree consolidation, explicit-ID override, idempotent re-resolution, missing-identity guidance, path-segment validation, pre-migration checkout DB byte preservation/non-selection, empty fresh repository DB state, repository-level row sharing across equivalent clone checkouts, credential-safe remote canonicalization, and concurrent first-open convergence (`nix build .#checks..cli-tests`). +Registered in `cli/src/services/mod.rs`. Consumed by: Agent Trace lifecycle setup and current-repository `sce trace status` (setup-safe entrypoint); hook runtime, exclusively, through `open_agent_trace_db_for_hook_runtime` (hook-runtime-safe entrypoint). `sce trace db list`/`shell` open explicit-path `RepositoryAgentTraceDb` handles directly rather than through this resolver. T05 changed the resolved DB handle to the repository-scoped adapter and validates the stored `repository_metadata.repository_id` before returning storage; T08 wired hooks/lifecycle to pass resolved config values into this context; T09 wired trace UX to repository-scoped storage (the checkout-scoped `sce trace --legacy` surface was later removed by the `retire-legacy-agent-trace-db` plan). The `stable-agent-trace-source-instance-identity` plan split the resolver into the setup-safe and hook-runtime-safe entrypoints above and added the `metadata` field. Covered by in-module tests: repository separation, SSH/HTTPS clone consolidation, linked-worktree consolidation, explicit-ID override, idempotent re-resolution, missing-identity guidance, path-segment validation, pre-migration checkout DB byte preservation/non-selection, empty fresh repository DB state, repository-level row sharing across equivalent clone checkouts, credential-safe remote canonicalization, concurrent first-open convergence, hook-runtime resolution failing with `sce setup` guidance before setup has run, and hook-runtime resolution reusing the same metadata setup already stored (`nix build .#checks..cli-tests`). See also: [repository-identity.md](repository-identity.md), [checkout-identity.md](checkout-identity.md), [default-path-catalog.md](default-path-catalog.md), [../sce/agent-trace-db.md](../sce/agent-trace-db.md) diff --git a/context/context-map.md b/context/context-map.md index 9bea165a..1a290ddd 100644 --- a/context/context-map.md +++ b/context/context-map.md @@ -12,7 +12,7 @@ Feature/domain context: - `context/cli/cli-command-surface.md` (CLI command surface including top-level help with ASCII art banner and gradient rendering, setup install flow with the repeatable `sce setup --workflow ` optional-workflow selection and its interactive post-target multi-select, WorkOS device authorization flow + token storage behavior, attribution-only hook routing with validated post-commit `--remote-url` plumbing plus DB-backed `diff-trace` dual persistence and post-commit Agent Trace payload persistence including range `content_hash`, setup-owned local DB + repository-scoped Agent Trace DB bootstrap plus doctor DB health coverage with credential-safe repository identity diagnostics, centralized Rust SCE web URL helpers in `services::agent_trace`, nested flake release package/app installability, Cargo local install + crates.io readiness policy, and hidden `sce policy bash` command adapter for bash-policy hook callers; `sce sync` command wiring is deferred to `0.4.0`; static `RuntimeCommand` enum dispatch lives in `services/command_registry.rs`, command payload structs for help/version/completion/auth/config/setup/doctor/hooks/policy are owned by their respective `services/{name}/command.rs` files, and clap-to-runtime conversion lives in `services/parse/command_runtime.rs`) - `context/cli/default-path-catalog.md` (canonical production CLI path-ownership contract centered on `cli/src/services/default_paths.rs`, including persisted auth/config files, named DB paths for auth/local/repository-scoped Agent Trace databases, the default observability log-dir accessor consumed by config resolution with Linux `${XDG_STATE_HOME:-~/.local/state}/sce/logs` fallback semantics, repo-relative, embedded-asset, install, hook, and context-path families plus the regression guard that keeps production path ownership centralized) - `context/cli/repository-identity.md` (repository identity module in `cli/src/services/repository_identity/`: pure scheme-neutral `host[:port]/path` canonicalization for SCP/`ssh://`/HTTPS/`git://` remote URLs with credential stripping, hostname lowercasing, default-port removal, and query/fragment/trailing-`.git` cleanup, trim-only explicit-identity handling, `sha256("sce-repository-id-v1\0" + canonical_identity)` repository IDs, credential-safe fieldless errors, plus the `resolve` runtime submodule applying explicit-config-then-configured-remote precedence with `git config --get remote..url` lookup, `RepositoryIdentitySource` provenance, and `.sce/config.json`-guidance resolution errors that never echo URLs; consumed by the T04 `agent_trace_storage` resolver) -- `context/cli/agent-trace-storage.md` (repository-scoped Agent Trace storage resolver in `cli/src/services/agent_trace_storage/`: `AgentTraceStorageContext` inputs mirroring the `agent_trace.*` config keys, `ResolvedAgentTraceStorage` carrying repository identity + checkout ID + `/sce/repos//agent-trace.db` path + open `RepositoryAgentTraceDb`, `resolve_agent_trace_storage{,_at_state_root}` entrypoints with idempotent concurrent-safe first open via bounded fast-path-then-migrate retry plus narrow one-file schema migration-metadata repair and repository metadata validation, path-unsafe repository ID rejection in `default_paths::agent_trace_db_path_for_repository{,_at}`, strict never-touch boundary for any pre-migration checkout-scoped/global DB files, and active hook/runtime plus Agent Trace lifecycle setup call-site consumption after T08) +- `context/cli/agent-trace-storage.md` (repository-scoped Agent Trace storage resolver in `cli/src/services/agent_trace_storage/`: `AgentTraceStorageContext` inputs mirroring the `agent_trace.*` config keys, `ResolvedAgentTraceStorage` carrying repository identity + checkout ID + `/sce/repos//agent-trace.db` path + open `RepositoryAgentTraceDb` + typed `RepositoryMetadata`, two resolver families sharing one internal open core: `resolve_agent_trace_storage{,_at_state_root}` (setup/lifecycle and `sce trace status` callers) with idempotent concurrent-safe first open via bounded fast-path-then-migrate retry plus narrow one-file schema migration-metadata repair and repository metadata validation, and `resolve_agent_trace_storage_for_hook_runtime{,_at_state_root}` (hook runtime only) which never creates, migrates, or repairs schema/migration metadata and fails with `sce setup` guidance on a missing/incomplete database, path-unsafe repository ID rejection in `default_paths::agent_trace_db_path_for_repository{,_at}`, and strict never-touch boundary for any pre-migration checkout-scoped/global DB files) - `context/cli/checkout-identity.md` (current checkout identity infrastructure in `cli/src/services/checkout/`, including `/sce/checkout-id` UUIDv7 storage, setup/hook integration that creates/reuses checkout identity as repository-scoped Agent Trace diagnostic metadata, the removed per-checkout DB opener/path helper, `sce doctor` checkout identity display, and the never-touch on-disk handling of pre-migration checkout-scoped DB files that are no longer inspectable via the CLI) - `context/cli/patch-service.md` (standalone patch domain model, parser, JSON load helpers, and set operations in `cli/src/services/patch.rs` for in-memory parsed unified-diff representation, capturing only touched lines plus minimal per-file/per-hunk metadata, supporting both `Index:` SVN-style and `diff --git` git-style formats, with `ParseError` for actionable malformed-input diagnostics, `PatchLoadError`/`load_patch_from_json`/`load_patch_from_json_bytes` for storage-agnostic JSON reconstruction, `intersect_patches` for target-shaped overlap with exact-match-first and historical `kind`+`content` fallback semantics plus matched-constructed-line `session_id` and matched-constructed-hunk `model_id` provenance inheritance, and `combine_patches` for ordered patch combination with later-wins conflict resolution plus winning-hunk `model_id` provenance inheritance; `parse_patch`, `intersect_patches`, and `combine_patches` are consumed by the active post-commit hook runtime) - `context/cli/structured-patch-service.md` (Claude structured editor-hook derivation in `cli/src/services/structured_patch.rs`, including `Write` structured-update hunks, `Write` `tool_input.content` create fallback, `Edit` structured patches, deterministic skip reasons, `ParsedPatch` output semantics, and Rust golden fixture coverage) @@ -61,7 +61,7 @@ Feature/domain context: - `context/sce/local-db.md` (implemented `cli/src/services/local_db/mod.rs` local database spec with `LocalDb = TursoDb`, canonical local DB path resolution, zero local migrations, and inherited retry-backed blocking `execute`/`query`/`query_map` methods using the shared Turso adapter) - `context/sce/shared-turso-db.md` (current shared `cli/src/services/db/mod.rs` Turso database infrastructure seam, including `DbSpec`, generic `TursoDb`, encrypted `EncryptedTursoDb`, build-time generated migration constants from `cli/build.rs`/Cargo `OUT_DIR`, config-driven constructor/open-connect retry via `run_with_retry_sync`, no-migration `TursoDb::open_without_migrations()` / explicit-path `open_without_migrations_at(path)` for hot runtime paths, migration-running `new()` / explicit-path `new_at(path)` / `run_migrations()` with per-database `__sce_migrations` tracking, config-driven operation retry for `execute`/`query`/`query_values`/`query_map` with a `<= 2_000ms` default query failure budget, raw-value row fetching for deterministic operator-facing rendering, row-mapping excluded from retry, generic embedded migration execution, non-mutating `migration_metadata_problems()` and `ensure_schema_ready(setup_guidance)` readiness methods on `TursoDb`, and concrete wrappers for `LocalDb`, `AuthDb`, plus `RepositoryAgentTraceDb`) - `context/sce/auth-db.md` (encrypted `AuthDb = EncryptedTursoDb` adapter, canonical `/sce/auth.db` path, build-time generated `AUTH_MIGRATIONS` from `cli/migrations/auth/`, auth credential schema and updated-at trigger baseline, lifecycle setup/doctor integration, encrypted token-storage persistence, and `SCE_AUTH_DB_ENCRYPTION_KEY`/OS credential-store key handling) -- `context/sce/agent-trace-db.md` (implemented Agent Trace database adapter: the sole repository-scoped `RepositoryAgentTraceDb` backed by one fresh multi-statement schema file with `repository_metadata` validation, narrow concurrent-first-open repair for missing one-file baseline migration metadata after all required schema tables exist, no trace-table `checkout_id` columns, repository-level typed insert helpers for diff traces, post-commit intersections, Agent Trace rows, messages, and parts, repository-level recent diff-trace reads without checkout filtering, on-demand command/hook initialization with no daemon/background service, and the never-touch on-disk boundary for any pre-migration checkout-scoped/global DB files; the checkout-scoped `AgentTraceDb` adapter, its `agent_trace_db_path()`/`agent_trace_db_path_for_checkout()` helpers, and the 15-file `cli/migrations/agent-trace/` chain were removed by the `retire-legacy-agent-trace-db` plan; active hook writers/readers and Agent Trace setup/lifecycle resolve repository storage through `agent_trace_storage`) +- `context/sce/agent-trace-db.md` (implemented Agent Trace database adapter: the sole repository-scoped `RepositoryAgentTraceDb` backed by one fresh multi-statement baseline schema file plus an additive `002_repository_source_instance_id` migration, typed `RepositoryMetadata { repository_id, source_instance_id }` with atomic once-only source-instance initialization, `repository_metadata` validation, narrow concurrent-first-open repair for missing one-file baseline migration metadata after all required schema tables exist, no trace-table `checkout_id` columns, repository-level typed insert helpers for diff traces, post-commit intersections, Agent Trace rows, messages, and parts, repository-level recent diff-trace reads without checkout filtering, on-demand command/hook initialization with no daemon/background service, and the never-touch on-disk boundary for any pre-migration checkout-scoped/global DB files; the checkout-scoped `AgentTraceDb` adapter, its `agent_trace_db_path()`/`agent_trace_db_path_for_checkout()` helpers, and the 15-file `cli/migrations/agent-trace/` chain were removed by the `retire-legacy-agent-trace-db` plan; active hook writers/readers and Agent Trace setup/lifecycle resolve repository storage through `agent_trace_storage`) - `context/sce/agent-trace-core-schema-migrations.md` (historical reference for removed local DB schema bootstrap behavior; T03 now implements the actual local DB with migrations) - `context/sce/agent-trace-retry-queue-observability.md` (inactive local-hook retry path plus historical retry/metrics reference) - `context/sce/agent-trace-local-hooks-mvp-contract-gap-matrix.md` (T01 Local Hooks MVP production contract freeze and deterministic gap matrix for `agent-trace-local-hooks-production-mvp`) diff --git a/context/glossary.md b/context/glossary.md index 67510f9a..83bac5ad 100644 --- a/context/glossary.md +++ b/context/glossary.md @@ -37,6 +37,7 @@ - `checkout identity`: Stable UUIDv7 identifier assigned to a cloned repository or linked Git worktree, stored in `/sce/checkout-id` (never committed) and resolved via `git rev-parse --git-dir`. The identity is created or reused by `sce setup` through `AgentTraceDbLifecycle::setup()` and also auto-created by hook runtime when `sce setup` has not been run. Checkout identity is now diagnostic metadata for repository-scoped Agent Trace storage; it does not select the active DB and is not stored on Agent Trace rows. Any pre-migration per-checkout DB files at `/sce/agent-trace-{checkout_id}.db` are never touched by SCE and are no longer inspectable via the CLI (the checkout-scoped DB surface was removed by the `retire-legacy-agent-trace-db` plan). See `context/cli/checkout-identity.md`. - `repository identity`: Stable identity of a logical Git repository used to select the active repository-scoped Agent Trace DB path `/sce/repos//agent-trace.db` through the `agent_trace_storage` resolver. Resolved by `cli/src/services/repository_identity/` with precedence: explicit `agent_trace.repository_id` config value, then the URL of the configured Git remote (`agent_trace.repository_remote`, default `origin`), otherwise an actionable `.sce/config.json` error. Remote URLs canonicalize to a scheme-neutral, credential-free `host[:port]/path` form so equivalent SSH/SCP/HTTPS remotes converge, and the repository ID is `sha256("sce-repository-id-v1\0" + canonical_identity)` hex. Distinct from `checkout identity`, which stays per clone/worktree for diagnostics. See `context/cli/repository-identity.md`. - `repository-scoped Agent Trace DB`: Active Agent Trace storage shape where one logical Git repository maps to `/sce/repos//agent-trace.db`. The current seam is `RepositoryAgentTraceDb = TursoDb` in `cli/src/services/agent_trace_db/repository.rs`, backed by one fresh multi-statement schema file with `repository_metadata` plus repository-level trace tables, no `checkout_id` columns, and typed repository-level insert helpers for diff traces, post-commit intersections, Agent Trace rows, messages, and parts. Hook runtime, Agent Trace setup/lifecycle, and `sce trace` status/list/shell flows resolve repository-scoped storage through `agent_trace_storage`. This is the sole Agent Trace DB adapter; the checkout-scoped adapter and the `sce trace --legacy` inspection surface were removed by the `retire-legacy-agent-trace-db` plan. +- `source-instance identity`: Stable UUIDv4-style identity of one independently created repository-scoped Agent Trace database *file* (a lineage), distinct from `repository identity` (the logical Git repository, shared by every DB file resolved for it) and `checkout identity` (per clone/worktree, never persisted). Typed as `RepositoryMetadata { repository_id, source_instance_id }` and generated/validated by `generate_source_instance_id()` / `is_valid_source_instance_id(value)` in `cli/src/services/agent_trace_db/repository.rs`; never derived from repository ID, checkout ID, remote, hostname, or path. `RepositoryAgentTraceDb::verify_or_initialize_repository_metadata(repository_id)` seeds it once per database file (migration `002_repository_source_instance_id` starts every row at an empty placeholder that fails validation), fills a missing value through a conditional `UPDATE ... WHERE source_instance_id = ''` so every racing caller reads back the one stored winner, and never replaces an existing valid value. Two independently created database files for the same repository ID have different `source_instance_id` values; clones/worktrees that resolve the same physical DB file observe the same one. See `context/sce/agent-trace-db.md`. - `checkout registry` (removed): The central JSON registry at `/sce/checkout-registry.json` was removed in the `remove-checkout-registry` plan. `sce trace db list` now discovers checkouts by scanning `/sce/agent-trace-*.db` files on disk. `checkout_id`, `database_path`, and `last_seen` (from file mtime) are derived from the filesystem; `path` and `remote_url` are no longer rendered. See `context/cli/checkout-identity.md`. - `generated OpenCode plugin registration contract`: Current generated-config contract where `config/.opencode/opencode.json` serializes the OpenCode `plugin` field from canonical Pkl sources for SCE-managed plugins only; the current registered paths are `./plugins/sce-bash-policy.ts` and `./plugins/sce-agent-trace.ts`. Claude does not use an OpenCode-style plugin manifest; Claude bash-policy enforcement is registered through generated `.claude/settings.json` as a `PreToolUse` `Bash` command hook running `sce policy bash`. - `root Biome contract`: Repository-root formatting/linting contract owned by `biome.json`, currently scoped only to `npm/**` and the shared `config/lib/**` plugin package root with package-local `node_modules/**` excluded; the canonical execution path is the root Nix dev shell (`nix develop -c biome ...`). @@ -81,7 +82,7 @@ - `DbSpec`: Service-specific database metadata trait in `cli/src/services/db/mod.rs` that supplies a diagnostic database name, canonical path resolver, ordered embedded migration list, and config-file lookup key (`db_config_key()`) for `TursoDb`. - `TursoDb`: Generic unencrypted Turso database adapter in `cli/src/services/db/mod.rs`; owns parent-directory creation and Turso local open/connect flow wrapped in config-driven connection-open retry, then delegates synchronous `execute()`/`query()`/`query_map()` wrappers with config-driven query retry and migration execution through the shared internal `TursoConnectionCore` for a `DbSpec` implementation. - `TursoConnectionCore`: Internal shared operation core in `cli/src/services/db/mod.rs` used by both `TursoDb` and `EncryptedTursoDb`; owns the Turso connection and tokio current-thread runtime bridging used by the public adapter methods; generic embedded migration execution with per-database `__sce_migrations` metadata is delegated to `run_embedded_migrations` helpers. -- `no-migration DB open path`: `TursoDb::open_without_migrations()` / `TursoDb::open_without_migrations_at(path)` plus Agent Trace adapter-specific no-migration seams; opens/connects a local Turso database with parent-directory creation and configured connection-open retry but does not create `__sce_migrations` or run embedded schema migrations. Active Agent Trace hook callers first try the repository-scoped no-migration path and then fall back to migration-running initialization when readiness or repository metadata validation fails. +- `no-migration DB open path`: `TursoDb::open_without_migrations()` / `TursoDb::open_without_migrations_at(path)` plus Agent Trace adapter-specific no-migration seams; opens/connects a local Turso database with parent-directory creation and configured connection-open retry but does not create `__sce_migrations` or run embedded schema migrations. Active Agent Trace hook callers (`agent_trace_storage::resolve_agent_trace_storage_for_hook_runtime(...)`) use only this path: a missing or migration-incomplete database fails readiness with `sce setup` guidance instead of falling back to migration-running initialization. Setup/lifecycle and `sce trace status` callers (`agent_trace_storage::resolve_agent_trace_storage(...)`) still try this path first and fall back to migration-running initialization when readiness or repository metadata validation fails. - `TursoDb migration readiness check`: Public methods on `TursoDb` in `cli/src/services/db/mod.rs` for non-mutating schema-readiness verification: `migration_metadata_problems(&self) -> Result>` queries `__sce_migrations` metadata and compares applied IDs against `M::migrations()`, returning problems (missing table, incomplete migrations, unexpected migrations) or an empty list when ready; `ensure_schema_ready(&self, setup_guidance: &str) -> Result<()>` calls `migration_metadata_problems()` and bails with a formatted error including `M::db_name()` and the caller-provided guidance string when problems are found. `RepositoryAgentTraceDb::ensure_schema_ready_for_hooks()` delegates to `TursoDb::ensure_schema_ready()` with the Agent Trace–specific `AGENT_TRACE_SCHEMA_SETUP_GUIDANCE` constant. - `database_retry config namespace`: Nested config namespace under `policies.database_retry` in `sce/config.json`, authored in `config/pkl/base/sce-config-schema.pkl` and parsed/resolved in `cli/src/services/config/mod.rs`. Supports per-database overrides (`local_db`, `agent_trace_db`, `auth_db`) each with optional `connection_open` and `query` objects containing `max_attempts`, `timeout_ms`, `initial_backoff_ms`, `max_backoff_ms`. Validated against JSON Schema at config load and surfaced in `sce config show`/`validate`. Wired into DB adapter constructors and operation methods via config-aware retry resolution with fallback to hardcoded defaults. - `DatabaseRetryConfig`: Rust type in `cli/src/services/config/mod.rs` holding parsed and validated per-database retry policy overrides (`local_db`/`agent_trace_db`/`auth_db`, each `Option`) from the `policies.database_retry` config namespace. Initialized at app startup via `DATABASE_RETRY_CONFIG` `OnceLock` and consumed by config-aware retry resolution in DB adapters. @@ -90,7 +91,7 @@ - `DB query retry policy`: Retry policy used by `TursoDb::execute()`, `TursoDb::query()`, `TursoDb::query_map()`, `EncryptedTursoDb::execute()`, `EncryptedTursoDb::query()`, and `EncryptedTursoDb::query_map()` for local Turso operation retry, resolved from `policies.database_retry..query` via the `DATABASE_RETRY_CONFIG` `OnceLock` with fallback to hardcoded defaults (`5` attempts, `200ms` elapsed-attempt timeout, `25ms` initial backoff, `100ms` max backoff; default worst-case failure budget `<= 2_000ms`) through `run_with_retry_sync`. `query_map()` retries the initial query and row-fetch loop, then runs caller row mapping outside retry. - `__sce_migrations`: Per-database migration metadata table created by the shared `TursoConnectionCore` migration path behind public adapter `run_migrations()` methods; records applied migration IDs after successful execution so later setup/lifecycle initialization applies only migrations not yet recorded, while existing metadata-less DBs are brought forward by re-applying the current idempotent migration set and recording each ID. - `CLI generated migration manifest`: Build-time Rust source at `OUT_DIR/generated_migrations.rs` written by `cli/build.rs` from immediate `cli/migrations//*.sql` directories after staging SQL under `OUT_DIR/static/migrations`; constants are named from the database directory (for example `AGENT_TRACE_REPOSITORY_MIGRATIONS`, `AUTH_MIGRATIONS`), sorted by the numeric filename prefix before `_`, and embed staged SQL via `include_str!`. -- `sync command deferral`: Current plan/state note that a user-invocable `sce sync` command is not wired yet and is deferred to `0.4.0`; local DB bootstrap and setup-time repository-scoped Agent Trace DB initialization flow through lifecycle providers aggregated by the setup command, while hook runtime keeps a lazy repository Agent Trace DB fallback for repositories where setup has not run or schema metadata is incomplete, and DB health/repair flows through the doctor surface. +- `sync command deferral`: Current plan/state note that a user-invocable `sce sync` command is not wired yet and is deferred to `0.4.0`; local DB bootstrap and setup-time repository-scoped Agent Trace DB initialization flow through lifecycle providers aggregated by the setup command, hook runtime instead requires `sce setup` to have already prepared the repository Agent Trace DB (it never creates or migrates one), and DB health/repair flows through the doctor surface. - `CLI bounded resilience wrapper`: Shared policy in `cli/src/services/resilience.rs` (`RetryPolicy`, async `run_with_retry`, sync `run_with_retry_sync`) that applies deterministic retries/timeouts/capped backoff to transient operations, emits retry observability events, and returns actionable terminal failure guidance. The sync helper is currently wired into shared database constructors for local open/connect retry and into `TursoDb`/`EncryptedTursoDb` operation retry for `execute()`/`query()`/`query_map()`. - `setup service orchestration`: Setup execution logic in `cli/src/services/setup/command.rs` that resolves the repository root, always ensures the durable-context baseline, then for normal modes derives a repo-root-scoped `AppContext` from the runtime command context, dispatches `setup` through the static lifecycle provider catalog (config → local_db → auth_db → agent_trace_db → hooks when requested), handles interactive target selection for config asset installation, and emits deterministic success messaging per target. - `setup target flags`: Mutually-exclusive `sce setup` target selectors (`--opencode`, `--claude`, `--pi`, `--all`) that force non-interactive mode for automation; `--all` expands to opencode+claude+pi and replaced the removed `--both` flag. diff --git a/context/overview.md b/context/overview.md index ff109f4c..a615839d 100644 --- a/context/overview.md +++ b/context/overview.md @@ -21,7 +21,7 @@ The same runtime also emits stable user-facing stderr error classes (`SCE-ERR-PA The app runtime now also includes a structured observability baseline in `cli/src/services/observability.rs`: deterministic env-controlled log threshold/format (`SCE_LOG_LEVEL` defaults to `error`; `SCE_LOG_FORMAT` defaults to `text`), default-backed log-directory routing (`SCE_LOG_DIR` / config-file `log_dir` / `/sce/logs`) with per-operation machine-local dated file selection, optional session filename partitioning, and creation-triggered retention of direct regular `*.log` files to 10 entries, stable lifecycle event IDs, stderr primary emission so stdout command payloads remain pipe-safe, and `observability::traits` boundaries for logger and telemetry behavior. The app command dispatcher now enforces a centralized stdout/stderr stream contract in `cli/src/app.rs`: command success payloads are emitted on stdout only, while redacted user-facing diagnostics are emitted on stderr. `cli/src/app.rs` also now runs through explicit startup phases — dependency check, observability config resolution, runtime initialization, command parse/execute, and output rendering — with the app runtime carrying logger/telemetry plus static command-catalog state across those phases while preserving the existing exit-code and degraded-startup contracts. Within that lifecycle, `parse_command_phase` delegates clap-to-runtime conversion to `cli/src/services/parse/command_runtime.rs`, which returns a static `RuntimeCommand` enum, `services::app_support::execute_command_phase` logs around enum-owned `execute(...)` dispatch, and generic `RunOutcome` rendering logs classified errors through the logger trait boundary without coupling render support to the production logger type. Command payload structs for `help`, `version`, `completion`, `auth`, `config`, `setup`, `doctor`, and `hooks` live in service-owned `command.rs` files; `cli/src/services/command_registry.rs` owns the deterministic static command-name catalog and enum variants instead of boxed command trait objects. The CLI now also enforces a shared output-format parser contract in `cli/src/services/output_format.rs`, with canonical `--format ` parsing and command-specific actionable invalid-value guidance reused by `config` and `version` services. A compile-safe service lifecycle seam also exists in `cli/src/services/lifecycle.rs`: `ServiceLifecycle` exposes default no-op `diagnose`, `fix`, and `setup` methods against the narrow `HasRepoRoot` accessor, uses lifecycle-owned health/fix/setup result types, and owns the shared static `LifecycleProvider` enum catalog/factory with deterministic config → local_db → auth_db → agent_trace_db → hooks ordering and no boxed provider aggregation. Hooks has a `services/hooks/lifecycle.rs` provider for hook rollout diagnosis/fix/setup, config has a `services/config/lifecycle.rs` provider for global/repo-local config validation plus repo-local config bootstrap, local_db has a `services/local_db/lifecycle.rs` provider for canonical local DB path health, parent-directory readiness/bootstrap, and `LocalDb::new()` setup, auth_db has a `services/auth_db/lifecycle.rs` provider for canonical auth DB path health, parent-directory readiness/bootstrap, and `AuthDb::new()` setup, and agent_trace_db has a `services/agent_trace_db/lifecycle.rs` provider for repository-scoped Agent Trace DB setup and repository DB path health/parent readiness from resolved repository identity, returning an actionable "requires a Git repository" diagnostic outside repository context (no global/checkout fallback path). Doctor runtime aggregates the full shared provider catalog for `diagnose` and `fix` and adapts lifecycle records into doctor-owned output records; setup command aggregates the shared provider catalog for `setup` with hooks included only when requested and adapts lifecycle setup outcomes before rendering setup-owned messages. -Agent Trace lifecycle setup now resolves repository storage, creates/reuses checkout identity for diagnostics, and initializes `/sce/repos//agent-trace.db` via `RepositoryAgentTraceDb`; hook runtime lazy initialization uses the same repository storage resolver when setup has not prepared the DB or schema metadata is incomplete. +Agent Trace lifecycle setup now resolves repository storage, creates/reuses checkout identity for diagnostics, and initializes `/sce/repos//agent-trace.db` via `RepositoryAgentTraceDb`, atomically seeding a typed `RepositoryMetadata { repository_id, source_instance_id }` record; hook runtime resolves the same repository identity through a separate no-migration storage path that only reads/validates readiness and fails with `sce setup` guidance when the database is missing or its schema is incomplete, never creating or migrating it. The CLI now also includes a shared text styling service in `cli/src/services/style.rs` that provides deterministic color enablement via `owo-colors`, automatic TTY detection, and `NO_COLOR` compliance for human-facing text output; stdout help/text surfaces, stderr diagnostics, and interactive prompt-adjacent text now reuse that shared styling policy while JSON, completion, and other non-interactive/machine-readable flows remain unstyled. The service exports color-detection, conditional styling, help/diagnostic/label/prompt styling, and `banner_with_gradient()` helpers for use across command surfaces while preserving pipe-safe output for non-interactive environments. The `setup` command includes an `inquire`-backed target-selection flow: default interactive selection for OpenCode/Claude/Pi/All with required-hook installation in the same run, explicit non-interactive target flags (`--opencode`, `--claude`, `--pi`, `--all`), standalone `--bootstrap-context` for additive durable-context baseline creation without integration installs, deterministic mutually-exclusive validation, and non-destructive cancellation exits; the former `--both` flag was removed in favor of `--all` (opencode+claude+pi). Every normal successful setup path also ensures the same context baseline after the Git gate. Workflows the catalog marks optional are installed only when a repository opts in: the repeatable `sce setup --workflow ` flag names the selection for a run, an omitted flag reuses the selection persisted in `integrations.optional_workflows`, and the resolved selection filters the installed target assets and is written back to repo-local config. Interactive runs ask for the selection instead: a multi-select prompt follows target selection with every row unchecked on a first run and pre-checked from the persisted selection afterwards, cancelling it exits non-destructively like the target prompt, and the prompt is skipped when the catalog marks no workflow optional. `brownfield` is currently the only optional workflow, so a default run installs the five core workflows and no brownfield assets. `sce doctor` scopes its integration checks to that same recorded selection, so it never reports an unselected optional workflow's files as missing. For repository generation consumers, `config/pkl/generator-inputs.txt` declares the canonical Pkl/plugin input set and `scripts/produce-cli-generated-input.sh` owns its discovery, two-pass `config/pkl/generate.pkl` evaluation, determinism comparison, payload/input inventories, in-flight input-mutation rejection, atomic handoff publication, and staging cleanup. `scripts/run-cli-cargo.sh` creates a fresh temporary destination, delegates generation to that producer, invokes the requested Cargo workflow with `SCE_CLI_GENERATED_INPUT_DIR`, and removes the handoff after Cargo success, failure, or handled signals. `config/pkl/check-generated.sh` delegates the same production mechanics while retaining contract and path assertions. `scripts/prepare-cli-generated-assets.sh` moves the producer-validated Pkl payload and checksums into the unchanged package fallback, adds hooks, migrations, and the Agent Trace schema, and appends only those static checksums to the combined inventory. The root flake's pre-Cargo `cliGeneratedInput` derivation invokes the same producer from a declarative source containing the producer plus its declared inputs. `cli/build.rs` rejects missing, incomplete, modified, or stale repository handoffs, copies the validated payload into Cargo `OUT_DIR/pkl-generated`, stages static inputs under `OUT_DIR/static`, and writes setup-asset, optional-workflow-catalog, and migration Rust manifests into `OUT_DIR`; it never invokes Pkl. Published crates carry the ignored packaging-only fallback, and unpacked downstream builds validate and copy it into their own `OUT_DIR` without requiring Pkl or parent repository paths. @@ -36,7 +36,7 @@ The same config resolver now also owns the attribution-hooks gate used by local The config service split now includes `cli/src/services/config/resolver.rs` as the focused owner for config-file discovery, file-layer merging, env/flag/default precedence, auth-key resolution, observability resolution, attribution-hooks resolution, and default-discovered invalid-file degradation; `cli/src/services/config/mod.rs` remains the facade/rendering orchestration surface while preserving existing `services::config` imports. Generated config now includes repo-local OpenCode plugin assets: `sce-bash-policy.ts` plus `sce-agent-trace.ts` are emitted under `config/.opencode/plugins/`; the OpenCode agent-trace plugin extracts `{ sessionID, diff, time, model_id }` from user `message.updated` events with diffs, tracks per-session OpenCode client version from `session.created`/`session.updated`, and sends payloads to `sce hooks diff-trace` with `tool_name="opencode"` plus optional `tool_version`. Claude generated config now routes supported `PostToolUse Write|Edit|MultiEdit|NotebookEdit` events directly to `sce hooks diff-trace`; it no longer registers a `SessionStart` hook or calls `sce hooks session-model`. Rust handles extraction, validation, and persistence without a TypeScript intermediary; the former `config/.claude/plugins/sce-agent-trace.ts` Bun runtime was removed in T07 of the `claude-rust-diff-trace` plan. The Rust hook validates required fields, uses only direct payload `model_id` and `tool_version` (no longer resolves from `session_models`), and persists tool-prefixed `session_id` values (`oc_` for OpenCode, `cc_` for Claude, `pi_` for Pi), `model_id`, `tool_name`, and nullable `tool_version` into `diff_traces` through AgentTraceDb. Bash-policy now delegates OpenCode enforcement to the Rust `sce policy bash` command: the generated OpenCode plugin at `config/.opencode/plugins/sce-bash-policy.ts` is a thin wrapper that calls `sce policy bash --input normalized --output json` via `spawnSync` and throws on deny decisions; it no longer contains independent TypeScript policy logic. The former `bash-policy/runtime.ts` TypeScript runtime has been removed. Preset... Claude bash-policy enforcement is also generated through `.claude/settings.json` as a `PreToolUse` `Bash` command hook running `sce policy bash`, so Claude and OpenCode both delegate to the Rust policy evaluator without a Claude TypeScript runtime. Pi bash-policy enforcement is delegated the same way through a project-local Pi extension (`config/lib/pi-plugin/sce-pi-extension.ts`, emitted to `config/.pi/extensions/sce/index.ts`) whose `tool_call` handler blocks denied bash commands via `sce policy bash` and fails open when the policy check cannot run (see `context/sce/pi-extension-runtime.md`). -Local database bootstrap is now owned by `LocalDbLifecycle::setup` and `AgentTraceDbLifecycle::setup` aggregated by the setup command. Agent Trace setup creates/reuses the current checkout ID for diagnostics and initializes the repository-scoped `/sce/repos//agent-trace.db` with the repository schema; hook runtime lazily creates or upgrades that repository DB when setup has not run or schema metadata is incomplete. Doctor validates the repository-scoped DB path/health and can bootstrap missing parent directories; outside a Git repository it reports an actionable "requires a Git repository" diagnostic instead of probing a sentinel path. The `sce trace` group operates only on repository-scoped DBs for list/status/status-all/shell UX; the checkout-scoped `--legacy` surface was removed by the `retire-legacy-agent-trace-db` plan (see `context/decisions/2026-07-17-retire-legacy-agent-trace-db.md`). Wiring a user-invocable `sce sync` command is deferred to `0.4.0`. +Local database bootstrap is now owned by `LocalDbLifecycle::setup` and `AgentTraceDbLifecycle::setup` aggregated by the setup command. Agent Trace setup creates/reuses the current checkout ID for diagnostics and initializes the repository-scoped `/sce/repos//agent-trace.db` with the repository schema; hook runtime resolves the same repository DB through a separate no-migration path that never creates, migrates, or repairs it, failing readiness with `sce setup` guidance when setup has not run or schema metadata is incomplete. Doctor validates the repository-scoped DB path/health and can bootstrap missing parent directories; outside a Git repository it reports an actionable "requires a Git repository" diagnostic instead of probing a sentinel path. The `sce trace` group operates only on repository-scoped DBs for list/status/status-all/shell UX; the checkout-scoped `--legacy` surface was removed by the `retire-legacy-agent-trace-db` plan (see `context/decisions/2026-07-17-retire-legacy-agent-trace-db.md`). Wiring a user-invocable `sce sync` command is deferred to `0.4.0`. The repository-root flake (`flake.nix`) applies a Rust overlay-backed stable toolchain pinned to `1.95.0` (with `rustfmt` and `clippy`), reads package/check version from the repo-root `.version` file, and builds `packages.sce` through a Crane `buildDepsOnly` + `buildPackage` pipeline. One deterministic pre-Cargo Nix derivation invokes the shared generated-input producer and supplies its validated `SCE_CLI_GENERATED_INPUT_DIR` store path to native, release, test, and Clippy Cargo derivations. Pkl is absent from those Cargo environments; dependency-only and format derivations do not receive the handoff, so canonical generation changes invalidate compiling outputs without invalidating dependency artifacts or formatting. `cli-tests`, `cli-clippy`, and `cli-fmt` remain Crane-backed check derivations. The root flake splits native and release outputs: `packages.sce` and `packages.default` build the **native** development binary (`scePackage`), while `packages.sce-release` builds the release binary (`sceReleasePackage`: static musl on Linux, native on Darwin). So `nix build .#sce` / `.#default`, `nix run . -- --help`, `nix run .#sce -- --help`, and `nix profile install github:crocoder-dev/shared-context-engineering` target the native binary, and `nix build .#sce-release` / `nix run .#sce-release -- ...` (plus `nix run .#release-artifacts`, which builds `.#sce-release`) target the release binary. On Linux the native and release outputs are distinct store paths, and the release output passes the native portability audit. `packages..ci-checks` is the explicit long-running validation tier: `nix build .#ci-checks` builds the `.#sce-release` package and, on Linux, audits the real release binary for forbidden `/nix/store/` references, so the expensive work stays out of `nix flake check` (which never builds `.#sce-release`). Git-commit embedding is **release-only**: `SCE_GIT_COMMIT` is injected via a `releaseCommitArgs` fragment applied only to the release derivations (`scePackageMusl` on Linux, `sceReleasePackageNative` on Darwin), not to `commonCargoArgs`. So native `.#sce`/`.#default` and every `nix flake check` derivation (`cli-tests`, `cli-clippy`, `cli-fmt`) build without the commit in their inputs and stay cache-reusable across commits (native `sce version` reports `unknown`), while `.#sce-release` still reports the real commit via `sce version`. `cli/build.rs` `emit_git_commit` emits `SCE_GIT_COMMIT` only when the env var is explicitly set — no `git rev-parse` fallback and no `.git/HEAD`/`.git/packed-refs` rerun watches. On Darwin the release now uses a distinct native-toolchain derivation (native toolchain + commit), so it diverges from `.#sce` to carry the commit while native stays commit-independent. diff --git a/context/patterns.md b/context/patterns.md index 98082dde..fe0cca00 100644 --- a/context/patterns.md +++ b/context/patterns.md @@ -161,7 +161,7 @@ - For commit-msg co-author policy seams, gate canonical trailer insertion on runtime controls (`SCE_DISABLED` plus the shared attribution-hooks enablement gate) plus the staged-diff AI-overlap evidence gate (`StagedDiffAiOverlapResult::Overlap` maps to `ai_contribution_present = true`; `NoOverlap` and `Error` both map to `false`), and enforce idempotent dedupe so allowed cases end with exactly one `Co-authored-by: SCE ` trailer. - For local hook attribution flows, resolve the top-level enablement gate through the shared config precedence model (`SCE_ATTRIBUTION_HOOKS_DISABLED` opt-out env over `policies.attribution_hooks.enabled`, default `true`) so commit-msg attribution is enabled by default while explicit config `enabled = false` and truthy env opt-out still suppress it without adding hook-specific config parsing. - Do not assume conversation-trace retry/backfill/artifact persistence, retry replay, remap ingestion, or rewrite trace transformation are active in the current local-hook runtime; those paths are removed from or deferred beyond the current baseline. -- For the current local DB baseline, resolve one deterministic per-user persistent DB target (Linux: `${XDG_STATE_HOME:-~/.local/state}/sce/local.db`; platform-equivalent state roots elsewhere), keep the path neutral rather than Agent Trace-branded, create parent directories before first use, and route initialization through `LocalDb::new()`. As database services split, keep path/migration ownership in each `DbSpec`: `LocalDbSpec` owns the neutral local DB path with zero migrations, `AuthDbSpec` owns encrypted `/sce/auth.db` plus ordered auth migrations, `RepositoryAgentTraceDbSpec` owns the repository-scoped `/sce/repos//agent-trace.db` (via `agent_trace_db_path_for_repository`) plus the one-file `agent-trace-repository/001_repository_schema.sql` baseline for `diff_traces` (including `payload_type`), `post_commit_patch_intersections`, `agent_traces`, `messages`, and `parts` with supporting indexes and triggers (the checkout-scoped `AgentTraceDbSpec` and its 15-file migration chain were removed by the `retire-legacy-agent-trace-db` plan), and shared Turso mechanics plus migration metadata stay in `TursoDb` / `EncryptedTursoDb`. +- For the current local DB baseline, resolve one deterministic per-user persistent DB target (Linux: `${XDG_STATE_HOME:-~/.local/state}/sce/local.db`; platform-equivalent state roots elsewhere), keep the path neutral rather than Agent Trace-branded, create parent directories before first use, and route initialization through `LocalDb::new()`. As database services split, keep path/migration ownership in each `DbSpec`: `LocalDbSpec` owns the neutral local DB path with zero migrations, `AuthDbSpec` owns encrypted `/sce/auth.db` plus ordered auth migrations, `RepositoryAgentTraceDbSpec` owns the repository-scoped `/sce/repos//agent-trace.db` (via `agent_trace_db_path_for_repository`) plus the one-file `agent-trace-repository/001_repository_schema.sql` baseline for `diff_traces` (including `payload_type`), `post_commit_patch_intersections`, `agent_traces`, `messages`, and `parts` with supporting indexes and triggers, plus the additive `002_repository_source_instance_id.sql` migration and typed `RepositoryMetadata { repository_id, source_instance_id }` (the checkout-scoped `AgentTraceDbSpec` and its 15-file migration chain were removed by the `retire-legacy-agent-trace-db` plan), and shared Turso mechanics plus migration metadata stay in `TursoDb` / `EncryptedTursoDb`. - For hosted event intake seams, verify provider signatures before payload parsing (GitHub `sha256=` HMAC over body, GitLab token-equality secret check), resolve old/new heads from provider payload fields, and derive deterministic reconciliation run idempotency keys from provider+event+repo+head tuple material. - For hosted rewrite mapping seams, resolve candidates deterministically in strict precedence order (patch-id exact, then range-diff score, then fuzzy score), classify top-score ties as `ambiguous`, enforce low-confidence unresolved behavior below `0.60`, and preserve stable outcome ordering via canonical candidate SHA sorting. - For hosted reconciliation observability, publish run-level mapped/unmapped counts, confidence histogram buckets, runtime timing, and normalized error-class labels so retry/quality drift can be monitored without requiring a full dashboard surface. diff --git a/context/plans/stable-agent-trace-source-instance-identity.md b/context/plans/stable-agent-trace-source-instance-identity.md new file mode 100644 index 00000000..cc7867ac --- /dev/null +++ b/context/plans/stable-agent-trace-source-instance-identity.md @@ -0,0 +1,133 @@ +# Plan: stable-agent-trace-source-instance-identity + +## Change summary + +Extend the repository-scoped Agent Trace database metadata so each independently created database lineage has a stable `source_instance_id` in addition to the logical `repository_id`. New databases generate one UUID-style value once; existing databases preserve it across reopen and setup, while migrated baseline databases acquire one exactly once through the supported setup/initialization path. The repository adapter will expose both metadata values through a typed API and will retain repository-mismatch failures. + +Add a repository Agent Trace migration after the existing baseline without rewriting migration `001`. Use the existing UUID dependency and atomic conditional initialization so concurrent callers converge on the stored source identity. Keep hook/readiness paths non-mutating with respect to schema migrations: an old database lacking the required migration reports the existing setup guidance, while source-instance repair occurs only through the safe initialization/setup path. Update focused Agent Trace context documentation to distinguish logical repository identity, database-lineage identity, and checkout identity. + +## Acceptance criteria + +How this plan is proven complete. Each criterion is observable and names the check that proves it. `/validate` runs these checks; no task in the stack performs final validation. + +- [x] AC1: A newly initialized repository Agent Trace database stores the expected `repository_id` and one valid, non-empty UUID-style `source_instance_id`; reopening it and repeating setup never changes either value. + - Validate: Repository adapter/storage tests create, reopen, and repeatedly initialize the same database and assert identical typed metadata values. +- [x] AC2: Two independently created database files for the same `repository_id` receive different source-instance identities, while clones/worktrees that resolve the same physical repository-scoped file observe the same identity. + - Validate: Storage/adapter tests compare metadata from two explicit paths and from repeated resolution of one repository-scoped path. +- [x] AC3: Existing repository databases with the old baseline metadata receive one persisted source identity through the supported migration/setup path, and that identity remains stable after reopening. + - Validate: A fixture created with migration `001_repository_schema` is upgraded through setup initialization; the test asserts the new column/value and stable reopen result. +- [x] AC4: Concurrent initialization of one missing source identity has one persisted winner and every caller returns that same `source_instance_id`; no caller replaces an existing valid identity. + - Validate: Concurrent adapter/storage initialization tests assert all returned metadata values equal the value read from `repository_metadata` afterward. +- [x] AC5: Repository metadata validation still rejects a database whose stored `repository_id` differs from the expected repository, and valid existing metadata is returned rather than regenerated. + - Validate: Existing mismatch coverage plus new typed metadata tests assert the stable mismatch diagnostic and preservation behavior. +- [x] AC6: Normal hook/readiness opens do not apply migrations or silently repair an old schema; a database missing the required migration fails readiness with actionable `sce setup` guidance, while source-instance initialization is available through the setup-safe path. + - Validate: Hook/readiness tests open an old-schema fixture without migrations, assert no schema mutation and setup guidance, and separately exercise setup initialization/repair. +- [x] AC7: Downstream Rust callers can obtain `repository_id` and `source_instance_id` from `RepositoryAgentTraceDb` through the typed metadata API rather than ad hoc metadata SQL. + - Validate: Compile-time/use-site tests call the adapter metadata API and assert its `RepositoryMetadata` result. +- [x] AC8: Agent Trace documentation explains the distinction between logical repository identity, source database-lineage identity, and checkout identity, including shared physical DB behavior, independent DB behavior, and the future ETL lineage tuple. + - Validate: Documentation inspection of the updated Agent Trace DB/storage context and synchronized root context confirms all required distinctions and tuple terminology. + +### Full validation + +Repository-wide checks `/validate` runs after the last task, regardless of which criterion they map to. + +- `nix flake check` +- `nix run .#pkl-check-generated` +- `git diff --check` + +### Context sync + +- Update `context/sce/agent-trace-db.md` and `context/cli/agent-trace-storage.md` with the migration, typed metadata, initialization, and hot-path readiness contract. +- Update `context/overview.md`, `context/architecture.md`, `context/glossary.md`, and `context/context-map.md` where their current repository Agent Trace claims say the schema is fresh-only, source identity is absent, or hooks may migrate without the new readiness boundary. + +## Constraints and non-goals + +- **In scope:** `repository_metadata` schema/migrations, `RepositoryAgentTraceDb` metadata initialization and validation, repository storage/lifecycle/hook readiness boundaries, focused Rust tests, and Agent Trace context documentation. +- **Out of scope:** Turso Sync, ETL/DWH schemas, watermarks, `sce trace sync`, control-plane or machine registration, device syncing, archive logic, row-schema changes solely for ETL, and checkout identity changes. +- **Constraints:** Preserve migration `001_repository_schema`; add a later repository migration. Persist UUID-style text generated with the existing `uuid` crate (UUIDv4 is the current acceptable convention). Never derive the source identity from repository ID, checkout ID, remote, hostname, or path. Preserve repository mismatch diagnostics and the existing setup/readiness lifecycle. Hook/read paths must not run schema migrations. +- **Non-goal:** Do not make `source_instance_id` a checkout identity or create a new database per clone/worktree; all callers using the same physical repository-scoped database share its source identity. + +## Assumptions + +- The existing `uuid` dependency and its `v4` feature are sufficient; no new dependency is needed. +- SQLite's staged migration limitation is handled by adding the column with a temporary empty/default storage value, then atomically filling it in initialization code and enforcing non-empty/valid UUID semantics through typed validation/readiness. The migration must not invent a pseudo-UUID. +- The current shared storage resolver needs an explicit setup/migration-capable path separate from hook/readiness resolution so existing lazy first-open behavior remains supported without allowing old schemas to be silently migrated from a hot path. + +## Task stack + +- [x] T01: `Add repository source-instance migration and typed metadata primitives` (status:done) + - Task ID: T01 + - Goal: Add the post-baseline repository migration and the typed `RepositoryMetadata`/UUID validation primitives needed by the repository adapter. + - Boundaries (in/out of scope): In — `002` migration under `cli/migrations/agent-trace-repository/`, UUIDv4 generation using the existing dependency, typed metadata representation and validation helpers, build-generated migration wiring as required, and unit-level pure validation coverage. Out — hook/lifecycle call-site behavior, ETL, and documentation. + - Dependencies: none + - Done when: The migration extends `repository_metadata` without modifying `001`, generated migration discovery includes it, the storage shape is compatible with old rows that need initialization, and the Rust layer can generate/validate a UUID-style source identity without deriving it from repository data. + - Verification notes (commands or checks): `nix flake check`; targeted Agent Trace compilation/tests through the repository Cargo wrapper if needed. + - Evidence: Added `cli/migrations/agent-trace-repository/002_repository_source_instance_id.sql` (`ALTER TABLE repository_metadata ADD COLUMN source_instance_id TEXT NOT NULL DEFAULT ''`, additive to `001`). Added `RepositoryMetadata { repository_id, source_instance_id }`, `generate_source_instance_id()` (UUIDv4 via the existing `uuid` crate), and `is_valid_source_instance_id()` to `cli/src/services/agent_trace_db/repository.rs` (all `#[allow(dead_code)]` pending T02 call-site wiring). Updated the module doc comment and the `open_at_initializes_the_full_schema_from_one_migration` test (renamed `..._from_all_migrations`) to reflect the now-two-migration chain; added tests for ID generation/uniqueness, placeholder rejection, malformed-value rejection, and the fresh-row placeholder value. Migration discovery required no build.rs change — `build.rs` already discovers migration files per directory by numeric filename prefix. Verification: `nix flake check` passed (clippy, fmt, cli-tests, pkl-generated); targeted `repository::` module tests 12/12 passed; `git diff --check` clean. No deviations from the reviewed task boundaries. + +- [x] T02: `Integrate atomic metadata initialization and migration-safe storage paths` (status:done) + - Task ID: T02 + - Goal: Update `RepositoryAgentTraceDb` and repository storage/lifecycle integration so fresh, existing, and concurrently racing initializations converge on one stable typed metadata record while hot readiness paths remain non-migrating. + - Boundaries (in/out of scope): In — metadata insert/conditional repair/validation API, preservation of repository mismatch errors, setup-safe initialization of missing source identity, explicit distinction between migration-running setup and no-migration hook/readiness opens, actionable setup guidance for old schemas, and propagation of metadata where the existing storage/lifecycle result surfaces need it. Out — changing Agent Trace row schemas, adding ETL consumers, or changing checkout identity. + - Dependencies: T01 + - Done when: New DB initialization atomically seeds repository and source identities; existing valid identities are returned unchanged; missing identities are conditionally filled once; races return the stored winner; setup can upgrade old repository baselines; no-migration hook/readiness access neither applies the new migration nor replaces an old-schema error with a silent migration. + - Verification notes (commands or checks): Targeted `agent_trace_db`, `agent_trace_storage`, lifecycle, and hook/readiness tests via `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml ...` as needed; inspect SQL/schema metadata before and after no-migration opens. + - Evidence: `RepositoryAgentTraceDb::verify_or_initialize_repository_metadata` (`cli/src/services/agent_trace_db/repository.rs`) now returns typed `RepositoryMetadata`, atomically claims a missing `source_instance_id` via a conditional `UPDATE ... WHERE source_instance_id = ''` (race-safe: every caller re-reads the stored winner), and leaves an existing valid identity untouched; repository-ID mismatch diagnostics are preserved. `cli/src/services/agent_trace_storage/mod.rs` now exposes two resolution paths sharing one `open_storage` core: the existing `resolve_agent_trace_storage`/`_at_state_root` (setup/lifecycle/diagnostic callers) still create and migrate via `open_repository_db_concurrently_safe`, while new `resolve_agent_trace_storage_for_hook_runtime`/`_at_state_root` (used by `hooks/mod.rs::open_agent_trace_db_for_hook_runtime`) call `open_repository_db_for_hook_runtime`, which only opens without migrating, calls `ensure_schema_ready_for_hooks`, and fails with the existing `sce setup` guidance on any missing/incomplete schema — never repairing or migrating. `ResolvedAgentTraceStorage` gained a `pub metadata: RepositoryMetadata` field, propagated into `lifecycle.rs`'s setup message (`Agent Trace source-instance identity: ...`). Removed the now-unneeded `#[allow(dead_code)]` on `RepositoryMetadata`/`generate_source_instance_id`/`is_valid_source_instance_id`. Added/updated tests: `repository.rs::fresh_repository_database_receives_a_valid_source_instance_id_once` (fresh DB gets a valid identity that repeated setup never changes); `agent_trace_storage::hook_runtime_resolution_fails_with_setup_guidance_before_setup_ran` (hook path on a missing DB fails with `sce setup` guidance and creates no schema); `agent_trace_storage::hook_runtime_resolution_succeeds_and_reuses_metadata_after_setup` (hook path after setup returns identical metadata). Verification: `nix flake check` passed (clippy, fmt, cli-tests, pkl-generated); `nix run .#pkl-check-generated` passed; targeted `agent_trace_db::repository::` (12/12), `agent_trace_storage::` (13/13), `services::hooks` (8/8), and `trace::` (35/35) tests passed; `git diff --check` clean. No deviations from the reviewed task boundaries; `sce trace status` and `sce setup` intentionally keep the existing create/migrate-capable resolver, since only the hooks git-hook runtime entry point is the "high-frequency hook path" the plan's non-migrating constraint targets. + +- [x] T03: `Add source-instance regression and concurrency coverage` (status:done) + - Task ID: T03 + - Goal: Cover the complete PR behavior with focused repository adapter/storage tests, including migration fixtures and concurrent missing-identity initialization. + - Boundaries (in/out of scope): In — fresh DB, stable reopen, repeated setup, same-repository independent files, wrong repository, old-schema migration/setup, concurrent convergence, and old-schema hook/readiness guidance tests; add only small testability seams required by those cases. Out — broad refactors, ETL tests, or full-suite-only cleanup. + - Dependencies: T02 + - Done when: Every requested behavior has deterministic automated coverage, including assertions that identities are persisted and never replaced and that the no-migration path leaves an old database schema unchanged. + - Verification notes (commands or checks): Narrow module/name-filtered tests through `scripts/run-cli-cargo.sh`; run the relevant Crane-backed test derivation when filesystem/database tests require the repository validation environment. + - Evidence: Fresh DB/stable-reopen/repeated-setup and wrong-repository coverage already existed from T01/T02 and needed no changes. Added the remaining gaps: `repository.rs::independently_created_databases_for_the_same_repository_receive_different_source_instance_ids` (AC2 — two `new_at`-created DBs with the same `repository_id` get different, both-valid `source_instance_id`s); `repository.rs::baseline_only_fixture_gains_a_stable_source_instance_id_through_setup_migration` (AC3 — a fixture seeded with only `001_repository_schema` plus a baseline metadata row is upgraded via `run_migrations()` and gets a stable identity across reopen); `repository.rs::concurrent_missing_source_instance_id_initialization_converges_on_one_persisted_winner` (AC4 — 8 threads, each with its own connection to one fresh, unseeded DB file, call `verify_or_initialize_repository_metadata` behind a `Barrier`; every returned value equals the value read back afterward); `agent_trace_storage/mod.rs::hook_runtime_resolution_fails_with_setup_guidance_on_a_baseline_only_schema_without_mutating_it` (AC6 — a migration-`002`-missing fixture at the resolved hook DB path fails `resolve_agent_trace_storage_for_hook_runtime_at_state_root` with `sce setup` guidance and `__sce_migrations` still shows only `001_repository_schema` afterward, proving no migration ran). Testability seam: added `TursoDb::::run_migrations_up_to(count)` (`cli/src/services/db/mod.rs`, `#[cfg(test)]`) so tests can build a fixture that predates later migrations without hand-rolling batch SQL execution; it reuses the existing private `run_embedded_migrations` helper against a `&M::migrations()[..count]` slice. Verification: `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml agent_trace_db::repository::` (15/15 passed) and `... agent_trace_storage::` (14/14 passed); full `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml` (193/193 passed); `nix flake check` passed (one transient, reproducibility-confirmed flake in the sandboxed `cli-tests` derivation on the first run, passing cleanly on immediate rebuild — not caused by these changes, no code change made in response); `git diff --check` clean. No deviations from the reviewed task boundaries. + +- [x] T04: `Document repository and source-instance lineage semantics` (status:done) + - Task ID: T04 + - Goal: Update durable Agent Trace documentation to describe source-instance identity and the final migration/readiness contract. + - Boundaries (in/out of scope): In — Agent Trace DB and storage context plus required root context summaries/map/glossary/architecture repairs, including `(repository_id, source_instance_id, source_table, source_row_id)` future ETL lineage wording and the explicit non-checkout identity distinction. Out — implementation behavior, ETL/DWH work, and unrelated context cleanup. + - Dependencies: T03 + - Done when: Documentation states that same physical repository-scoped DB users share `source_instance_id`, independently created DB files differ, clones/worktrees share it only when they share the DB, and `source_instance_id` is not checkout identity; current migration and hot-path claims match code. + - Verification notes (commands or checks): Review the changed context files against the adapter/storage code; run `git diff --check` and the generated-output check if any generated contract is touched. + - Evidence: `context/sce/agent-trace-db.md` and `context/cli/agent-trace-storage.md` already carried the two-migration chain, typed `RepositoryMetadata`, and hook-runtime no-migration contract from T01–T03's context sync; `context/glossary.md` already had an accurate `source-instance identity` entry distinguishing repository/checkout/source-instance identity — reviewed, no changes needed. Found and fixed three stale/incomplete root-context claims: `context/overview.md` said hook runtime "lazily creates or upgrades" the repository DB (now states it resolves through a separate no-migration path that never creates, migrates, or repairs, failing with `sce setup` guidance); `context/architecture.md`'s and `context/context-map.md`'s `agent_trace_db`/`agent-trace-db.md` descriptions only mentioned the `001_repository_schema.sql` baseline (now note the additive `002_repository_source_instance_id` migration and typed `RepositoryMetadata`). Added the missing AC8 future-ETL-lineage wording to `context/sce/agent-trace-db.md`: a new paragraph states `source_instance_id` identifies a database-lineage (shared by same-physical-DB clones/worktrees, distinct per independently created DB file), is distinct from diagnostic-only never-persisted `checkout_id`, and that a future ETL/DWH consumer is expected to key row provenance on `(repository_id, source_instance_id, source_table, source_row_id)`. Verification: reviewed each changed file's claims against `repository.rs` (`verify_or_initialize_repository_metadata`, migration `002`) and `agent_trace_storage/mod.rs` (hook-runtime-safe vs. setup-safe resolution) from T01–T03; `git diff --check` clean. No deviations from the reviewed task boundaries. + +## Open questions + +None. The request specifies the identity representation, migration ordering, lifecycle boundary, required tests, and non-goals; the only SQLite detail is recorded as an implementation assumption rather than a scope decision. + +## Validation Report + +**Status:** validated +**Date:** 2026-08-08 + +### Commands run + +- `nix flake check` -> exit 0 (all checks passed) +- `nix run .#pkl-check-generated` -> exit 0 ("Ephemeral Pkl generation passed: 101 files") +- `git diff --check` -> exit 0 (no whitespace errors) +- `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml agent_trace_db::repository::` -> exit 0 (15/15 passed) +- `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml agent_trace_storage::` -> exit 0 (14/14 passed) +- `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml services::hooks::` -> exit 0 (8/8 passed) + +### Scaffolding removed + +- None. + +### Success-criteria verification + +- [x] AC1: Fresh DB stores valid `repository_id`/`source_instance_id`, stable across reopen/repeated setup -> `repository_metadata_is_seeded_once_and_validated_on_reopen`, `fresh_repository_database_receives_a_valid_source_instance_id_once` pass (15/15 repository suite). +- [x] AC2: Independently created DB files differ; shared-path resolution stays identical -> `independently_created_databases_for_the_same_repository_receive_different_source_instance_ids` pass; storage suite's `repeated_resolution_is_idempotent`, `clones_of_the_same_repository_share_the_db_path_with_distinct_checkout_ids`, `linked_worktree_shares_the_db_path_with_a_distinct_checkout_id` pass (14/14 storage suite). +- [x] AC3: Old baseline fixture gains a stable identity through setup migration -> `baseline_only_fixture_gains_a_stable_source_instance_id_through_setup_migration` pass. +- [x] AC4: Concurrent missing-identity initialization converges on one persisted winner -> `concurrent_missing_source_instance_id_initialization_converges_on_one_persisted_winner` (8-thread barrier test) pass. +- [x] AC5: Repository-ID mismatch still rejected; valid existing metadata preserved -> `mismatched_repository_metadata_errors_on_open` pass. +- [x] AC6: Hook/readiness opens never migrate an old schema and fail with `sce setup` guidance; setup-safe path still repairs -> `hook_runtime_resolution_fails_with_setup_guidance_before_setup_ran`, `hook_runtime_resolution_fails_with_setup_guidance_on_a_baseline_only_schema_without_mutating_it`, `hook_runtime_resolution_succeeds_and_reuses_metadata_after_setup` pass; `services::hooks::` suite (8/8) unaffected. +- [x] AC7: Downstream callers use the typed `RepositoryMetadata` API -> inspected `cli/src/services/agent_trace_storage/mod.rs`, which imports and threads `RepositoryMetadata` from `agent_trace_db::repository` through `ResolvedAgentTraceStorage.metadata` rather than ad hoc metadata SQL. +- [x] AC8: Documentation distinguishes repository/database-lineage/checkout identity and states the future ETL lineage tuple -> inspected `context/sce/agent-trace-db.md` (new paragraph on `source_instance_id` vs. `checkout_id` and the `(repository_id, source_instance_id, source_table, source_row_id)` tuple) plus corroborating edits in `context/overview.md`, `context/architecture.md`, `context/context-map.md`, `context/patterns.md` reflecting the additive `002` migration and non-migrating hook-runtime contract. + +### Failed checks and follow-ups + +- None. + +### Residual risks + +- None identified. diff --git a/context/sce/agent-trace-db.md b/context/sce/agent-trace-db.md index f2c33f53..75d6fc06 100644 --- a/context/sce/agent-trace-db.md +++ b/context/sce/agent-trace-db.md @@ -36,11 +36,15 @@ pub type RepositoryAgentTraceDb = TursoDb; ``` -This adapter has no canonical `DbSpec::db_path()`; callers must resolve `/sce/repos//agent-trace.db` first and use explicit-path `TursoDb` constructors. Its migration list is `generated_migrations::AGENT_TRACE_REPOSITORY_MIGRATIONS`, currently one fresh multi-statement SQL file at `cli/migrations/agent-trace-repository/001_repository_schema.sql`. The schema includes `repository_metadata` plus the existing repository-level Agent Trace tables, indexes, and triggers, and intentionally has no `checkout_id` columns on trace tables. `RepositoryAgentTraceDb::verify_or_initialize_repository_metadata(repository_id)` inserts the singleton metadata row on first initialization and errors if an existing DB stores a different repository ID. `RepositoryAgentTraceDb::repair_missing_repository_schema_migration_metadata()` is a narrow concurrent-first-open repair seam: it never creates trace tables, but if every required repository schema table already exists and only the one-file baseline migration record is missing, it records `001_repository_schema` and rechecks readiness. +This adapter has no canonical `DbSpec::db_path()`; callers must resolve `/sce/repos//agent-trace.db` first and use explicit-path `TursoDb` constructors. Its migration list is `generated_migrations::AGENT_TRACE_REPOSITORY_MIGRATIONS`: the fresh multi-statement baseline `cli/migrations/agent-trace-repository/001_repository_schema.sql` plus `002_repository_source_instance_id.sql`, which adds a `source_instance_id TEXT NOT NULL DEFAULT ''` column to `repository_metadata` without modifying `001`. The schema includes `repository_metadata` plus the existing repository-level Agent Trace tables, indexes, and triggers, and intentionally has no `checkout_id` columns on trace tables. `RepositoryAgentTraceDb::repair_missing_repository_schema_migration_metadata()` is a narrow concurrent-first-open repair seam: it never creates trace tables, but if every required repository schema table already exists and only the one-file baseline migration record is missing, it records `001_repository_schema` and rechecks readiness. + +`repository.rs` defines the typed `RepositoryMetadata { repository_id, source_instance_id }` struct plus `generate_source_instance_id()` (mints a UUIDv4 string via the existing `uuid` crate; never derived from repository ID, checkout ID, remote, hostname, or path) and `is_valid_source_instance_id(value)` (rejects the empty placeholder and any non-UUID string). `RepositoryAgentTraceDb::verify_or_initialize_repository_metadata(repository_id) -> Result` inserts the singleton metadata row on first initialization, errors if an existing DB stores a different repository ID, and returns the typed metadata: an existing valid `source_instance_id` is returned unchanged, while a missing one (the migration's empty placeholder) is atomically claimed through `UPDATE repository_metadata SET source_instance_id = ?1 WHERE id = 1 AND source_instance_id = ''` and re-read — the conditional `WHERE` means only one concurrent caller's `UPDATE` changes a row, so every racing caller reads back the same stored winner and no caller ever replaces an already-valid identity. + +`source_instance_id` identifies a database-lineage (one independently created database file), not a checkout: every clone/worktree that resolves the same physical repository-scoped DB file shares one `source_instance_id`, while two independently created DB files for the same `repository_id` (for example after a manual copy or an out-of-band re-initialization) receive different ones. This is distinct from `checkout_id`, which is per clone/worktree, diagnostic-only, and never persisted on Agent Trace rows or `repository_metadata`. A future ETL/DWH consumer is expected to key row provenance on the tuple `(repository_id, source_instance_id, source_table, source_row_id)` — the logical repository, the database lineage that produced the row, the source table name, and the row's local primary key — so rows from independently created database files for the same repository never collide during ingestion; no such consumer exists yet in this repository. `RepositoryAgentTraceDb` exposes repository-level write helpers for the current row families by delegating to the same typed insert payloads and parameterized SQL used by the checkout-scoped adapter: `insert_diff_trace`, `insert_post_commit_patch_intersection`, `insert_agent_trace`, `insert_message`, `insert_messages`, `insert_part`, and `insert_parts`. It also exposes `recent_diff_trace_patches(cutoff_time_ms, end_time_ms)` by delegating to the shared recent diff-trace query/parser helper, so repository-scoped attribution reads use the same chronological inclusive window semantics without a checkout filter. These methods preserve the existing row shapes and do not add checkout provenance columns or checkout-scoped write/query APIs. -The repository-scoped adapter is consumed by `agent_trace_storage`, active hook runtime opening, Agent Trace setup/doctor lifecycle, and `sce trace` status/list/shell flows. Hook writers/readers resolve the current repository storage context before using `RepositoryAgentTraceDb`. It also exposes `open_for_hooks_without_migrations_at(path)` — the explicit-path no-migration runtime-open used by the trace read paths (`stats`, `discovery`/readiness, `shell`) — plus the migration-running `new_at(path)` constructor used by setup and hook-runtime fallback initialization. There is no longer a checkout-scoped adapter. +The repository-scoped adapter is consumed by `agent_trace_storage`, active hook runtime opening, Agent Trace setup/doctor lifecycle, and `sce trace` status/list/shell flows. Hook writers/readers resolve the current repository storage context before using `RepositoryAgentTraceDb`. It also exposes `open_for_hooks_without_migrations_at(path)` — the explicit-path no-migration runtime-open used by the trace read paths (`stats`, `discovery`/readiness, `shell`) — plus the migration-running `new_at(path)` constructor used only by the setup-safe `agent_trace_storage` resolver, never by hook runtime (see [Migrations](#migrations) below). There is no longer a checkout-scoped adapter. ## Non-goals @@ -73,15 +77,21 @@ SCE creates one Agent Trace DB per logical Git repository on demand through setu ## Migrations -`RepositoryAgentTraceDbSpec::migrations()` returns `generated_migrations::AGENT_TRACE_REPOSITORY_MIGRATIONS`, generated from `cli/migrations/agent-trace-repository/` at build time. It is currently one fresh multi-statement baseline file: +`RepositoryAgentTraceDbSpec::migrations()` returns `generated_migrations::AGENT_TRACE_REPOSITORY_MIGRATIONS`, generated from `cli/migrations/agent-trace-repository/` at build time (`build.rs` discovers migration files per directory by numeric filename prefix; adding a file needs no other build wiring). It is a fresh multi-statement baseline file plus one later additive migration: - `001_repository_schema.sql` (migration ID `001_repository_schema`) — creates `repository_metadata`, `diff_traces` (including `payload_type TEXT NOT NULL DEFAULT 'patch'`), `post_commit_patch_intersections`, `agent_traces`, `messages`, and `parts`, plus the lookup indexes and `updated_at` triggers, in one `execute_batch` statement recorded as a single migration ID. +- `002_repository_source_instance_id.sql` (migration ID `002_repository_source_instance_id`) — adds `repository_metadata.source_instance_id TEXT NOT NULL DEFAULT ''`. SQLite's `ALTER TABLE ADD COLUMN` can only supply a fixed default, so the migration never invents a UUID itself; every row (fresh or upgraded) holds the empty placeholder until initialization code fills it in exactly once. The former checkout-scoped `AGENT_TRACE_MIGRATIONS` constant and its 15-file `cli/migrations/agent-trace/` chain (`001_create_diff_traces` … `015_add_diff_traces_payload_type`) were removed by the `retire-legacy-agent-trace-db` plan; `build.rs` auto-discovers migration directories, so deleting the directory dropped the constant on regeneration. The repository schema captures the same tables/columns/indexes/triggers that the old incremental chain produced. The shared `TursoDb` runner records applied IDs in the database-local `__sce_migrations` table. Migration SQL is executed with `execute_batch`, so the one-file repository baseline can contain multiple statements while still recording one migration ID. -Repository-scoped hook DB resolution first resolves `agent_trace.repository_id` / `agent_trace.repository_remote` through config, then uses `agent_trace_storage::resolve_agent_trace_storage(...)`. The storage resolver tries `RepositoryAgentTraceDb::open_without_migrations_at(path)` + `ensure_schema_ready_for_hooks()` + repository metadata validation first. If setup has not initialized the repository DB, metadata is absent, or migrations are incomplete, it falls back to migration-running `RepositoryAgentTraceDb::new_at(path)` and validates/seeds `repository_metadata.repository_id` before returning. The resolver retries the fast-path/migration sequence for a bounded window during concurrent first opens; if another opener completed the one-file schema but the baseline migration record is missing, the repository adapter records that metadata only after verifying all required repository schema tables already exist. When the fallback also fails, the error context includes the fast-path failure reason (`(fast-path attempt: {fast_error})`) so both failure causes are visible in diagnostics. Normal readiness is based on exact migration metadata parity with `AGENT_TRACE_REPOSITORY_MIGRATIONS`; table introspection is used only by the narrow concurrent-first-open metadata repair seam. +Repository-scoped DB resolution first resolves `agent_trace.repository_id` / `agent_trace.repository_remote` through config, then chooses one of two `agent_trace_storage` entrypoints depending on the caller (see [context/cli/agent-trace-storage.md](../cli/agent-trace-storage.md)): + +- **Setup/lifecycle and `sce trace status`** use `agent_trace_storage::resolve_agent_trace_storage(...)`. It tries `RepositoryAgentTraceDb::open_without_migrations_at(path)` + `ensure_schema_ready_for_hooks()` + `verify_or_initialize_repository_metadata(repository_id)` first. If the repository DB does not exist, metadata is absent, or migrations are incomplete, it falls back to migration-running `RepositoryAgentTraceDb::new_at(path)` and the same metadata call before returning. The resolver retries the fast-path/migration sequence for a bounded window during concurrent first opens; if another opener completed the one-file schema but the baseline migration record is missing, the repository adapter records that metadata only after verifying all required repository schema tables already exist. When the fallback also fails, the error context includes the fast-path failure reason (`(fast-path attempt: {fast_error})`) so both failure causes are visible in diagnostics. +- **Hook runtime** (every `sce hooks` subcommand, exclusively through `open_agent_trace_db_for_hook_runtime`) uses `agent_trace_storage::resolve_agent_trace_storage_for_hook_runtime(...)`. It only tries `open_without_migrations_at(path)` + `ensure_schema_ready_for_hooks()` + `verify_or_initialize_repository_metadata(repository_id)`; there is no fallback, no bounded retry, and no migration-metadata repair. A missing or migration-incomplete repository DB fails immediately with the existing `Run 'sce setup'.` guidance instead of creating or migrating one. `verify_or_initialize_repository_metadata` still runs on this path because it only fills a missing `source_instance_id` through an already-existing-column `UPDATE`, never a schema migration. + +Normal readiness is based on exact migration metadata parity with `AGENT_TRACE_REPOSITORY_MIGRATIONS`; table introspection is used only by the narrow concurrent-first-open metadata repair seam available on the setup/lifecycle path. The `diff_traces` baseline migration creates: @@ -166,7 +176,7 @@ Both triggers compare `OLD.*` vs `NEW.*` for all mutable columns (excluding `upd - `diagnose()` resolves repository identity from config or the configured Git remote and reports the repository-scoped Agent Trace DB path and parent-directory readiness. When the DB file exists, it opens the file via `RepositoryAgentTraceDb::open_without_migrations_at` and verifies schema readiness via `ensure_schema_ready_for_hooks`, reporting `AgentTraceDbConnectionFailed` if open fails or `AgentTraceDbSchemaNotReady` if the schema is incomplete. Missing repository identity is a manual problem with `.sce/config.json` / remote guidance. Outside repository context there is no repository identity to select a DB and no global/checkout fallback path; the lifecycle returns an actionable "requires a Git repository" diagnostic, surfaced by `diagnose_agent_trace_db_health` as a manual-only `UnableToResolveStateRoot` problem. - `fix()` bootstraps the resolved repository DB parent directory for auto-fixable parent-readiness problems, with the same global parent fallback outside repository context. -- `setup()` resolves repository storage through `agent_trace_storage`, creates/reuses the current checkout identity for diagnostics, opens/creates `/sce/repos//agent-trace.db` with the repository schema, validates `repository_metadata.repository_id`, and emits setup messaging with the repository ID, checkout ID, and initialized DB path. Hook runtime lazy initialization remains available for repositories where setup has not run or schema metadata is incomplete. +- `setup()` resolves repository storage through `agent_trace_storage`, creates/reuses the current checkout identity for diagnostics, opens/creates `/sce/repos//agent-trace.db` with the repository schema, validates/seeds typed `RepositoryMetadata { repository_id, source_instance_id }`, and emits setup messaging with the repository ID, source-instance identity, checkout ID, and initialized DB path. Hook runtime no longer has a lazy-creation fallback: it requires `sce setup` to have already prepared the repository DB and fails readiness with setup guidance otherwise (see [Migrations](#migrations) above). - `sce doctor` surfaces checkout identity facts where available and lifecycle-owned repository Agent Trace DB health in the `Configuration` section, with `[PASS]`/`[FAIL]`/`[MISS]` status tokens. Outside repository context the lifecycle reports the actionable "requires a Git repository" diagnostic instead of probing a sentinel path. JSON output includes `checkout_identity` when available plus the resolved `agent_trace_db` field. - `sce trace db list` discovers repository DBs under `/sce/repos//agent-trace.db`, reporting text or JSON sorted by mtime descending. See [context/cli/trace-command.md](../cli/trace-command.md).