diff --git a/cli/src/app.rs b/cli/src/app.rs index ccbcec067..9f39312f0 100644 --- a/cli/src/app.rs +++ b/cli/src/app.rs @@ -57,21 +57,16 @@ pub(crate) trait HasLogger { fn logger(&self) -> &Self::Logger; } -#[allow(dead_code)] pub(crate) trait HasTelemetry { type Telemetry: TelemetryTrait; fn telemetry(&self) -> &Self::Telemetry; } -#[allow(dead_code)] pub(crate) trait HasFs { type Fs: services::capabilities::FsOps; - - fn fs(&self) -> &Self::Fs; } -#[allow(dead_code)] pub(crate) trait HasGit { type Git: services::capabilities::GitOps; @@ -113,23 +108,12 @@ where HasLogger::logger(self) } - #[allow(dead_code)] - pub(crate) fn fs(&self) -> &F { - HasFs::fs(self) - } - - #[allow(dead_code)] - pub(crate) fn git(&self) -> &G { - HasGit::git(self) - } - fn telemetry(&self) -> &T { HasTelemetry::telemetry(self) } /// Returns a context for a command-scoped repository root while preserving /// the runtime logger, telemetry, and capability dependencies. - #[allow(dead_code)] pub(crate) fn with_repo_root(&self, repo_root: impl Into) -> Self { Self { logger: self.logger, @@ -139,15 +123,6 @@ where repo_root: Some(repo_root.into()), } } - - /// Returns the resolved repository root path when available. - /// - /// Lifecycle providers use this during setup to avoid re-resolving - /// the repository root independently. - #[allow(dead_code)] - pub fn repo_root(&self) -> Option<&Path> { - HasRepoRoot::repo_root(self) - } } impl HasLogger for AppContext<'_, L, T, F, G> @@ -186,10 +161,6 @@ where G: services::capabilities::GitOps, { type Fs = F; - - fn fs(&self) -> &Self::Fs { - self.fs - } } impl HasGit for AppContext<'_, L, T, F, G> diff --git a/cli/src/main.rs b/cli/src/main.rs index 39cd9686f..313c38b66 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -2,7 +2,6 @@ mod app; mod cli_schema; mod command_surface; -#[allow(dead_code)] mod generated_migrations { include!(concat!(env!("OUT_DIR"), "/generated_migrations.rs")); } diff --git a/cli/src/services/agent_trace.rs b/cli/src/services/agent_trace.rs index 15c2bebf3..0120affdf 100644 --- a/cli/src/services/agent_trace.rs +++ b/cli/src/services/agent_trace.rs @@ -188,34 +188,22 @@ fn parse_commit_timestamp(commit_timestamp: &str) -> Result = OnceLock::new(); #[derive(Debug, Eq, PartialEq)] -#[allow(dead_code)] pub(crate) enum AgentTraceValidationError { - FileRead { path: String, message: String }, - InvalidJson { message: String }, SchemaValidation { errors: Vec }, } impl fmt::Display for AgentTraceValidationError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Self::FileRead { path, message } => { - write!(f, "Agent Trace JSON file could not be read at '{path}': {message}") - } - Self::InvalidJson { message } => { - write!(f, "Agent Trace JSON must be valid JSON: {message}") - } Self::SchemaValidation { errors } => write!( f, "Agent Trace JSON failed schema validation against embedded schema '{AGENT_TRACE_SCHEMA_PATH}': {}", @@ -227,7 +215,6 @@ impl fmt::Display for AgentTraceValidationError { impl Error for AgentTraceValidationError {} -#[allow(dead_code)] fn agent_trace_schema_validator() -> &'static Validator { AGENT_TRACE_SCHEMA_VALIDATOR.get_or_init(|| { let schema: Value = serde_json::from_str(AGENT_TRACE_SCHEMA_JSON) @@ -236,7 +223,6 @@ fn agent_trace_schema_validator() -> &'static Validator { }) } -#[allow(dead_code)] pub(crate) fn validate_agent_trace_value(value: &Value) -> Result<(), AgentTraceValidationError> { let mut errors = agent_trace_schema_validator() .iter_errors(value) @@ -252,26 +238,6 @@ pub(crate) fn validate_agent_trace_value(value: &Value) -> Result<(), AgentTrace Err(AgentTraceValidationError::SchemaValidation { errors }) } -#[allow(dead_code)] -pub(crate) fn validate_agent_trace_json(raw: &str) -> Result<(), AgentTraceValidationError> { - let value: Value = - serde_json::from_str(raw).map_err(|error| AgentTraceValidationError::InvalidJson { - message: error.to_string(), - })?; - - validate_agent_trace_value(&value) -} - -#[allow(dead_code)] -pub(crate) fn validate_agent_trace_file(path: &Path) -> Result<(), AgentTraceValidationError> { - let raw = - std::fs::read_to_string(path).map_err(|error| AgentTraceValidationError::FileRead { - path: path.display().to_string(), - message: error.to_string(), - })?; - - validate_agent_trace_json(&raw) -} /// Classification of a single hunk's origin relative to the AI candidate patch. #[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "snake_case")] @@ -305,7 +271,6 @@ pub struct Conversation { /// A related resource for a conversation entry. #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "snake_case")] -#[allow(dead_code)] pub struct ConversationRelated { /// Free-form related resource type. #[serde(rename = "type")] @@ -371,40 +336,6 @@ pub struct AgentTrace { pub files: Vec, } -/// Classify a single `post_commit_patch` hunk against the corresponding -/// `intersection_patch` hunk (if any). -/// -/// Two hunks correspond when they share the same `old_start` value within the -/// same file. This is the slot-matching rule that aligns `intersection_patch` -/// hunks to `post_commit_patch` hunks for comparison. -/// -/// Returns: -/// - `HunkContributor::Ai` when the `intersection_patch` hunk exists and its -/// touched lines match the `post_commit_patch` hunk's touched lines exactly -/// (same count, same kind, same `line_number`, same content, in the same order). -/// - `HunkContributor::Mixed` when the `intersection_patch` hunk exists but its -/// touched lines differ from the `post_commit_patch` hunk's touched lines. -/// - `HunkContributor::Unknown` when no `intersection_patch` hunk with the same -/// `old_start` exists for this `post_commit_patch` hunk. -#[allow(dead_code)] -pub fn classify_hunk( - post_commit_hunk: &PatchHunk, - intersection_hunks: &[PatchHunk], -) -> HunkContributor { - let Some(intersection_hunk) = intersection_hunks - .iter() - .find(|h| h.old_start == post_commit_hunk.old_start) - else { - return HunkContributor::Unknown; - }; - - if hunks_match_exactly(post_commit_hunk, intersection_hunk) { - HunkContributor::Ai - } else { - HunkContributor::Mixed - } -} - type CoverageLineKey<'a> = (TouchedLineKind, u64, &'a str); fn coverage_line_key(line: &TouchedLine) -> CoverageLineKey<'_> { @@ -473,7 +404,6 @@ fn combined_model_id( } } -#[allow(dead_code)] pub(crate) fn patches_have_overlap( candidate_patch: &ParsedPatch, target_patch: &ParsedPatch, @@ -490,17 +420,6 @@ pub(crate) fn patch_has_touched_lines(patch: &ParsedPatch) -> bool { .any(|file| file.hunks.iter().any(|hunk| !hunk.lines.is_empty())) } -/// Check whether two hunks have identical touched lines in the same order. -fn hunks_match_exactly(left: &PatchHunk, right: &PatchHunk) -> bool { - if left.lines.len() != right.lines.len() { - return false; - } - left.lines.iter().zip(right.lines.iter()).all(|(ll, rl)| { - ll.kind == rl.kind && ll.line_number == rl.line_number && ll.content == rl.content - }) -} - -#[allow(dead_code)] pub(crate) fn range_content_hash(hunk: &PatchHunk) -> String { let mut input = Vec::new(); input.extend_from_slice(RANGE_CONTENT_HASH_INPUT_VERSION); @@ -650,25 +569,6 @@ fn build_trace_file( }) } -#[allow(dead_code)] -pub fn build_agent_trace( - constructed_patch: &ParsedPatch, - post_commit_patch: &ParsedPatch, - metadata: AgentTraceMetadataInput<'_>, -) -> Result { - let empty_mutation_ai_patch = ParsedPatch { files: Vec::new() }; - - build_agent_trace_from_evidence( - AgentTraceEvidence { - direct_patch: constructed_patch, - mutation_ai_patch: &empty_mutation_ai_patch, - }, - post_commit_patch, - metadata, - ) -} - -#[allow(dead_code)] pub fn build_agent_trace_from_evidence( evidence: AgentTraceEvidence<'_>, post_commit_patch: &ParsedPatch, diff --git a/cli/src/services/agent_trace/tests.rs b/cli/src/services/agent_trace/tests.rs index f6b1ef8e6..22ae65678 100644 --- a/cli/src/services/agent_trace/tests.rs +++ b/cli/src/services/agent_trace/tests.rs @@ -1,6 +1,6 @@ use super::{ - build_agent_trace, build_agent_trace_from_evidence, validate_agent_trace_value, - AgentTraceEvidence, AgentTraceMetadataInput, AgentTraceVcsType, AGENT_TRACE_VERSION, + build_agent_trace_from_evidence, validate_agent_trace_value, AgentTraceEvidence, + AgentTraceMetadataInput, AgentTraceVcsType, AGENT_TRACE_VERSION, }; use crate::services::{ agent_trace::agent_trace_conversation_url, @@ -8,6 +8,9 @@ use crate::services::{ }; use serde_json::Value; +const TEST_COMMIT_TIMESTAMP: &str = "2026-04-23T10:20:30Z"; +const TEST_COMMIT_REVISION: &str = "a0b1c2d3e4f5a6b7c8d9e0f11223344556677889"; + #[derive(Clone, Copy)] struct AgentTraceScenario { incremental: &'static [&'static str], @@ -15,9 +18,6 @@ struct AgentTraceScenario { golden: &'static str, } -const TEST_COMMIT_TIMESTAMP: &str = "2026-04-23T10:20:30Z"; -const TEST_COMMIT_REVISION: &str = "a0b1c2d3e4f5a6b7c8d9e0f11223344556677889"; - fn parse_fixtures(fixtures: &[&str]) -> Vec { fixtures .iter() @@ -60,8 +60,12 @@ fn assert_builds_expected_agent_trace(scenario: AgentTraceScenario) { parse_patch(scenario.post_commit, None).expect("fixture patch should parse"); let golden: Value = serde_json::from_str(scenario.golden).expect("golden json should load"); validate_agent_trace_value(&golden).expect("golden json should validate against schema"); - let actual = build_agent_trace( - &constructed_patch, + let empty_mutation_ai_patch = ParsedPatch { files: Vec::new() }; + let actual = build_agent_trace_from_evidence( + AgentTraceEvidence { + direct_patch: &constructed_patch, + mutation_ai_patch: &empty_mutation_ai_patch, + }, &post_commit_patch, AgentTraceMetadataInput { commit_timestamp: TEST_COMMIT_TIMESTAMP, diff --git a/cli/src/services/agent_trace_db/lifecycle.rs b/cli/src/services/agent_trace_db/lifecycle.rs index dd9f4fe86..8263c2191 100644 --- a/cli/src/services/agent_trace_db/lifecycle.rs +++ b/cli/src/services/agent_trace_db/lifecycle.rs @@ -8,7 +8,7 @@ use crate::services::db::{bootstrap_db_parent, collect_db_path_health, DbSpec}; use crate::services::default_paths::agent_trace_db_path_for_repository; use crate::services::lifecycle::{ FixOutcome, FixResultRecord, HealthCategory, HealthFixability, HealthProblem, - HealthProblemKind, HealthSeverity, LifecycleProviderId, ServiceLifecycle, SetupOutcome, + HealthProblemKind, HealthSeverity, ServiceLifecycle, SetupOutcome, }; use crate::services::repository_identity::resolve::{ resolve_repository_identity, RepositoryIdentitySource, @@ -20,10 +20,6 @@ use super::repository::{RepositoryAgentTraceDb, RepositoryAgentTraceDbSpec}; pub struct AgentTraceDbLifecycle; impl ServiceLifecycle for AgentTraceDbLifecycle { - fn id(&self) -> LifecycleProviderId { - LifecycleProviderId::AgentTraceDb - } - async fn diagnose(&self, ctx: &C) -> Vec { diagnose_agent_trace_db_health(ctx.repo_root()).await } diff --git a/cli/src/services/agent_trace_db/mod.rs b/cli/src/services/agent_trace_db/mod.rs index 8c42cc44a..9c69240ea 100644 --- a/cli/src/services/agent_trace_db/mod.rs +++ b/cli/src/services/agent_trace_db/mod.rs @@ -408,23 +408,6 @@ fn claude_model_state_observation_from_turso( }) } -#[allow(dead_code)] -async fn insert_message_with( - db: &TursoDb, - input: InsertMessageInsert, -) -> Result { - db.execute( - INSERT_MESSAGE_SQL, - ( - input.session_id, - input.message_id, - input.role.to_string(), - input.generated_at_unix_ms, - ), - ) - .await -} - async fn insert_messages_with( db: &TursoDb, inputs: Vec, @@ -453,21 +436,6 @@ async fn insert_messages_with( db.execute(&sql, params).await } -#[allow(dead_code)] -async fn insert_part_with(db: &TursoDb, input: InsertPartInsert) -> Result { - db.execute( - INSERT_PART_SQL, - ( - input.part_type.to_string(), - input.text, - input.message_id, - input.session_id, - input.generated_at_unix_ms, - ), - ) - .await -} - async fn insert_parts_with( db: &TursoDb, inputs: Vec, diff --git a/cli/src/services/agent_trace_db/repository.rs b/cli/src/services/agent_trace_db/repository.rs index 6c0b083e1..59bfd78ba 100644 --- a/cli/src/services/agent_trace_db/repository.rs +++ b/cli/src/services/agent_trace_db/repository.rs @@ -10,11 +10,10 @@ use crate::{ use super::{ insert_agent_trace_with, insert_conversation_text_event_with, insert_diff_trace_with, - insert_message_with, insert_messages_with, insert_part_with, insert_parts_with, - insert_post_commit_patch_intersection_with, recent_diff_trace_patches_with, - upsert_claude_model_state_with, AgentTraceInsert, ClaudeModelStateObservation, DiffTraceInsert, - InsertMessageInsert, InsertPartInsert, PostCommitPatchIntersectionInsert, - RecentDiffTracePatches, + insert_messages_with, insert_parts_with, insert_post_commit_patch_intersection_with, + recent_diff_trace_patches_with, upsert_claude_model_state_with, AgentTraceInsert, + ClaudeModelStateObservation, DiffTraceInsert, InsertMessageInsert, InsertPartInsert, + PostCommitPatchIntersectionInsert, RecentDiffTracePatches, }; const REPOSITORY_AGENT_TRACE_SCHEMA_SETUP_GUIDANCE: &str = "Run 'sce setup'."; @@ -284,25 +283,12 @@ impl RepositoryAgentTraceDb { recent_diff_trace_patches_with(self, cutoff_time_ms, end_time_ms).await } - /// Insert a message row, ignoring duplicate `(session_id, message_id)` - /// rows. - #[allow(dead_code)] - pub async fn insert_message(&self, input: InsertMessageInsert) -> Result { - insert_message_with(self, input).await - } - /// Insert message rows with one multi-row statement, ignoring duplicate /// `(session_id, message_id)` rows. pub async fn insert_messages(&self, inputs: Vec) -> Result { insert_messages_with(self, inputs).await } - /// Append a part row (no upsert; multiple rows per message allowed). - #[allow(dead_code)] - pub async fn insert_part(&self, input: InsertPartInsert) -> Result { - insert_part_with(self, input).await - } - /// Append part rows with one multi-row statement. pub async fn insert_parts(&self, inputs: Vec) -> Result { insert_parts_with(self, inputs).await diff --git a/cli/src/services/agent_trace_export/mod.rs b/cli/src/services/agent_trace_export/mod.rs index 5046ac895..64b1e77d1 100644 --- a/cli/src/services/agent_trace_export/mod.rs +++ b/cli/src/services/agent_trace_export/mod.rs @@ -76,6 +76,10 @@ pub struct AgentTracePartExportRow { } /// Owned, wire-compatible export row for the `diff_traces` capture stream. +#[allow( + dead_code, + reason = "diff_traces compatibility surface retained per context/decisions/2026-10-01-retire-diff-traces-remote-sync-compatibility-gate.md" +)] #[derive(Clone, Debug, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct AgentTraceDiffTraceExportRow { @@ -116,6 +120,10 @@ WHERE id > ?1 ORDER BY id ASC LIMIT ?2"; +#[allow( + dead_code, + reason = "diff_traces compatibility surface retained per context/decisions/2026-10-01-retire-diff-traces-remote-sync-compatibility-gate.md" +)] const SELECT_DIFF_TRACES_AFTER_SQL: &str = "SELECT id, session_id, time_ms, patch, model_id, tool_name, tool_version, payload_type FROM diff_traces @@ -200,6 +208,10 @@ impl<'a> AgentTraceExportReader<'a> { /// Read `diff_traces` rows with `id > cursor`, ordered by `id ASC`, /// capped at `limit`. `patch` and `payload_type` are returned raw and /// unmodified: no patch parsing or normalization is performed. + #[allow( + dead_code, + reason = "diff_traces compatibility surface retained per context/decisions/2026-10-01-retire-diff-traces-remote-sync-compatibility-gate.md" + )] pub async fn read_diff_traces_after( &self, cursor: i64, @@ -283,6 +295,10 @@ fn part_export_row_from_turso(row: &turso::Row) -> Result Result { Ok(AgentTraceDiffTraceExportRow { source_row_id: row.get(0).context("failed to read diff_traces.id")?, diff --git a/cli/src/services/agent_trace_storage/mod.rs b/cli/src/services/agent_trace_storage/mod.rs index 62192f9f9..9cecacebd 100644 --- a/cli/src/services/agent_trace_storage/mod.rs +++ b/cli/src/services/agent_trace_storage/mod.rs @@ -55,20 +55,6 @@ pub async fn resolve_agent_trace_storage( open_storage(repository_identity, db_path).await } -/// Resolution core against an explicit state root, so tests can exercise the -/// full path without touching the real user state directory. -pub async fn resolve_agent_trace_storage_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(repository_identity, db_path).await -} - /// Resolves repository-scoped Agent Trace storage for high-frequency hook /// runtime callers using the canonical state root. /// diff --git a/cli/src/services/agent_trace_sync/control_plane.rs b/cli/src/services/agent_trace_sync/control_plane.rs index 0bf079ab5..ddc7d623c 100644 --- a/cli/src/services/agent_trace_sync/control_plane.rs +++ b/cli/src/services/agent_trace_sync/control_plane.rs @@ -384,6 +384,10 @@ impl AuthenticatedControlPlaneClient { self.post_batch(request).await } + #[allow( + dead_code, + reason = "diff_traces compatibility surface retained per context/decisions/2026-10-01-retire-diff-traces-remote-sync-compatibility-gate.md" + )] pub async fn ingest_diff_traces( &self, request: &AgentTraceIngestionBatchRequest, @@ -633,7 +637,10 @@ mod tests { use tokio::sync::{Notify, Semaphore}; - use super::{AuthenticatedControlPlaneClient, ControlPlaneError, CredentialStore}; + use super::{ + AgentTraceIngestionStateRequest, AgentTraceIngestionStateResponse, + AuthenticatedControlPlaneClient, ControlPlaneError, CredentialStore, + }; use crate::services::auth::TokenResponse; use crate::services::token_storage::StoredTokens; @@ -846,4 +853,121 @@ mod tests { REPLACEMENT_REFRESH_TOKEN ); } + + struct FixedStore; + + impl CredentialStore for FixedStore { + async fn load(&self) -> Result, ControlPlaneError> { + Ok(Some(StoredTokens { + access_token: "access".to_string(), + token_type: "Bearer".to_string(), + expires_in: 3600, + refresh_token: "refresh".to_string(), + scope: None, + stored_at_unix_seconds: SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("clock after epoch") + .as_secs(), + })) + } + + async fn save(&self, _token: &TokenResponse) -> Result { + unreachable!("fixed credentials never refresh") + } + } + + fn start_state_server(payload: &str) -> String { + let listener = TcpListener::bind("127.0.0.1:0").expect("bind state server"); + let base_url = format!("http://{}", listener.local_addr().expect("server addr")); + let payload = payload.to_string(); + thread::spawn(move || { + for stream in listener.incoming() { + let Ok(mut stream) = stream else { + continue; + }; + let mut reader = BufReader::new(stream.try_clone().expect("clone stream")); + let mut content_length = 0usize; + loop { + let mut line = String::new(); + if reader.read_line(&mut line).unwrap_or(0) == 0 { + break; + } + let line = line.trim_end_matches(['\r', '\n']); + if line.is_empty() { + break; + } + if let Some((name, value)) = line.split_once(':') { + if name.eq_ignore_ascii_case("content-length") { + content_length = value.trim().parse().unwrap_or(0); + } + } + } + let mut body = vec![0u8; content_length]; + let _ = reader.read_exact(&mut body); + let response = format!( + "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{payload}", + payload.len() + ); + let _ = stream.write_all(response.as_bytes()); + } + }); + base_url + } + + async fn request_state( + payload: &str, + ) -> Result { + let client = AuthenticatedControlPlaneClient::with_credential_store( + reqwest::Client::builder() + .tls_certs_only(Vec::new()) + .build() + .expect("test http client"), + start_state_server(payload), + "http://127.0.0.1:1", + "client", + FixedStore, + ); + client + .ingestion_state(&AgentTraceIngestionStateRequest { + repository_id: "repo".to_string(), + source_instance_id: "source".to_string(), + }) + .await + } + + #[tokio::test] + async fn state_response_decodes_required_diff_traces_cursor() { + let state = + request_state(r#"{"cursors":{"messages":1,"parts":2,"diffTraces":7,"agentTraces":4}}"#) + .await + .expect("valid state"); + + assert_eq!(state.cursors.diff_traces, 7); + } + + #[tokio::test] + async fn state_response_rejects_missing_diff_traces_cursor() { + let error = request_state(r#"{"cursors":{"messages":1,"parts":2,"agentTraces":4}}"#) + .await + .expect_err("missing diffTraces"); + + assert!( + matches!(&error, ControlPlaneError::InvalidResponse(_)), + "{error:?}" + ); + } + + #[tokio::test] + async fn state_response_rejects_out_of_range_diff_traces_cursor() { + let error = request_state( + r#"{"cursors":{"messages":1,"parts":2,"diffTraces":-1,"agentTraces":4}}"#, + ) + .await + .expect_err("negative diffTraces"); + + assert!( + matches!(&error, ControlPlaneError::InvalidResponse(reason) if reason.contains("cursors.diffTraces")), + "{error:?}" + ); + } } diff --git a/cli/src/services/auth_db/lifecycle.rs b/cli/src/services/auth_db/lifecycle.rs index ab8ca2f56..ffc82fd0b 100644 --- a/cli/src/services/auth_db/lifecycle.rs +++ b/cli/src/services/auth_db/lifecycle.rs @@ -5,7 +5,7 @@ use crate::services::db::{bootstrap_db_parent, collect_db_path_health, DbSpec}; use crate::services::default_paths::auth_db_path; use crate::services::lifecycle::{ FixOutcome, FixResultRecord, HealthCategory, HealthFixability, HealthProblem, - HealthProblemKind, HealthSeverity, LifecycleProviderId, ServiceLifecycle, SetupOutcome, + HealthProblemKind, HealthSeverity, ServiceLifecycle, SetupOutcome, }; use super::{AuthDb, AuthDbSpec}; @@ -14,10 +14,6 @@ use super::{AuthDb, AuthDbSpec}; pub struct AuthDbLifecycle; impl ServiceLifecycle for AuthDbLifecycle { - fn id(&self) -> LifecycleProviderId { - LifecycleProviderId::AuthDb - } - async fn diagnose(&self, _ctx: &C) -> Vec { diagnose_auth_db_health() } diff --git a/cli/src/services/capabilities.rs b/cli/src/services/capabilities.rs index 7d6ab0f0c..ed255b47c 100644 --- a/cli/src/services/capabilities.rs +++ b/cli/src/services/capabilities.rs @@ -1,51 +1,18 @@ -#![allow(dead_code)] - -use std::fs::{self, Metadata}; -use std::path::{Path, PathBuf}; +use std::path::Path; use std::process::Command; use anyhow::{anyhow, Context, Result}; -pub trait FsOps: Send + Sync { - fn read_file(&self, path: &Path) -> Result; - - fn write_file(&self, path: &Path, content: &str) -> Result<()>; - - fn metadata(&self, path: &Path) -> Result; - - fn exists(&self, path: &Path) -> bool; -} +pub trait FsOps: Send + Sync {} #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] pub struct StdFsOps; -impl FsOps for StdFsOps { - fn read_file(&self, path: &Path) -> Result { - fs::read_to_string(path) - .with_context(|| format!("Failed to read file '{}'", path.display())) - } - - fn write_file(&self, path: &Path, content: &str) -> Result<()> { - fs::write(path, content) - .with_context(|| format!("Failed to write file '{}'", path.display())) - } - - fn metadata(&self, path: &Path) -> Result { - fs::metadata(path).with_context(|| format!("Failed to inspect path '{}'", path.display())) - } - - fn exists(&self, path: &Path) -> bool { - path.exists() - } -} +impl FsOps for StdFsOps {} pub trait GitOps: Send + Sync { fn run_command(&self, repo: &Path, args: &[&str]) -> Result; - fn resolve_repository_root(&self, dir: &Path) -> Result; - - fn resolve_hooks_directory(&self, repo: &Path) -> Result; - fn is_available(&self) -> bool; } @@ -57,22 +24,6 @@ impl GitOps for ProcessGitOps { run_git_command(repo, args) } - fn resolve_repository_root(&self, dir: &Path) -> Result { - let output = run_git_command(dir, &["rev-parse", "--show-toplevel"])?; - Ok(PathBuf::from(output.trim())) - } - - fn resolve_hooks_directory(&self, repo: &Path) -> Result { - let output = run_git_command(repo, &["rev-parse", "--git-path", "hooks"])?; - let hooks_path = PathBuf::from(output.trim()); - - if hooks_path.is_absolute() { - Ok(hooks_path) - } else { - Ok(repo.join(hooks_path)) - } - } - fn is_available(&self) -> bool { Command::new("git") .arg("--version") diff --git a/cli/src/services/command_registry.rs b/cli/src/services/command_registry.rs index 0e6700023..bc8ff9023 100644 --- a/cli/src/services/command_registry.rs +++ b/cli/src/services/command_registry.rs @@ -54,15 +54,6 @@ impl RuntimeCommand { } } - #[allow(dead_code)] - pub async fn execute(&self, context: &C) -> Result - where - C: HasLogger + ContextWithRepoRoot + crate::app::HasGit + crate::app::HasFs, - { - let mut stderr = std::io::sink(); - self.execute_with_stderr(context, &mut stderr).await - } - pub async fn execute_with_stderr( &self, context: &C, diff --git a/cli/src/services/config/lifecycle.rs b/cli/src/services/config/lifecycle.rs index 541ea608f..b5178488a 100644 --- a/cli/src/services/config/lifecycle.rs +++ b/cli/src/services/config/lifecycle.rs @@ -6,7 +6,7 @@ use crate::app::HasRepoRoot; use crate::services::default_paths::{resolve_sce_default_locations, RepoPaths}; use crate::services::lifecycle::{ HealthCategory, HealthFixability, HealthProblem, HealthProblemKind, HealthSeverity, - LifecycleProviderId, ServiceLifecycle, SetupOutcome, + ServiceLifecycle, SetupOutcome, }; use crate::services::setup::bootstrap_repo_local_config; @@ -16,10 +16,6 @@ use super::validate_config_file; pub struct ConfigLifecycle; impl ServiceLifecycle for ConfigLifecycle { - fn id(&self) -> LifecycleProviderId { - LifecycleProviderId::Config - } - async fn diagnose(&self, ctx: &C) -> Vec { let repository_root = match ctx.repo_root() { Some(path) => path.to_path_buf(), diff --git a/cli/src/services/config/policy.rs b/cli/src/services/config/policy.rs index 3a3302c2a..b64ac1904 100644 --- a/cli/src/services/config/policy.rs +++ b/cli/src/services/config/policy.rs @@ -33,7 +33,6 @@ pub(crate) struct BashPolicyConfig { } #[derive(Clone, Debug, Eq, PartialEq)] -#[allow(dead_code)] pub(crate) struct RuntimeBashPolicyPreset { pub(crate) id: String, pub(crate) argv_prefixes: Vec>, @@ -121,7 +120,6 @@ fn builtin_bash_policy_catalog() -> &'static BuiltinBashPolicyCatalog { }) } -#[allow(dead_code)] pub(crate) fn runtime_bash_policy_presets() -> Vec { builtin_bash_policy_catalog() .presets diff --git a/cli/src/services/config/types.rs b/cli/src/services/config/types.rs index 7de321d86..bed656c23 100644 --- a/cli/src/services/config/types.rs +++ b/cli/src/services/config/types.rs @@ -42,16 +42,6 @@ impl LogLevel { } } - pub(crate) fn parse_env(raw: &str, key: &str) -> anyhow::Result { - match raw { - "error" => Ok(Self::Error), - "warn" => Ok(Self::Warn), - "info" => Ok(Self::Info), - "debug" => Ok(Self::Debug), - _ => anyhow::bail!("Invalid {key} '{raw}'. Valid values: error, warn, info, debug."), - } - } - pub(crate) fn as_str(self) -> &'static str { match self { Self::Error => "error", @@ -88,14 +78,6 @@ impl LogFormat { } } - pub(crate) fn parse_env(raw: &str, key: &str) -> anyhow::Result { - match raw { - "text" => Ok(Self::Text), - "json" => Ok(Self::Json), - _ => anyhow::bail!("Invalid {key} '{raw}'. Valid values: text, json."), - } - } - pub(crate) fn as_str(self) -> &'static str { match self { Self::Text => "text", diff --git a/cli/src/services/db/mod.rs b/cli/src/services/db/mod.rs index fa61ef4f7..c2d4ff16d 100644 --- a/cli/src/services/db/mod.rs +++ b/cli/src/services/db/mod.rs @@ -12,7 +12,6 @@ use std::{ use anyhow::{Context, Result}; use rand::SeedableRng; -use turso::Value as TursoValue; use crate::services::config::{AgentTraceDbRetryConfig, DatabaseRetryConfig}; use crate::services::lifecycle::{ @@ -188,29 +187,24 @@ async fn run_embedded_migrations( Ok(()) } - async fn ensure_migrations_table(conn: &turso::Connection, db_name: &str) -> Result<()> { - async { - conn.execute(MIGRATIONS_TABLE_SQL, ()) - .await - .map_err(|e| anyhow::anyhow!("{db_name} migration metadata setup failed: {e}")) - } - .await?; + conn.execute(MIGRATIONS_TABLE_SQL, ()) + .await + .map_err(|e| anyhow::anyhow!("{db_name} migration metadata setup failed: {e}"))?; Ok(()) } async fn is_migration_applied(conn: &turso::Connection, db_name: &str, id: &str) -> Result { - async { - let mut rows = conn.query(SELECT_MIGRATION_SQL, (id,)).await.map_err(|e| { - anyhow::anyhow!("{db_name} migration metadata query failed for {id}: {e}") - })?; + let mut rows = conn + .query(SELECT_MIGRATION_SQL, (id,)) + .await + .map_err(|e| anyhow::anyhow!("{db_name} migration metadata query failed for {id}: {e}"))?; - rows.next().await.map(|row| row.is_some()).map_err(|e| { - anyhow::anyhow!("{db_name} migration metadata row fetch failed for {id}: {e}") - }) - } - .await + rows.next() + .await + .map(|row| row.is_some()) + .map_err(|e| anyhow::anyhow!("{db_name} migration metadata row fetch failed for {id}: {e}")) } async fn apply_migration( @@ -219,23 +213,18 @@ async fn apply_migration( id: &str, sql: &str, ) -> Result<()> { - async { - // Migration files may contain multiple statements (the repository - // Agent Trace baseline is one multi-statement schema file), so batch - // execution is required; `execute` would stop after the first - // statement. - conn.execute_batch(sql) - .await - .map_err(|e| anyhow::anyhow!("{db_name} migration {id} failed: {e}"))?; - conn.execute(INSERT_MIGRATION_SQL, (id,)) - .await - .map_err(|e| { - anyhow::anyhow!("{db_name} migration metadata record failed for {id}: {e}") - })?; + // Migration files may contain multiple statements (the repository + // Agent Trace baseline is one multi-statement schema file), so batch + // execution is required; `execute` would stop after the first + // statement. + conn.execute_batch(sql) + .await + .map_err(|e| anyhow::anyhow!("{db_name} migration {id} failed: {e}"))?; + conn.execute(INSERT_MIGRATION_SQL, (id,)) + .await + .map_err(|e| anyhow::anyhow!("{db_name} migration metadata record failed for {id}: {e}"))?; - Ok(()) - } - .await + Ok(()) } /// Body of [`TursoDb::execute_transactional_insert_pair_if_absent`], run @@ -293,7 +282,6 @@ async fn execute_insert_pair_if_absent_body( Ok(true) } -#[allow(dead_code)] pub struct TransactionStatement<'a> { sql: &'a str, params: turso::params::Params, @@ -301,7 +289,6 @@ pub struct TransactionStatement<'a> { } impl<'a> TransactionStatement<'a> { - #[allow(dead_code)] pub fn new(sql: &'a str, params: impl turso::params::IntoParams) -> Result { let params = turso::params::IntoParams::into_params(params) .map_err(|e| anyhow::anyhow!("parameter conversion failed: {sql}: {e}"))?; @@ -313,7 +300,6 @@ impl<'a> TransactionStatement<'a> { }) } - #[allow(dead_code)] pub fn expect_rows_affected(mut self, expected: u64) -> Self { self.expected_rows_affected = Some(expected); self @@ -465,13 +451,11 @@ fn classify_turso_error(db_name: &str, action: &str, error: &turso::Error) -> Wr } } -#[allow(dead_code)] enum CasBatchAttemptOutcome { Settled(bool), Deterministic(anyhow::Error), } -#[allow(dead_code)] fn cas_batch_failure_into_attempt_result( failure: WriteAttemptFailure, ) -> Result { @@ -481,7 +465,6 @@ fn cas_batch_failure_into_attempt_result( } } -#[allow(dead_code)] async fn execute_cas_batch_body( tx: &turso::transaction::Transaction<'_>, db_name: &str, @@ -806,15 +789,6 @@ pub struct TursoDb { core: TursoConnectionCore, } -/// Fully fetched SQL query result for deterministic rendering outside the -/// async Turso row iterator lifetime. -#[derive(Clone, Debug, PartialEq)] -#[allow(dead_code)] -pub struct QueryRows { - pub columns: Vec, - pub rows: Vec>, -} - /// Generic encrypted Turso database adapter. /// /// Mirrors the structural seams of [`TursoDb`] while reserving encrypted local @@ -977,96 +951,6 @@ impl TursoDb { .await } - /// Execute a SQL query that returns rows. - /// - /// # Arguments - /// * `sql` - SQL query, which may contain `?` placeholders. - /// * `params` - Parameter values implementing `IntoParams`. - /// - /// # Returns - /// A `turso::Rows` iterator over the result set. - #[allow(dead_code)] - pub async fn query( - &self, - sql: &str, - params: impl turso::params::IntoParams, - ) -> Result { - let params = turso::params::IntoParams::into_params(params).map_err(|e| { - anyhow::anyhow!("{} parameter conversion failed: {sql}: {e}", M::db_name()) - })?; - let operation_name = format!("query {} database", M::db_name()); - - run_with_retry_elapsed( - resolve_query_retry_policy::(), - &operation_name, - QUERY_RETRY_HINT, - async |_| { - async { - self.core - .conn - .query(sql, params.clone()) - .await - .map_err(|e| anyhow::anyhow!("{} query failed: {sql}: {e}", M::db_name())) - } - .await - }, - ) - .await - } - - /// Execute a SQL query and synchronously fetch column names plus raw values. - #[allow(dead_code)] - pub async fn query_values( - &self, - sql: &str, - params: impl turso::params::IntoParams, - ) -> Result { - let params = turso::params::IntoParams::into_params(params).map_err(|e| { - anyhow::anyhow!("{} parameter conversion failed: {sql}: {e}", M::db_name()) - })?; - let operation_name = format!("query and fetch {} database values", M::db_name()); - - run_with_retry_elapsed( - resolve_query_retry_policy::(), - &operation_name, - QUERY_RETRY_HINT, - async |_| { - async { - let mut rows = - self.core - .conn - .query(sql, params.clone()) - .await - .map_err(|e| { - anyhow::anyhow!("{} query failed: {sql}: {e}", M::db_name()) - })?; - let columns = rows.column_names(); - let column_count = rows.column_count(); - let mut fetched_rows = Vec::new(); - - while let Some(row) = rows.next().await.map_err(|e| { - anyhow::anyhow!("{} row fetch failed: {sql}: {e}", M::db_name()) - })? { - let mut values = Vec::with_capacity(column_count); - for column_index in 0..column_count { - values.push(row.get_value(column_index).map_err(|e| { - anyhow::anyhow!("{} value fetch failed: {sql}: {e}", M::db_name()) - })?); - } - fetched_rows.push(values); - } - - Ok(QueryRows { - columns, - rows: fetched_rows, - }) - } - .await - }, - ) - .await - } - /// Run an "insert row pair if absent" write transaction. /// /// If `exists_sql` (bound to `exists_params`) finds a matching row, no @@ -1203,7 +1087,6 @@ impl TursoDb { /// the whole transaction and no other operation on this adapter can run /// inside it. Dropping the future uncommitted schedules a rollback that /// runs on the connection's next use rather than immediately. - #[allow(dead_code)] pub async fn execute_transactional_cas_batch( &mut self, operation_name: &str, @@ -1470,43 +1353,6 @@ impl EncryptedTursoDb { .await } - /// Execute a SQL query that returns rows. - /// - /// # Arguments - /// * `sql` - SQL query, which may contain `?` placeholders. - /// * `params` - Parameter values implementing `IntoParams`. - /// - /// # Returns - /// A `turso::Rows` iterator over the result set. - #[allow(dead_code)] - pub async fn query( - &self, - sql: &str, - params: impl turso::params::IntoParams, - ) -> Result { - let params = turso::params::IntoParams::into_params(params).map_err(|e| { - anyhow::anyhow!("{} parameter conversion failed: {sql}: {e}", M::db_name()) - })?; - let operation_name = format!("query encrypted {} database", M::db_name()); - - run_with_retry_elapsed( - resolve_query_retry_policy::(), - &operation_name, - QUERY_RETRY_HINT, - async |_| { - async { - self.core - .conn - .query(sql, params.clone()) - .await - .map_err(|e| anyhow::anyhow!("{} query failed: {sql}: {e}", M::db_name())) - } - .await - }, - ) - .await - } - /// Execute a SQL query and synchronously map all returned rows. pub async fn query_map( &self, diff --git a/cli/src/services/default_paths.rs b/cli/src/services/default_paths.rs index 7155b947c..86ec44208 100644 --- a/cli/src/services/default_paths.rs +++ b/cli/src/services/default_paths.rs @@ -7,8 +7,11 @@ mod roots { use anyhow::{anyhow, Result}; - #[allow(dead_code)] #[derive(Clone, Copy, Debug, Eq, PartialEq)] + #[allow( + dead_code, + reason = "constructed only under cfg(target_os) on non-Linux platforms" + )] pub(crate) enum PlatformFamily { Linux, Macos, @@ -56,11 +59,6 @@ mod roots { pub(crate) fn state_root(&self) -> &Path { &self.state_root } - - #[allow(dead_code)] - pub(crate) fn cache_root(&self) -> &Path { - &self.cache_root - } } #[derive(Clone, Debug, Eq, PartialEq)] @@ -76,15 +74,6 @@ mod roots { pub(crate) fn global_config_file(&self) -> PathBuf { self.roots.config_root().join("sce").join("config.json") } - - #[allow(dead_code)] - pub(crate) fn persisted_artifact_locations(&self) -> Vec { - vec![super::PersistedArtifactLocation { - id: super::PersistedArtifactId::GlobalConfig, - root_kind: super::PersistedArtifactRootKind::Config, - path: self.global_config_file(), - }] - } } pub(crate) fn resolve_state_data_root() -> Result { @@ -322,62 +311,26 @@ fn sce_state_dir() -> anyhow::Result { .join("sce")) } -#[allow(dead_code)] -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub(crate) enum PersistedArtifactRootKind { - Config, - State, - Cache, -} - -#[allow(dead_code)] -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub(crate) enum PersistedArtifactId { - GlobalConfig, -} - -#[allow(dead_code)] -#[derive(Clone, Debug, Eq, PartialEq)] -pub(crate) struct PersistedArtifactLocation { - pub id: PersistedArtifactId, - pub root_kind: PersistedArtifactRootKind, - pub path: PathBuf, -} - pub(crate) mod repo_dir { pub const SCE: &str = ".sce"; pub const OPENCODE: &str = ".opencode"; pub const CLAUDE: &str = ".claude"; pub const PI: &str = ".pi"; pub const CODEX: &str = ".codex"; - pub const GIT: &str = ".git"; } pub(crate) mod repo_file { pub const SCE_CONFIG: &str = "config.json"; pub const OPENCODE_MANIFEST: &str = "opencode.json"; - pub const GIT_COMMIT_EDITMSG: &str = "COMMIT_EDITMSG"; -} - -pub(crate) mod hook_dir { - pub const HOOKS: &str = "hooks"; - pub const PRE_COMMIT: &str = "pre-commit"; - pub const COMMIT_MSG: &str = "commit-msg"; - pub const POST_COMMIT: &str = "post-commit"; } -#[allow(dead_code)] pub(crate) mod opencode_asset { - pub const OPENCODE_DIR: &str = "opencode"; pub const PLUGINS_DIR: &str = "plugins"; - pub const PLUGIN_FILE: &str = "sce-bash-policy.ts"; - pub const PLUGIN_MANIFEST_ENTRY: &str = "./plugins/sce-bash-policy.ts"; pub const LIB_DIR: &str = "lib"; pub const PRESET_CATALOG: &str = "bash-policy-presets.json"; pub const OPENCODE_AGENT_DIR: &str = "agent"; pub const OPENCODE_COMMAND_DIR: &str = "command"; pub const SKILLS_DIR: &str = "skills"; - pub const AGENTS_DIR: &str = "agents"; } pub(crate) mod claude_asset { @@ -415,7 +368,6 @@ pub(crate) mod context_file { pub const PATTERNS: &str = "patterns.md"; pub const CONTEXT_MAP: &str = "context-map.md"; pub const TMP_GITIGNORE: &str = ".gitignore"; - pub const SKILL_DEFINITION: &str = "SKILL.md"; } pub(crate) mod schema { @@ -428,7 +380,6 @@ pub(crate) struct RepoPaths { root: PathBuf, } -#[allow(dead_code)] impl RepoPaths { pub(crate) fn new(root: impl Into) -> Self { Self { root: root.into() } @@ -462,22 +413,6 @@ impl RepoPaths { self.root.join(repo_dir::CODEX) } - pub(crate) fn git_dir(&self) -> PathBuf { - self.root.join(repo_dir::GIT) - } - - pub(crate) fn git_hooks_dir(&self) -> PathBuf { - self.git_dir().join(hook_dir::HOOKS) - } - - pub(crate) fn git_hook_file(&self, hook_name: &str) -> PathBuf { - self.git_hooks_dir().join(hook_name) - } - - pub(crate) fn git_commit_editmsg(&self) -> PathBuf { - self.git_dir().join(repo_file::GIT_COMMIT_EDITMSG) - } - pub(crate) fn context_dir(&self) -> PathBuf { self.root.join(context_dir::CONTEXT_ROOT) } @@ -528,7 +463,6 @@ pub(crate) struct InstallTargetPaths { repo_root: PathBuf, } -#[allow(dead_code)] impl InstallTargetPaths { pub(crate) fn new(repo_root: impl Into) -> Self { Self { @@ -555,45 +489,9 @@ impl InstallTargetPaths { self.repo_root.clone() } - pub(crate) fn opencode_plugin_target(&self) -> PathBuf { - self.opencode_target_dir() - .join(opencode_asset::PLUGINS_DIR) - .join(opencode_asset::PLUGIN_FILE) - } - pub(crate) fn opencode_preset_catalog_target(&self) -> PathBuf { self.opencode_target_dir() .join(opencode_asset::LIB_DIR) .join(opencode_asset::PRESET_CATALOG) } - - pub(crate) fn skill_tile_relative_path(skill_name: &str) -> String { - format!( - "{}/{}/{}", - opencode_asset::SKILLS_DIR, - skill_name, - context_file::SKILL_DEFINITION - ) - } - - pub(crate) fn pre_commit_hook_path(&self) -> PathBuf { - self.repo_root - .join(repo_dir::GIT) - .join(hook_dir::HOOKS) - .join(hook_dir::PRE_COMMIT) - } - - pub(crate) fn commit_msg_hook_path(&self) -> PathBuf { - self.repo_root - .join(repo_dir::GIT) - .join(hook_dir::HOOKS) - .join(hook_dir::COMMIT_MSG) - } - - pub(crate) fn post_commit_hook_path(&self) -> PathBuf { - self.repo_root - .join(repo_dir::GIT) - .join(hook_dir::HOOKS) - .join(hook_dir::POST_COMMIT) - } } diff --git a/cli/src/services/doctor/inspect.rs b/cli/src/services/doctor/inspect.rs index f4437bafe..ef3de754b 100644 --- a/cli/src/services/doctor/inspect.rs +++ b/cli/src/services/doctor/inspect.rs @@ -50,7 +50,6 @@ pub(super) async fn build_report_with_lifecycle_problems( impl crate::services::capabilities::GitOps, impl Fn() -> anyhow::Result, impl Fn() -> anyhow::Result, - impl Fn(&Path) -> anyhow::Result<()>, impl Fn() -> crate::services::codex_hook_policy::CodexHookPolicyReadiness, >, lifecycle_problems: Vec, @@ -78,7 +77,6 @@ async fn build_report_without_service_owned_problem_checks( impl crate::services::capabilities::GitOps, impl Fn() -> anyhow::Result, impl Fn() -> anyhow::Result, - impl Fn(&Path) -> anyhow::Result<()>, impl Fn() -> crate::services::codex_hook_policy::CodexHookPolicyReadiness, >, mut problems: Vec, @@ -566,7 +564,6 @@ fn collect_global_state_locations( impl crate::services::capabilities::GitOps, impl Fn() -> anyhow::Result, impl Fn() -> anyhow::Result, - impl Fn(&Path) -> anyhow::Result<()>, impl Fn() -> crate::services::codex_hook_policy::CodexHookPolicyReadiness, >, ) -> GlobalStateHealth { @@ -744,81 +741,6 @@ fn hook_managed_block_content_state( } } -#[allow(dead_code)] -fn inspect_repository_hooks( - repository_root: &Path, - git_available: bool, - bare_repository: bool, - detected_repository_root: Option<&Path>, - hooks_directory: Option<&Path>, - problems: &mut Vec, -) -> Vec { - if !git_available { - problems.push(DoctorProblem { - kind: ProblemKind::GitUnavailable, - category: ProblemCategory::RepositoryTargeting, - severity: ProblemSeverity::Error, - fixability: ProblemFixability::ManualOnly, - summary: String::from("Git is not available on this machine."), - remediation: String::from("Install an accessible 'git' binary and ensure it is on PATH before rerunning 'sce doctor'."), - next_action: "manual_steps", - scope: None, - mutation_scope_target: None, - }); - return Vec::new(); - } - - if bare_repository { - problems.push(DoctorProblem { - kind: ProblemKind::BareRepository, - category: ProblemCategory::RepositoryTargeting, - severity: ProblemSeverity::Error, - fixability: ProblemFixability::ManualOnly, - summary: String::from( - "The current repository is bare and does not support local SCE hook rollout.", - ), - remediation: String::from("Run 'sce doctor' from a non-bare working tree clone to inspect repo-scoped SCE hook health."), - next_action: "manual_steps", - scope: None, - mutation_scope_target: None, - }); - return Vec::new(); - } - - if detected_repository_root.is_none() { - problems.push(DoctorProblem { - kind: ProblemKind::NotInsideGitRepository, - category: ProblemCategory::RepositoryTargeting, - severity: ProblemSeverity::Error, - fixability: ProblemFixability::ManualOnly, - summary: String::from("The current directory is not inside a git repository."), - remediation: String::from("Run 'sce doctor' from inside the target repository working tree to inspect repo-scoped SCE hook health."), - next_action: "manual_steps", - scope: None, - mutation_scope_target: None, - }); - return Vec::new(); - } - - if let Some(directory) = hooks_directory { - return collect_hook_health(directory, problems); - } - - let _ = repository_root; - problems.push(DoctorProblem { - kind: ProblemKind::UnableToResolveGitHooksDirectory, - category: ProblemCategory::RepositoryTargeting, - severity: ProblemSeverity::Error, - fixability: ProblemFixability::ManualOnly, - summary: String::from("Unable to resolve git hooks directory."), - remediation: String::from("Verify that git repository inspection succeeds and rerun 'sce doctor' inside a non-bare git repository."), - next_action: "manual_steps", - scope: None, - mutation_scope_target: None, - }); - Vec::new() -} - /// Returns `true` when the doctor was able to check for integration targets /// and found none (neither configured in `.sce/config.json` nor detected /// via repo-root `.opencode/` / `.claude/` / `.pi/` directories). @@ -1109,235 +1031,6 @@ fn repair_merge_target_if_mismatched( ) } -#[allow(dead_code)] -fn collect_global_state_health( - repository_root: &Path, - problems: &mut Vec, - dependencies: &DoctorDependencies< - '_, - impl crate::services::capabilities::GitOps, - impl Fn() -> anyhow::Result, - impl Fn() -> anyhow::Result, - impl Fn(&Path) -> anyhow::Result<()>, - impl Fn() -> crate::services::codex_hook_policy::CodexHookPolicyReadiness, - >, -) -> GlobalStateHealth { - let mut state_root_health = None; - let mut config_locations = Vec::new(); - - match (dependencies.resolve_state_root)() { - Ok(state_root) => { - state_root_health = Some(FileLocationHealth { - label: "State root", - state: if state_root.exists() { "present" } else { "expected" }, - path: state_root.clone(), - }); - } - Err(error) => problems.push(DoctorProblem { - kind: ProblemKind::UnableToResolveStateRoot, - category: ProblemCategory::GlobalState, - severity: ProblemSeverity::Error, - fixability: ProblemFixability::ManualOnly, - summary: format!("Unable to resolve expected state root: {error}"), - remediation: String::from("Verify that the current platform exposes a writable SCE state directory before rerunning 'sce doctor'."), - next_action: "manual_steps", - scope: None, - mutation_scope_target: None, - }), - } - - match (dependencies.resolve_global_config_path)() { - Ok(global_path) => { - if global_path.exists() { - if let Err(error) = (dependencies.validate_config_file)(&global_path) { - problems.push(DoctorProblem { - kind: ProblemKind::GlobalConfigValidationFailed, - category: ProblemCategory::GlobalState, - severity: ProblemSeverity::Error, - fixability: ProblemFixability::ManualOnly, - summary: format!( - "Global config file '{}' failed validation: {error}", - global_path.display() - ), - remediation: format!( - "Repair or remove the invalid global config file at '{}' and rerun 'sce doctor'.", - global_path.display() - ), - next_action: "manual_steps", - scope: None, - mutation_scope_target: None, - }); - } - } - config_locations.push(FileLocationHealth { - label: "Global config", - state: if global_path.exists() { "present" } else { "expected" }, - path: global_path, - }); - } - Err(error) => problems.push(DoctorProblem { - kind: ProblemKind::UnableToResolveGlobalConfigPath, - category: ProblemCategory::GlobalState, - severity: ProblemSeverity::Error, - fixability: ProblemFixability::ManualOnly, - summary: format!("Unable to resolve expected global config path: {error}"), - remediation: String::from("Verify that the current platform exposes a writable SCE config directory before rerunning 'sce doctor'."), - next_action: "manual_steps", - scope: None, - mutation_scope_target: None, - }), - } - - let local_path = RepoPaths::new(repository_root).sce_config_file(); - if local_path.exists() { - if let Err(error) = (dependencies.validate_config_file)(&local_path) { - problems.push(DoctorProblem { - kind: ProblemKind::LocalConfigValidationFailed, - category: ProblemCategory::GlobalState, - severity: ProblemSeverity::Error, - fixability: ProblemFixability::ManualOnly, - summary: format!( - "Local config file '{}' failed validation: {error}", - local_path.display() - ), - remediation: format!( - "Repair or remove the invalid local config file at '{}' and rerun 'sce doctor'.", - local_path.display() - ), - next_action: "manual_steps", - scope: None, - mutation_scope_target: None, - }); - } - } - config_locations.push(FileLocationHealth { - label: "Local config", - state: if local_path.exists() { - "present" - } else { - "expected" - }, - path: local_path, - }); - - GlobalStateHealth { - state_root: state_root_health, - config_locations, - } -} - -#[allow(dead_code)] -#[allow(clippy::too_many_lines)] -fn collect_hook_health(directory: &Path, problems: &mut Vec) -> Vec { - if !directory.exists() { - problems.push(DoctorProblem { - kind: ProblemKind::HooksDirectoryMissing, - category: ProblemCategory::HookRollout, - severity: ProblemSeverity::Error, - fixability: ProblemFixability::AutoFixable, - summary: format!("Hooks directory '{}' does not exist.", directory.display()), - remediation: format!( - "Run 'sce doctor --fix' to install the canonical SCE-managed hooks into '{}', or run 'sce setup --hooks' directly.", - directory.display() - ), - next_action: "doctor_fix", - scope: None, - mutation_scope_target: None, - }); - } else if !directory.is_dir() { - problems.push(DoctorProblem { - kind: ProblemKind::HooksPathNotDirectory, - category: ProblemCategory::HookRollout, - severity: ProblemSeverity::Error, - fixability: ProblemFixability::ManualOnly, - summary: format!("Hooks path '{}' is not a directory.", directory.display()), - remediation: format!( - "Replace '{}' with a writable hooks directory, then rerun 'sce doctor' or 'sce setup --hooks'.", - directory.display() - ), - next_action: "manual_steps", - scope: None, - mutation_scope_target: None, - }); - } - - REQUIRED_HOOKS - .iter() - .map(|hook_name| { - let hook_path = directory.join(hook_name); - let metadata = fs::metadata(&hook_path).ok(); - let exists = metadata.is_some(); - let executable = metadata - .as_ref() - .is_some_and(|entry| entry.is_file() && is_executable(entry)); - let content_state = inspect_hook_content_state(hook_name, &hook_path, exists, problems); - - if !exists { - problems.push(DoctorProblem { - kind: ProblemKind::RequiredHookMissing, - category: ProblemCategory::HookRollout, - severity: ProblemSeverity::Error, - fixability: ProblemFixability::AutoFixable, - summary: format!( - "Missing required hook '{}' at '{}'.", - hook_name, - hook_path.display() - ), - remediation: format!( - "Run 'sce doctor --fix' to install the canonical '{hook_name}' hook, or run 'sce setup --hooks' directly." - ), - next_action: "doctor_fix", - scope: None, - mutation_scope_target: None, - }); - } else if !executable { - problems.push(DoctorProblem { - kind: ProblemKind::HookNotExecutable, - category: ProblemCategory::HookRollout, - severity: ProblemSeverity::Error, - fixability: ProblemFixability::AutoFixable, - summary: format!("Hook '{hook_name}' exists but is not executable."), - remediation: format!( - "Run 'sce doctor --fix' to restore the canonical executable hook, or run 'sce setup --hooks' / 'chmod +x {}' manually.", - hook_path.display() - ), - next_action: "doctor_fix", - scope: None, - mutation_scope_target: None, - }); - } - - if content_state == HookContentState::Stale { - problems.push(DoctorProblem { - kind: ProblemKind::HookContentStale, - category: ProblemCategory::HookRollout, - severity: ProblemSeverity::Error, - fixability: ProblemFixability::AutoFixable, - summary: format!( - "Hook '{}' at '{}' differs from the canonical SCE-managed content.", - hook_name, - hook_path.display() - ), - remediation: format!( - "Run 'sce doctor --fix' to reinstall the canonical '{hook_name}' hook content, or run 'sce setup --hooks' directly." - ), - next_action: "doctor_fix", - scope: None, - mutation_scope_target: None, - }); - } - - HookFileHealth { - name: hook_name, - path: hook_path, - exists, - executable, - content_state, - } - }) - .collect() -} - fn inspect_opencode_integration_health( repository_root: &Path, integration_groups: &[IntegrationGroupHealth], @@ -2708,49 +2401,6 @@ fn path_is_file(path: &Path) -> bool { fs::metadata(path).is_ok_and(|metadata| metadata.is_file()) } -#[allow(dead_code)] -fn inspect_hook_content_state( - hook_name: &str, - hook_path: &Path, - exists: bool, - problems: &mut Vec, -) -> HookContentState { - if !exists { - return HookContentState::Missing; - } - - let Some(expected_hook) = - iter_required_hook_assets().find(|asset| asset.relative_path == hook_name) - else { - return HookContentState::Unknown; - }; - - match fs::read(hook_path) { - Ok(bytes) => hook_managed_block_content_state(hook_name, &bytes, expected_hook.bytes), - Err(error) => { - problems.push(DoctorProblem { - kind: ProblemKind::HookReadFailed, - category: ProblemCategory::FilesystemPermissions, - severity: ProblemSeverity::Error, - fixability: ProblemFixability::ManualOnly, - summary: format!( - "Unable to read hook '{}' at '{}': {error}", - hook_name, - hook_path.display() - ), - remediation: format!( - "Verify that '{}' is readable before rerunning 'sce doctor'.", - hook_path.display() - ), - next_action: "manual_steps", - scope: None, - mutation_scope_target: None, - }); - HookContentState::Unknown - } - } -} - fn doctor_git_output(git: &impl GitOps, repository_root: &Path, args: &[&str]) -> Option { let output = git.run_command(repository_root, args).ok()?; let trimmed = output.trim(); diff --git a/cli/src/services/doctor/mod.rs b/cli/src/services/doctor/mod.rs index 450974ad8..e55670f30 100644 --- a/cli/src/services/doctor/mod.rs +++ b/cli/src/services/doctor/mod.rs @@ -50,11 +50,10 @@ pub struct DoctorRequest { pub format: DoctorFormat, } -struct DoctorDependencies<'a, G, S, C, V, P> { +struct DoctorDependencies<'a, G, S, C, P> { git: &'a G, resolve_state_root: &'a S, resolve_global_config_path: &'a C, - validate_config_file: &'a V, probe_codex_hook_policy: &'a P, } @@ -111,7 +110,6 @@ async fn execute_doctor_with_context( resolve_global_config_path: &|| { Ok(resolve_sce_default_locations()?.global_config_file()) }, - validate_config_file: &crate::services::config::validate_config_file, probe_codex_hook_policy: &codex_hook_policy::probe_default, }, mutation_scope_seam, @@ -128,7 +126,6 @@ async fn execute_doctor_with_lifecycle_providers( impl crate::services::capabilities::GitOps, impl Fn() -> Result, impl Fn() -> Result, - impl Fn(&Path) -> Result<()>, impl Fn() -> crate::services::codex_hook_policy::CodexHookPolicyReadiness, >, mutation_scope_seam: &impl std::ops::AsyncFn( diff --git a/cli/src/services/doctor/types.rs b/cli/src/services/doctor/types.rs index 317b2e2f9..f03489545 100644 --- a/cli/src/services/doctor/types.rs +++ b/cli/src/services/doctor/types.rs @@ -179,31 +179,6 @@ impl IntegrationGroupHealth { pub(super) const fn display_label(&self) -> &'static str { self.key.display_label() } - - #[allow(dead_code)] - pub(super) fn display_node(&self) -> DoctorDisplayNode { - let children = self - .children - .iter() - .map(IntegrationChildHealth::display_node) - .collect::>(); - let status = children - .iter() - .fold(DoctorDisplayStatus::Pass, |status, child| { - status.worst(if child.status == DoctorDisplayStatus::Miss { - DoctorDisplayStatus::Fail - } else { - child.status - }) - }); - DoctorDisplayNode::branch_with_status( - DoctorDisplayNodeKind::Group, - self.display_label(), - status, - Vec::new(), - children, - ) - } } #[derive(Clone, Debug, Eq, PartialEq)] @@ -247,7 +222,6 @@ pub(super) enum IntegrationContentState { } #[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] -#[allow(dead_code)] pub(super) enum DoctorDisplayStatus { Pass, Warn, @@ -256,7 +230,6 @@ pub(super) enum DoctorDisplayStatus { } impl DoctorDisplayStatus { - #[allow(dead_code)] pub(super) const fn worst(self, other: Self) -> Self { match (self, other) { (Self::Fail, _) | (_, Self::Fail) => Self::Fail, @@ -268,7 +241,6 @@ impl DoctorDisplayStatus { } #[derive(Clone, Debug, Eq, PartialEq)] -#[allow(dead_code)] pub(super) enum DoctorDisplayDetail { MissingPath(PathBuf), ContentMismatch { @@ -309,7 +281,6 @@ pub(super) enum DoctorDisplayDetail { } #[derive(Clone, Copy, Debug, Eq, PartialEq)] -#[allow(dead_code)] pub(super) enum DoctorDisplayNodeKind { Domain, Group, @@ -317,7 +288,6 @@ pub(super) enum DoctorDisplayNodeKind { } #[derive(Clone, Debug, Eq, PartialEq)] -#[allow(dead_code)] pub(super) struct DoctorDisplayNode { pub(super) kind: DoctorDisplayNodeKind, pub(super) label: String, @@ -327,17 +297,6 @@ pub(super) struct DoctorDisplayNode { } impl DoctorDisplayNode { - #[allow(dead_code)] - pub(super) fn domain(label: impl Into, children: Vec) -> Self { - Self::branch(DoctorDisplayNodeKind::Domain, label, children) - } - - #[allow(dead_code)] - pub(super) fn group(label: impl Into, children: Vec) -> Self { - Self::branch(DoctorDisplayNodeKind::Group, label, children) - } - - #[allow(dead_code)] fn asset( label: impl Into, status: DoctorDisplayStatus, @@ -352,11 +311,6 @@ impl DoctorDisplayNode { } } - #[allow(dead_code)] - fn branch(kind: DoctorDisplayNodeKind, label: impl Into, children: Vec) -> Self { - Self::branch_with_status(kind, label, DoctorDisplayStatus::Pass, Vec::new(), children) - } - pub(super) fn branch_with_status( kind: DoctorDisplayNodeKind, label: impl Into, @@ -388,7 +342,6 @@ impl DoctorDisplayNode { } impl IntegrationChildHealth { - #[allow(dead_code)] pub(super) fn display_node(&self) -> DoctorDisplayNode { let (status, detail) = match &self.content_state { IntegrationContentState::Match => (DoctorDisplayStatus::Pass, None), diff --git a/cli/src/services/error.rs b/cli/src/services/error.rs index e97579828..a99303543 100644 --- a/cli/src/services/error.rs +++ b/cli/src/services/error.rs @@ -71,7 +71,6 @@ impl UserError { } } - #[allow(dead_code)] pub fn key(&self) -> &'static str { match self { Self::NotAuthenticated => "auth.not_authenticated", @@ -114,7 +113,6 @@ impl std::fmt::Display for UserError { /// observability and diagnostic rendering. #[derive(Debug)] pub enum CliError { - #[allow(dead_code)] User { error: UserError, source: Option, @@ -126,15 +124,6 @@ pub enum CliError { } impl CliError { - #[allow(dead_code)] - pub fn user(error: UserError) -> Self { - Self::User { - error, - source: None, - } - } - - #[allow(dead_code)] pub fn user_with_source(error: UserError, source: impl Into) -> Self { Self::User { error, @@ -206,7 +195,10 @@ mod tests { #[test] fn user_error_not_authenticated_classifies_as_runtime() { - let error = CliError::user(UserError::NotAuthenticated); + let error = CliError::User { + error: UserError::NotAuthenticated, + source: None, + }; assert_eq!(error.class(), FailureClass::Runtime); assert_eq!(error.code(), "SCE-ERR-RUNTIME"); @@ -216,7 +208,10 @@ mod tests { #[test] fn not_git_repository_has_stable_runtime_catalog_mapping() { - let error = CliError::user(UserError::NotGitRepository); + let error = CliError::User { + error: UserError::NotGitRepository, + source: None, + }; assert_eq!(error.class(), FailureClass::Runtime); assert_eq!(error.code(), "SCE-ERR-RUNTIME"); @@ -229,7 +224,10 @@ mod tests { #[test] fn unexpected_failure_has_stable_runtime_catalog_mapping() { - let error = CliError::user(UserError::UnexpectedFailure); + let error = CliError::User { + error: UserError::UnexpectedFailure, + source: None, + }; assert_eq!(error.class(), FailureClass::Runtime); assert_eq!(error.code(), "SCE-ERR-RUNTIME"); diff --git a/cli/src/services/hooks/claude_mutation_scope/events.rs b/cli/src/services/hooks/claude_mutation_scope/events.rs index 226b8cd2e..1191fa1ca 100644 --- a/cli/src/services/hooks/claude_mutation_scope/events.rs +++ b/cli/src/services/hooks/claude_mutation_scope/events.rs @@ -59,10 +59,6 @@ impl ClaudeToolIdentity { tool_use_id: self.tool_use_id.clone(), } } - - pub(crate) fn is_subagent(&self) -> bool { - self.agent_id.is_some() - } } #[derive(Clone, Debug, Eq, PartialEq)] diff --git a/cli/src/services/hooks/claude_mutation_scope/mod.rs b/cli/src/services/hooks/claude_mutation_scope/mod.rs index 75fecddcd..3a93ed92d 100644 --- a/cli/src/services/hooks/claude_mutation_scope/mod.rs +++ b/cli/src/services/hooks/claude_mutation_scope/mod.rs @@ -1,5 +1,3 @@ -#![allow(dead_code)] - mod events; pub(crate) mod health; mod lifecycle; diff --git a/cli/src/services/hooks/claude_mutation_scope/state.rs b/cli/src/services/hooks/claude_mutation_scope/state.rs index 67cb07919..9cf2cfd8b 100644 --- a/cli/src/services/hooks/claude_mutation_scope/state.rs +++ b/cli/src/services/hooks/claude_mutation_scope/state.rs @@ -348,14 +348,6 @@ pub(crate) async fn remove_attempt(git_dir: &Path, scope_id: &str) -> Result<()> .await } -pub(crate) async fn mark_recovery_pending(git_dir: &Path) -> Result<()> { - with_locked_state(git_dir, |state| { - state.recovery_pending = true; - Ok(StateTransaction::Persist(())) - }) - .await -} - #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub(crate) enum ClearRecoveryOutcome { Cleared, diff --git a/cli/src/services/hooks/codex/apply_patch/normalize.rs b/cli/src/services/hooks/codex/apply_patch/normalize.rs index 20193640a..3a91288e8 100644 --- a/cli/src/services/hooks/codex/apply_patch/normalize.rs +++ b/cli/src/services/hooks/codex/apply_patch/normalize.rs @@ -29,7 +29,6 @@ fn normalize_error(message: impl Into) -> CodexPatchNormalizeError { } } -#[allow(dead_code)] pub(crate) fn normalize_codex_patch( patch: &CodexPatch, tool_use_id: &str, diff --git a/cli/src/services/hooks/codex/apply_patch/parser.rs b/cli/src/services/hooks/codex/apply_patch/parser.rs index 08e6d4221..46a924032 100644 --- a/cli/src/services/hooks/codex/apply_patch/parser.rs +++ b/cli/src/services/hooks/codex/apply_patch/parser.rs @@ -9,13 +9,11 @@ const END_OF_FILE_MARKER: &str = "*** End of File"; const CHANGE_CONTEXT_MARKER: &str = "@@"; const CHANGE_CONTEXT_MARKER_WITH_TEXT: &str = "@@ "; -#[allow(dead_code)] #[derive(Clone, Debug, Eq, PartialEq)] pub(crate) struct CodexPatch { pub(crate) operations: Vec, } -#[allow(dead_code)] #[derive(Clone, Debug, Eq, PartialEq)] pub(crate) enum CodexFileOperation { Add { @@ -32,7 +30,6 @@ pub(crate) enum CodexFileOperation { }, } -#[allow(dead_code)] #[derive(Clone, Debug, Eq, PartialEq)] pub(crate) struct CodexHunk { pub(crate) context: Option, @@ -40,7 +37,6 @@ pub(crate) struct CodexHunk { pub(crate) is_end_of_file: bool, } -#[allow(dead_code)] #[derive(Clone, Debug, Eq, PartialEq)] pub(crate) enum CodexHunkLine { Context(String), @@ -48,7 +44,6 @@ pub(crate) enum CodexHunkLine { Removed(String), } -#[allow(dead_code)] #[derive(Clone, Debug, Eq, PartialEq)] pub(crate) struct CodexPatchParseError { pub(crate) message: String, @@ -68,7 +63,6 @@ fn error(message: impl Into) -> CodexPatchParseError { } } -#[allow(dead_code)] pub(crate) fn normalize_outer_apply_patch_input(raw: &str) -> Result { let trimmed = raw.trim(); let lines: Vec<&str> = trimmed.lines().collect(); @@ -106,7 +100,6 @@ fn has_canonical_boundaries(lines: &[&str]) -> bool { && lines.last().map(|line| line.trim()) == Some(END_PATCH_MARKER) } -#[allow(dead_code)] pub(crate) fn parse_codex_apply_patch(raw: &str) -> Result { let trimmed = raw.trim(); let lines: Vec<&str> = trimmed.lines().collect(); diff --git a/cli/src/services/hooks/codex/apply_patch/path.rs b/cli/src/services/hooks/codex/apply_patch/path.rs index 9b6402f32..410ed5816 100644 --- a/cli/src/services/hooks/codex/apply_patch/path.rs +++ b/cli/src/services/hooks/codex/apply_patch/path.rs @@ -32,17 +32,6 @@ pub(crate) fn resolve_codex_patch_paths( Ok(()) } -#[allow(dead_code)] -pub(crate) fn resolve_codex_patch_path( - repository_root: &Path, - event_cwd: &str, - codex_path: &str, -) -> Result { - let git_root = resolve_git_root(repository_root)?; - let event_cwd = resolve_event_cwd(&git_root, event_cwd)?; - resolve_path_from_cwd(&git_root, &event_cwd, codex_path) -} - fn resolve_git_root(repository_root: &Path) -> Result { let output = Command::new("git") .args(["rev-parse", "--show-toplevel"]) diff --git a/cli/src/services/hooks/codex/mod.rs b/cli/src/services/hooks/codex/mod.rs index 2e6181605..3f9d5b9a3 100644 --- a/cli/src/services/hooks/codex/mod.rs +++ b/cli/src/services/hooks/codex/mod.rs @@ -42,7 +42,6 @@ where } #[derive(Debug, Deserialize)] -#[allow(dead_code)] pub(crate) struct CodexHookEvent { pub(crate) hook_event_name: String, #[serde(default)] @@ -60,8 +59,6 @@ pub(crate) struct CodexHookEvent { #[serde(default)] pub(crate) tool_input: Option, #[serde(default)] - pub(crate) tool_response: Option, - #[serde(default)] pub(crate) prompt: Option, #[serde(default, deserialize_with = "deserialize_nullable_field")] pub(crate) last_assistant_message: NullableField, diff --git a/cli/src/services/hooks/codex_mutation_scope/events.rs b/cli/src/services/hooks/codex_mutation_scope/events.rs index c83864032..80dccd113 100644 --- a/cli/src/services/hooks/codex_mutation_scope/events.rs +++ b/cli/src/services/hooks/codex_mutation_scope/events.rs @@ -50,10 +50,6 @@ impl CodexToolIdentity { tool_use_id: self.tool_use_id.clone(), } } - - pub(crate) fn is_subagent(&self) -> bool { - self.agent_id.is_some() - } } #[derive(Clone, Debug, Eq, PartialEq)] @@ -104,12 +100,6 @@ pub(crate) enum ToolClassification { pub(super) const TRACKED_MUTATION_TOOL_NAMES: &[&str] = &["Bash", "apply_patch"]; pub(super) const DELEGATION_TOOL_NAMES: &[&str] = &["collaborationspawn_agent", "collaborationwait_agent"]; -pub(super) const MCP_TOOL_NAME_PREFIX: &str = "mcp__"; - -pub(crate) fn is_mcp_tool_name(tool_name: &str) -> bool { - tool_name.starts_with(MCP_TOOL_NAME_PREFIX) -} - pub(crate) fn classify_tool(tool_name: &str) -> ToolClassification { if TRACKED_MUTATION_TOOL_NAMES.contains(&tool_name) { ToolClassification::TrackedMutation diff --git a/cli/src/services/hooks/codex_mutation_scope/mod.rs b/cli/src/services/hooks/codex_mutation_scope/mod.rs index 003de8242..e29e38078 100644 --- a/cli/src/services/hooks/codex_mutation_scope/mod.rs +++ b/cli/src/services/hooks/codex_mutation_scope/mod.rs @@ -1,5 +1,3 @@ -#![allow(dead_code)] - mod events; pub(crate) mod health; mod lifecycle; @@ -25,9 +23,8 @@ use serde_json::{json, Map, Value}; #[allow(unused_imports)] pub(crate) use events::{ classify_tool, codex_scope_close_event_id, codex_scope_start_event_id, format_codex_scope_id, - is_mcp_tool_name, parse_codex_hook_event, AttemptKey, CodexAgentIdentity, CodexHookEvent, - CodexSessionIdentity, CodexToolExecution, CodexToolIdentity, CodexTurnIdentity, - ToolClassification, + parse_codex_hook_event, AttemptKey, CodexAgentIdentity, CodexHookEvent, CodexSessionIdentity, + CodexToolExecution, CodexToolIdentity, CodexTurnIdentity, ToolClassification, }; #[allow(unused_imports)] use events::{ diff --git a/cli/src/services/hooks/codex_mutation_scope/state.rs b/cli/src/services/hooks/codex_mutation_scope/state.rs index 950b52024..bbd73b521 100644 --- a/cli/src/services/hooks/codex_mutation_scope/state.rs +++ b/cli/src/services/hooks/codex_mutation_scope/state.rs @@ -39,12 +39,6 @@ pub(crate) enum RecoveryState { }, } -impl RecoveryState { - pub(crate) fn is_clear(&self) -> bool { - matches!(self, RecoveryState::Clear) - } -} - #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] pub(crate) struct AdapterAttempt { pub attempt_seq: u64, diff --git a/cli/src/services/hooks/lifecycle.rs b/cli/src/services/hooks/lifecycle.rs index e76744cf5..437d4c6e3 100644 --- a/cli/src/services/hooks/lifecycle.rs +++ b/cli/src/services/hooks/lifecycle.rs @@ -7,8 +7,8 @@ use anyhow::{anyhow, Context, Result}; use crate::app::HasRepoRoot; use crate::services::lifecycle::{ FixOutcome, FixResultRecord, HealthCategory, HealthFixability, HealthProblem, - HealthProblemKind, HealthSeverity, LifecycleProviderId, RequiredHookInstallStatus, - RequiredHooksInstallOutcome, ServiceLifecycle, SetupOutcome, + HealthProblemKind, HealthSeverity, RequiredHookInstallStatus, RequiredHooksInstallOutcome, + ServiceLifecycle, SetupOutcome, }; use crate::services::setup::{ hook_merge, install_required_git_hooks, iter_required_hook_assets, @@ -28,10 +28,6 @@ enum HookContentState { } impl ServiceLifecycle for HooksLifecycle { - fn id(&self) -> LifecycleProviderId { - LifecycleProviderId::Hooks - } - async fn diagnose(&self, ctx: &C) -> Vec { let repository_root = match ctx.repo_root() { Some(path) => path.to_path_buf(), diff --git a/cli/src/services/hooks/mutation_scope_health.rs b/cli/src/services/hooks/mutation_scope_health.rs index 122628d98..6ea7e77cd 100644 --- a/cli/src/services/hooks/mutation_scope_health.rs +++ b/cli/src/services/hooks/mutation_scope_health.rs @@ -1,7 +1,6 @@ use crate::services::mutation_trace::types::ActorKind; #[derive(Clone, Copy, Debug, Eq, PartialEq)] -#[allow(dead_code)] pub(crate) enum MutationScopeHealthStatus { Healthy, Recovering, @@ -10,14 +9,12 @@ pub(crate) enum MutationScopeHealthStatus { } #[derive(Clone, Copy, Debug, Eq, PartialEq)] -#[allow(dead_code)] pub(crate) enum Repairability { AutoFixable, ManualOnly, } #[derive(Clone, Debug, Eq, PartialEq)] -#[allow(dead_code)] pub(crate) struct MutationScopeAdapterHealth { pub(crate) adapter: ActorKind, pub(crate) status: MutationScopeHealthStatus, @@ -25,7 +22,6 @@ pub(crate) struct MutationScopeAdapterHealth { pub(crate) detail: Option, } -#[allow(dead_code)] impl MutationScopeAdapterHealth { pub(crate) fn new( adapter: ActorKind, diff --git a/cli/src/services/hooks/opencode_mutation_scope/mod.rs b/cli/src/services/hooks/opencode_mutation_scope/mod.rs index b7ef588b8..c5c5160e3 100644 --- a/cli/src/services/hooks/opencode_mutation_scope/mod.rs +++ b/cli/src/services/hooks/opencode_mutation_scope/mod.rs @@ -1,5 +1,3 @@ -#![allow(dead_code)] - mod events; pub(crate) mod health; mod lifecycle; diff --git a/cli/src/services/hooks/opencode_mutation_scope/state.rs b/cli/src/services/hooks/opencode_mutation_scope/state.rs index 722bf91a0..e19403105 100644 --- a/cli/src/services/hooks/opencode_mutation_scope/state.rs +++ b/cli/src/services/hooks/opencode_mutation_scope/state.rs @@ -43,12 +43,6 @@ pub(crate) enum RecoveryState { }, } -impl RecoveryState { - pub(crate) fn is_clear(&self) -> bool { - matches!(self, RecoveryState::Clear) - } -} - #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] pub(crate) struct AdapterAttempt { pub scope_id: String, @@ -366,22 +360,6 @@ pub(crate) async fn normalize_recovery_after_boundary_lock_acquired(git_dir: &Pa .await } -pub(crate) async fn arm_recovery(git_dir: &Path) -> Result { - with_locked_state(git_dir, |state| { - let generation = match state.recovery { - RecoveryState::Pending { generation } => generation, - RecoveryState::Clear | RecoveryState::Flushing { .. } => { - let generation = state.next_recovery_generation; - state.next_recovery_generation += 1; - generation - } - }; - state.recovery = RecoveryState::Pending { generation }; - Ok(StateTransaction::Persist(generation)) - }) - .await -} - fn transition_to_pending_abandon_and_arm_flush( state: &mut AdapterState, scope_ids: &[String], @@ -451,24 +429,6 @@ pub(crate) async fn reprove_dead_owner_pending_start_and_begin_repair( .await } -pub(crate) async fn arm_and_begin_recovery_flush(git_dir: &Path) -> Result { - with_locked_state(git_dir, |state| { - let generation = match state.recovery { - RecoveryState::Pending { generation } | RecoveryState::Flushing { generation } => { - generation - } - RecoveryState::Clear => { - let generation = state.next_recovery_generation; - state.next_recovery_generation += 1; - generation - } - }; - state.recovery = RecoveryState::Flushing { generation }; - Ok(StateTransaction::Persist(generation)) - }) - .await -} - pub(crate) async fn complete_recovery_flush( git_dir: &Path, generation: u64, diff --git a/cli/src/services/hooks/pi_mutation_scope/mod.rs b/cli/src/services/hooks/pi_mutation_scope/mod.rs index aee856725..3ee6f8559 100644 --- a/cli/src/services/hooks/pi_mutation_scope/mod.rs +++ b/cli/src/services/hooks/pi_mutation_scope/mod.rs @@ -1,5 +1,3 @@ -#![allow(dead_code)] - mod events; pub(crate) mod health; mod lifecycle; diff --git a/cli/src/services/hooks/pi_mutation_scope/state.rs b/cli/src/services/hooks/pi_mutation_scope/state.rs index db4d19fc2..05759bc45 100644 --- a/cli/src/services/hooks/pi_mutation_scope/state.rs +++ b/cli/src/services/hooks/pi_mutation_scope/state.rs @@ -43,12 +43,6 @@ pub(crate) enum RecoveryState { }, } -impl RecoveryState { - pub(crate) fn is_clear(&self) -> bool { - matches!(self, RecoveryState::Clear) - } -} - #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] pub(crate) struct AdapterAttempt { pub attempt_seq: u64, diff --git a/cli/src/services/lifecycle.rs b/cli/src/services/lifecycle.rs index 027f5ecc9..a343f1b2c 100644 --- a/cli/src/services/lifecycle.rs +++ b/cli/src/services/lifecycle.rs @@ -132,9 +132,6 @@ pub struct SetupOutcome { } pub(crate) trait ServiceLifecycle: Send + Sync { - #[allow(dead_code)] - fn id(&self) -> LifecycleProviderId; - async fn diagnose(&self, _ctx: &C) -> Vec { Vec::new() } diff --git a/cli/src/services/local_db/lifecycle.rs b/cli/src/services/local_db/lifecycle.rs index 27893d4bc..329e661ef 100644 --- a/cli/src/services/local_db/lifecycle.rs +++ b/cli/src/services/local_db/lifecycle.rs @@ -5,7 +5,7 @@ use crate::services::db::{bootstrap_db_parent, collect_db_path_health, DbSpec}; use crate::services::default_paths::local_db_path; use crate::services::lifecycle::{ FixOutcome, FixResultRecord, HealthCategory, HealthFixability, HealthProblem, - HealthProblemKind, HealthSeverity, LifecycleProviderId, ServiceLifecycle, SetupOutcome, + HealthProblemKind, HealthSeverity, ServiceLifecycle, SetupOutcome, }; use super::{LocalDb, LocalDbSpec}; @@ -14,10 +14,6 @@ use super::{LocalDb, LocalDbSpec}; pub struct LocalDbLifecycle; impl ServiceLifecycle for LocalDbLifecycle { - fn id(&self) -> LifecycleProviderId { - LifecycleProviderId::LocalDb - } - async fn diagnose(&self, _ctx: &C) -> Vec { diagnose_local_db_health() } diff --git a/cli/src/services/mod.rs b/cli/src/services/mod.rs index 1f7690f8e..899ef6907 100644 --- a/cli/src/services/mod.rs +++ b/cli/src/services/mod.rs @@ -1,16 +1,12 @@ pub mod agent_trace; pub mod agent_trace_db; -#[allow(dead_code)] pub mod agent_trace_export; -#[allow(dead_code)] pub mod agent_trace_storage; -#[allow(dead_code)] pub mod agent_trace_sync; pub mod app_support; pub mod auth; pub mod auth_command; pub mod auth_db; -#[allow(dead_code)] pub mod bash_policy; pub mod capabilities; pub(crate) mod codex_hook_config; @@ -27,13 +23,11 @@ pub mod help; pub mod hooks; pub mod lifecycle; pub mod local_db; -#[allow(dead_code)] pub mod mutation_trace; pub mod observability; pub mod output_format; pub mod parse; pub mod patch; -#[allow(dead_code)] pub mod repository_identity; pub mod resilience; pub mod security; diff --git a/cli/src/services/mutation_trace/lineage.rs b/cli/src/services/mutation_trace/lineage.rs index 74e69527a..4f28203ed 100644 --- a/cli/src/services/mutation_trace/lineage.rs +++ b/cli/src/services/mutation_trace/lineage.rs @@ -72,10 +72,6 @@ impl MutationLineage { } } - pub fn reset_file(&mut self, path: &str, content: Option<&str>) { - self.files.insert(path.to_owned(), unknown_lines(content)); - } - pub fn reset_all(&mut self, files: &BTreeMap>) { self.files = files .iter() @@ -83,10 +79,6 @@ impl MutationLineage { .collect(); } - pub fn tracked_paths(&self) -> impl Iterator { - self.files.keys() - } - pub fn apply( &mut self, patch: &ParsedPatch, diff --git a/cli/src/services/mutation_trace/lineage/tests.rs b/cli/src/services/mutation_trace/lineage/tests.rs index d2f0f093a..4a49dd069 100644 --- a/cli/src/services/mutation_trace/lineage/tests.rs +++ b/cli/src/services/mutation_trace/lineage/tests.rs @@ -57,7 +57,6 @@ fn a_removed_line_loses_its_provenance_permanently() { lineage.provenance_at("f.rs", 2, "foo"), LineProvenance::Unknown ); - assert_eq!(lineage.tracked_paths().count(), 1); } #[test] @@ -161,31 +160,3 @@ fn duplicate_lines_do_not_let_provenance_jump_between_occurrences() { LineProvenance::Unknown ); } - -#[test] -fn a_deleted_file_drops_out_of_the_lineage() { - let mut lineage = baseline(&[("f.rs", Some("a\nb\n"))]); - lineage - .apply( - &diff("diff --git a/f.rs b/f.rs\ndeleted file mode 100644\n--- a/f.rs\n+++ /dev/null\n@@ -1,2 +0,0 @@\n-a\n-b\n"), - &TransitionOrigin::MutationNonAi, - ) - .expect("delete"); - assert_eq!(lineage.tracked_paths().count(), 0); -} - -#[test] -fn reset_file_returns_a_file_to_a_conservative_baseline() { - let mut lineage = baseline(&[("f.rs", Some("a\n"))]); - lineage - .apply( - &diff("diff --git a/f.rs b/f.rs\n--- a/f.rs\n+++ b/f.rs\n@@ -1,1 +1,2 @@\n a\n+foo\n"), - &TransitionOrigin::MutationAi(scope("ai")), - ) - .expect("add"); - lineage.reset_file("f.rs", Some("a\nfoo\n")); - assert_eq!( - lineage.provenance_at("f.rs", 2, "foo"), - LineProvenance::Unknown - ); -} diff --git a/cli/src/services/mutation_trace/mbt/model.rs b/cli/src/services/mutation_trace/mbt/model.rs index c257e4088..4876c3451 100644 --- a/cli/src/services/mutation_trace/mbt/model.rs +++ b/cli/src/services/mutation_trace/mbt/model.rs @@ -443,6 +443,10 @@ pub struct ModelState { /// variables are still accepted, but only via an explicit [`IgnoredAny`] /// field — modeling a new Quint variable here as `IgnoredAny` is a /// deliberate "not semantic state" decision, not an oversight. +#[allow( + dead_code, + reason = "verification-only Quint variables are deserialized to satisfy deny_unknown_fields and never read" +)] #[derive(Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] struct WireModelState { diff --git a/cli/src/services/mutation_trace/runtime/coordinator.rs b/cli/src/services/mutation_trace/runtime/coordinator.rs index ddfe688c4..b6d5306e7 100644 --- a/cli/src/services/mutation_trace/runtime/coordinator.rs +++ b/cli/src/services/mutation_trace/runtime/coordinator.rs @@ -50,10 +50,30 @@ pub enum RuntimeBoundary { #[derive(Debug)] pub struct CoordinateOutcome { + #[allow( + dead_code, + reason = "outcome payload carried but never read; suspected defect recorded in context/plans/cli-async-persistence-cleanup-pr3.md" + )] pub worktree_id: WorktreeId, + #[allow( + dead_code, + reason = "outcome payload carried but never read; suspected defect recorded in context/plans/cli-async-persistence-cleanup-pr3.md" + )] pub observed_tree: TreeId, + #[allow( + dead_code, + reason = "outcome payload carried but never read; suspected defect recorded in context/plans/cli-async-persistence-cleanup-pr3.md" + )] pub revision: u64, + #[allow( + dead_code, + reason = "outcome payload carried but never read; suspected defect recorded in context/plans/cli-async-persistence-cleanup-pr3.md" + )] pub evaluation: protocol::CommitEvaluation, + #[allow( + dead_code, + reason = "outcome payload carried but never read; suspected defect recorded in context/plans/cli-async-persistence-cleanup-pr3.md" + )] pub mutation_event: Option, } @@ -90,6 +110,10 @@ pub enum CoordinateError { /// next invocation conservatively recovers. MarkerClearAfterCommit { source: anyhow::Error, + #[allow( + dead_code, + reason = "outcome payload carried but never read; suspected defect recorded in context/plans/cli-async-persistence-cleanup-pr3.md" + )] committed: Box, }, /// The caller-supplied Agent Trace DB provider returned `Err` after the diff --git a/cli/src/services/mutation_trace/runtime/external_mutation_guard.rs b/cli/src/services/mutation_trace/runtime/external_mutation_guard.rs index 42f6b65fe..966cea930 100644 --- a/cli/src/services/mutation_trace/runtime/external_mutation_guard.rs +++ b/cli/src/services/mutation_trace/runtime/external_mutation_guard.rs @@ -40,6 +40,7 @@ pub(crate) enum GuardError { Wait(std::io::Error), Finish(CoordinateError), SupervisorWorker(tokio::task::JoinError), + #[allow(dead_code, reason = "constructed only under cfg(not(unix))")] UnsupportedPlatform, } @@ -104,21 +105,6 @@ where Err(GuardError::UnsupportedPlatform) } -#[cfg(not(unix))] -pub(crate) fn run_external_mutation_guard( - _repository_root: &Path, - _request: &GuardRequest, - _open_db: P, - _on_event: E, - _cancel_rx: mpsc::Receiver<()>, -) -> Result -where - P: std::ops::AsyncFnOnce() -> anyhow::Result, - E: FnMut(GuardEvent), -{ - Err(GuardError::UnsupportedPlatform) -} - #[cfg(unix)] mod unix_impl { use std::fs::File; @@ -824,30 +810,6 @@ mod unix_impl { .await } - pub(crate) async fn run_external_mutation_guard( - repository_root: &Path, - request: &GuardRequest, - open_db: P, - mut on_event: E, - cancel_rx: mpsc::Receiver<()>, - ) -> Result - where - P: std::ops::AsyncFnOnce() -> anyhow::Result, - E: FnMut(GuardEvent), - { - let guard = arm_external_mutation_guard( - repository_root, - open_db, - || { - on_event(GuardEvent::Armed); - Ok(()) - }, - cancel_rx, - ) - .await?; - guard.exec(request, on_event).await - } - #[cfg(test)] mod tests { use std::path::{Path, PathBuf}; @@ -857,11 +819,37 @@ mod unix_impl { use crate::services::agent_trace_db::repository::RepositoryAgentTraceDb; - use super::{run_external_mutation_guard, GuardEvent, GuardRequest}; + use super::{ + arm_external_mutation_guard, GuardError, GuardEvent, GuardOutcome, GuardRequest, + }; const WORKER_PROGRESS_LIMIT: Duration = Duration::from_millis(500); const READY_TIMEOUT: Duration = Duration::from_secs(30); + async fn arm_and_exec_guard( + repository_root: &Path, + request: &GuardRequest, + open_db: P, + mut on_event: E, + cancel_rx: mpsc::Receiver<()>, + ) -> Result + where + P: std::ops::AsyncFnOnce() -> anyhow::Result, + E: FnMut(GuardEvent), + { + let guard = arm_external_mutation_guard( + repository_root, + open_db, + || { + on_event(GuardEvent::Armed); + Ok(()) + }, + cancel_rx, + ) + .await?; + guard.exec(request, on_event).await + } + fn git(repo_root: &Path, args: &[&str]) { let output = Command::new("git") .args(args) @@ -972,7 +960,7 @@ mod unix_impl { let guard_task = tokio::spawn(async move { let mut ready_tx = Some(ready_tx); - run_external_mutation_guard( + arm_and_exec_guard( &repo_root, &guard_request("printf ready; sleep 2"), async || RepositoryAgentTraceDb::new_at(&db_path).await, @@ -1018,7 +1006,7 @@ mod unix_impl { let (_cancel_tx, cancel_rx) = mpsc::channel(); let mut events = Vec::new(); - let outcome = run_external_mutation_guard( + let outcome = arm_and_exec_guard( &repo_root, &guard_request("printf out; printf err >&2; exit 7"), async || RepositoryAgentTraceDb::new_at(&db_path).await, @@ -1042,6 +1030,4 @@ mod unix_impl { } #[cfg(unix)] -pub(crate) use unix_impl::{ - arm_external_mutation_guard, run_external_mutation_guard, ArmedExternalMutationGuard, -}; +pub(crate) use unix_impl::{arm_external_mutation_guard, ArmedExternalMutationGuard}; diff --git a/cli/src/services/mutation_trace/runtime/git_snapshot.rs b/cli/src/services/mutation_trace/runtime/git_snapshot.rs index 6d1271b33..ab05c5112 100644 --- a/cli/src/services/mutation_trace/runtime/git_snapshot.rs +++ b/cli/src/services/mutation_trace/runtime/git_snapshot.rs @@ -19,6 +19,10 @@ const REF_NAMESPACE: &str = "refs/sce/mutation-cursor"; /// target (empty for a direct ref). NUL-separated so no field can be split or /// trimmed ambiguously; the trailing symref field is always present (possibly /// empty), so every well-formed line has exactly four fields. +#[allow( + dead_code, + reason = "ref reconciliation retained and unwired; see context/plans/mutation-cursor-ref-reconciliation.md" +)] const FOR_EACH_REF_PIN_FORMAT: &str = "--format=%(refname)%00%(objectname)%00%(objecttype)%00%(symref)"; @@ -160,6 +164,10 @@ impl GitSnapshotService { /// `git for-each-ref` execution or exit failure is /// [`PinInventoryError::Git`]; anything malformed inside the namespace is /// [`PinInventoryError::MalformedRef`], matchable separately. + #[allow( + dead_code, + reason = "ref reconciliation retained and unwired; see context/plans/mutation-cursor-ref-reconciliation.md" + )] pub async fn list_pins( &self, worktree_id: &WorktreeId, @@ -199,6 +207,10 @@ impl GitSnapshotService { /// into a symbolic ref — this returns `Err` and deletes nothing, preferring /// failure over acting on unexpected namespace state. An empty slice is a /// successful no-op. + #[allow( + dead_code, + reason = "ref reconciliation retained and unwired; see context/plans/mutation-cursor-ref-reconciliation.md" + )] pub async fn delete_pins(&self, lease: WorktreeLockLease, pins: &[PinnedRef]) -> Result<()> { self.delete_pins_inner(lease, pins, || {}).await } @@ -212,6 +224,10 @@ impl GitSnapshotService { /// preflight — is what aborts the batch. This is the only proof that the /// Git transaction itself is atomic; the preflight proves a different /// property (unexpected ref state before the transaction is even attempted). + #[allow( + dead_code, + reason = "ref reconciliation retained and unwired; see context/plans/mutation-cursor-ref-reconciliation.md" + )] async fn delete_pins_inner( &self, lease: WorktreeLockLease, @@ -249,6 +265,10 @@ impl GitSnapshotService { Ok(()) } + #[allow( + dead_code, + reason = "ref reconciliation retained and unwired; see context/plans/mutation-cursor-ref-reconciliation.md" + )] async fn run_ref_mutation( &self, lease: WorktreeLockLease, @@ -285,6 +305,10 @@ impl GitSnapshotService { /// residual sub-transaction race is still contained by `--no-deref` plus /// the per-`delete` old-value condition, which together cannot follow a /// symbolic ref or mutate a ref the caller did not name. + #[allow( + dead_code, + reason = "ref reconciliation retained and unwired; see context/plans/mutation-cursor-ref-reconciliation.md" + )] async fn assert_pins_are_unchanged_direct_refs(&self, pins: &[PinnedRef]) -> Result<()> { let mut args: Vec<&str> = vec!["for-each-ref", FOR_EACH_REF_PIN_FORMAT]; args.extend(pins.iter().map(|pin| pin.ref_name.as_str())); @@ -405,6 +429,10 @@ impl GitSnapshotService { /// One SCE-owned snapshot pin: a ref under /// `refs/sce/mutation-cursor//` and the tree object it protects. #[derive(Clone, Debug, Eq, PartialEq)] +#[allow( + dead_code, + reason = "ref reconciliation retained and unwired; see context/plans/mutation-cursor-ref-reconciliation.md" +)] pub struct PinnedRef { pub ref_name: String, pub tree: TreeId, @@ -412,6 +440,10 @@ pub struct PinnedRef { /// Why a worktree's pin inventory could not be produced. #[derive(Debug)] +#[allow( + dead_code, + reason = "ref reconciliation retained and unwired; see context/plans/mutation-cursor-ref-reconciliation.md" +)] pub enum PinInventoryError { /// `git for-each-ref` itself failed to execute or exited non-zero. Git(anyhow::Error), @@ -436,6 +468,10 @@ impl std::fmt::Display for PinInventoryError { impl std::error::Error for PinInventoryError {} +#[allow( + dead_code, + reason = "ref reconciliation retained and unwired; see context/plans/mutation-cursor-ref-reconciliation.md" +)] fn parse_pin_line(line: &str, prefix: &str) -> std::result::Result { let fields: Vec<&str> = line.split('\0').collect(); let [ref_name, object_name, object_type, symref] = fields.as_slice() else { @@ -498,6 +534,10 @@ fn parse_pin_line(line: &str, prefix: &str) -> std::result::Result String { format!("{REF_NAMESPACE}/{}/", worktree_id.0) } diff --git a/cli/src/services/mutation_trace/runtime/mod.rs b/cli/src/services/mutation_trace/runtime/mod.rs index a7db7c910..cd99b7307 100644 --- a/cli/src/services/mutation_trace/runtime/mod.rs +++ b/cli/src/services/mutation_trace/runtime/mod.rs @@ -15,8 +15,8 @@ pub(crate) use coordinator::{ }; #[allow(unused_imports)] pub(crate) use external_mutation_guard::{ - arm_external_mutation_guard, run_external_mutation_guard, ArmedExternalMutationGuard, - GuardError, GuardEvent, GuardOutcome, GuardRequest, + arm_external_mutation_guard, ArmedExternalMutationGuard, GuardError, GuardEvent, GuardOutcome, + GuardRequest, }; #[allow(unused_imports)] pub(crate) use git_snapshot::{resolve_git_dir, resolve_worktree_id}; diff --git a/cli/src/services/mutation_trace/runtime/ref_reconciliation.rs b/cli/src/services/mutation_trace/runtime/ref_reconciliation.rs index 0b7da0d9c..10f04bfa9 100644 --- a/cli/src/services/mutation_trace/runtime/ref_reconciliation.rs +++ b/cli/src/services/mutation_trace/runtime/ref_reconciliation.rs @@ -12,6 +12,10 @@ use crate::services::mutation_trace::types::TreeId; use super::git_snapshot::{resolve_git_dir, GitSnapshotService, PinInventoryError, PinnedRef}; use super::worktree_lock::{acquire_inner_async, WorktreeLockError}; +#[allow( + dead_code, + reason = "ref reconciliation retained and unwired; see context/plans/mutation-cursor-ref-reconciliation.md" +)] const RECONCILIATION_LOCK_TIMEOUT: Duration = Duration::from_secs(10); /// Outcome counts of one successful reconciliation pass. @@ -32,6 +36,10 @@ const RECONCILIATION_LOCK_TIMEOUT: Duration = Duration::from_secs(10); /// `ReconciliationOutcome::SkippedNoCheckoutIdentity` carries no report, so no /// report invariant applies to it. #[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[allow( + dead_code, + reason = "ref reconciliation retained and unwired; see context/plans/mutation-cursor-ref-reconciliation.md" +)] pub struct ReconciliationReport { pub local_required: usize, pub retained: usize, @@ -42,6 +50,10 @@ pub struct ReconciliationReport { /// [`ReconciliationReport`], versus a skip because no current checkout identity /// could be derived (an `Ok`, never an `Err`). #[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[allow( + dead_code, + reason = "ref reconciliation retained and unwired; see context/plans/mutation-cursor-ref-reconciliation.md" +)] pub enum ReconciliationOutcome { Reconciled(ReconciliationReport), SkippedNoCheckoutIdentity, @@ -52,6 +64,10 @@ pub enum ReconciliationOutcome { /// non-`Ok` outcome leaves the SCE ref namespace in a consistent state: either /// untouched, or (only on `Ok`) with exactly the stale refs gone. #[derive(Debug)] +#[allow( + dead_code, + reason = "ref reconciliation retained and unwired; see context/plans/mutation-cursor-ref-reconciliation.md" +)] pub enum ReconcileError { /// `resolve_git_dir` failed. GitDir(anyhow::Error), @@ -122,6 +138,10 @@ impl std::error::Error for ReconcileError {} /// Module-private to `runtime`, exactly like `coordinate` — never re-exported /// outside mutation-trace `runtime`. It is a one-line delegation to /// [`reconcile_worktree_inner`] with a no-op lock-contention closure. +#[allow( + dead_code, + reason = "ref reconciliation retained and unwired; see context/plans/mutation-cursor-ref-reconciliation.md" +)] pub async fn reconcile_worktree

( repository_root: &Path, open_db: P, @@ -132,6 +152,10 @@ where reconcile_worktree_inner(repository_root, open_db, || {}).await } +#[allow( + dead_code, + reason = "ref reconciliation retained and unwired; see context/plans/mutation-cursor-ref-reconciliation.md" +)] pub(super) async fn reconcile_worktree_inner( repository_root: &Path, open_db: P, diff --git a/cli/src/services/mutation_trace/runtime/scope_runtime.rs b/cli/src/services/mutation_trace/runtime/scope_runtime.rs index 59bc27cde..c8e220bb6 100644 --- a/cli/src/services/mutation_trace/runtime/scope_runtime.rs +++ b/cli/src/services/mutation_trace/runtime/scope_runtime.rs @@ -60,6 +60,10 @@ pub enum AbandonScopeError { }, MarkerClearAfterCompletion { source: anyhow::Error, + #[allow( + dead_code, + reason = "outcome payload carried but never read; suspected defect recorded in context/plans/cli-async-persistence-cleanup-pr3.md" + )] completed: Box, }, Other(anyhow::Error), diff --git a/cli/src/services/mutation_trace/runtime/worktree_lock.rs b/cli/src/services/mutation_trace/runtime/worktree_lock.rs index 0610826da..cd7a404f3 100644 --- a/cli/src/services/mutation_trace/runtime/worktree_lock.rs +++ b/cli/src/services/mutation_trace/runtime/worktree_lock.rs @@ -15,7 +15,6 @@ const LOCK_POLL_INTERVAL: Duration = Duration::from_millis(100); #[derive(Debug)] struct WorktreeLockInner { file: File, - path: PathBuf, unlock_on_drop: AtomicBool, } @@ -55,10 +54,6 @@ impl std::fmt::Display for WorktreeLockError { impl std::error::Error for WorktreeLockError {} impl WorktreeLock { - pub fn acquire(git_dir: &Path, timeout: Duration) -> Result { - acquire_inner(git_dir, timeout, || {}) - } - pub async fn acquire_async( git_dir: &Path, timeout: Duration, @@ -121,7 +116,6 @@ where return Ok(WorktreeLock { inner: Arc::new(WorktreeLockInner { file, - path: lock_path, unlock_on_drop: AtomicBool::new(true), }), }); diff --git a/cli/src/services/mutation_trace/store.rs b/cli/src/services/mutation_trace/store.rs index f200db063..cab0f7448 100644 --- a/cli/src/services/mutation_trace/store.rs +++ b/cli/src/services/mutation_trace/store.rs @@ -194,6 +194,10 @@ pub fn encode_boundary_kind(kind: BoundaryKind) -> &'static str { } /// Decodes a [`BoundaryKind`] from `mutation_trace_events.boundary_kind`. +#[allow( + dead_code, + reason = "ref reconciliation retained and unwired; see context/plans/mutation-cursor-ref-reconciliation.md" +)] pub fn decode_boundary_kind(value: &str) -> Result { match value { "start" => Ok(BoundaryKind::Start), @@ -216,6 +220,10 @@ const SELECT_SCOPE_PROVENANCE_SQL: &str = "SELECT scope_id, session_id, model_id FROM mutation_trace_scope_provenance WHERE scope_id = ?1"; const SELECT_PROCESSED_EVENT_SQL: &str = "SELECT 1 FROM mutation_trace_processed_events WHERE scope_id = ?1 AND event_id = ?2"; +#[allow( + dead_code, + reason = "ref reconciliation retained and unwired; see context/plans/mutation-cursor-ref-reconciliation.md" +)] const SELECT_MUTATION_EVENT_SQL: &str = "SELECT before_tree, after_tree, tainted, failure_kind, attribution_kind, attribution_scope_id, boundary_kind, boundary_scope_id, boundary_event_id FROM mutation_trace_events WHERE worktree_id = ?1 AND revision = ?2"; @@ -236,12 +244,20 @@ const SELECT_LATEST_MUTATION_EVENT_REVISION_SQL: &str = "SELECT revision FROM mu WHERE worktree_id = ?1 ORDER BY revision DESC LIMIT 1"; +#[allow( + dead_code, + reason = "ref reconciliation retained and unwired; see context/plans/mutation-cursor-ref-reconciliation.md" +)] const SELECT_MUTATION_EVENT_ACTIVE_SCOPES_SQL: &str = "SELECT scope_id FROM mutation_trace_event_active_scopes WHERE worktree_id = ?1 AND revision = ?2"; /// One worktree's complete durable tree root set — its cursor tree plus the /// `before_tree` / `after_tree` of every historical `mutation_trace_events` /// row — as a single `UNION` statement so the whole set is read from one /// database snapshot, never assembled from independent `SELECT`s. +#[allow( + dead_code, + reason = "ref reconciliation retained and unwired; see context/plans/mutation-cursor-ref-reconciliation.md" +)] const SELECT_TREE_ROOTS_BY_WORKTREE_SQL: &str = "SELECT cursor_tree AS tree FROM mutation_trace_worktrees WHERE worktree_id = ?1 UNION @@ -251,6 +267,10 @@ const SELECT_TREE_ROOTS_BY_WORKTREE_SQL: &str = /// The same three `TreeId` columns unioned across **every** worktree in the /// repository, in one statement / one snapshot — the reconciler's /// repository-wide retention set. +#[allow( + dead_code, + reason = "ref reconciliation retained and unwired; see context/plans/mutation-cursor-ref-reconciliation.md" +)] const SELECT_ALL_TREE_ROOTS_SQL: &str = "SELECT cursor_tree AS tree FROM mutation_trace_worktrees UNION SELECT before_tree AS tree FROM mutation_trace_events @@ -688,6 +708,10 @@ impl<'a> MutationTraceStore<'a> { /// revision)`, decoding its full `Attribution` and `Boundary`, or `None` /// when no such row exists. Never called from `load_worktree` or from /// any hook-boundary path. + #[allow( + dead_code, + reason = "ref reconciliation retained and unwired; see context/plans/mutation-cursor-ref-reconciliation.md" + )] pub async fn load_mutation_event( &self, worktree: &WorktreeId, @@ -815,6 +839,10 @@ impl<'a> MutationTraceStore<'a> { /// the single statement observes either the pre-commit snapshot /// (`cursor_tree` still contains `T`) or the post-commit snapshot /// (`before_tree` contains `T`). + #[allow( + dead_code, + reason = "ref reconciliation retained and unwired; see context/plans/mutation-cursor-ref-reconciliation.md" + )] pub async fn load_tree_roots(&self, worktree: &WorktreeId) -> Result> { let rows = self .db @@ -841,6 +869,10 @@ impl<'a> MutationTraceStore<'a> { /// statement through one `query_map` call, so it cannot tear across a /// concurrent atomic `cursor T -> X` + `event T -> X` commit on another /// worktree. + #[allow( + dead_code, + reason = "ref reconciliation retained and unwired; see context/plans/mutation-cursor-ref-reconciliation.md" + )] pub async fn load_all_tree_roots(&self) -> Result> { let rows = self .db @@ -985,6 +1017,10 @@ impl<'a> MutationTraceStore<'a> { Ok(!rows.is_empty()) } + #[allow( + dead_code, + reason = "ref reconciliation retained and unwired; see context/plans/mutation-cursor-ref-reconciliation.md" + )] async fn load_mutation_event_active_scopes( &self, worktree: &WorktreeId, @@ -1154,6 +1190,10 @@ fn validate_health_encoding(tainted: bool, failure_kind: FailureKind) -> Result< Ok(()) } +#[allow( + dead_code, + reason = "ref reconciliation retained and unwired; see context/plans/mutation-cursor-ref-reconciliation.md" +)] fn tree_root_row_from_turso(row: &turso::Row) -> Result { let tree: String = row .get(0) @@ -1275,6 +1315,10 @@ fn mutation_event_page_row_from_turso(row: &turso::Row) -> Result, } +#[allow( + dead_code, + reason = "ref reconciliation retained and unwired; see context/plans/mutation-cursor-ref-reconciliation.md" +)] fn mutation_event_row_from_turso(row: &turso::Row) -> Result { let before_tree: String = row .get(0) @@ -1346,6 +1394,10 @@ fn reconstruct_attribution(kind: AttributionKind, scope_id: Option) -> R } } +#[allow( + dead_code, + reason = "ref reconciliation retained and unwired; see context/plans/mutation-cursor-ref-reconciliation.md" +)] fn reconstruct_boundary( kind: BoundaryKind, worktree: &WorktreeId, diff --git a/cli/src/services/mutation_trace/tests.rs b/cli/src/services/mutation_trace/tests.rs index 979a5c14a..912dfadfe 100644 --- a/cli/src/services/mutation_trace/tests.rs +++ b/cli/src/services/mutation_trace/tests.rs @@ -125,20 +125,15 @@ fn prepare_and_commit( } #[test] -fn scope_status_live_and_terminal_table() { +fn scope_status_live_table() { let cases = [ - (ScopeStatus::NeverSeen, false, false), - (ScopeStatus::Active, true, false), - (ScopeStatus::Closed, false, true), - (ScopeStatus::Abandoned, false, true), + (ScopeStatus::NeverSeen, false), + (ScopeStatus::Active, true), + (ScopeStatus::Closed, false), + (ScopeStatus::Abandoned, false), ]; - for (status, expected_live, expected_terminal) in cases { + for (status, expected_live) in cases { assert_eq!(is_live(status), expected_live, "is_live({status:?})"); - assert_eq!( - is_terminal(status), - expected_terminal, - "is_terminal({status:?})" - ); } } diff --git a/cli/src/services/mutation_trace/types.rs b/cli/src/services/mutation_trace/types.rs index 30a781c5c..7ec4584e1 100644 --- a/cli/src/services/mutation_trace/types.rs +++ b/cli/src/services/mutation_trace/types.rs @@ -152,12 +152,6 @@ pub fn is_live(status: ScopeStatus) -> bool { status == ScopeStatus::Active } -/// A scope has ended and can never be reactivated. Refines `isTerminal` -/// (`spec/mutation_cursor.qnt:169-170`). -pub fn is_terminal(status: ScopeStatus) -> bool { - matches!(status, ScopeStatus::Closed | ScopeStatus::Abandoned) -} - impl ScopeState { /// The worktree this scope belongs to. Refines `scopeWorktree` /// (`spec/mutation_cursor.qnt:151-157`). @@ -175,21 +169,10 @@ impl ScopeState { self.worktree_id.clone() } - /// The harness that owns this scope. Refines `scopeActor` - /// (`spec/mutation_cursor.qnt:159-165`). - pub fn scope_actor(&self) -> ActorKind { - self.actor_kind - } - /// Refines `isLive` applied to this scope's status. pub fn is_live(&self) -> bool { is_live(self.status) } - - /// Refines `isTerminal` applied to this scope's status. - pub fn is_terminal(&self) -> bool { - is_terminal(self.status) - } } /// The worktree a boundary applies to. Refines `boundaryWorktree` diff --git a/cli/src/services/observability.rs b/cli/src/services/observability.rs index 5e6adb641..ed37f9ac8 100644 --- a/cli/src/services/observability.rs +++ b/cli/src/services/observability.rs @@ -14,9 +14,7 @@ use chrono::{Local, NaiveDate, Utc}; use serde_json::json; use tracing::Level; -use crate::services::config::{ - self, LogFormat, LogLevel, ENV_LOG_DIR, ENV_LOG_FORMAT, ENV_LOG_LEVEL, -}; +use crate::services::config::{self, LogFormat, LogLevel, ENV_LOG_DIR}; use crate::services::error::CliError; use crate::services::security::redact_sensitive_text; @@ -74,34 +72,6 @@ impl Logger { }) } - #[allow(dead_code)] - fn from_env_lookup(lookup: F) -> Result - where - F: Fn(&str) -> Option, - { - let mut config = ObservabilityConfig::default(); - - if let Some(raw) = lookup(ENV_LOG_LEVEL) { - config.level = LogLevel::parse_env(&raw, ENV_LOG_LEVEL)?; - } - - if let Some(raw) = lookup(ENV_LOG_FORMAT) { - config.format = LogFormat::parse_env(&raw, ENV_LOG_FORMAT)?; - } - - let mut log_dir = None; - if let Some(raw) = lookup(ENV_LOG_DIR) { - validate_log_dir(&raw)?; - log_dir = Some(PathBuf::from(raw)); - } - - Ok(Self { - config, - log_dir, - log_file_retention_limit: config::DEFAULT_LOG_FILE_RETENTION_LIMIT, - }) - } - pub fn info( &self, event_id: &str, @@ -132,7 +102,6 @@ impl Logger { self.log_forced(LogLevel::Warn, event_id, message, fields, session_id); } - #[cfg_attr(not(test), allow(dead_code))] pub fn error( &self, event_id: &str, @@ -737,7 +706,10 @@ mod tests { #[test] fn observability_fields_for_user_error_without_source_omit_error_source() { - let error = CliError::user(UserError::NotAuthenticated); + let error = CliError::User { + error: UserError::NotAuthenticated, + source: None, + }; let fields = cli_error_fields(&error); diff --git a/cli/src/services/observability/traits.rs b/cli/src/services/observability/traits.rs index 330854885..a0d71d32c 100644 --- a/cli/src/services/observability/traits.rs +++ b/cli/src/services/observability/traits.rs @@ -46,7 +46,6 @@ pub trait Telemetry: Send + Sync { } #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] -#[allow(dead_code)] pub struct NoopLogger; impl Logger for NoopLogger { diff --git a/cli/src/services/output_format.rs b/cli/src/services/output_format.rs index 2e980c6f3..d403befcf 100644 --- a/cli/src/services/output_format.rs +++ b/cli/src/services/output_format.rs @@ -1,4 +1,3 @@ -use anyhow::{bail, Result}; use clap::ValueEnum; #[derive(Clone, Copy, Debug, Default, Eq, PartialEq, ValueEnum)] @@ -7,16 +6,3 @@ pub enum OutputFormat { Text, Json, } - -impl OutputFormat { - #[allow(dead_code)] - pub fn parse(raw: &str, help_command: &str) -> Result { - match raw { - "text" => Ok(Self::Text), - "json" => Ok(Self::Json), - _ => bail!( - "Invalid --format value '{raw}'. Valid values: text, json. Run '{help_command}' to see valid usage." - ), - } - } -} diff --git a/cli/src/services/patch.rs b/cli/src/services/patch.rs index 9e68c31ba..60572dda6 100644 --- a/cli/src/services/patch.rs +++ b/cli/src/services/patch.rs @@ -95,7 +95,6 @@ pub enum TouchedLineKind { Removed, } -#[allow(dead_code)] #[derive(Clone, Debug, Eq, PartialEq)] pub struct ParseError { pub message: String, @@ -115,7 +114,6 @@ impl std::error::Error for ParseError {} /// payload could not be reconstructed into a valid `ParsedPatch`. Common /// causes include malformed JSON syntax, missing required fields, or type /// mismatches in the serialized structure. -#[allow(dead_code)] #[derive(Clone, Debug, Eq, PartialEq)] pub struct PatchLoadError { pub message: String, @@ -147,24 +145,6 @@ pub fn load_patch_from_json(input: &str) -> Result }) } -/// Load a `ParsedPatch` from JSON bytes previously produced by serializing -/// a `ParsedPatch`. -/// -/// This is the bytes-oriented counterpart to [`load_patch_from_json`], -/// convenient when the caller has raw bytes (for example, from a database -/// BLOB column or a file read) rather than a UTF-8 string. -/// -/// # Errors -/// -/// Returns `PatchLoadError` with an actionable message when the input is -/// not valid JSON or does not match the expected `ParsedPatch` structure. -#[allow(dead_code)] -pub fn load_patch_from_json_bytes(input: &[u8]) -> Result { - serde_json::from_slice(input).map_err(|e| PatchLoadError { - message: format!("invalid patch JSON: {e}"), - }) -} - /// Compute the touched-line intersection of two patches. /// /// Returns a `ParsedPatch` containing only the touched lines from @@ -196,7 +176,6 @@ pub fn load_patch_from_json_bytes(input: &[u8]) -> Result(full_path: &[&'a str], suffix_candidate: &[&'a s /// /// let combined = combine_patches(&[patch_a, patch_b]); /// ``` -#[allow(dead_code)] pub fn combine_patches(patches: &[ParsedPatch]) -> ParsedPatch { use std::collections::HashMap; @@ -514,7 +492,6 @@ pub fn combine_patches(patches: &[ParsedPatch]) -> ParsedPatch { /// /// Returns `ParseError` with an actionable message when the input is malformed, /// such as an invalid hunk header or a `---`/`+++` line that cannot be parsed. -#[allow(dead_code)] pub fn parse_patch(input: &str, session_id: Option<&str>) -> Result { let mut files: Vec = Vec::new(); let mut current_file: Option = None; diff --git a/cli/src/services/repository_identity/mod.rs b/cli/src/services/repository_identity/mod.rs index 405d0b875..c795169d7 100644 --- a/cli/src/services/repository_identity/mod.rs +++ b/cli/src/services/repository_identity/mod.rs @@ -26,15 +26,6 @@ pub struct RepositoryIdentity { pub repository_id: String, } -impl RepositoryIdentity { - /// Human-readable on-disk directory segment (`-`) for this - /// identity. Convenience wrapper over [`repository_dir_segment`]; the - /// authoritative identity is still [`RepositoryIdentity::repository_id`]. - pub fn dir_segment(&self) -> String { - repository_dir_segment(&self.canonical_identity) - } -} - /// Canonicalization failure. Variants carry no input fragments so /// credential-bearing URLs never leak into error output. #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -101,55 +92,6 @@ pub fn derive_repository_id(canonical_identity: &str) -> String { hex_encode(&hasher.finalize()) } -/// Length in hex chars of the disambiguating short hash in a directory segment. -const DIR_SEGMENT_SHORT_HASH_LEN: usize = 4; - -/// Derives the human-readable on-disk directory segment for a canonical -/// identity as `-`, where `slug` is the lowercased canonical -/// identity with every run of non-alphanumeric characters collapsed to a -/// single `-` and leading/trailing `-` trimmed, and `short` is the first four -/// hex chars of `SHA256(canonical_identity)` computed with **no** domain -/// prefix. The short hash is deliberately distinct from [`derive_repository_id`], -/// which keeps its `sce-repository-id-v1\0` domain separation. This is a pure -/// display/layout helper: the authoritative identity remains the repository ID. -pub fn repository_dir_segment(canonical_identity: &str) -> String { - let slug = slugify(canonical_identity); - let short = derive_short_hash(canonical_identity); - if slug.is_empty() { - short - } else { - format!("{slug}-{short}") - } -} - -/// Lowercases and collapses every run of non-alphanumeric characters to a -/// single `-`, trimming leading and trailing `-`. -fn slugify(input: &str) -> String { - let mut slug = String::with_capacity(input.len()); - let mut pending_dash = false; - for ch in input.chars() { - if ch.is_ascii_alphanumeric() { - if pending_dash && !slug.is_empty() { - slug.push('-'); - } - pending_dash = false; - slug.push(ch.to_ascii_lowercase()); - } else { - pending_dash = true; - } - } - slug -} - -/// First [`DIR_SEGMENT_SHORT_HASH_LEN`] hex chars of the un-prefixed -/// `SHA256(canonical_identity)`. -fn derive_short_hash(canonical_identity: &str) -> String { - let mut hasher = Sha256::new(); - hasher.update(canonical_identity.as_bytes()); - let full = hex_encode(&hasher.finalize()); - full[..DIR_SEGMENT_SHORT_HASH_LEN].to_string() -} - /// Canonicalizes a Git remote URL to the scheme-neutral form /// `host[:port]/path` with credentials stripped, hostname lowercased, /// default ports removed, and query/fragment/trailing-slash/trailing-`.git` @@ -483,26 +425,6 @@ mod tests { ); } - fn un_prefixed_sha256_prefix(input: &str, len: usize) -> String { - let mut hasher = Sha256::new(); - hasher.update(input.as_bytes()); - hex_encode(&hasher.finalize())[..len].to_string() - } - - #[test] - fn dir_segment_matches_slug_and_un_prefixed_short_hash() { - // Slug: lowercased, non-alphanumeric runs (`.`,`/`) collapsed to `-`, - // trimmed. Short: first 4 hex of the un-prefixed SHA-256, deliberately - // distinct from the domain-prefixed repository ID. - let canonical = "github.com/crocoder-dev/shared-context-engineering"; - let short = un_prefixed_sha256_prefix(canonical, 4); - assert_eq!( - repository_dir_segment(canonical), - format!("github-com-crocoder-dev-shared-context-engineering-{short}") - ); - assert_ne!(short, derive_repository_id(canonical)[..4]); - } - #[test] fn missing_path_variants_error() { assert_eq!( diff --git a/cli/src/services/resilience.rs b/cli/src/services/resilience.rs index 5523290bb..3fca25337 100644 --- a/cli/src/services/resilience.rs +++ b/cli/src/services/resilience.rs @@ -1,10 +1,9 @@ use std::future::Future; -use std::thread; use std::time::{Duration, Instant}; use std::fmt; -use anyhow::{anyhow, ensure, Result}; +use anyhow::{anyhow, Result}; #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum RetryPolicyError { @@ -159,78 +158,6 @@ where )) } -#[allow(dead_code)] -pub fn run_with_retry_sync( - policy: RetryPolicy, - operation_name: &str, - retry_hint: &str, - mut operation: Op, -) -> Result -where - Op: FnMut(u32) -> Result, -{ - ensure!( - policy.max_attempts > 0, - "Retry policy requires max_attempts >= 1" - ); - ensure!( - policy.timeout_ms > 0, - "Retry policy requires timeout_ms >= 1" - ); - ensure!( - policy.max_backoff_ms >= policy.initial_backoff_ms, - "Retry policy requires max_backoff_ms >= initial_backoff_ms" - ); - - let mut last_error = String::new(); - - for attempt in 1..=policy.max_attempts { - let started_at = Instant::now(); - let outcome = operation(attempt); - - match outcome { - Ok(value) => return Ok(value), - Err(error) => { - last_error = if started_at.elapsed() >= policy.timeout() { - format!( - "attempt {attempt} exceeded {}ms and failed: {error}", - policy.timeout_ms - ) - } else { - error.to_string() - }; - } - } - - if attempt == policy.max_attempts { - break; - } - - let backoff = policy.backoff_for_attempt(attempt + 1); - tracing::warn!( - event_id = "sce.resilience.retry", - operation = operation_name, - attempt, - max_attempts = policy.max_attempts, - timeout_ms = policy.timeout_ms, - backoff_ms = u64::try_from(backoff.as_millis()).unwrap_or(u64::MAX), - error = %last_error, - "Retrying operation after transient failure" - ); - thread::sleep(backoff); - } - - Err(anyhow!( - "Operation '{operation_name}' failed after {} attempt(s) (timeout={}ms, backoff={}..{}ms). Last error: {}. Try: {}", - policy.max_attempts, - policy.timeout_ms, - policy.initial_backoff_ms, - policy.max_backoff_ms, - last_error, - retry_hint - )) -} - /// One retryable attempt. Closures returning a future implement it directly; /// operations that must lend an exclusive borrow (such as a transaction on a /// single connection) implement it on a struct, because an `FnMut` closure diff --git a/cli/src/services/setup/mod.rs b/cli/src/services/setup/mod.rs index 7e55db104..0c8e9ace1 100644 --- a/cli/src/services/setup/mod.rs +++ b/cli/src/services/setup/mod.rs @@ -97,14 +97,6 @@ pub struct EmbeddedAsset { pub sha256: [u8; 32], } -#[allow(dead_code)] -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum RequiredHookAsset { - PreCommit, - CommitMsg, - PostCommit, -} - #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct OptionalWorkflow { pub id: &'static str, @@ -121,19 +113,6 @@ pub fn iter_required_hook_assets() -> std::slice::Iter<'static, EmbeddedAsset> { HOOK_EMBEDDED_ASSETS.iter() } -#[allow(dead_code)] -pub fn get_required_hook_asset(hook: RequiredHookAsset) -> Option<&'static EmbeddedAsset> { - let hook_name = match hook { - RequiredHookAsset::PreCommit => default_paths::hook_dir::PRE_COMMIT, - RequiredHookAsset::CommitMsg => default_paths::hook_dir::COMMIT_MSG, - RequiredHookAsset::PostCommit => default_paths::hook_dir::POST_COMMIT, - }; - - HOOK_EMBEDDED_ASSETS - .iter() - .find(|asset| asset.relative_path == hook_name) -} - fn embedded_assets_for_concrete_target(target: SetupTarget) -> &'static [EmbeddedAsset] { match target { SetupTarget::OpenCode => OPENCODE_EMBEDDED_ASSETS, @@ -952,19 +931,6 @@ fn setup_prompt_target_label(target: SetupPromptTarget) -> String { prompt::setup_prompt_target_label(target) } -#[allow(dead_code)] -fn setup_prompt_target_label_with_color_policy( - target: SetupPromptTarget, - color_enabled: bool, -) -> String { - prompt::setup_prompt_target_label_with_color_policy(target, color_enabled) -} - -#[allow(dead_code)] -fn setup_prompt_title_with_color_policy(color_enabled: bool) -> String { - prompt::setup_prompt_title_with_color_policy(color_enabled) -} - mod prompt; pub fn resolve_setup_dispatch

( diff --git a/cli/src/services/setup/prompt.rs b/cli/src/services/setup/prompt.rs index cf92217b4..2e69e192a 100644 --- a/cli/src/services/setup/prompt.rs +++ b/cli/src/services/setup/prompt.rs @@ -1,9 +1,7 @@ use anyhow::{bail, Result}; use inquire::{Confirm, InquireError, MultiSelect, Select}; -use crate::services::style::{ - prompt_label, prompt_label_with_color_policy, prompt_value_with_color_policy, -}; +use crate::services::style::{prompt_label, prompt_value_with_color_policy}; use super::{OptionalWorkflow, SetupDispatch, SetupMode, SetupPromptTarget, SetupTarget}; @@ -182,8 +180,3 @@ pub(super) fn setup_prompt_target_label_with_color_policy( prompt_value_with_color_policy(label, color_enabled) } - -#[allow(dead_code)] -pub(super) fn setup_prompt_title_with_color_policy(color_enabled: bool) -> String { - prompt_label_with_color_policy("Select setup target", color_enabled) -} diff --git a/cli/src/services/style.rs b/cli/src/services/style.rs index 7c1f2e8e3..329f22ffb 100644 --- a/cli/src/services/style.rs +++ b/cli/src/services/style.rs @@ -259,9 +259,7 @@ fn is_help_placeholder(token: &str) -> bool { /// Gradient color endpoints for the ASCII art banner. /// Right side: cyan (0, 255, 255), Left side: magenta (255, 0, 255). -#[allow(dead_code)] const GRADIENT_COLOR_RIGHT: (u8, u8, u8) = (0, 255, 255); -#[allow(dead_code)] const GRADIENT_COLOR_LEFT: (u8, u8, u8) = (255, 0, 255); /// Applies a right-to-left color gradient to ASCII art banner lines. @@ -270,7 +268,6 @@ const GRADIENT_COLOR_LEFT: (u8, u8, u8) = (255, 0, 255); /// column position using a cyan-to-magenta gradient (cyan on the right, /// magenta on the left). Spaces are left unstyled. /// When color is disabled, returns the plain ASCII art unchanged. -#[allow(dead_code)] #[must_use] pub fn banner_with_gradient(lines: &[&str]) -> String { banner_with_gradient_with_color_policy(lines, supports_color()) @@ -278,7 +275,7 @@ pub fn banner_with_gradient(lines: &[&str]) -> String { /// Internal variant of [`banner_with_gradient`] that accepts an explicit /// color policy flag for testability. -#[allow(dead_code, clippy::cast_precision_loss)] +#[allow(clippy::cast_precision_loss)] #[must_use] pub(crate) fn banner_with_gradient_with_color_policy( lines: &[&str], @@ -325,7 +322,7 @@ pub(crate) fn banner_with_gradient_with_color_policy( /// /// Returns `a + (b - a) * t`, clamped to the `u8` range by construction /// since `a`, `b` are `u8` and `t` is in `[0, 1]`. -#[allow(dead_code, clippy::cast_possible_truncation, clippy::cast_sign_loss)] +#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] fn lerp_u8(a: u8, b: u8, t: f64) -> u8 { (f64::from(a) + (f64::from(b) - f64::from(a)) * t).round() as u8 } diff --git a/cli/src/services/sync/command.rs b/cli/src/services/sync/command.rs index ce97b1f5b..77adb11fb 100644 --- a/cli/src/services/sync/command.rs +++ b/cli/src/services/sync/command.rs @@ -43,15 +43,6 @@ fn classify_sync_error(err: TraceSyncError) -> CliError { } impl SyncCommand { - #[allow(dead_code)] - pub async fn execute(&self, context: &C) -> Result - where - C: ContextWithRepoRoot, - { - let mut stderr = std::io::sink(); - self.execute_with_stderr(context, &mut stderr).await - } - pub async fn execute_with_stderr( &self, context: &C, diff --git a/cli/src/services/sync/mod.rs b/cli/src/services/sync/mod.rs index fac259368..62046d2a4 100644 --- a/cli/src/services/sync/mod.rs +++ b/cli/src/services/sync/mod.rs @@ -1,6 +1,5 @@ //! Top-level `sce sync` command and Agent Trace synchronization service. -#[allow(dead_code)] pub mod auto_sync; pub mod command; pub mod progress; diff --git a/cli/src/services/sync/render_sync.rs b/cli/src/services/sync/render_sync.rs index 2bdcd8d0e..987eb4aca 100644 --- a/cli/src/services/sync/render_sync.rs +++ b/cli/src/services/sync/render_sync.rs @@ -56,3 +56,55 @@ fn stream_json(stream: &StreamSyncReport) -> serde_json::Value { "batches": stream.batches, }) } + +#[cfg(test)] +mod tests { + use serde_json::{json, Value}; + + use super::render; + use crate::services::output_format::OutputFormat; + use crate::services::sync::sync::{AgentTraceSyncReport, StreamSyncReport, StreamSyncReports}; + + const DIFF_TRACES_CURSOR: i64 = 42; + + fn stream(uploaded: usize) -> StreamSyncReport { + StreamSyncReport { + uploaded, + initial_cursor: 0, + final_cursor: i64::try_from(uploaded).expect("small count"), + batches: usize::from(uploaded > 0), + } + } + + #[test] + fn json_report_keeps_zero_upload_diff_traces_stream() { + let report = AgentTraceSyncReport { + repository_id: "repo".to_string(), + source_instance_id: "source".to_string(), + streams: StreamSyncReports { + messages: stream(3), + parts: stream(0), + diff_traces: StreamSyncReport { + uploaded: 0, + initial_cursor: DIFF_TRACES_CURSOR, + final_cursor: DIFF_TRACES_CURSOR, + batches: 0, + }, + agent_traces: stream(0), + }, + }; + + let rendered = render(&report, OutputFormat::Json).expect("render json"); + let rendered: Value = serde_json::from_str(&rendered).expect("valid json"); + + assert_eq!( + rendered["streams"]["diffTraces"], + json!({ + "uploaded": 0, + "initialCursor": DIFF_TRACES_CURSOR, + "finalCursor": DIFF_TRACES_CURSOR, + "batches": 0, + }) + ); + } +} diff --git a/cli/src/services/sync/sync.rs b/cli/src/services/sync/sync.rs index f3abde4ae..a97e82681 100644 --- a/cli/src/services/sync/sync.rs +++ b/cli/src/services/sync/sync.rs @@ -18,14 +18,14 @@ use crate::services::agent_trace_storage::{resolve_agent_trace_storage, AgentTra use crate::services::agent_trace_sync::control_plane::{ AgentTraceCursors, AgentTraceIngestionBatchRequest, AgentTraceIngestionBatchResponse, AgentTraceIngestionStateRequest, AuthenticatedControlPlaneClient, ControlPlaneError, - IngestionStream, + CredentialStore, IngestionStream, }; use crate::services::agent_trace_sync::{ sync_stream, AgentTraceExportRow, BatchAttemptOutcome, StreamSyncError, }; use crate::services::auth; use crate::services::config; -use crate::services::sync::progress::{NoopProgressReporter, ProgressReporter}; +use crate::services::sync::progress::ProgressReporter; #[derive(Clone, Debug, PartialEq, Eq)] pub struct AgentTraceSyncReport { @@ -127,7 +127,6 @@ impl TraceSyncError { /// (`ControlPlane`) or from a stream's batch/refresh path (`Stream`). /// `Runtime` never carries a `ControlPlaneError` and is never an /// authentication failure. - #[allow(dead_code)] pub fn is_authentication_failure(&self) -> bool { match self { Self::Runtime(_) => false, @@ -148,24 +147,6 @@ impl TraceSyncError { } } -#[allow(dead_code)] -pub async fn run_current_sync(repo_root: &Path) -> Result { - let mut progress = NoopProgressReporter; - run_current_sync_with_progress(repo_root, &mut progress).await -} - -/// Production entry point with an injectable progress sink. -pub async fn run_current_sync_with_progress( - repo_root: &Path, - progress: &mut S, -) -> Result -where - S: ProgressReporter, -{ - let clock = SystemSyncProgressClock; - run_current_sync_with_progress_and_clock(repo_root, progress, &clock).await -} - /// Production sync entry point with injectable progress sink and clock. pub async fn run_current_sync_with_progress_and_clock( repo_root: &Path, @@ -203,31 +184,24 @@ where let storage = resolve_agent_trace_storage(&context) .await .map_err(|error| TraceSyncError::Runtime(format!("{error:#}")))?; - let result = async { - let storage = &storage; - - let auth_config = config::resolve_auth_runtime_config(repo_root) - .map_err(|error| TraceSyncError::Runtime(format!("{error:#}")))?; - let client = AuthenticatedControlPlaneClient::new( - reqwest::Client::new(), - auth_config.control_plane_base_url.value.unwrap_or_default(), - auth::WORKOS_DEFAULT_BASE_URL, - auth_config.workos_client_id.value.unwrap_or_default(), - ); - - let result = run_sync_against_without_progress( - &storage.metadata.repository_id, - &storage.metadata.source_instance_id, - &storage.db, - &client, - progress, - ) - .await; - client.wait_for_pending_refresh().await; - result - } + let auth_config = config::resolve_auth_runtime_config(repo_root) + .map_err(|error| TraceSyncError::Runtime(format!("{error:#}")))?; + let client = AuthenticatedControlPlaneClient::new( + reqwest::Client::new(), + auth_config.control_plane_base_url.value.unwrap_or_default(), + auth::WORKOS_DEFAULT_BASE_URL, + auth_config.workos_client_id.value.unwrap_or_default(), + ); + + let result = run_sync_against_without_progress( + &storage.metadata.repository_id, + &storage.metadata.source_instance_id, + &storage.db, + &client, + progress, + ) .await; - drop(storage); + client.wait_for_pending_refresh().await; result } @@ -246,15 +220,16 @@ where run_sync_async(repository_id, source_instance_id, &reader, client, progress).await } -async fn run_sync_async<'a, S>( +async fn run_sync_async<'a, S, K>( repository_id: &'a str, source_instance_id: &'a str, reader: &'a AgentTraceExportReader<'a>, - client: &'a AuthenticatedControlPlaneClient, + client: &'a AuthenticatedControlPlaneClient, progress: &'a mut S, ) -> Result where S: ProgressReporter + 'a, + K: CredentialStore, { let state_request = AgentTraceIngestionStateRequest { repository_id: repository_id.to_string(), @@ -283,7 +258,7 @@ where state.cursors.messages, "messages", async |cursor, limit| reader.read_messages_after(cursor, limit).await, - async |request| async move { client.ingest_messages(&request).await }.await, + async |request| client.ingest_messages(&request).await, Rc::clone(&progress), &terminated, ), @@ -295,7 +270,7 @@ where state.cursors.parts, "parts", async |cursor, limit| reader.read_parts_after(cursor, limit).await, - async |request| async move { client.ingest_parts(&request).await }.await, + async |request| client.ingest_parts(&request).await, Rc::clone(&progress), &terminated, ), @@ -307,7 +282,7 @@ where state.cursors.agent_traces, "agent_traces", async |cursor, limit| reader.read_agent_traces_after(cursor, limit).await, - async |request| async move { client.ingest_agent_traces(&request).await }.await, + async |request| client.ingest_agent_traces(&request).await, Rc::clone(&progress), &terminated, ), @@ -408,8 +383,8 @@ impl JoinSlot { } #[allow(clippy::too_many_arguments, clippy::too_many_lines)] -async fn sync_one_stream<'a, T, ReadFn, IngestFn, S>( - client: &'a AuthenticatedControlPlaneClient, +async fn sync_one_stream<'a, T, ReadFn, IngestFn, S, K>( + client: &'a AuthenticatedControlPlaneClient, repository_id: &'a str, source_instance_id: &'a str, stream: IngestionStream, @@ -423,6 +398,7 @@ async fn sync_one_stream<'a, T, ReadFn, IngestFn, S>( where T: AgentTraceExportRow + Clone + 'a, S: ProgressReporter + 'a, + K: CredentialStore, ReadFn: std::ops::AsyncFnMut(i64, usize) -> anyhow::Result> + 'a, IngestFn: std::ops::AsyncFnMut( AgentTraceIngestionBatchRequest, @@ -792,3 +768,187 @@ mod tests { assert_eq!(sibling_ingests.get(), 0); } } + +#[cfg(test)] +mod contract_tests { + use std::io::{BufRead, BufReader, Read, Write}; + use std::net::TcpListener; + use std::sync::{Arc, Mutex}; + use std::thread; + use std::time::{SystemTime, UNIX_EPOCH}; + + use serde_json::{json, Value}; + + use super::run_sync_async; + use crate::services::agent_trace_db::transaction_tests::{message, open, part}; + use crate::services::agent_trace_db::{AgentTraceInsert, DiffTraceInsert}; + use crate::services::agent_trace_export::AgentTraceExportReader; + use crate::services::agent_trace_sync::control_plane::{ + AuthenticatedControlPlaneClient, ControlPlaneError, CredentialStore, + }; + use crate::services::auth::TokenResponse; + use crate::services::sync::progress::NoopProgressReporter; + use crate::services::token_storage::StoredTokens; + + const DIFF_TRACES_CURSOR: i64 = 7; + + struct FixedCredentialStore; + + impl CredentialStore for FixedCredentialStore { + async fn load(&self) -> Result, ControlPlaneError> { + Ok(Some(StoredTokens { + access_token: "access".to_string(), + token_type: "Bearer".to_string(), + expires_in: 3600, + refresh_token: "refresh".to_string(), + scope: None, + stored_at_unix_seconds: SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("clock after epoch") + .as_secs(), + })) + } + + async fn save(&self, _token: &TokenResponse) -> Result { + unreachable!("fixed credentials never refresh") + } + } + + type CapturedRequests = Arc>>; + + fn start_control_plane() -> (String, CapturedRequests) { + let listener = TcpListener::bind("127.0.0.1:0").expect("bind control plane"); + let base_url = format!("http://{}", listener.local_addr().expect("server addr")); + let requests = Arc::new(Mutex::new(Vec::new())); + let thread_requests = Arc::clone(&requests); + thread::spawn(move || { + for stream in listener.incoming() { + let Ok(mut stream) = stream else { + continue; + }; + let mut reader = BufReader::new(stream.try_clone().expect("clone stream")); + let mut request_line = String::new(); + let _ = reader.read_line(&mut request_line); + let path = request_line + .split_whitespace() + .nth(1) + .unwrap_or_default() + .to_string(); + let mut content_length = 0usize; + loop { + let mut line = String::new(); + if reader.read_line(&mut line).unwrap_or(0) == 0 { + break; + } + let line = line.trim_end_matches(['\r', '\n']); + if line.is_empty() { + break; + } + if let Some((name, value)) = line.split_once(':') { + if name.eq_ignore_ascii_case("content-length") { + content_length = value.trim().parse().unwrap_or(0); + } + } + } + let mut body = vec![0u8; content_length]; + let _ = reader.read_exact(&mut body); + let body: Value = serde_json::from_slice(&body).unwrap_or(Value::Null); + + let payload = if path.ends_with("/state") { + json!({"cursors": { + "messages": 0, + "parts": 0, + "diffTraces": DIFF_TRACES_CURSOR, + "agentTraces": 0, + }}) + } else { + let rows = body["rows"].as_array().cloned().unwrap_or_default(); + let cursor = rows + .last() + .and_then(|row| row["sourceRowId"].as_i64()) + .unwrap_or(0); + json!({"accepted": rows.len(), "cursor": cursor}) + }; + thread_requests + .lock() + .expect("requests lock") + .push((path, body)); + let payload = payload.to_string(); + let response = format!( + "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{payload}", + payload.len() + ); + let _ = stream.write_all(response.as_bytes()); + } + }); + (base_url, requests) + } + + #[tokio::test] + async fn sync_uploads_only_three_streams_and_reports_zero_upload_diff_traces() { + let dir = tempfile::tempdir().expect("tempdir"); + let mut db = open(&dir.path().join("agent-trace.db")).await; + db.insert_conversation_text_event(message("m1"), part("m1")) + .await + .expect("insert message and part"); + db.insert_agent_trace(AgentTraceInsert { + commit_id: "commit", + commit_time_ms: 1, + trace_json: "{}", + agent_trace_id: "trace", + url: "https://example.test/trace", + remote_url: "https://example.test/repo.git", + }) + .await + .expect("insert agent trace"); + db.insert_diff_trace(DiffTraceInsert { + time_ms: 1, + session_id: "session", + patch: "patch", + model_id: None, + tool_name: "tool", + tool_version: None, + payload_type: "patch", + }) + .await + .expect("insert diff trace"); + + let (base_url, requests) = start_control_plane(); + let client = AuthenticatedControlPlaneClient::with_credential_store( + reqwest::Client::builder() + .tls_certs_only(Vec::new()) + .build() + .expect("test http client"), + base_url, + "http://127.0.0.1:1", + "client", + FixedCredentialStore, + ); + let reader = AgentTraceExportReader::new(&db); + let mut reporter = NoopProgressReporter; + + let report = run_sync_async("repo", "source", &reader, &client, &mut reporter) + .await + .expect("sync converges"); + + let requests = requests.lock().expect("requests lock").clone(); + let mut streams: Vec<&str> = requests + .iter() + .filter(|(path, _)| path.ends_with("/batch")) + .map(|(_, body)| body["stream"].as_str().expect("stream field")) + .collect(); + streams.sort_unstable(); + assert_eq!(streams, ["agent_traces", "messages", "parts"]); + + assert_eq!(report.streams.messages.uploaded, 1); + assert_eq!(report.streams.parts.uploaded, 1); + assert_eq!(report.streams.agent_traces.uploaded, 1); + assert_eq!(report.streams.diff_traces.uploaded, 0); + assert_eq!(report.streams.diff_traces.batches, 0); + assert_eq!( + report.streams.diff_traces.initial_cursor, + DIFF_TRACES_CURSOR + ); + assert_eq!(report.streams.diff_traces.final_cursor, DIFF_TRACES_CURSOR); + } +} diff --git a/context/architecture.md b/context/architecture.md index 927e56149..5f5b3854f 100644 --- a/context/architecture.md +++ b/context/architecture.md @@ -132,7 +132,7 @@ The repository includes a new placeholder Rust binary crate at `cli/`. - `cli/src/services/config/mod.rs` is the config service facade and `sce config` orchestration surface (`show`, `validate`, `--help`), with bare `sce config` routed by `cli/src/app.rs` to the same help payload as `sce config --help`. Focused submodules own the implementation slices: `types.rs` owns shared config/runtime primitives, `schema.rs` owns generated schema embedding plus typed file parsing, `policy.rs` owns bash-policy semantic validation plus policy-specific formatting and runtime preset-catalog access for the Rust evaluator, `resolver.rs` owns deterministic config-file discovery, file-layer merging, explicit value precedence (`flags > env > config file > defaults` where flag-backed), shared auth-key resolution, observability-runtime resolution, attribution-hooks runtime gate and `agent_trace.auto_sync` resolution, database-retry config resolution and `DATABASE_RETRY_CONFIG` `OnceLock` initialization, default-discovered invalid-file degradation, strict invalid-discovered-layer errors for Agent Trace storage, and explicit-path fatal errors for `--config` / `SCE_CONFIG_FILE`, and private `render.rs` owns `sce config show` / `sce config validate` text and JSON output construction plus rendering-specific display-value helpers. The facade preserves existing `services::config` imports for startup/auth/hooks callers while delegating command execution to resolution plus rendering submodules. - `cli/src/services/output_format.rs` defines the canonical shared CLI output-format contract (`OutputFormat`) for supporting commands, with deterministic `text|json` parsing and command-scoped actionable invalid-value guidance. - `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/capabilities.rs` defines the current broad CLI capability traits consumed by the borrowed, compile-time-typed `AppContext`: `FsOps` with `StdFsOps` as a method-less filesystem capability marker and `GitOps` with `ProcessGitOps` for git command execution and availability checks. 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 creates or migrates the repository-scoped DB through `agent_trace_storage::resolve_agent_trace_storage(...)`; hook runtime uses the same storage identity/path resolution and no-migration open path, with missing or stale schema failing open through the existing `Run 'sce setup'.` guidance. - `cli/src/services/auth_command/mod.rs` defines the implemented auth command surface for `sce auth login|logout|whoami`, including device-flow login, stored-credential validation/renewal through login with device-flow fallback, logout, and Control Plane `/me`-backed whoami rendering in text/JSON formats; text mode uses flat `Email`, `First Name`, `Last Name`, `Role`, `Permissions`, and `Organization Name` labels, with optional names and missing role/permissions/workspace values handled deterministically. Logged-out text returns exact login guidance and renewal reports retain the `login` operation label. `cli/src/services/auth_command/command.rs` owns the `AuthCommand` payload used by the static `RuntimeCommand` enum. There is no public renewal or status subcommand. @@ -152,7 +152,7 @@ The repository includes a new placeholder Rust binary crate at `cli/`. - `context/cli/agent-trace-auto-sync.md` documents the sync-owned, one-shot post-commit launcher boundary: it reuses `sce sync`, has no daemon or local retry machinery, and fails open when child startup cannot be completed; doctor reports this capability without invoking the launcher. - `cli/src/services/sync/progress.rs` owns the sync-local, consumer-typed progress seam: generic `ProgressReporter` supports event delivery plus explicit successful finalization, closure-based collectors, and a no-op implementation alongside the fixed `indicatif` stderr presentation adapter. `cli/src/services/sync/sync.rs` owns `SyncProgressEvent` and its payload semantics for the three remotely synchronized streams (`messages`, `parts`, `agent_traces`; `diff_traces` is captured locally but not synchronized), while `sync/command.rs` selects the terminal adapter for text and the no-op reporter for JSON. There is no top-level `cli/src/services/progress/` module; sync orchestration depends only on its sync-owned contract, so terminal-library details stay at the sync presentation boundary. - `sce sync [--format text|json]` is implemented: `cli/src/services/sync/sync.rs` resolves repository-scoped Agent Trace storage, authenticates against the control plane with stored WorkOS credentials, uses the config-resolved `control_plane_base_url` with baked default `https://sce.crocoderlab.dev`, calls the ingestion `/state` endpoint once, then starts the three remote `messages`/`parts`/`agent_traces` stream state machines concurrently (`diff_traces` remains a local-only compatibility report) via `AgentTraceExportReader` and a shared per-stream reconciliation engine. Batches and cursor refreshes remain sequential within each stream, while fixed stream order is retained for final and stream-completion reporting; `cli/src/services/sync/render_sync.rs` renders the converged `AgentTraceSyncReport` as concise per-stream text or `camelCase` JSON without a nested subcommand field (see `context/cli/sync-command.md`). Local DB bootstrap and setup-time repository-scoped Agent Trace DB initialization otherwise still flow through lifecycle providers aggregated by setup, while repository-scoped DB health/repair flows through the doctor surface. The former trace database inspection and nested sync surfaces are unavailable. -- `cli/src/services/patch.rs` defines the standalone patch domain model (`ParsedPatch`, `PatchFileChange`, `FileChangeKind`, `PatchHunk`, `TouchedLine`, `TouchedLineKind`) for in-memory parsed unified-diff representation, capturing only touched lines (added/removed) plus minimal per-file/per-hunk metadata while excluding non-hunk headers and unchanged context lines. All types are `serde`-serializable/deserializable with `snake_case` JSON field naming. The module also provides `parse_patch`, a public parser function that converts raw unified-diff text (both `Index:` SVN-style and `diff --git` git-style formats) into `ParsedPatch` structs, with `ParseError` for actionable malformed-input diagnostics. Storage-agnostic JSON load helpers (`load_patch_from_json` for string input, `load_patch_from_json_bytes` for byte input) reconstruct `ParsedPatch` from serialized JSON content with `PatchLoadError` for actionable deserialization diagnostics. Its patch-set operations now include deterministic ordered combination plus target-shaped intersection that prefers exact touched-line matches and falls back to historical `kind`+`content` matching when incremental diffs and canonical post-commit diffs have drifted line numbers; `parse_patch`, `combine_patches`, and `intersect_patches` are consumed by the active post-commit hook runtime. +- `cli/src/services/patch.rs` defines the standalone patch domain model (`ParsedPatch`, `PatchFileChange`, `FileChangeKind`, `PatchHunk`, `TouchedLine`, `TouchedLineKind`) for in-memory parsed unified-diff representation, capturing only touched lines (added/removed) plus minimal per-file/per-hunk metadata while excluding non-hunk headers and unchanged context lines. All types are `serde`-serializable/deserializable with `snake_case` JSON field naming. The module also provides `parse_patch`, a public parser function that converts raw unified-diff text (both `Index:` SVN-style and `diff --git` git-style formats) into `ParsedPatch` structs, with `ParseError` for actionable malformed-input diagnostics. Storage-agnostic JSON load helper (`load_patch_from_json` for string input) reconstruct `ParsedPatch` from serialized JSON content with `PatchLoadError` for actionable deserialization diagnostics. Its patch-set operations now include deterministic ordered combination plus target-shaped intersection that prefers exact touched-line matches and falls back to historical `kind`+`content` matching when incremental diffs and canonical post-commit diffs have drifted line numbers; `parse_patch`, `combine_patches`, and `intersect_patches` are consumed by the active post-commit hook runtime. - `cli/src/services/structured_patch.rs` defines the synchronous structured editor-hook derivation seam. It derives Claude `PostToolUse` `Write` structured-update hunks, `Write` `tool_input.content` create fallback, and `Edit` structured-patch payloads into canonical `ParsedPatch` values plus Claude session/tool metadata, returning deterministic skip reasons for unsupported events/tools/payload shapes. The module is pure and side-effect-free. It is wired into `sce hooks diff-trace` for Claude payload classification at intake (T04) and into `RepositoryAgentTraceDb::recent_diff_trace_patches` for post-commit structured payload parsing dispatch at read time (T05). - `cli/src/services/` contains module boundaries for command_registry, lifecycle, auth_command, config, setup, doctor, hooks, checkout identity, bash_policy, version, completion, help, patch, sync, SCE web URL helpers, shared database infrastructure, local DB adapters, encrypted auth DB adapters, and Agent Trace DB adapters with explicit trait seams for future implementations. `cli/src/services/checkout/` owns checkout ID file infrastructure (the per-checkout Agent Trace DB opener/path helper was removed by the `retire-legacy-agent-trace-db` plan); active setup/hooks use `agent_trace_storage` to establish checkout identity as diagnostics and initialize/open the repository-scoped DB, while `sce doctor` surfaces checkout identity facts plus credential-safe repository Agent Trace DB metadata. There is no checkout-scoped discovery or former trace inspection surface; any pre-migration `agent-trace-*.db` files on disk are never touched and no longer inspectable via the CLI. `cli/src/services/bash_policy.rs` owns both the CLI-agnostic evaluator logic and the hidden `sce policy bash` command adapter used by OpenCode, Claude, and Pi callers; Codex delegates in-process to the same evaluator for its native `PreToolUse(Bash)` response. `cli/src/services/command_registry.rs` defines the static `RuntimeCommand` enum, deterministic `CommandRegistry` name catalog, and `build_default_registry()` function for command dispatch metadata. Service-owned command modules own the runtime command payload structs for help/help-text, version, completion, auth, config, setup, doctor, hooks, policy, and sync. - `cli/README.md` is the crate-local onboarding and usage source of truth for placeholder behavior, safety limitations, and roadmap mapping back to service contracts. @@ -231,15 +231,20 @@ caller: `Handle::try_current` + `spawn_blocking` (inline removal without a runtime). The task is fire-and-forget and holds no lock. A killed process can leave an orphaned `index-.lock`/temporary index file behind; this is an accepted - limitation (a sweep is a PR3 candidate). + limitation. PR3 audited it and adds no automatic sweeper: the temporary + index has no ownership or lock record, and a file name or age alone is not + evidence of abandonment, so a sweep is deferred to a separate change that + first defines a safe ownership protocol. - The guarded-shell supervisor (`cli/src/services/mutation_trace/runtime/external_mutation_guard.rs`) runs the synchronous `Child::wait`/descriptor supervision. The worker owns the `ProtectedWorktree` and the guarded child, ends when the child exits or cancellation is observed, and keeps the protected lock until it returns. The - lifetime pipe is created close-on-exec atomically and the guarded child alone - receives the protected descriptors through `pre_exec`, so unrelated spawned - processes do not inherit them. + lifetime pipe is created close-on-exec atomically only where `pipe2` exists + (Linux); elsewhere it is `pipe` then `fcntl`, which leaves a small + fork/exec window that is an unresolved limitation (not fixed in PR3). The + guarded child alone clears close-on-exec on the protected descriptors in + `pre_exec`. Runtime shutdown waits for these workers; each is bounded by its lock timeout, file I/O or child process. No other production `spawn_blocking` or diff --git a/context/cli/agent-trace-storage.md b/context/cli/agent-trace-storage.md index d26c8ced3..509e1323d 100644 --- a/context/cli/agent-trace-storage.md +++ b/context/cli/agent-trace-storage.md @@ -7,7 +7,6 @@ Module at `cli/src/services/agent_trace_storage/` (T04 of the `repository-scoped - `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, db_path, db, metadata }` — resolved repository identity (`ResolvedRepositoryIdentity` including source provenance), the repository-scoped DB path, the open `RepositoryAgentTraceDb`, and the typed `RepositoryMetadata { repository_id, source_instance_id }` produced by the same verification/initialization call that opened `db` (see [../sce/agent-trace-db.md](../sce/agent-trace-db.md) for the `source_instance_id` identity contract). - `resolve_agent_trace_storage(context)` — setup/lifecycle production entrypoint using the canonical state root from the default-path catalog; may run migrations, including migration `002`. -- `resolve_agent_trace_storage_at_state_root(context, state_root).await` — async setup/lifecycle resolution core against an explicit state root; used to exercise the full path without touching the real user state directory. - `resolve_agent_trace_storage_for_hook_runtime(context).await` — async hook-runtime production entrypoint using the canonical state root; never runs migrations, including migration `002`. - `resolve_agent_trace_storage_for_hook_runtime_at_state_root(context, state_root).await` — async hook-runtime resolution core against an explicit state root. diff --git a/context/cli/async-persistence-guarantees.md b/context/cli/async-persistence-guarantees.md index 9ef09496e..66fbfe97a 100644 --- a/context/cli/async-persistence-guarantees.md +++ b/context/cli/async-persistence-guarantees.md @@ -26,8 +26,34 @@ Each statement points to a test, a type-level argument or a named limitation. boundary lease until it ends, and guarded-shell descriptors are inherited only by the guarded child; see [adapter lifecycle](opencode-mutation-scope-adapter-lifecycle.md) and [external mutation guard](mutation-trace-external-mutation-guard.md). -- **Known limits:** large futures keep `#![recursion_limit = "256"]` pressure - (measured in PR3); the temporary-index cleanup limitation above stands. +- **Known limits:** + - Future size and `recursion_limit` (PR3 audit, Linux debug build, + `RUSTC_BOOTSTRAP=1 RUSTFLAGS=-Zprint-type-sizes`, disposable worktree): + the top-level `app::run` future is 14000 bytes and `main`'s `block_on` + future 14016; the chain `run_with_dependency_check` 13952, + `run_command_lifecycle` 13568, `execute_command_phase` 13168, + `SyncCommand::execute_with_stderr` 13072, `run_current_sync_without_progress` + 12784, `run_sync_async` 12040 and `join_three_to_completion` 11832 carries + almost all of it. The dependency's own `DatabaseSyncEngine::open_db` future + is 12704 bytes, so the size is dominated by a third-party future captured + inline and each wrapper adds roughly 50-200 bytes. No safe reduction exists + without boxing (out of scope). Building without the override fails with + "queries overflow the depth limit" (depth increased by 130 computing the + layout of `app::run`; the default limit is 128), so `#![recursion_limit = + "256"]` in `cli/src/main.rs` is kept. + - Temporary Git index: `TempIndexGuard` removes its file on completion or drop, + but a killed process can leave an orphaned `index-` file under the + runtime tmp directory. No automatic sweeper is added: there is no ownership + or lock record for the file and name or age alone is not evidence of + abandonment. A sweep needs a separate change that first defines that + protocol. + - Guarded-shell descriptor atomicity: the lifetime pipe is created + close-on-exec atomically with `pipe2` on Linux only; elsewhere `pipe` then + `fcntl` leaves a small fork/exec window. This race is an unresolved + limitation and is not fixed here. + - Blocking sites: the 14 production `spawn_blocking` sites match the + retained-worker list in [architecture](../architecture.md#application-execution-runtime); + none runs a database or Turso future. The ordinary Rust suites deleted during the async migration are not restored; correctness evidence is the focused regression set, the Quint MBT suite and surviving targeted tests. @@ -36,6 +62,6 @@ Each statement points to a test, a type-level argument or a named limitation. - **PR1** (complete): application-owned multi-thread Tokio runtime and static async command dispatch. - **PR2**: async Turso core and the callers it required (credentials, token storage, lifecycle, hooks, mutation trace, sync/export, setup/doctor); the Git/process/filesystem async work and cancellation protections above; transaction isolation, credential-rotation, terminal-sync, adapter-boundary and FD-inheritance fixes; removal of the DB runtime bridge and its blocking scopes. -- **PR3**: post-migration cleanup and runtime audit — redundant `async` wrappers, unused `run_with_retry_sync`, redundant `drop(storage)`, stale `#[allow(dead_code)]`, orphaned test support and unused seams, `join_three_to_completion` re-evaluation, large-future/recursion-limit measurement and an optional temporary-index sweep. Cosmetic and maintainability only. +- **PR3** (implementation complete; final `/validate` pending): post-migration cleanup and runtime audit. Verified-dead code and blanket dead-code allowances were deleted, redundant `async` wrappers, `run_with_retry_sync` and the redundant `drop(storage)` were removed, `join_three_to_completion` was retained, the `recursion_limit = "256"` override was kept, and no temporary-index sweeper was added. The final unsuppressed audit on Linux reports 32 binary-target and 27 test-target diagnostics, all covered by declaration-level `#[allow(dead_code, reason = ...)]` exceptions: the `diff_traces` compatibility surface (cites the 2026-10-01 ADR), the retained-and-unwired ref-reconciliation cluster (cites `context/plans/mutation-cursor-ref-reconciliation.md`), cfg-gated `PlatformFamily` variants and `GuardError::UnsupportedPlatform`, and never-read `CoordinateOutcome`/`committed`/`completed` fields kept as suspected defects. Linux-only diagnostics do not prove cross-platform safety. Cosmetic and maintainability only. - **PR4**: OpenTelemetry (not implemented in PR2). diff --git a/context/cli/capability-traits.md b/context/cli/capability-traits.md index c919e0143..07cd66bd8 100644 --- a/context/cli/capability-traits.md +++ b/context/cli/capability-traits.md @@ -4,14 +4,12 @@ The CLI exposes broad capability traits in `cli/src/services/capabilities.rs` an ## Current contract -- `FsOps: Send + Sync` defines broad filesystem operations: `read_file`, `write_file`, `metadata`, and `exists`. -- `StdFsOps` is the production filesystem implementation and wraps `std::fs` / `Path::exists` with `anyhow` context on fallible operations. -- `GitOps: Send + Sync` defines broad git process capabilities: `run_command`, `resolve_repository_root`, `resolve_hooks_directory`, and `is_available`. +- `FsOps: Send + Sync` is a method-less filesystem capability marker; the unused read/write/metadata/exists operations were removed. +- `StdFsOps` is the production `FsOps` implementation. +- `GitOps: Send + Sync` defines broad git process capabilities: `run_command` and `is_available`. - `ProcessGitOps` is the production git implementation and shells out to `git`, returning stdout as UTF-8 text with command/directory context on failure. -- `resolve_repository_root` uses `git rev-parse --show-toplevel`. -- `resolve_hooks_directory` uses `git rev-parse --git-path hooks` and resolves relative hook paths against the provided repository root. - `AppRuntime` owns concrete `StdFsOps` and `ProcessGitOps` production dependencies alongside concrete observability dependencies; these dependencies are borrowed by `AppContext` rather than stored behind type-erased runtime containers. -- `AppContext` is generic over logger, telemetry, filesystem, and git capability implementations and borrows them from the runtime; it is passed through directly awaited static `RuntimeCommand::execute` (enum defined in `cli/src/services/command_registry.rs`) behind narrow accessor bounds. Because it borrows from `AppRuntime`, `AppContext` is command-scoped: borrowing across directly awaited execution is safe while the owner lives, but detached work cannot retain that borrowed view. No new `Send`, `Sync`, or `'static` command bounds are required. Its inherent `logger()` / `telemetry()` / `fs()` / `git()` helpers return the concrete generic references (`&L`, `&T`, `&F`, `&G`), and `HasLogger`, `HasTelemetry`, `HasFs`, and `HasGit` use associated capability types that return `&Self::{Capability}` instead of object-erased `&dyn ...` values. This is the current static-DI boundary: command and service call sites express narrow capability requirements while preserving concrete borrowed dependency types. `ContextWithRepoRoot` derives a repository-scoped context while preserving the borrowed runtime capability objects. +- `AppContext` is generic over logger, telemetry, filesystem, and git capability implementations and borrows them from the runtime; it is passed through directly awaited static `RuntimeCommand::execute` (enum defined in `cli/src/services/command_registry.rs`) behind narrow accessor bounds. Because it borrows from `AppRuntime`, `AppContext` is command-scoped: borrowing across directly awaited execution is safe while the owner lives, but detached work cannot retain that borrowed view. No new `Send`, `Sync`, or `'static` command bounds are required. Its inherent `logger()` helper returns the concrete generic reference (`&L`), and `HasLogger`, `HasTelemetry`, and `HasGit` use associated capability types that return `&Self::{Capability}`, while `HasFs` only declares its associated `Fs` type as a bound, instead of object-erased `&dyn ...` values. This is the current static-DI boundary: command and service call sites express narrow capability requirements while preserving concrete borrowed dependency types. `ContextWithRepoRoot` derives a repository-scoped context while preserving the borrowed runtime capability objects. - Current command execution bounds are capability-oriented: central dispatch requires logger access plus repo-root scoping, context-free commands accept any context, hooks require logger access, and setup/doctor require repo-root scoping. - Test-only `UnimplementedFsOps` and `UnimplementedGitOps` stubs are available under `capabilities::test_stubs` for tests that need to satisfy trait bounds before providing focused fakes. diff --git a/context/cli/default-path-catalog.md b/context/cli/default-path-catalog.md index 2e4ef6e60..50c44cf6c 100644 --- a/context/cli/default-path-catalog.md +++ b/context/cli/default-path-catalog.md @@ -25,13 +25,12 @@ - `.opencode/`, `.opencode/opencode.json` - `.claude/` - `.pi/` -- `.git/`, `.git/hooks/`, `.git/COMMIT_EDITMSG` - `context/`, `context/plans/`, `context/decisions/`, `context/handovers/`, `context/tmp/`, `context/tmp/.gitignore` via `RepoPaths::context_tmp_gitignore_file()` ### Install paths - OpenCode plugin/catalog targets under `.opencode/` -- required git hook install targets under `.git/hooks/` +- required git hook install targets are resolved from git truth by the setup service, not by this catalog (the former `.git/`, hooks and `COMMIT_EDITMSG` path helpers were removed as unused) Compile-time payload locations are build-script-owned `OUT_DIR` paths rather than default runtime paths and are intentionally absent from this catalog. @@ -41,7 +40,7 @@ Compile-time payload locations are build-script-owned `OUT_DIR` paths rather tha - `cli/src/services/config/mod.rs` now resolves the default repo-local config path through `RepoPaths::sce_config_file()` during config discovery. - `cli/src/services/doctor/inspect.rs` now resolves the repo-local config path through `RepoPaths::sce_config_file()` for local-config health reporting and validation. - `cli/src/services/doctor/inspect.rs` also resolves OpenCode manifest/plugin/preset locations through shared `RepoPaths` and `InstallTargetPaths` accessors instead of owning those paths locally. -- `cli/src/services/setup/mod.rs` now resolves setup target directory names and required hook identifiers through `default_paths.rs` constants/accessors instead of owning those path literals locally. +- `cli/src/services/setup/mod.rs` now resolves setup target directory names through `default_paths.rs` constants/accessors instead of owning those path literals locally. - `cli/src/services/default_paths.rs` includes a regression test that scans non-test Rust source under `cli/src/` and fails when new centralized production path literals appear outside the default-path service. - Active hook runtime no longer resolves or writes collision-safe JSON artifacts under `context/tmp/`; `context/tmp/` remains a repo-relative scratch/session path owned by the default path catalog. - Active hook runtime and `cli/src/services/agent_trace_db/lifecycle.rs` resolve repository-scoped Agent Trace DB files through `agent_trace_storage` and `agent_trace_db_path_for_repository(repository_id)`. Outside a Git repository, lifecycle code returns an actionable "requires a Git repository" diagnostic instead of resolving any sentinel path. diff --git a/context/cli/mutation-trace-agent-attribution.md b/context/cli/mutation-trace-agent-attribution.md index b714ae4df..ee0ab64fb 100644 --- a/context/cli/mutation-trace-agent-attribution.md +++ b/context/cli/mutation-trace-agent-attribution.md @@ -186,8 +186,6 @@ observable tree difference. committed `src/lib.rs` even with identical added text and line number; an exact nested path still attributes; multiple similar nested mutation paths stay independent of the target; a shared basename alone is not a match. -- `runtime/tests.rs` — a still-relevant event behind 128 newer events is never - loaded or reconstructed. - `hooks/mod.rs` (`mutation_attribution_e2e`) — real Git/DB: mutation-only `ai` without provenance; direct+mutation completion; a newer non-exclusive event keeps a line non-AI; adversarial linked-worktree isolation; the three diff --git a/context/cli/mutation-trace-external-mutation-guard.md b/context/cli/mutation-trace-external-mutation-guard.md index 58e70598e..a345c9525 100644 --- a/context/cli/mutation-trace-external-mutation-guard.md +++ b/context/cli/mutation-trace-external-mutation-guard.md @@ -1,6 +1,6 @@ # External-mutation guard -`run_external_mutation_guard`, in +`arm_external_mutation_guard` (which yields an `ArmedExternalMutationGuard` whose `exec` runs the shell), in `cli/src/services/mutation_trace/runtime/external_mutation_guard.rs`, is a harness-neutral runtime primitive — **not** Pi-specific, despite being built alongside the Pi adapter to back Pi's `!`/`!!` `user_bash` — that lets a @@ -27,7 +27,7 @@ composition rather than attributing the ambiguous interval to AI. ```text caller starts the hidden supervisor with {"operation":"arm"} -> acquire ProtectedWorktree (WorktreeLock + ExternalTaintMarker, write-ahead) - -> create a Unix pipe (atomically CLOEXEC via pipe2 on Linux): supervisor + -> create a Unix pipe (CLOEXEC set atomically via pipe2 on Linux only; pipe+fcntl elsewhere leaves an unresolved fork/exec window): supervisor owns both ends while waiting -> durably establish the lifetime-token infrastructure -> write and flush {"status":"armed"} @@ -268,7 +268,7 @@ eventual explicit unlock. wrapper around the existing (still-private) `coordinate_protected`, plus ordinary OS process/fd mechanics. - No Windows support: the guard is Unix-only; a non-Unix build's - `run_external_mutation_guard` unconditionally returns + `arm_external_mutation_guard` unconditionally returns `GuardError::UnsupportedPlatform`. - No recovery beyond the guard's own crash semantics above — a stale, never-armed marker left by some other failure mode is out of scope here. diff --git a/context/cli/mutation-trace-external-taint.md b/context/cli/mutation-trace-external-taint.md index 0f1542f6d..5dbe935cb 100644 --- a/context/cli/mutation-trace-external-taint.md +++ b/context/cli/mutation-trace-external-taint.md @@ -176,7 +176,7 @@ window: the recovery is durable (cursor rebaselined, scope abandoned, revision advanced), the triggering boundary is unprocessed with no `MutationEvent`, the on-disk marker survives, and a later `coordinate()` inherits it and recovers conservatively again without resurrecting the abandoned scope. -`runtime/tests.rs`'s +The former `runtime/tests.rs` test `a_marker_clear_failure_after_a_durable_boundary_keeps_the_marker_for_a_later_recovery` proves an attributable `Advance` commits durably, the returned `MarkerClearAfterCommit` carries the matching `committed` outcome diff --git a/context/cli/mutation-trace-ref-reconciliation.md b/context/cli/mutation-trace-ref-reconciliation.md index dc74874cb..71297c02b 100644 --- a/context/cli/mutation-trace-ref-reconciliation.md +++ b/context/cli/mutation-trace-ref-reconciliation.md @@ -30,6 +30,24 @@ durable evidence.** False retention is acceptable; false deletion is not. no hook / command / `diff_traces` wiring yet (invocation timing is the harness-wiring PR's). +## Status: retained and unwired (PR3) + +The `cli-async-persistence-cleanup-pr3` plan chose **retain and defer**: this +pass and its helpers are kept, but nothing in production calls them and PR3 adds +no caller, hook, command or doctor wiring. Accepted consequence: pins created +by `pin_tree` in `coordinator.rs` are never deleted in production, so pins left +by crashed or failed `coordinate()` paths accumulate until removed by hand +(`git update-ref -d refs/sce/mutation-cursor//`). + +The reconciliation-only cluster (`ref_reconciliation.rs`; `list_pins`, +`delete_pins`, `PinnedRef`, `PinInventoryError` and their helpers in +`git_snapshot.rs`; the durable-root readers, SQL constants and row mappers in +`store.rs`) carries declaration-level `#[allow(dead_code, reason = ...)]` +naming `context/plans/mutation-cursor-ref-reconciliation.md` as the integration +path; the former module-wide allow on `mutation_trace` is gone. Wiring is +future work (see also the `ref-rec` branch); deleting the cluster to shrink +dead-code counts was rejected. + ## Entry point and identity ```rust @@ -221,32 +239,11 @@ objects on its own schedule. ## Testing boundary -`ref_reconciliation.rs`'s inline `#[cfg(test)] mod tests` uses a RAII `Fixture` -(`tempfile::TempDir`, real `git init` repo, checkout id via -`get_or_create_checkout_id`, a schema `RepositoryAgentTraceDb` beside the -worktree so it never perturbs a captured tree) with raw-SQL row seeders, -following the filesystem-touching inline-test precedent (`context/patterns.md`). -Coverage: orphan pin deleted (with/without a worktree row); current-cursor pin -retained without a referencing event; historical `before`/`after` pins retained -after the cursor advances; a pin another worktree durably requires retained -(the `retained > local_required` case); `MissingRequiredPins` fail-closed; a -malformed / symbolic ref fail-closed; idempotence; refs deleted without object -reclamation; and the no-checkout-identity skip (`SkippedNoCheckoutIdentity`; -`open_db` never called; the pin ref byte-identical across the skip). - -`runtime/tests.rs` holds the cross-module integration suite (T09), driven -through the public `reconcile_worktree` (and, for setup, `coordinate`) against -real Git and a real `RepositoryAgentTraceDb`: active-worktree orphan deletion; -current-cursor retention with no referencing event; historical `before`/`after` -retention through a real `A → B → C → D` `coordinate()` history; idempotence; -linked-worktree isolation and cross-worktree degraded-tree retention -(byte-identical pins); a missing required pin failing closed despite another -worktree pinning the same tree; a malformed / symbolic ref failing closed; no -protocol/DB/marker write; no object reclamation; the no-checkout-identity skip; -and two deterministic lock-race regressions (no sleeps): -`reconciliation_blocks_on_the_worktree_lock_and_retains_a_pin_that_becomes_durable_under_it` -(the generic `WorktreeLock` happens-before edge) and -`reconciliation_blocks_until_a_real_coordinate_cas_commits_the_pinned_tree` -(the same edge across the real `coordinate()` `pin → store CAS` path, test-only). +`ref_reconciliation.rs` has no inline tests and `runtime/tests.rs` no longer +exists: the earlier fixtures and `reconcile_worktree` integration and lock-race +suites were removed and are not restored while reconciliation is unwired. The +rules above are the specification the future wiring PR must re-prove with +focused tests. The pin helpers are still exercised by the `git_snapshot` pin +tests. See also: [`mutation-trace-runtime-coordinator.md`](mutation-trace-runtime-coordinator.md), [`mutation-trace-snapshot-service.md`](mutation-trace-snapshot-service.md) (`list_pins` / `delete_pins`), [`mutation-trace-store.md`](mutation-trace-store.md) (`load_tree_roots` / `load_all_tree_roots`), [`mutation-trace-protocol.md`](mutation-trace-protocol.md). diff --git a/context/cli/mutation-trace-runtime-coordinator.md b/context/cli/mutation-trace-runtime-coordinator.md index 07683b292..02fe87a79 100644 --- a/context/cli/mutation-trace-runtime-coordinator.md +++ b/context/cli/mutation-trace-runtime-coordinator.md @@ -30,11 +30,11 @@ The per-worktree runtime lock, the isolated Git snapshot service (which also derives `WorktreeId` from Git topology), the coordinator's internal protocol-integration pipeline, and the public, lock-wrapped `coordinate()` entrypoint that owns the external-taint fence and DB provider around that -pipeline all exist, with cross-module integration tests in `runtime/tests.rs` -exercising the public API end to end. Only harness/command wiring remains. +pipeline all exist (the former cross-module integration tests in `runtime/tests.rs` were removed; +see Testing boundary). Only harness/command wiring remains. - `cli/src/services/mutation_trace/runtime/worktree_lock.rs` — - `WorktreeLock::acquire(git_dir: &Path, timeout: Duration) -> + `WorktreeLock::acquire_async(git_dir: &Path, timeout: Duration) -> Result` opens/creates `/sce/mutation-cursor.lock` and polls `std::fs::File::try_lock()` on a 100ms interval against the caller-supplied bounded `timeout`, rather @@ -289,28 +289,15 @@ durable, the boundary unprocessed with no `MutationEvent`, the marker still present, and a later `coordinate()` re-recovering off it; another proves an attributable `Advance` that commits then fails its trailing `marker.clear()` surfaces `MarkerClearAfterCommit` with the matching committed outcome. The -`after_load` seam is exercised by the reconciliation pin→CAS lock-race regression -([`mutation-trace-ref-reconciliation.md`](mutation-trace-ref-reconciliation.md)), pausing a real `coordinate()` between `pin` and CAS. +The `after_load` seam was used by the removed reconciliation pin→CAS lock-race regression and is unused while that suite is gone. -`runtime/tests.rs` is `runtime`'s own `#[cfg(test)] mod tests`, holding -cross-module integration tests that drive the public `coordinate()` and -`abandon_scope()` APIs against real Git repositories (`git init`, `git worktree -add`) and real temp-file `RepositoryAgentTraceDb`s, following the same -unique-temp-path precedent: two linked worktrees of one repository (different -`git_dir` → different lock paths → different `WorktreeId`s) are proven -independently locked by holding one worktree's `WorktreeLock` across a -synchronous `coordinate()` call for the other and observing that call return -`Ok` before the held guard is dropped — a shared lock could not be acquired -while the guard is alive, and no wall-clock timing is used. Each call is handed -a provider closure that opens the one shared repository-scoped DB path -(`coordinate()` never resolves the DB), and both distinct worktree rows then -coexist in it. A full failure/recovery cycle and the cross-runtime abandonment -regressions also run through the public entrypoints. The same module covers the -public `reconcile_worktree` integration suite and the -`coordinate_inner` / `reconcile_worktree_inner` / `abandon_scope_inner` -lock-race seams, including orphan reclamation, durable-root retention, -missing-pin fail-closed behavior, malformed refs, linked-worktree scoping, and -the real coordinator and abandonment CAS races. +There is no `runtime/tests.rs` cross-module integration suite: the ordinary +coordinator, abandonment and reconciliation test suites were removed during the +async-persistence work and are deliberately not restored (PR3 does not restore +tests). Coverage of `coordinate()` / `abandon_scope()` now rests on the Quint +MBT suite in `mutation_trace/mbt`, the `mutation_trace` unit tests and the +inline `worktree_lock.rs` / `git_snapshot.rs` tests; `reconcile_worktree` has no +test coverage while it remains unwired. ## Status @@ -319,7 +306,7 @@ pipeline, the public `coordinate()` entrypoint (prefix → DB provider → pipel → `complete()` on success), and the second `abandon_scope()` entrypoint sharing that prefix are all implemented and both `pub(crate)` re-exported from `runtime/mod.rs` ([`mutation-scope-runtime.md`](mutation-scope-runtime.md)), -with `runtime/tests.rs` covering `coordinate()` end to end and both entrypoints +with the former `runtime/tests.rs` end-to-end coverage removed (see Testing boundary) and both entrypoints driven together; an inherited external-taint marker is overlaid onto `database_failure` recovery on the next invocation. The generic `sce hooks mutation-scope` CLI ingress now drives both entrypoints diff --git a/context/cli/mutation-trace-scope-abandonment.md b/context/cli/mutation-trace-scope-abandonment.md index 247b0c089..9993f2cda 100644 --- a/context/cli/mutation-trace-scope-abandonment.md +++ b/context/cli/mutation-trace-scope-abandonment.md @@ -163,7 +163,7 @@ whole transition back (below); and a `clear()` failure returning A `pub(super) abandon_scope_inner(.., after_load)` seam (mirroring `coordinate_inner`) fires once per CAS attempt after the projection loads, so a -test — in this module or in `runtime/tests.rs` — can land a competing write +test in this module can land a competing write inside the CAS window. It is invisible outside `runtime`; production passes a no-op. @@ -188,8 +188,8 @@ exists, and the external-taint marker is still armed — the general rule that ### Cross-runtime regressions -`runtime/tests.rs` ([`mutation-trace-runtime-coordinator.md`](mutation-trace-runtime-coordinator.md#testing-boundary)) -drives `coordinate()` and `abandon_scope()` together over real `git init` / +The former `runtime/tests.rs` cross-module suite was removed ([`mutation-trace-runtime-coordinator.md`](mutation-trace-runtime-coordinator.md#testing-boundary)); it +drove `coordinate()` and `abandon_scope()` together over real `git init` / `git worktree add` repositories and one real repository-scoped Agent Trace DB, covering what a single-module test with no real Git cannot: diff --git a/context/cli/mutation-trace-snapshot-service.md b/context/cli/mutation-trace-snapshot-service.md index 2ba710f5d..24e220938 100644 --- a/context/cli/mutation-trace-snapshot-service.md +++ b/context/cli/mutation-trace-snapshot-service.md @@ -119,8 +119,8 @@ second, read-only caller: the bounded mutation-history attribution consumer transition and reads baseline file content through its own `TreeReadSource` trait (see [`mutation-trace-agent-attribution.md`](mutation-trace-agent-attribution.md)). -`list_pins` / `delete_pins` are consumed by the per-worktree -ref-reconciliation maintenance pass. +`list_pins` / `delete_pins` are consumed only by the per-worktree +ref-reconciliation maintenance pass, which is retained but unwired (PR3 retain-and-defer): no production code deletes pins, so pins orphaned by crashed or failed `coordinate()` paths accumulate until removed by hand. Their declarations carry `#[allow(dead_code, reason = ...)]` pointing at `context/plans/mutation-cursor-ref-reconciliation.md`. See [`mutation-trace-ref-reconciliation.md`](mutation-trace-ref-reconciliation.md). ## Testing boundary diff --git a/context/cli/mutation-trace-store.md b/context/cli/mutation-trace-store.md index eec5790fe..52b26de42 100644 --- a/context/cli/mutation-trace-store.md +++ b/context/cli/mutation-trace-store.md @@ -136,7 +136,7 @@ worktree is still an `Err` there. cold-path, read-only queries — siblings of `load_mutation_event`, never reached from `load_worktree` or a hook-boundary path — that expose the set of Git tree SHAs the mutation-cursor protocol still durably depends on, for the -per-worktree ref-reconciliation pass (see +per-worktree ref-reconciliation pass, which is retained but unwired (PR3 retain-and-defer; these readers, `load_mutation_event` and their SQL/row-mapping helpers carry declaration-level `dead_code` allows citing `context/plans/mutation-cursor-ref-reconciliation.md`; see [`mutation-trace-runtime-coordinator.md`](mutation-trace-runtime-coordinator.md)). - `load_tree_roots(worktree) -> BTreeSet` returns one worktree's roots: diff --git a/context/cli/patch-service.md b/context/cli/patch-service.md index 20d8ae307..4ede0648b 100644 --- a/context/cli/patch-service.md +++ b/context/cli/patch-service.md @@ -38,7 +38,6 @@ All types derive `Clone, Debug, Deserialize, Eq, PartialEq, Serialize` and suppo Storage-agnostic helpers for reconstructing `ParsedPatch` from serialized JSON content: - `load_patch_from_json(input: &str) -> Result` — loads a `ParsedPatch` from a JSON string; callers who have already read JSON from a database or file can pass the string directly -- `load_patch_from_json_bytes(input: &[u8]) -> Result` — loads a `ParsedPatch` from JSON bytes; convenient when the caller has raw bytes (for example, from a database BLOB column or file read) rather than a UTF-8 string Both functions wrap `serde_json::from_str`/`serde_json::from_slice` and map serde errors to actionable `PatchLoadError` messages. `PatchLoadError` carries a `message` field describing why the JSON payload could not be reconstructed into a valid `ParsedPatch`. @@ -96,7 +95,7 @@ builder, pending state, or schema migration is introduced. | Operation | Wired into | Notes | |-----------|-----------|-------| | `parse_patch` | Hook runtime, Agent Trace DB recent-row parsing, tests | Consumed by `post-commit` capture flow and `recent_diff_trace_patches` to parse stored raw `diff_traces.patch` text | -| `load_patch_from_json` / `load_patch_from_json_bytes` | Storage-agnostic JSON reconstruction callers | Reconstructs serialized `ParsedPatch` JSON when callers already have JSON payloads; not used for raw `diff_traces.patch` text in `recent_diff_trace_patches` | +| `load_patch_from_json` | Storage-agnostic JSON reconstruction callers | Reconstructs serialized `ParsedPatch` JSON when callers already have JSON payloads; not used for raw `diff_traces.patch` text in `recent_diff_trace_patches` | | `intersect_patches` | Post-commit hook runtime | Combines recent patches then intersects with current commit patch | | `combine_patches` | Post-commit hook runtime | Combines chronological recent patches before intersection | diff --git a/context/cli/repository-identity.md b/context/cli/repository-identity.md index e14b35b7a..890d78a49 100644 --- a/context/cli/repository-identity.md +++ b/context/cli/repository-identity.md @@ -12,8 +12,6 @@ The root module (`mod.rs`) performs no I/O: it never opens databases, reads Git - `canonicalize_remote_url(raw) -> Result` — canonicalization without hashing. - `derive_repository_id(canonical_identity) -> String` — domain-separated SHA-256 hex. - `REPOSITORY_ID_HASH_DOMAIN` — the `b"sce-repository-id-v1\0"` prefix constant. -- `repository_dir_segment(canonical_identity) -> String` — pure human-readable on-disk directory segment `-`, where `slug` is the lowercased canonical identity with non-alphanumeric runs collapsed to a single `-` and leading/trailing `-` trimmed, and `short` is the first 4 hex chars of `SHA256(canonical_identity)` with **no** domain prefix (deliberately distinct from `repository_id`, which keeps its `sce-repository-id-v1\0` prefix). All-non-alphanumeric input slugs to empty and the segment falls back to just `short`. Display/layout helper only — the authoritative identity stays `repository_id`. Not yet wired into path construction (T02 of `human-readable-repo-db-directory`). -- `RepositoryIdentity::dir_segment(&self) -> String` — convenience wrapper over `repository_dir_segment` for `self.canonical_identity`. ## Canonicalization rules (remote URLs) diff --git a/context/context-map.md b/context/context-map.md index 479207d19..01c79e81d 100644 --- a/context/context-map.md +++ b/context/context-map.md @@ -16,7 +16,7 @@ Feature/domain context: - `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` (async repository-scoped Agent Trace storage resolver in `cli/src/services/agent_trace_storage/`: `AgentTraceStorageContext` inputs mirroring the `agent_trace.*` config keys, shared degradation of invalid default-discovered config before identity fallback while explicit selections remain fatal, `ResolvedAgentTraceStorage` carrying repository identity + `/sce/repos//agent-trace.db` path + open `RepositoryAgentTraceDb` + typed `RepositoryMetadata`, `resolve_agent_trace_storage{,_at_state_root}` setup/lifecycle 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, a separate no-migration `resolve_agent_trace_storage_for_hook_runtime{,_at_state_root}` pair that high-frequency hook callers use exclusively and that never runs migration `002` or any migration, 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/checkout-identity.md` (checkout identity infrastructure, `cli/src/services/checkout/` and its `/sce/checkout-id` UUIDv7 storage, was removed by the `remove-checkout-id` plan; documents the removal plus the never-touch on-disk handling of any pre-existing checkout-id/pre-migration checkout-scoped DB files left behind) -- `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; repository structured-row reconstruction supplies persisted hunk-model and canonical touched-line-session provenance before these operations; `parse_patch`, `intersect_patches`, and `combine_patches` are consumed by the active post-commit hook runtime) +- `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` 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; repository structured-row reconstruction supplies persisted hunk-model and canonical touched-line-session provenance before these operations; `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, Rust golden fixture coverage, and repository read-time enrichment that assigns persisted row `model_id` to each hunk and canonical row `session_id` to each touched line) - `context/cli/styling-service.md` (CLI text-mode output styling with `owo-colors`, TTY/`NO_COLOR` policy, shared helper API for human-facing surfaces including sync completion markers, and per-column right-to-left RGB gradient banner rendering) - `context/cli/agent-trace-auto-sync.md` (setup-enabled post-commit Agent Trace synchronization with explicit-false opt-out plus doctor readiness reporting: a newly created repo-local config supplies the explicit `true` opt-in while omitted runtime values resolve to `false`; the existing `sce sync` command is launched once through the current executable after local persistence, with detached null-standard-stream behavior, fail-open startup, canonical managed-block proof of hook readiness, stable text/JSON enabled/disabled/not-ready/not-applicable states, no daemon/queue/high-frequency trigger, and retryability through manual sync and control-plane cursor authority) @@ -24,16 +24,16 @@ Feature/domain context: - `context/cli/async-persistence-guarantees.md` (verified guarantees and named limits of the directly awaited Turso persistence migration — exclusive `&mut` transactions, deferred rollback, credential-rotation and terminal-sync behavior, lease/descriptor protection — plus PR1–PR4 staging and the PR3 cleanup ledger) - `context/cli/agent-trace-sync-command.md` (composed local-to-control-plane `sce sync` architecture: the `hooks/plugins → repository Agent Trace DB → AgentTraceExportReader → sce sync → HTTPS + WorkOS Bearer → control plane` data flow, the `sce auth login` / `cd ` / `sce sync` user flow, the no-local-cursor/no-`agent-trace-sync.db`/no-Turso-Sync/no-`BridgeLock`/no-local-DWH invariants, the four-local-capture-streams versus three-remote-streams boundary with its `diff_traces` release gate, and `401`/`409`/ambiguous-batch-failure recovery semantics) - `context/cli/config-precedence-contract.md` (implemented `sce config` show/validate command contract, deterministic `flags > env > config file > defaults` resolution order, focused `config/resolver.rs` ownership for config discovery/merge/runtime precedence plus default-discovered invalid-file degradation, focused `config/render.rs` ownership for `show`/`validate` text+JSON output construction, canonical `$schema` acceptance for startup-loaded `sce/config.json` files, shared auth-key env/config/optional baked-default support starting with `workos_client_id`, shared runtime resolution for flat logging observability keys including config-file/default `log_to_file`, `log_dir` / `SCE_LOG_DIR` with `/sce/logs` defaulting plus config-file/default-only positive `log_file_retention_limit`, config-file-only `agent_trace.repository_id`/`agent_trace.repository_remote` repository-identity keys with default remote `origin`, `agent_trace.auto_sync` resolution with omitted-value fallback `false`, explicit-false opt-out, and setup-written explicit `true` bootstrap behavior for the post-commit trigger boundary, the catalog-derived `integrations.optional_workflows` optional-workflow selection key, JSON-pointer-prefixed schema-validation errors, canonical Pkl-generated `sce/config.json` schema ownership plus CLI embedding/reuse contract including `policies.attribution_hooks.enabled` default-true/explicit-false opt-out metadata, config-file selection order, `show` provenance output, and trimmed `validate` output contract) -- `context/cli/capability-traits.md` (current broad CLI capability seam in `cli/src/services/capabilities.rs`, including `FsOps`/`StdFsOps`, `GitOps`/`ProcessGitOps`, git root/hooks resolution behavior, compile-time-typed borrowed AppContext wiring with scoped borrowing across directly awaited commands with associated-type narrow capability accessors plus `ContextWithRepoRoot` repo-root-scoped context derivation, generic command execution bounds, and test-only unimplemented stubs; current service internals do not consume fs/git traits until later lifecycle migration tasks) +- `context/cli/capability-traits.md` (current broad CLI capability seam in `cli/src/services/capabilities.rs`, including `FsOps`/`StdFsOps` (method-less marker), `GitOps`/`ProcessGitOps` (command execution and availability), compile-time-typed borrowed AppContext wiring with scoped borrowing across directly awaited commands with associated-type narrow capability accessors plus `ContextWithRepoRoot` repo-root-scoped context derivation, generic command execution bounds, and test-only unimplemented stubs; current service internals do not consume fs/git traits until later lifecycle migration tasks) - `context/cli/service-lifecycle.md` (current async lifecycle seam in `cli/src/services/lifecycle.rs`, including default no-op `ServiceLifecycle` diagnose/fix/setup methods against narrow `HasRepoRoot`, lifecycle-owned health/fix/setup result types with generic setup messages, doctor/setup adapter boundaries, the static `LifecycleProvider` enum catalog/dispatcher, hook/config/local_db/auth_db/agent_trace_db lifecycle providers including setup-time repository-scoped Agent Trace DB initialization plus checkout identity diagnostics, implemented doctor aggregation over diagnose/fix providers, and implemented setup aggregation over `setup` providers in order config → local_db → auth_db → agent_trace_db → hooks when requested) - `context/cli/mutation-trace-protocol.md` (pure, dependency-free `cli/src/services/mutation_trace/` refinement of the verified `spec/mutation_cursor.qnt` mutation-cursor protocol: the full pure action set — `prepare`/`commit` transition logic, attribution/mutation-event materialization, snapshot-failure/database-failure taint actions, scope abandonment, and recovery with an explicit observed-tree input — plus cross-action sequence/invariant tests and a module-level Quint refinement matrix are all complete (`types.rs`, `protocol.rs`, `mod.rs`, `tests.rs`, registered with `#[allow(dead_code)]`); opaque-newtype identity refinement decisions vs. the Quint model's bounded enums, and the `coordinator.rs` target end-state seam this layout leaves room for but does not create — `store.rs`, `runtime/git_snapshot.rs`, and `runtime/coordinator.rs` (including its public `coordinate()` entrypoint) now exist, built out by the `mutation-cursor-store-persistence` and `mutation-cursor-runtime-coordinator` plans respectively; `protocol.rs` is invoked only through the runtime, and the runtime is now driven by the generic `sce hooks mutation-scope` CLI ingress and a first Claude Code adapter driver, registered by `sce setup` and reachable by a real Claude Code session, now proven against real Git repositories and a real Agent Trace DB by the `claude-mutation-scope-integration` plan's T08 regressions) - `context/cli/mutation-trace-runtime-materialization.md` (how the finite, pre-populated Quint `SCOPES`/`WORKTREES` universes map onto the Rust protocol's unbounded identity space: the coordinator/store layer materializes scope and worktree state, scope identity is immutable once materialized, and a missing scope is distinct from a `NeverSeen` scope; `protocol.rs` only transitions already-known identities) -- `context/cli/mutation-trace-agent-attribution.md` (causal mutation-lineage attribution: direct target-shaped coverage is resolved first, then the newest-128 events for the invoking worktree (bounded also by a commit attribution cut — `revision <= latest_mutation_event_revision` captured under the worktree lock) are replayed oldest-to-newest as one ordered sequence of tree transitions. `lineage.rs` is a pure module propagating per-line `LineProvenance` (`Unknown` / `MutationAi{scope}` / `MutationNonAi`) forward through structurally-applied hunks: context carries, removed is permanently deleted, added takes only the introducing transition's origin, replacement never transfers by text. A committed line is AI only if an AI event's line survives every later transition — including a history-gap reload and the unobserved latest-tree→commit-tree tail — into the committed tree; anything unproven stays `Unknown`. Historical patches are never matched against the committed patch. `attribution.rs` keeps only `exclude_direct_coverage` + `patch_for_locations`. The `runtime/mutation_attribution.rs` consumer drives it over `MutationEventPageSource` + `TreeReadSource` (`diff_trees` + `file_at_tree`) seams with conservative fail-closed reloads; wired into post-commit via the read-only `runtime::resolve_post_commit_mutation_ai_patch` entrypoint — existing checkout identity only, direct-only fallback on absent identity/history/unavailable cut, no identity creation, no mutation-cursor write, `diff_traces` / `post_commit_patch_intersections` unchanged; see `agent-trace-hooks-command-routing.md` and `agent-trace-minimal-generator.md` — with real Git/DB post-commit regressions in `cli/src/services/hooks/mod.rs` and a real 128/129-horizon regression in `runtime/tests.rs`) +- `context/cli/mutation-trace-agent-attribution.md` (causal mutation-lineage attribution: direct target-shaped coverage is resolved first, then the newest-128 events for the invoking worktree (bounded also by a commit attribution cut — `revision <= latest_mutation_event_revision` captured under the worktree lock) are replayed oldest-to-newest as one ordered sequence of tree transitions. `lineage.rs` is a pure module propagating per-line `LineProvenance` (`Unknown` / `MutationAi{scope}` / `MutationNonAi`) forward through structurally-applied hunks: context carries, removed is permanently deleted, added takes only the introducing transition's origin, replacement never transfers by text. A committed line is AI only if an AI event's line survives every later transition — including a history-gap reload and the unobserved latest-tree→commit-tree tail — into the committed tree; anything unproven stays `Unknown`. Historical patches are never matched against the committed patch. `attribution.rs` keeps only `exclude_direct_coverage` + `patch_for_locations`. The `runtime/mutation_attribution.rs` consumer drives it over `MutationEventPageSource` + `TreeReadSource` (`diff_trees` + `file_at_tree`) seams with conservative fail-closed reloads; wired into post-commit via the read-only `runtime::resolve_post_commit_mutation_ai_patch` entrypoint — existing checkout identity only, direct-only fallback on absent identity/history/unavailable cut, no identity creation, no mutation-cursor write, `diff_traces` / `post_commit_patch_intersections` unchanged; see `agent-trace-hooks-command-routing.md` and `agent-trace-minimal-generator.md` — with real Git/DB post-commit regressions in `cli/src/services/hooks/mod.rs`) - `context/cli/mutation-trace-revision-refinement.md` (the Quint `revision: int` → Rust `WorktreeState::revision: u64` bounded-integer refinement: the private `next_revision` checked-arithmetic helper `commit`/`taint`/`abandon`/`recover` all route through instead of a raw `+ 1`, so a worktree at `revision: u64::MAX` is a guarded no-op/rejection rather than a silent wrap to `0`) - `context/cli/mutation-trace-quint-connect.md` (`#[cfg(test)]`-only Quint Connect model-based-testing harness in `cli/src/services/mutation_trace/mbt/` continuously checking `protocol.rs` against `spec/mutation_cursor.qnt`: the verification-only `mbtAction`/`MbtAction` record-payload transport excluded from comparison, the operation-identity-vs-`MbtStutter` distinction on guarded/no-op branches with its two deterministic regressions, finite ID mapping, the AC5 comparable-state field list, `randomPrepare` staying a single `step` branch, deterministic/generated (500×30, seed-reproducible) test coverage, and the two Nix checks — generic `checks.cli-tests` and dedicated `checks.mutation-trace-quint-connect` — that both require the pinned Quint binary plus the top-level `spec/` directory in `workspaceSrc`'s Nix fileset) - `context/cli/mutation-trace-store.md` (durable persistence for the mutation-cursor protocol in `cli/src/services/mutation_trace/store.rs`, built by the `mutation-cursor-store-persistence` plan: the one-directional `protocol.rs` -> `DurableTransition::between` (pure structural diff) -> `store.rs` (SQL translation) -> `RepositoryAgentTraceDb` boundary; migration `004_mutation_trace_protocol.sql`'s five tables; migration `006_mutation_trace_health_invariant.sql`'s transactional rebuild of the worktree and event tables with a cross-column `tainted`/`failure_kind` health `CHECK`, `failure_kind`-authoritative legacy normalization, and a re-runnable SQL body, plus the shared fail-closed `validate_health_encoding` used by all three health-row decoders; `AttemptState`/`external_taint` non-persistence; the 8-byte big-endian `BLOB` revision encoding plus explicit non-`Debug` enum codecs; the bounded hot-path `load_worktree` read vs. the cold-path `load_mutation_event` and descending, exact-worktree, cursor-paged `load_mutation_event_page` reader capped at 32 rows; the public cold-path single-row `load_scope` scope seam that returns one `mutation_trace_scopes` row as `Option` without widening into a projection and, unlike `load_worktree`, never adjudicates worktree identity (a cross-worktree scope is returned as-is, and the mismatch is the caller's decision); the cold-path read-only `load_tree_roots` (one worktree) / `load_all_tree_roots` (repository-wide) durable-tree-SHA queries for ref reconciliation, each a single-statement `UNION` of `cursor_tree`/`before_tree`/`after_tree` read from one DB snapshot; `commit`'s single-`BEGIN IMMEDIATE` CAS batch via `TursoDb::execute_transactional_cas_batch`, distinguishing `Conflict`/retryable-transient/deterministic-`Err` outcomes; and the store's non-goals — no Git/filesystem I/O, no attribution/boundary-kind decisions, no retry-after-`Conflict` loop, no row deletion) - `context/cli/mutation-scope-provenance.md` (the insert-once `mutation_trace_scope_provenance` table from migration `005_mutation_scope_provenance.sql` and its `MutationTraceStore::register_scope_provenance` / `load_scope_provenance` seams: `scope_id -> session_id + model_id?` metadata *about* an established scope, stored beside but never inside protocol state — outside `ProtocolState` and the CAS batch, and never an input to `IneligibleUnscoped` / `AiExclusive` / `AiContended`; `session_id` is immutable identity and `model_id` immutable first-observed metadata, so the first persisted row wins in both directions and only a differing `session_id` for an existing `scope_id` errors; the supported `MutationTraceStore` write seam requires an existing owning `mutation_trace_scopes` row and never creates one implicitly, enforced in the store rather than by a `FOREIGN KEY`, leaving `004_mutation_trace_protocol.sql` unchanged; creation is additionally admission-bounded in the runtime — a row may be created only while the durable scope is `NeverSeen`, so after admission absent provenance stays absent permanently and a later `Start` replay cannot backfill it, while an existing row is still validated on every provenance-carrying `Start`; producers own canonicalization — both Codex and Claude adapters supply provenance today, with Claude resolving exact `(cc_, agent_id)` model state at admission and degrading unavailable models to `NULL`) -- `context/cli/mutation-trace-runtime-coordinator.md` (imperative-shell runtime layer in `cli/src/services/mutation_trace/runtime/`, built by the `mutation-cursor-runtime-coordinator` plan: the per-worktree OS advisory lock, `runtime::worktree_lock::WorktreeLock::acquire(git_dir, timeout)` at `/sce/mutation-cursor.lock`, bounded `try_lock()` polling with a distinct matchable timeout error and RAII release, and its distinction from the separate checkout-identity-creation lock; the isolated Git snapshot service `runtime::git_snapshot::GitSnapshotService` (documented in `mutation-trace-snapshot-service.md`); and `runtime::coordinator`'s protocol-integration pipeline (`RuntimeBoundary`/`CoordinateOutcome`/`CoordinateError`, the load → recover-if-needed → prepare/commit CAS-retry loop, and its own bounded snapshot-failure taint-retry loop) plus the public `coordinate(repository_root, boundary, open_db)` entrypoint that runs the shared `runtime::protected_worktree` prefix (`WorktreeLock` -> external-taint write-ahead fence -> `WorktreeId`, extracted so a second entrypoint cannot drift from it and documented in `mutation-trace-protected-worktree.md`), invokes the caller-supplied `open_db` provider so DB acquisition falls inside that fence, drives the pipeline under one held lock, and clears the marker through `ProtectedWorktree::complete()` only on success; `runtime/tests.rs` covers the public `coordinate()` API end to end against real linked worktrees and a real Agent Trace DB; an inherited external-taint marker is overlaid onto `protocol::database_failure` recovery on the next invocation, against the single captured snapshot and re-injected across a losing recovery CAS; the private `runtime::ref_reconciliation` per-worktree snapshot-ref maintenance pass (documented in `mutation-trace-ref-reconciliation.md`) shares the same `WorktreeLock`; `coordinate()` and `abandon_scope()` are now `pub(crate)` re-exported from `runtime/mod.rs` (documented in `mutation-scope-runtime.md`) while the runtime submodules themselves stay private; a generic `sce hooks mutation-scope` CLI ingress now drives both entrypoints, joined by current Claude Code and Codex adapter drivers (registered by `sce setup`/`sce setup --codex` and reachable) and an OpenCode adapter driver (lifecycle + recovery wired, and now generated as the final `sce-mutation-scope.ts` plugin and installed by `sce setup`), documented in `mutation-scope-runtime.md`) +- `context/cli/mutation-trace-runtime-coordinator.md` (imperative-shell runtime layer in `cli/src/services/mutation_trace/runtime/`, built by the `mutation-cursor-runtime-coordinator` plan: the per-worktree OS advisory lock, `runtime::worktree_lock::WorktreeLock::acquire_async(git_dir, timeout)` at `/sce/mutation-cursor.lock`, bounded `try_lock()` polling with a distinct matchable timeout error and RAII release, and its distinction from the separate checkout-identity-creation lock; the isolated Git snapshot service `runtime::git_snapshot::GitSnapshotService` (documented in `mutation-trace-snapshot-service.md`); and `runtime::coordinator`'s protocol-integration pipeline (`RuntimeBoundary`/`CoordinateOutcome`/`CoordinateError`, the load → recover-if-needed → prepare/commit CAS-retry loop, and its own bounded snapshot-failure taint-retry loop) plus the public `coordinate(repository_root, boundary, open_db)` entrypoint that runs the shared `runtime::protected_worktree` prefix (`WorktreeLock` -> external-taint write-ahead fence -> `WorktreeId`, extracted so a second entrypoint cannot drift from it and documented in `mutation-trace-protected-worktree.md`), invokes the caller-supplied `open_db` provider so DB acquisition falls inside that fence, drives the pipeline under one held lock, and clears the marker through `ProtectedWorktree::complete()` only on success; an inherited external-taint marker is overlaid onto `protocol::database_failure` recovery on the next invocation, against the single captured snapshot and re-injected across a losing recovery CAS; the private `runtime::ref_reconciliation` per-worktree snapshot-ref maintenance pass (documented in `mutation-trace-ref-reconciliation.md`) shares the same `WorktreeLock`; `coordinate()` and `abandon_scope()` are now `pub(crate)` re-exported from `runtime/mod.rs` (documented in `mutation-scope-runtime.md`) while the runtime submodules themselves stay private; a generic `sce hooks mutation-scope` CLI ingress now drives both entrypoints, joined by current Claude Code and Codex adapter drivers (registered by `sce setup`/`sce setup --codex` and reachable) and an OpenCode adapter driver (lifecycle + recovery wired, and now generated as the final `sce-mutation-scope.ts` plugin and installed by `sce setup`), documented in `mutation-scope-runtime.md`) - `context/cli/mutation-trace-protected-worktree.md` (the shared safety prefix every mutation-cursor runtime entrypoint runs behind, in `cli/src/services/mutation_trace/runtime/protected_worktree.rs`, extracted from `coordinate()` by the `mutation-scope-runtime-integration` plan so a second entrypoint cannot drift from it: `ProtectedWorktree::acquire(repository_root)` running the safety-critical fixed order resolve `git_dir` → `WorktreeLock` (module-owned 10s `WORKTREE_LOCK_TIMEOUT`) → `ExternalTaintMarker::exists()` → `persist()` (fence armed write-ahead of every fallible step that follows, including DB acquisition) → `get_or_create_checkout_id` as `WorktreeId`; the `worktree_id()` / `inherited_external_taint()` / consuming `complete()` surface, where `complete()` clears the marker under the still-held lock and is the only thing that ever clears it while `Drop` releases only the lock; and the one-variant-per-prefix-step `ProtectedWorktreeError` (`GitDirResolution` | `LockAcquisition` | `ExternalTaintMarker { operation, source }` | `CheckoutIdentity`) each entrypoint maps onto its own error surface — `coordinate()` onto exactly the `CoordinateError` variants that step produced before the extraction) - `context/cli/mutation-trace-scope-abandonment.md` (the mutation-cursor runtime's second protected entrypoint in `cli/src/services/mutation_trace/runtime/scope_runtime.rs`, built by the `mutation-scope-runtime-integration` plan and the first production call site for `protocol::abandon`: `abandon_scope(repository_root, scope, open_db) -> Result` retires a scope whose final worktree boundary was never observed, sharing `coordinate()`'s `ProtectedWorktree` prefix but deliberately capturing **no** Git snapshot, pin, diff, reconciliation, scope registration, or worktree initialization — so abandonment is not a `RuntimeBoundary` and needs no Quint change; the classify-before-transition order that recovers what `protocol::abandon`'s uniform guarded no-op cannot report (`load_scope` first for the missing-row and cross-worktree cases the projection seam treats as errors, then `load_worktree` for the `NeverSeen` / terminal / `Active` split); the `Abandoned` / `AlreadyTerminal` / `RecoveryRequired` outcomes and the `InheritedExternalTaint` | `MissingScope` | `NeverSeenScope` | `MissingWorktreeState` recovery reasons; the inherited-marker short-circuit that returns before the DB provider is ever invoked; the fence-completion rule that clears the marker only for a settled abandonment or proven-terminal no-op and leaves it armed for every recovery-required outcome and every error, with `MarkerClearAfterCompletion` carrying the already-settled outcome; the CAS retry bounded by the coordinator's shared `MAX_CAS_RETRY_ATTEMPTS`, settling on a competitor's terminal status rather than overwriting it; and the deliberate false-negative tradeoff whereby a missing or `NeverSeen` target forces conservative strong recovery that may abandon unrelated live scopes, because attribution safety outranks preserving potentially valid evidence) - `context/cli/mutation-scope-runtime.md` (the crate-visible mutation-trace runtime seam and the lifecycle contract every current or future harness adapter must uphold, recorded by the `mutation-scope-runtime-integration` plan: the ten `pub(crate)` re-exports in `runtime/mod.rs` (`coordinate`, `RuntimeBoundary`, `StartProvenance`, `CoordinateOutcome`, `CoordinateError`, `ExternalTaintOperation`, `abandon_scope`, `AbandonScopeOutcome`, `AbandonRecoveryReason`, `AbandonScopeError`), with `ExternalTaintOperation` riding through `coordinator.rs`'s own `pub use` because `CoordinateError::ExternalTaintMarker` carries it — so the type becomes crate-visible without `protected_worktree` becoming a public module — while every `mod` declaration stays private and `ProtectedWorktree`/`ProtectedWorktreeError`/`WORKTREE_LOCK_TIMEOUT`/`reconcile_worktree` stay internal, kept clippy-clean by `#[allow(unused_imports)]` on the re-exports per the `services/style.rs` precedent rather than a placeholder consumer; and the adapter obligations themselves — a scope is one independently mutation-capable execution so a concurrent main agent and subagent need distinct `ScopeId`s, `Start`/`Advance`/`Close` semantics including that only `Start` may carry the optional `StartProvenance`, registered after scope registration and before the protocol commits and never inside `ProtocolState` or the CAS transition, with that registration conditional on the `ScopeState` `register_scope` returns so a provenance row may only be created while the scope is `NeverSeen` (an admission-time snapshot that a post-admission replay cannot backfill) while an existing row is still validated on every provenance-carrying `Start`, that a failed tool still requires `Advance` (the boundary is an observation, not a successful edit) and that a `ScopeId` is never reused after a terminal status (the `NeverSeen` guard silently refuses to reactivate it), `abandon_scope()` requiring positive staleness evidence and never inferring it from `ActorKind`, the `abandon` → `coordinate(Start(successor))` sequence with what each outcome implies for it including that a failed abandonment must never be treated as a safely started successor, that abandonment is not a `RuntimeBoundary` and needs no Quint change, the D1 tradeoff whereby a missing or `NeverSeen` target deliberately forces conservative strong recovery that may invalidate unrelated live scopes because attribution safety outranks preserving potentially valid evidence, and the attribution boundary that `AiExclusive(scope)` means scope exclusivity only and is not standalone proof that no human edited the worktree; a generic `sce hooks mutation-scope` ingress now drives the seam, joined by current Claude Code and Codex adapter drivers (both registered by `sce setup` and reachable) and an OpenCode adapter driver (full lifecycle + recovery, now generated as the final `sce-mutation-scope.ts` plugin and registered by `sce setup`, so reachable by a real session) — a Pi adapter driver now also exists (`cli/src/services/hooks/pi_mutation_scope/`, hidden `sce hooks pi-mutation-scope`) and is driven by the canonical generated Pi extension in ordinary Pi sessions; its human `user_bash` path uses the separate guarded external-mutation supervisor) @@ -85,7 +85,7 @@ Feature/domain context: - `context/sce/agent-trace-post-rewrite-local-remap-ingestion.md` (current post-rewrite no-op baseline plus historical remap-ingestion reference) - `context/sce/agent-trace-rewrite-trace-transformation.md` (current post-rewrite no-op baseline plus historical rewrite-transformation reference) - `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 async `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 async constructor/open-connect retry via `run_with_retry`, 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 async operation retry for `execute`/`query`/`query_values`/`query_map` (retry-policy budget, not a wall-clock bound), per-`DbSpec` Turso busy timeout on connect (1000 ms for the Agent Trace DB, unset for other databases), the Agent Trace-only outer write-contention retry for replay-safe write units including the opt-in `execute_idempotent_write` and its structured contention-exhaustion error plus `sce.agent_trace_db.contention_exhausted` tracing event, 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`, non-mutating-data `passive_checkpoint()` PASSIVE WAL checkpoint method on `TursoDb` (not on `EncryptedTursoDb`, fail-open, no truncation guarantee, called once by `sce hooks post-commit` after successful Agent Trace persistence — see `context/sce/agent-trace-hooks-command-routing.md`), and async concrete wrappers for `LocalDb`, `AuthDb`, plus `RepositoryAgentTraceDb`; no adapter runtime bridge) +- `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 async constructor/open-connect retry via `run_with_retry`, 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 async operation retry for `execute`/`query_map` (retry-policy budget, not a wall-clock bound), per-`DbSpec` Turso busy timeout on connect (1000 ms for the Agent Trace DB, unset for other databases), the Agent Trace-only outer write-contention retry for replay-safe write units including the opt-in `execute_idempotent_write` and its structured contention-exhaustion error plus `sce.agent_trace_db.contention_exhausted` tracing event, 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`, non-mutating-data `passive_checkpoint()` PASSIVE WAL checkpoint method on `TursoDb` (not on `EncryptedTursoDb`, fail-open, no truncation guarantee, called once by `sce hooks post-commit` after successful Agent Trace persistence — see `context/sce/agent-trace-hooks-command-routing.md`), and async concrete wrappers for `LocalDb`, `AuthDb`, plus `RepositoryAgentTraceDb`; no adapter runtime bridge) - `context/sce/agent-trace-db-write-contention-evidence.md` (measured Agent Trace DB write-contention behavior under the busy-timeout/outer-retry/contention-deadline contract: the `lock_contention_tests.rs` suite with its lock-budget boundary, strict N=2–4 in-process and real-hook gates and N=8 stress characterization, policy history from 500 / 1250 to the current 1000 / 2250 defaults, the supported-load failure that motivated the tuning, before-vs-after comparison, and the bounded-policy exhaustion failure mode) - `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, async encrypted token-storage persistence through a Result-valued Tokio OnceCell, 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 the fresh multi-statement baseline schema plus additive `source_instance_id` and `claude_model_state` migrations, with `repository_metadata` carrying both `repository_id` and a concurrency-safe atomic-claim `source_instance_id` (physical database identity, independent of `repository_id`), 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, parts, and the non-exported exact-scope Claude model-state register, repository-level recent diff-trace reads without checkout filtering including persisted hunk-model plus canonical touched-line-session enrichment for structured rows, async 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`) @@ -124,7 +124,7 @@ Additional mutation-scope integration context: - `context/cli/opencode-mutation-scope-health.md` (the `doctor-mutation-scope-health` plan's T04: `classify_health` in `cli/src/services/hooks/opencode_mutation_scope/health.rs` maps the adapter's `RecoveryState`/`AttemptPhase` combinations onto `healthy | recovering | blocked | invalid`, proven by driving the real dispatch/recovery functions — `Clear` + `PendingAbandon` is `invalid`; any state containing a `PendingStart` attempt is `blocked` (except that `Clear` + `PendingAbandon` retains `invalid` precedence), because no boundary sweeps a stale `PendingStart` on behalf of an unrelated call, unlike Pi's dead-owner sweep or Codex's same-lane retry, so a crashed owning process leaves it a permanent wedge for new admissions even while a `Pending`/`Flushing` recovery for another attempt is also outstanding, since `resolve_recovery` never retires `PendingStart`; `Pending`/`Flushing` with no `PendingStart` outstanding is `recovering`; `Clear` with ordinary `Active`/no attempts is `healthy`; any read/parse failure is `invalid`) - `context/cli/pi-mutation-scope-integration.md` (the fourth concrete harness adapter, `cli/src/services/hooks/pi_mutation_scope/`, hidden command `sce hooks pi-mutation-scope`, driven by the canonical generated extension in ordinary Pi sessions: Pi's `bash`/`edit`/`write` tracked-tool allowlist with `tool_call` as the single universal pre-execution gate for every tool including `bash`; the `pi-tool-v1|n=|s=:|c=:` `ScopeId` whose checkout-local monotonic attempt sequence stops a reused `toolCallId` from ever reactivating a terminal scope; the `PendingStart`→`Executed`→`Closed`/`PendingAbandon` attempt-phase machine with no `Active` phase, because `PendingStart` is Pi's normal resting state for an entire in-flight execution (`tool_execution_start` fires unconditionally before the fail-closed `tool_call` gate and proves nothing; `tool_result` is the sole `Executed` evidence, and `tool_execution_end` with no preceding `tool_result` abandons rather than closes); why admission's fail-closed "uncertain attempt" check covers only a lingering `PendingAbandon` or non-`Clear` recovery state and never a sibling's `PendingStart`, unlike the other three adapters, so concurrent Pi tool calls stay distinct live scopes; a positive-process-death-only stale-`PendingStart`/`Executed` recovery (`getppid()`-captured owner plus Linux `/proc`-start-time PID-reuse proofing, no TTL/sweep), run on every tracked Start admission by independently inspecting persisted attempts rather than only ones matching the incoming key, and adapter reconciliation with the external-mutation guard's forced recovery, both added by a later task in this same plan; and the harness-neutral `external-mutation-guard` mechanism this task also built for Pi's `user_bash`, detailed separately in `context/cli/mutation-trace-external-mutation-guard.md`) - `context/cli/pi-mutation-scope-health.md` (the `doctor-mutation-scope-health` plan's T05: `classify_health` in `cli/src/services/hooks/pi_mutation_scope/health.rs` maps the adapter's `RecoveryState`/`AttemptPhase`/owner-liveness combinations onto `healthy | recovering | blocked | invalid`, proven by driving the real dispatch/recovery functions — unlike Codex and OpenCode, Pi's investigation found no persisted shape reachable through ordinary production dispatch that classifies `blocked`: `admit_tracked_attempt` never gates admission on a sibling's `PendingStart`/`Executed` phase (only on a co-existing `PendingAbandon`, itself proven unreachable under `Clear`), and the D10 dead-owner sweep (`reconcile_stale_owners`) unconditionally retires a dead-owner `PendingStart`/`Executed` attempt on the next tracked `tool_call` from any session, so such an attempt is `recovering`, not `blocked`; `Pending`/`Flushing` are always `recovering` regardless of attempt composition, since a recovery-capable fresh tracked Start — one whose key is not already represented by a nonterminal attempt — can claim and retry the flush, not lane- or key-scoped like Codex, though a duplicate Start for an already-tracked `PendingStart`/`Executed` key is instead reused idempotently before the recovery-state gate and so does not itself advance recovery; `Clear` with a `PendingAbandon` attempt and any read/parse failure are `invalid`) -- `context/cli/mutation-trace-external-mutation-guard.md` (the harness-neutral, not-Pi-specific D13 external-mutation supervisor process, `run_external_mutation_guard` in `cli/src/services/mutation_trace/runtime/external_mutation_guard.rs`, hidden `sce hooks external-mutation-guard`, Unix-only: acquires a `ProtectedWorktree`, reports it armed, spawns the human shell command as its own child in its own process group with a `dup()`-before-`spawn()` fd handed to the child so the `WorktreeLock` flock survives the supervisor's own death via the child's independent descriptor, finishes exclusively on the shell's own `wait()` (never on a cancel/control-channel signal), and only then forces `database_failure`+`recover` on the already-held worktree and calls `ProtectedWorktree::complete()` — reusing existing runtime primitives with zero `protocol.rs`/Quint change; called by the canonical Pi extension for human `user_bash`) +- `context/cli/mutation-trace-external-mutation-guard.md` (the harness-neutral, not-Pi-specific D13 external-mutation supervisor process, `arm_external_mutation_guard`/`ArmedExternalMutationGuard::exec` in `cli/src/services/mutation_trace/runtime/external_mutation_guard.rs`, hidden `sce hooks external-mutation-guard`, Unix-only: acquires a `ProtectedWorktree`, reports it armed, spawns the human shell command as its own child in its own process group with a `dup()`-before-`spawn()` fd handed to the child so the `WorktreeLock` flock survives the supervisor's own death via the child's independent descriptor, finishes exclusively on the shell's own `wait()` (never on a cancel/control-channel signal), and only then forces `database_failure`+`recover` on the already-held worktree and calls `ProtectedWorktree::complete()` — reusing existing runtime primitives with zero `protocol.rs`/Quint change; called by the canonical Pi extension for human `user_bash`) Working areas: diff --git a/context/glossary.md b/context/glossary.md index a5e17a2e8..5687fea4c 100644 --- a/context/glossary.md +++ b/context/glossary.md @@ -81,10 +81,10 @@ - `post-commit Agent Trace auto-sync readiness`: The doctor report fact that explains whether the enabled post-commit trigger is ready without invoking it. Doctor compares the installed `post-commit` hook's SCE managed block using the same currency semantics as setup and resolves config-file-only `agent_trace.auto_sync` with source metadata. JSON states are `ready`, `disabled`, `not_ready`, and `not_applicable`; explicit disable is healthy, while existing hook problems continue to own overall readiness and remediation. See [automatic Agent Trace synchronization](cli/agent-trace-auto-sync.md) and [doctor human text](sce/doctor-human-text-contract.md). - `structured patch service`: Pure synchronous Rust service in `cli/src/services/structured_patch.rs` that derives supported structured editor hook payloads into canonical `ParsedPatch` values. The current implemented source is Claude `PostToolUse` payloads for `Write` creates and `Edit` structured patches; wired into `sce hooks diff-trace` for Claude payload classification at intake and into `RepositoryAgentTraceDb::recent_diff_trace_patches` for post-commit structured payload parsing, where persisted row `model_id` is assigned to every hunk and persisted canonical row `session_id` to every touched line before downstream reconstruction. - `MutationLineage` / `causal mutation-lineage attribution`: `cli/src/services/mutation_trace/lineage.rs` is a pure module holding, per repo path, a vector of `(content, LineProvenance)` where `LineProvenance` is `Unknown`, `MutationAi { scope_id }`, or `MutationNonAi`. `MutationLineage::apply(patch, origin)` advances it one reconstructed tree transition at a time, applying hunks structurally: context lines carry provenance forward, removed lines are deleted permanently, added lines take only the introducing `TransitionOrigin` (`MutationAi` for healthy untainted `AiExclusive`, `MutationNonAi` for any other recorded event, `Unobserved` → `Unknown` for gap reloads and the latest-tree→commit-tree tail); a structurally inconsistent transition is a `LineageError` and the caller reloads that file to an all-`Unknown` baseline. Historical mutation patches are never matched independently against the committed patch — that earlier `MutationPatchEvidence` / strict-matcher model let stale evidence resurrect and is removed. `attribution.rs` now only supplies `exclude_direct_coverage` (drop directly covered lines) and `patch_for_locations`. The store supplies events through an exact-worktree-scoped `mutation-event page` ordered by descending 8-byte big-endian revision, continued with an exclusive revision cursor, capped at 32 rows; plus `latest_mutation_event_revision` for the commit attribution cut. The `bounded mutation-history consumer` (`resolve_bounded_mutation_attribution` in `cli/src/services/mutation_trace/runtime/mutation_attribution.rs`) loads the newest ≤128 in-cut events through `MutationEventPageSource` + `TreeReadSource` (`diff_trees` + `file_at_tree`) seams, replays them oldest-to-newest from a conservative baseline through the tail into `commit_tree`, then projects surviving provenance onto the committed patch; it is current-worktree-only and timestamp-independent, owns `MAX_MUTATION_ATTRIBUTION_EVENTS = 128` (event 128 may contribute, 129 is never loaded), tracks database (`loaded_pages`/`loaded_rows`) apart from Git (`inspected_events`/`reconstructed_events`/`gap_resets`) work, and records a `mutation-attribution barrier` on any page-query or reconstruction/tail failure while still returning conservative results. See [mutation-trace store](cli/mutation-trace-store.md) and [mutation-trace Agent Trace attribution](cli/mutation-trace-agent-attribution.md). -- `Agent Trace SCE metadata`: Implementation-owned top-level metadata emitted by `build_agent_trace(...)` under `metadata.sce`, carrying `version` (sourced from the compiled `sce` CLI package version via `env!("CARGO_PKG_VERSION")`) and `line_changes` (exact `{ai,mixed,unknown}` × `{added,removed}` `u64` touched-line attribution counts derived from canonical `post_commit_patch` hunks, reusing each hunk's existing `Conversation.contributor.type` classification with no independent second classification pass, `#[serde(default)]` for backward-compatible deserialization of pre-existing payloads); the whole object is schema-validated with the rest of the payload and persisted in AgentTraceDb `agent_traces.trace_json` without changing the top-level Agent Trace payload/schema `version`. +- `Agent Trace SCE metadata`: Implementation-owned top-level metadata emitted by `build_agent_trace_from_evidence(...)` under `metadata.sce`, carrying `version` (sourced from the compiled `sce` CLI package version via `env!("CARGO_PKG_VERSION")`) and `line_changes` (exact `{ai,mixed,unknown}` × `{added,removed}` `u64` touched-line attribution counts derived from canonical `post_commit_patch` hunks, reusing each hunk's existing `Conversation.contributor.type` classification with no independent second classification pass, `#[serde(default)]` for backward-compatible deserialization of pre-existing payloads); the whole object is schema-validated with the rest of the payload and persisted in AgentTraceDb `agent_traces.trace_json` without changing the top-level Agent Trace payload/schema `version`. - `ScopeProvenance`: Optional insert-once metadata keyed by a mutation `ScopeId`, storing canonical SCE `session_id` and nullable first-observed `model_id`. It is registered only at `Start` admission while the owning scope is `NeverSeen`, remains outside `ProtocolState` and the CAS transition, and is resolved during mutation projection. `ScopeId` proves mutation ownership; `ScopeProvenance` describes the owning scope. Model/session provenance is observational and never changes `AiExclusive`/`AiContended`/`IneligibleUnscoped` protocol decisions. -- `Agent Trace range content_hash`: Per-range `content_hash` emitted by `build_agent_trace(...)` inside every `ranges[]` entry as `murmur3:`, computed from the touched-line kind/content of the `post_commit_patch` or embedded-patch hunk used to emit that range while excluding positions, paths, metadata, and database IDs. -- `external-mutation guard` / `external-mutation supervisor`: `run_external_mutation_guard` (`cli/src/services/mutation_trace/runtime/external_mutation_guard.rs`, hidden `sce hooks external-mutation-guard`, Unix-only), the harness-neutral, not-Pi-specific mechanism that lets a human-initiated shell command (Pi's `!`/`!!` `user_bash`) mutate a worktree without ever creating a mutation scope: it acquires the same `ProtectedWorktree` guard `coordinate()` uses, reports it armed, itself spawns the human shell as its own child (never signaled or stopped by control-channel closure — only by an explicit cancel request, which it enacts by signaling the shell's own process group), finishes exclusively on the shell's real process termination, and only then forces the existing `database_failure`+`recover` composition on the already-held worktree via `coordinate_on_held_worktree` before clearing the marker with `ProtectedWorktree::complete()`. See [`mutation-trace-external-mutation-guard.md`](cli/mutation-trace-external-mutation-guard.md). +- `Agent Trace range content_hash`: Per-range `content_hash` emitted by `build_agent_trace_from_evidence(...)` inside every `ranges[]` entry as `murmur3:`, computed from the touched-line kind/content of the `post_commit_patch` or embedded-patch hunk used to emit that range while excluding positions, paths, metadata, and database IDs. +- `external-mutation guard` / `external-mutation supervisor`: `arm_external_mutation_guard` + `ArmedExternalMutationGuard::exec` (`cli/src/services/mutation_trace/runtime/external_mutation_guard.rs`, hidden `sce hooks external-mutation-guard`, Unix-only), the harness-neutral, not-Pi-specific mechanism that lets a human-initiated shell command (Pi's `!`/`!!` `user_bash`) mutate a worktree without ever creating a mutation scope: it acquires the same `ProtectedWorktree` guard `coordinate()` uses, reports it armed, itself spawns the human shell as its own child (never signaled or stopped by control-channel closure — only by an explicit cancel request, which it enacts by signaling the shell's own process group), finishes exclusively on the shell's real process termination, and only then forces the existing `database_failure`+`recover` composition on the already-held worktree via `coordinate_on_held_worktree` before clearing the marker with `ProtectedWorktree::complete()`. See [`mutation-trace-external-mutation-guard.md`](cli/mutation-trace-external-mutation-guard.md). - `Claude diff-trace attribution`: Diff-trace enrichment rule where one Claude `PostToolUse` event resolves its model with `direct > exact transcript > exact session/agent state > bridge-derived chain state > NULL`: direct top-level/nested metadata first, then that event's `transcript_path` matched by `tool_use_id` to an assistant envelope's `tool_use.id`, then one exact `(cc_, agent_id)` lookup in local `claude_model_state`. Only for a raw structured Claude payload in the exact main-session scope (`agent_id` empty), a state miss then falls back to `bridge session correlation` across the transcript's bridge-linked chain; when that resolves, persistence writes a `claude_model_state` row for the current session (`source="bridge_inherited"`) — the one diff-trace resolution path that writes state — and uses that model, so later traces of the same session resolve from their own exact-scope row. Model sources receive one `claude/` normalization step, ephemeral agent context is never exported, and subagents do not inherit main-session state. - `bridge session correlation`: Claude-specific local correlation using the `bridgeSessionId` in leading transcript records to relate a Claude session's transcript to its sibling transcripts sharing that ID. Selection resolves the newest `claude_model_state` observation across all bridge-linked chain members by `observed_at_ms` (deterministic session-ID tie-break), not the chain origin and not the most recently modified sibling transcript. Discovery is bounded (leading records only) and fail-open, uses existing exact-scope `claude_model_state` rows without persisting the bridge ID, and does not claim authoritative session ordering. One selection rule serves both call sites — the `diff-trace` state-miss path and the `sce hooks claude-model-state` `SessionStart` path — with no most-recently-modified-sibling pick left in the code. See [the read-path bridge-seeding decision](decisions/2026-09-10-claude-bridge-seeding-on-diff-trace.md) and [the bridge-session inheritance decision](decisions/2026-09-08-claude-bridge-session-model-inheritance.md). - `DiffTraceInsert`: Insert payload in `cli/src/services/agent_trace_db/mod.rs` carrying `time_ms`, tool-prefixed `session_id`, `patch`, `model_id`, `tool_name`, nullable `tool_version`, and `payload_type` for parameterized writes to the `diff_traces` table; `payload_type` uses `PAYLOAD_TYPE_PATCH` (`"patch"`) for `OpenCode` unified-diff payloads and `PAYLOAD_TYPE_STRUCTURED` (`"structured"`) for `Claude` `PostToolUse` structured payloads. @@ -101,13 +101,13 @@ - `__sce_migrations`: Per-database migration metadata table created by async shared adapter migration helpers; 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` (historical): Former plan/state note that a user-invocable sync command was deferred to `0.4.0`; superseded first by nested `sce trace sync` and now by top-level `sce sync` (see `context/cli/sync-command.md`). Local DB bootstrap and setup-time repository-scoped Agent Trace DB initialization still flow through lifecycle providers aggregated by the setup command, hook runtime still keeps a lazy repository Agent Trace DB fallback for repositories where setup has not run or schema metadata is incomplete, and DB health/repair still 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. Async database construction and operations use `run_with_retry`; `run_with_retry_sync` remains for pure blocking contexts. +- `CLI bounded resilience wrapper`: Shared policy in `cli/src/services/resilience.rs` (`RetryPolicy`, async `run_with_retry` and non-cancelling `run_with_retry_elapsed`) that applies deterministic retries/timeouts/capped backoff to transient operations, emits retry observability events, and returns actionable terminal failure guidance. Async database construction and operations use `run_with_retry`; there is no synchronous variant. - `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` and `setup mode contract`: Mutually-exclusive `sce setup` target selectors (`--opencode`, `--claude`, `--pi`, `--codex`, `--all`) force non-interactive `SetupMode::NonInteractive(SetupTarget)` when exactly one is provided; otherwise `SetupMode::Interactive` is the default. `--all` expands to opencode+claude+pi+codex and replaced the removed `--both` flag. - `setup interactive target prompt`: `inquire::Select` flow in `cli/src/services/setup/mod.rs` (`InquireSetupTargetPrompter`) that presents OpenCode, Claude, Pi, Codex, and All (OpenCode + Claude + Pi + Codex) when `sce setup` runs without target flags. - `setup dispatch outcome`: Execution model in `cli/src/services/setup/mod.rs` (`SetupDispatch`) where setup either proceeds with a selected/non-interactive target or exits as cancelled without file changes. Interactive dispatch also carries independent `agent_trace.auto_sync` and `policies.attribution_hooks.enabled` booleans from Yes-default confirmations; non-interactive dispatch carries neither, allowing persistence to preserve existing keys and omissions. - `setup embedded asset manifest`: Compile-time generated file index emitted by `cli/build.rs` into `OUT_DIR/setup_embedded_assets.rs`, embedding bytes from Pkl-generated `OUT_DIR/pkl-generated/config/.{opencode,claude,pi}/**` plus staged `OUT_DIR/static/hooks/**` as deterministic normalized relative-path entries consumed by `cli/src/services/setup/mod.rs`; `OPENCODE_EMBEDDED_ASSETS`, `CLAUDE_EMBEDDED_ASSETS`, and `PI_EMBEDDED_ASSETS` all back live setup targets. The manifest also carries `CODEX_EMBEDDED_ASSETS`, embedding Codex's two Pkl-generated output roots (`config/.agents/**`, `config/.codex/**`) merged by `cli/build.rs` into a build-time-only `OUT_DIR/pkl-generated/config/codex-target/` staging tree so its relative-path entries keep their `.agents/`/`.codex/` prefixes; `SetupTarget::Codex` now backs it as a fourth live setup target via `sce setup --codex`/`--all`, installing directly at the repository root (via `InstallTargetPaths::codex_target_dir()`) since its asset paths already carry their own output-root prefix, unlike the other three targets' single-subdirectory destinations. -- `setup required-hook embedded assets`: Setup-service accessors in `cli/src/services/setup/mod.rs` (`iter_required_hook_assets`, `get_required_hook_asset`) that expose canonical embedded templates for `pre-commit`, `commit-msg`, and `post-commit` without runtime config reads. +- `setup required-hook embedded assets`: Setup-service accessor in `cli/src/services/setup/mod.rs` (`iter_required_hook_assets`) that expose canonical embedded templates for `pre-commit`, `commit-msg`, and `post-commit` without runtime config reads. - `SCE managed block`: The CLI-presence check plus `sce hooks ` invocation in each canonical hook template (`cli/assets/hooks/{pre-commit,commit-msg,post-commit}`), delimited by `# >>> sce managed block (do not edit) >>>` / `# <<< sce managed block <<<` comment markers so the same block content can be embedded inside a foreign hook without disturbing content around it (see `context/sce/setup-githooks-hook-asset-packaging.md`). The block propagates an available `sce` command's exit status by capturing `$?` and calling `exit` explicitly rather than by `exec`, so it terminates the script deterministically even when appended after other content. A pure merge module computes hook install bytes against this marker pair (see `setup hook-merge seam`); both `sce setup --hooks` install (see `setup required-hook install orchestration`) and `sce doctor` hook inspection decide currency against this marker pair rather than whole-file byte comparison, so a hook a repository has extended around the block still reports current. - `setup hook-merge seam`: Pure module `cli/src/services/setup/hook_merge.rs`, covering `pre-commit`, `commit-msg`, and `post-commit`. `merge_or_create_hook(existing: Option<&[u8]>, canonical: &[u8], hook_name: &str) -> Result` returns `canonical` verbatim (`HookMergeKind::Created`) when no hook exists; otherwise it locates the `SCE managed block` marker pair by exact line match. A hook already carrying a balanced marker pair identical to the canonical block returns its bytes unchanged (`AlreadyCurrent`); one whose block differs gets that block spliced in place between the same marker lines, leaving surrounding content untouched (`ManagedBlockReplaced`); a marker-free hook containing the legacy pre-marker guidance URL (`https://sce.crocoder.dev/docs/getting-started#install-cli`) is treated as SCE-owned wholesale and replaced entirely with `canonical` (also `ManagedBlockReplaced`); any other marker-free hook is foreign and kept as an exact byte prefix with the canonical block appended after it (`AppendedToForeign`). An unbalanced or partial marker pair is a hard, deterministic error naming `hook_name`, with no bytes returned. For the `AppendedToForeign` case, `HookMerge.unreachable_block_advisory` is set when the foreign hook's last non-blank, non-comment line sits at zero indentation and starts with `exec ` or `exit` — a narrow heuristic (no shell parsing) flagging that the appended block would never run. This module is pure and filesystem-free per "Unit testing in Nix sandbox"; required-hook install calls it (see `setup required-hook install orchestration`), and doctor hook inspection (`cli/src/services/hooks/lifecycle.rs`, `cli/src/services/doctor/inspect.rs`) also calls it, reporting a hook `Current` only when merging the canonical template into its on-disk bytes is a no-op — including treating an unbalanced or partial marker pair as `Stale` rather than `Unknown`, so `sce doctor --fix` repairs it. - `setup required-hook install orchestration`: Setup-service flow in `cli/src/services/setup/mod.rs` (`install_required_git_hooks`, backed by the rename-injectable `install_required_git_hooks_with_rename`) that resolves repository root + effective hooks directory via git truth, then for each hook computes the bytes to stage with the `setup hook-merge seam` (`hook_merge::merge_or_create_hook`) instead of writing the canonical asset verbatim, reports deterministic per-hook outcomes (`Installed`, `Updated`, `Skipped`) against that merged content plus the executable bit, enforces executable permissions, sets `RequiredHookInstallResult.unreachable_block_advisory` (rendered as a named advisory line) when an appended block would be unreachable, and uses the `setup atomic-swap` policy (see `setup atomic-swap`) — staged content is renamed directly over an existing hook without unlinking it first — with deterministic recovery guidance on swap failure. @@ -142,9 +142,9 @@ - `sce shell completion contract`: Deterministic CLI completion contract where `sce completion --shell ` emits parser-aligned Bash/Zsh/Fish completion scripts for current top-level commands and supported options/subcommands. - `CLI default path catalog`: Canonical production path-ownership contract in `cli/src/services/default_paths.rs`; it is the shared owner for non-test production CLI path definitions, covering per-user persisted paths plus repo-relative, install, hook, and context-path definitions used by production CLI code; build-time payload paths are `OUT_DIR`-owned and excluded. - `SCE web URL owner`: Rust service module at `cli/src/services/agent_trace.rs`; owns `SCE_WEB_BASE_URL = "https://sce.crocoder.dev"` and helper functions for SCE-owned URL construction, including Agent Trace conversation URLs, persisted Agent Trace trace URLs, Agent Trace session URLs, and setup-created versioned config schema URLs. It does not own the `control_plane_base_url` used by `sce sync`, whose baked default is `https://sce.crocoderlab.dev`. -- `CLI capability traits`: Broad capability seam in `cli/src/services/capabilities.rs` consumed by the borrowed, compile-time-typed `AppContext`. `FsOps`/`StdFsOps` wrap filesystem operations and `GitOps`/`ProcessGitOps` wrap git process execution plus repository-root and hooks-directory resolution; current service internals do not consume them directly yet. -- `FsOps`: Filesystem capability trait in `cli/src/services/capabilities.rs` with `read_file`, `write_file`, `metadata`, and `exists`, implemented in production by `StdFsOps`. -- `GitOps`: Git capability trait in `cli/src/services/capabilities.rs` with `run_command`, `resolve_repository_root`, `resolve_hooks_directory`, and `is_available`, implemented in production by `ProcessGitOps`. +- `CLI capability traits`: Broad capability seam in `cli/src/services/capabilities.rs` consumed by the borrowed, compile-time-typed `AppContext`. `FsOps`/`StdFsOps` is a method-less filesystem capability marker and `GitOps`/`ProcessGitOps` wrap git command execution and availability; current service internals do not consume them directly yet. +- `FsOps`: Filesystem capability trait in `cli/src/services/capabilities.rs` with no methods (the previous unused file operations were removed), implemented in production by `StdFsOps`. +- `GitOps`: Git capability trait in `cli/src/services/capabilities.rs` with `run_command` and `is_available`, implemented in production by `ProcessGitOps`. - `SCE default path policy seam`: Canonical path resolver in `cli/src/services/default_paths.rs` that owns config/state/cache root resolution through an internal `roots` helper seam, named default paths, and an explicit inventory for the current default persisted artifacts (`global config`, `auth tokens`); named DB/log paths include `auth DB`, `local DB`, `Agent Trace DB`, and the default observability log directory. On Linux those defaults resolve to `$XDG_CONFIG_HOME/sce/config.json`, `$XDG_STATE_HOME/sce/auth/tokens.json`, `$XDG_STATE_HOME/sce/auth.db`, `$XDG_STATE_HOME/sce/local.db`, `$XDG_STATE_HOME/sce/repos//agent-trace.db`, and `$XDG_STATE_HOME/sce/logs`, with platform-equivalent `dirs` fallbacks elsewhere. The same module is also the canonical owner for broader production CLI path definitions, including repo-local `context/tmp/` scratch/session access, and is protected by a regression test that fails when new non-test production path literals are introduced outside `default_paths.rs`. - `cli config precedence contract`: Deterministic runtime value resolution in `cli/src/services/config/resolver.rs` with precedence `flags > env > config file > defaults`; `log_dir` resolves through `SCE_LOG_DIR` > config-file `log_dir` > `/sce/logs`. Config discovery order is `--config`, `SCE_CONFIG_FILE`, then default discovered global+local paths, with the canonical schema declaration accepted in parsed config files. - `shared runtime/config primitives seam`: Canonical ownership in `cli/src/services/config/types.rs` for the CLI's shared observability/config enums (`LogLevel`, `LogFormat`, `LogFileMode`), request/response primitives (`ConfigSubcommand`, `ConfigRequest`, `ReportFormat`), source metadata types (`ValueSource`, `ConfigPathSource`, `LoadedConfigPath`, `ResolvedValue`, `ResolvedOptionalValue`), resolved runtime config types (`ResolvedAuthRuntimeConfig`, `ResolvedObservabilityRuntimeConfig`, `ResolvedHookRuntimeConfig`), the `NAME` constant, observability env-key constants, and shared bool parsing helpers; re-exported through `cli/src/services/config/mod.rs` via `pub use types::*` so downstream modules continue importing through `services::config` unchanged. @@ -222,19 +222,16 @@ - `ParseError`: Error type in `cli/src/services/patch.rs` produced when `parse_patch` encounters malformed input (missing file headers, invalid hunk headers, missing `@@` delimiters); carries an actionable `message` field - `FileChangeKind`: Enum in `cli/src/services/patch.rs` classifying a file change as `Added`, `Modified`, `Deleted`, or `Renamed`; serialized as `snake_case` JSON strings - `TouchedLineKind`: Enum in `cli/src/services/patch.rs` classifying a touched line as `Added` or `Removed`; serialized as `snake_case` JSON strings; derives `Hash` to support set-based intersection operations -- `PatchLoadError`: Error type in `cli/src/services/patch.rs` produced when `load_patch_from_json` or `load_patch_from_json_bytes` encounters invalid JSON or a payload that does not match the expected `ParsedPatch` structure; carries an actionable `message` field +- `PatchLoadError`: Error type in `cli/src/services/patch.rs` produced when `load_patch_from_json` encounters invalid JSON or a payload that does not match the expected `ParsedPatch` structure; carries an actionable `message` field - `load_patch_from_json`: Public function in `cli/src/services/patch.rs` that reconstructs a `ParsedPatch` from a JSON string; storage-agnostic entrypoint for callers who have already read serialized JSON content from a database, file, or other source -- `load_patch_from_json_bytes`: Public function in `cli/src/services/patch.rs` that reconstructs a `ParsedPatch` from JSON bytes; bytes-oriented counterpart to `load_patch_from_json` for callers working with raw byte data - `intersect_patches`: Public function in `cli/src/services/patch.rs` that computes target-shaped touched-line intersection between two `ParsedPatch` values; takes `constructed_patch` and `post_commit_patch` as inputs, matches files by post-change path identity (exact `new_path` equality or absolute-vs-relative suffix-equivalent path segments), prefers exact touched-line matching by `kind` + `line_number` + `content`, falls back to historical matching by `kind` + `content` when line numbers drift across intermediate edits, and returns a `ParsedPatch` shaped from `post_commit_patch` file/hunk ranges while inheriting result-hunk `model_id` from matched `constructed_patch` hunk provenance when available; consumed by the active post-commit hook runtime - `combine_patches`: Public function in `cli/src/services/patch.rs` that merges multiple `ParsedPatch` values into one deterministic result with later-input-wins semantics; groups files by `new_path`, deduplicates touched lines by identity (`kind` + `line_number` + `content`) with later patches winning, preserves file metadata and hunk metadata from the last contributing patch, orders files by first encounter and hunks by `old_start`; consumed by the active post-commit hook runtime before intersection - `HunkContributor`: Enum in `cli/src/services/agent_trace.rs` classifying a `post_commit_patch` hunk from the union of direct AI coverage (`intersection_patch = intersect_patches(direct_patch, post_commit_patch)`) and any mutation-derived AI coverage supplied via `AgentTraceEvidence.mutation_ai_patch`: `Ai` (every touched line covered), `Mixed` (a non-empty proper subset covered), `Unknown` (no line covered); with no mutation evidence this reduces exactly to the direct-only slot rule (`Ai` exact line-by-line match, `Mixed` same slot different content, `Unknown` no matching `old_start` slot); serialized as `snake_case` JSON strings - `Conversation`: Struct in `cli/src/services/agent_trace.rs` representing one per-hunk entry in the minimal agent-trace payload, carrying a nested `contributor` object (`type` plus optional `model_id`) plus `ranges`, where the current implementation emits exactly one `{ start_line, end_line }` entry derived from the `post_commit_patch` hunk - `TraceFile`: Struct in `cli/src/services/agent_trace.rs` representing one per-file entry in the minimal agent-trace payload, carrying `path` (from `post_commit_patch`'s `new_path`) plus `conversations` (one per `post_commit_patch` hunk) - `AgentTraceVcs`: Top-level VCS metadata struct in `cli/src/services/agent_trace.rs` carrying `type` and `revision`; builder behavior maps `type` from caller metadata (`AgentTraceMetadataInput.vcs_type`, enum-backed) and maps revision from caller metadata when VCS metadata is present. -- `AgentTrace`: Top-level struct in `cli/src/services/agent_trace.rs` representing the minimal agent-trace payload, carrying top-level `version` (fixed to `0.1.0`, strict numeric `x.y.z`), `id` (UUIDv7 string derived from the same commit-time moment used for `timestamp` in `build_agent_trace(...)`), `timestamp` (caller-provided commit timestamp via `AgentTraceMetadataInput.commit_timestamp`, validated as RFC 3339), optional `vcs` (`Option`, omitted from serialized JSON when `None`), and `files` (`Vec`, one per `post_commit_patch` file); `serde`-serializable with `snake_case` field naming -- `classify_hunk`: Public function in `cli/src/services/agent_trace.rs` implementing the direct-only slot rule (retained primitive; the builder itself classifies through the internal combined direct+mutation line-coverage rule): matches a `post_commit_patch` hunk against `intersection_patch` hunks on `old_start` slot, returning `HunkContributor::Ai` for exact line-by-line match, `Mixed` for same-slot-but-different-content, or `Unknown` when no matching slot exists +- `AgentTrace`: Top-level struct in `cli/src/services/agent_trace.rs` representing the minimal agent-trace payload, carrying top-level `version` (fixed to `0.1.0`, strict numeric `x.y.z`), `id` (UUIDv7 string derived from the same commit-time moment used for `timestamp` in `build_agent_trace_from_evidence(...)`), `timestamp` (caller-provided commit timestamp via `AgentTraceMetadataInput.commit_timestamp`, validated as RFC 3339), optional `vcs` (`Option`, omitted from serialized JSON when `None`), and `files` (`Vec`, one per `post_commit_patch` file); `serde`-serializable with `snake_case` field naming - `AgentTraceMetadataInput`: Metadata input struct in `cli/src/services/agent_trace.rs` that carries `commit_timestamp` (RFC 3339 commit-time value used as `AgentTrace.timestamp`), `commit_revision` (mapped to `AgentTrace.vcs.revision` when VCS metadata is emitted), and optional `vcs_type` (`Option`, mapped to `AgentTrace.vcs.type` and controlling whether top-level `vcs` is emitted); `AgentTraceVcsType` is the schema-aligned `Git`/`Jj`/`Hg`/`Svn` enum serialized as `git`/`jj`/`hg`/`svn`. -- `build_agent_trace`: Public function in `cli/src/services/agent_trace.rs` — the direct-only compatibility path — that delegates to `build_agent_trace_from_evidence` with an empty `mutation_ai_patch`: computes `intersection_patch = intersect_patches(constructed_patch, post_commit_patch)`, iterates over `post_commit_patch` files and hunks, classifies each hunk, validates `AgentTraceMetadataInput.commit_timestamp` as RFC 3339, derives UUIDv7 `AgentTrace.id` from that same commit-time moment, and returns `Result` with top-level metadata fields plus one `Conversation` per `post_commit_patch` hunk; consumed by the active post-commit hook flow, with no standalone `sce agent-trace` command surface. `build_agent_trace_from_evidence(AgentTraceEvidence { direct_patch, mutation_ai_patch }, post_commit_patch, metadata)` is the separated-evidence entrypoint: identical top-level metadata behavior, combined direct+mutation hunk classification, deduplicated direct+mutation session links, and a contributor model only when the present evidence sources agree; top-level `tool` remains derived from the direct intersection only. - `agent-trace plugin diff extraction seam`: Helper `extractDiffTracePayload` in `config/lib/agent-trace-plugin/opencode-sce-agent-trace-plugin.ts` that accepts a typed `message` event and returns `{ sessionID, diff, time, model_id }` only for user-role messages with non-empty `summary?.diffs`; it joins present object-entry `patch` fields with `\n`, skips entries without `patch`, returns `undefined` when no usable patches remain, uses `Date.now()` for `time`, and builds `model_id` as `providerID/modelID` from `event.properties.info.model`. - `get_or_create_encryption_key`: Public keyring-backed helper in `cli/src/services/db/encryption_key.rs` that retrieves or generates a 64-character hex encryption key from the OS credential store (macOS Keychain, Linux Secret Service via zbus, Windows Credential Store); uses `keyring_core::Entry` with service name `"sce"` and the database name as username. Actively consumed by `EncryptedTursoDb::new()` via the shared adapter constructor. - `conversation-trace mixed batch`: Rust `sce hooks conversation-trace` STDIN contract accepting `{ payloads: [{ type: "message" | "message.part", ... }] }` with top-level `type` ignored and malformed-item skipping. See `context/sce/agent-trace-hooks-command-routing.md` and `context/sce/agent-trace-db.md`. diff --git a/context/overview.md b/context/overview.md index f502e29fc..7516e357d 100644 --- a/context/overview.md +++ b/context/overview.md @@ -45,7 +45,7 @@ for `/validate`. See This repository maintains shared assistant configuration for OpenCode, Claude, Pi, and Codex from a single canonical Pkl authoring source. One typed workflow catalog owns the six workflows' shared identity and target routing metadata, while canonical workflow/phase modules own behavior and migrated package-local documents, and target renderers own formatting. Generated target layouts are ephemeral: repository builds consume a pre-Cargo generated payload through `SCE_CLI_GENERATED_INPUT_DIR`, crates.io and Flatpak stage packaging-only fallbacks, and `config/.opencode`, `config/.claude`, `config/.pi`, `config/.agents`, and the generated working-tree SCE config schema are not committed; versioned SCE config schema snapshots live under `schema/v/`. The catalog also marks a workflow `optional` — currently only `brownfield` — which changes nothing about generation and is projected into a generated `config/optional-workflows.json` manifest for install-time consumers. `nix run .#pkl-check-generated` preserves its exact 142-path artifact, metadata/package, phase-reference, internal-reference, optional-workflow-manifest, workflow-orchestration, OpenCode-permission, required-path, and forbidden-path checks while delegating deterministic payload production and inventories to the shared generated-input producer; `nix flake check` runs the same contract. The target matrix contains one manual OpenCode profile plus Claude, Pi, and Codex; the former automated OpenCode profile has been removed. A fourth Pkl renderer, `config/pkl/renderers/codex-content.pkl`, also consumes the same canonical workflow composition to emit skills-only Codex output under `config/.agents/skills/**` (no per-target frontmatter, matching Pi, and no command/prompt layer). Each of the six catalog workflow skills (not `sce-decision`, which has no user-facing entrypoint on any target) additionally carries a Codex-only `agents/openai.yaml`, rendered by `config/pkl/renderers/codex-metadata.pkl` from the shared catalog's `title`/`description` plus an authored `default_prompt`, with `policy.allow_implicit_invocation: false` so these stateful lifecycle workflows activate only from explicit `$sce-` invocation or Codex's `/skills` discovery, never from conversational relevance alone. The Codex renderer also emits a Codex hook registration file (`config/.codex/hooks.json`) and its install-guidance helper (`config/.codex/hooks/run-sce-or-show-install-guidance.sh`). The existing conversation/diff registrations (`UserPromptSubmit`, `Stop`, `PreToolUse` for `Bash`, `PostToolUse` for `apply_patch`) route through `sce hooks codex` and retain their fail-open behavior where designed. The separate mutation-scope registrations route through `sce hooks codex-mutation-scope`: `PreToolUse` and `PostToolUse` use matcher `^(Bash|apply_patch)$`, while `Stop`, `Interrupt`, `SubagentStop`, and `SessionEnd` omit matcher and are unmatched. The tracked mutation `PreToolUse` bootstrap fails closed if Git-root resolution, helper or `sce` discovery, adapter startup, runtime `Start`, or Bash-policy evaluation cannot establish attribution; mutation-scope `PostToolUse` and cleanup hooks do not use this bootstrap behavior. Delegation, MCP, and unknown tools do not match these generated mutation Pre/Post registrations. `sce setup --codex` (and `--all`) now installs it as a fourth `SetupTarget`; setup merges the user-owned `.codex/hooks.json` through the shared structural Codex hook-config service, preserving unrelated valid handlers and rejecting malformed or Codex-invalid documents before the atomic swap. `sce doctor` diagnoses that file per required registration (`PresentAndCurrent`/`Missing`/`Stale`, or `Malformed` for the whole document) rather than by whole-document equality, reporting the mutation-scope registrations under distinct `#(mutation-scope)` rows separate from the `sce hooks codex` rows, and separately reports whether Codex has actually marked each structurally current registration trusted and whether its effective hook-discovery policy allows project hooks, by reading (never writing) Codex's own `$CODEX_HOME/config.toml` and policy state; `sce doctor --fix` repairs structurally unhealthy registrations through the same merge service but never touches trust or policy state (see `context/sce/doctor-human-text-contract.md`). `sce hooks codex` now exists as a typed dispatcher (`cli/src/services/hooks/codex/`): it parses the raw hook JSON into a `CodexHookEvent` and classifies `(hook_event_name, tool_name)` into `UserPromptSubmit`, `Stop`, `PreToolUse(Bash)`, `PostToolUse(apply_patch)`, or a fail-open `NoOp` fallthrough covering every other combination — including `apply_patch` under `PreToolUse`. `UserPromptSubmit` and `Stop` each capture one `messages`/`parts` row (`role="user"`/`role="assistant"`) into the repository Agent Trace DB under the idempotent `cx_` session prefix, `PreToolUse(Bash)` delegates to the existing Bash policy engine, and `PostToolUse(apply_patch)` parses Codex's own `apply_patch` text, resolves paths from the event `cwd` against the real Git root into safe repository-relative paths, normalizes Add/Update evidence into an SCE unified diff under deterministic event-scoped synthetic line identities derived from `tool_use_id`, and persists it as one `diff_traces` row when non-empty (see `context/sce/codex-integration-runtime.md`) — while invalid cwd/path resolution and malformed STDIN payloads fail open with empty stdout, reported model IDs are preserved without fabricated provider prefixes, and missing/blank apply_patch sessions produce no evidence. Delete-File operations and Bash filesystem effects do not become `diff_traces` in this existing conversation/diff pipeline, and its apply_patch evidence has separate operation limitations. Separately, the Codex mutation-scope adapter tracks `Bash` and `apply_patch` executions as `TrackedMutation` scopes; `AiExclusive` means tracked-scope exclusivity, not that Bash authored every mutation in the interval. -It also includes a Rust CLI (`sce`) for Shared Context Engineering workflows: auth, config inspection, setup, doctor, Agent Trace hooks and synchronization, bash-policy evaluation, and repository-scoped Agent Trace storage infrastructure. See `context/architecture.md` for module-level boundaries and `context/context-map.md` for the full domain file index. A new pure, dependency-free `cli/src/services/mutation_trace/` module now exists as a Rust refinement of the verified `spec/mutation_cursor.qnt` mutation-cursor protocol; it currently carries domain types and the full pure action set — `prepare`/`commit` transition logic, attribution/mutation-event materialization, snapshot-failure/database-failure taint actions, scope abandonment, and recovery with an explicit observed-tree input — is registered with `#[allow(dead_code)]`. A separate `cli/src/services/mutation_trace/store.rs` persistence layer (built out by the `mutation-cursor-store-persistence` plan) now provides a real, CAS-guarded database call site against the repository-scoped Agent Trace DB, and a `cli/src/services/mutation_trace/runtime/` coordinator layer (built out by the `mutation-cursor-runtime-coordinator` plan) adds the per-worktree advisory lock, the isolated Git snapshot/ref-pinning service, and a public `coordinate()` entrypoint that drives the protocol against a real worktree under that lock, plus a per-worktree `ref_reconciliation` maintenance pass (built out by the `mutation-cursor-ref-reconciliation` plan) that, only for the namespace of a checkout id a current worktree still derives, reclaims orphaned SCE-owned snapshot refs under that same lock while retaining every tree any durable mutation-cursor state still references (a namespace no current worktree owns — via a deleted linked worktree or checkout-id metadata loss/recreation — is out of reach and left to future repository-scoped work). That runtime now has a second protected entrypoint alongside `coordinate()`: `cli/src/services/mutation_trace/runtime/scope_runtime.rs`'s `abandon_scope()` (built out by the `mutation-scope-runtime-integration` plan) retires a scope whose final worktree boundary was never observed — a dead agent process leaves no `Close` behind — sharing `coordinate()`'s extracted `runtime/protected_worktree.rs` safety prefix but deliberately capturing no Git snapshot, so abandonment is not a `RuntimeBoundary` and needs no Quint change. Both entrypoints are now reachable crate-wide through nine `pub(crate)` re-exports in `runtime/mod.rs` while every module behind them stays private, and the lifecycle contract every current or future harness adapter must uphold is recorded in `context/cli/mutation-scope-runtime.md`. The protocol runtime entrypoints (`coordinate()` / `abandon_scope()`) are now driven by one harness-neutral CLI ingress, the hidden `sce hooks mutation-scope` command (`context/cli/mutation-scope-hook-ingress.md`), which reads a single normalized JSON lifecycle object from STDIN, strictly parses it into exactly `start`/`advance`/`close`/`flush`/`abandon`, maps it to a `RuntimeBoundary` (`start`/`advance`/`close`/`flush`) or an `abandon_scope()` call forwarding `scope_id`/`event_id`/`actor_kind` verbatim (any `worktree_id` key is refused — worktree identity is derived by the runtime from the invoking checkout), and invokes the runtime with a lazy DB provider closure so DB acquisition stays inside the protected-worktree ordering. Unlike the fail-open `diff-trace`/`conversation-trace` intakes, this ingress never discards a valid boundary: it classifies results by durable completion, returning a non-zero `CliError` for a malformed payload or a pre-completion runtime error while treating the two carried-outcome variants (`MarkerClearAfterCommit` / `MarkerClearAfterCompletion` — the durable transition succeeded and only the trailing external-taint marker cleanup failed) as empty-stdout success without retrying the transition. Every successful execution emits empty stdout and writes `mutation_trace_*` rows only. It is the generic transport seam. Concrete harness lifecycle adapters for Claude Code and Codex are wired to it in-process via a `pub(crate)` seam rather than by re-invoking the ingress command; they map each harness's lifecycle events onto one mutation `ScopeId` per mutation-capable tool execution with write-ahead fail-closed `Start`, terminal `Close`, cleanup, recovery barriers, and harness-specific unsupported-boundary handling. A Pi adapter driver (`cli/src/services/hooks/pi_mutation_scope/`) also now consumes this same `pub(crate)` in-process seam through its hidden `sce hooks pi-mutation-scope` command and canonical generated Pi extension, with a `PendingStart`→`Executed`→`Closed`/`PendingAbandon` attempt-phase lifecycle keyed on Pi's `tool_result` event rather than `tool_execution_start` (see `context/cli/pi-mutation-scope-integration.md`); ordinary Pi sessions now reach it, and its human `user_bash` path uses the separate guarded external-mutation supervisor. Codex execution identity and `ScopeId`/`EventId` derivation are owned by `context/cli/codex-mutation-scope-integration.md`. A separate **read-only** consumer of the same module's persisted mutation-event history is wired, though: the post-commit Agent Trace flow calls `mutation_trace::runtime::resolve_post_commit_mutation_ai_patch(...)` to attribute committed touched lines that direct `diff_traces` evidence does not cover, by replaying the newest 128 events for the invoking worktree (bounded also by a commit attribution cut captured under the worktree lock) oldest-to-newest as one causal per-line provenance lineage rather than matching historical patches independently, resolving the worktree's *existing* checkout identity only, creating no identity and writing no mutation-cursor state, and never inserting into `diff_traces` or `post_commit_patch_intersections` (see `context/cli/mutation-trace-agent-attribution.md` and `context/sce/agent-trace-hooks-command-routing.md`). See also `context/cli/mutation-trace-protocol.md`, `context/cli/mutation-trace-runtime-coordinator.md`, `context/cli/mutation-trace-ref-reconciliation.md`, `context/cli/mutation-trace-protected-worktree.md`, `context/cli/mutation-trace-scope-abandonment.md`, `context/cli/mutation-scope-runtime.md`, `context/cli/mutation-scope-hook-ingress.md`, `context/cli/claude-mutation-scope-integration.md`, and `context/cli/mutation-trace-external-mutation-guard.md` (the harness-neutral process supervisor that lets Pi's `user_bash` mutate a worktree, guarded by the same `WorktreeLock`, without ever creating a scope). +It also includes a Rust CLI (`sce`) for Shared Context Engineering workflows: auth, config inspection, setup, doctor, Agent Trace hooks and synchronization, bash-policy evaluation, and repository-scoped Agent Trace storage infrastructure. See `context/architecture.md` for module-level boundaries and `context/context-map.md` for the full domain file index. A new pure, dependency-free `cli/src/services/mutation_trace/` module now exists as a Rust refinement of the verified `spec/mutation_cursor.qnt` mutation-cursor protocol; it currently carries domain types and the full pure action set — `prepare`/`commit` transition logic, attribution/mutation-event materialization, snapshot-failure/database-failure taint actions, scope abandonment, and recovery with an explicit observed-tree input — is registered without a module-wide dead-code allowance (only declaration-level, reasoned exceptions remain for the retained-and-unwired ref-reconciliation cluster). A separate `cli/src/services/mutation_trace/store.rs` persistence layer (built out by the `mutation-cursor-store-persistence` plan) now provides a real, CAS-guarded database call site against the repository-scoped Agent Trace DB, and a `cli/src/services/mutation_trace/runtime/` coordinator layer (built out by the `mutation-cursor-runtime-coordinator` plan) adds the per-worktree advisory lock, the isolated Git snapshot/ref-pinning service, and a public `coordinate()` entrypoint that drives the protocol against a real worktree under that lock, plus a per-worktree `ref_reconciliation` maintenance pass (built out by the `mutation-cursor-ref-reconciliation` plan) that, only for the namespace of a checkout id a current worktree still derives, reclaims orphaned SCE-owned snapshot refs under that same lock while retaining every tree any durable mutation-cursor state still references (a namespace no current worktree owns — via a deleted linked worktree or checkout-id metadata loss/recreation — is out of reach and left to future repository-scoped work). That runtime now has a second protected entrypoint alongside `coordinate()`: `cli/src/services/mutation_trace/runtime/scope_runtime.rs`'s `abandon_scope()` (built out by the `mutation-scope-runtime-integration` plan) retires a scope whose final worktree boundary was never observed — a dead agent process leaves no `Close` behind — sharing `coordinate()`'s extracted `runtime/protected_worktree.rs` safety prefix but deliberately capturing no Git snapshot, so abandonment is not a `RuntimeBoundary` and needs no Quint change. Both entrypoints are now reachable crate-wide through nine `pub(crate)` re-exports in `runtime/mod.rs` while every module behind them stays private, and the lifecycle contract every current or future harness adapter must uphold is recorded in `context/cli/mutation-scope-runtime.md`. The protocol runtime entrypoints (`coordinate()` / `abandon_scope()`) are now driven by one harness-neutral CLI ingress, the hidden `sce hooks mutation-scope` command (`context/cli/mutation-scope-hook-ingress.md`), which reads a single normalized JSON lifecycle object from STDIN, strictly parses it into exactly `start`/`advance`/`close`/`flush`/`abandon`, maps it to a `RuntimeBoundary` (`start`/`advance`/`close`/`flush`) or an `abandon_scope()` call forwarding `scope_id`/`event_id`/`actor_kind` verbatim (any `worktree_id` key is refused — worktree identity is derived by the runtime from the invoking checkout), and invokes the runtime with a lazy DB provider closure so DB acquisition stays inside the protected-worktree ordering. Unlike the fail-open `diff-trace`/`conversation-trace` intakes, this ingress never discards a valid boundary: it classifies results by durable completion, returning a non-zero `CliError` for a malformed payload or a pre-completion runtime error while treating the two carried-outcome variants (`MarkerClearAfterCommit` / `MarkerClearAfterCompletion` — the durable transition succeeded and only the trailing external-taint marker cleanup failed) as empty-stdout success without retrying the transition. Every successful execution emits empty stdout and writes `mutation_trace_*` rows only. It is the generic transport seam. Concrete harness lifecycle adapters for Claude Code and Codex are wired to it in-process via a `pub(crate)` seam rather than by re-invoking the ingress command; they map each harness's lifecycle events onto one mutation `ScopeId` per mutation-capable tool execution with write-ahead fail-closed `Start`, terminal `Close`, cleanup, recovery barriers, and harness-specific unsupported-boundary handling. A Pi adapter driver (`cli/src/services/hooks/pi_mutation_scope/`) also now consumes this same `pub(crate)` in-process seam through its hidden `sce hooks pi-mutation-scope` command and canonical generated Pi extension, with a `PendingStart`→`Executed`→`Closed`/`PendingAbandon` attempt-phase lifecycle keyed on Pi's `tool_result` event rather than `tool_execution_start` (see `context/cli/pi-mutation-scope-integration.md`); ordinary Pi sessions now reach it, and its human `user_bash` path uses the separate guarded external-mutation supervisor. Codex execution identity and `ScopeId`/`EventId` derivation are owned by `context/cli/codex-mutation-scope-integration.md`. A separate **read-only** consumer of the same module's persisted mutation-event history is wired, though: the post-commit Agent Trace flow calls `mutation_trace::runtime::resolve_post_commit_mutation_ai_patch(...)` to attribute committed touched lines that direct `diff_traces` evidence does not cover, by replaying the newest 128 events for the invoking worktree (bounded also by a commit attribution cut captured under the worktree lock) oldest-to-newest as one causal per-line provenance lineage rather than matching historical patches independently, resolving the worktree's *existing* checkout identity only, creating no identity and writing no mutation-cursor state, and never inserting into `diff_traces` or `post_commit_patch_intersections` (see `context/cli/mutation-trace-agent-attribution.md` and `context/sce/agent-trace-hooks-command-routing.md`). See also `context/cli/mutation-trace-protocol.md`, `context/cli/mutation-trace-runtime-coordinator.md`, `context/cli/mutation-trace-ref-reconciliation.md`, `context/cli/mutation-trace-protected-worktree.md`, `context/cli/mutation-trace-scope-abandonment.md`, `context/cli/mutation-scope-runtime.md`, `context/cli/mutation-scope-hook-ingress.md`, `context/cli/claude-mutation-scope-integration.md`, and `context/cli/mutation-trace-external-mutation-guard.md` (the harness-neutral process supervisor that lets Pi's `user_bash` mutate a worktree, guarded by the same `WorktreeLock`, without ever creating a scope). The generated `/next-task` workflow persists task-level context-synchronization lifecycle state in each plan (`pending`, `synced`, or `blocked`) so unresolved task synchronization debt survives a session boundary and gates new implementation. Successful `/next-task` execution hands task synchronization an explicit, pre-edit-Git-baseline-relative changed-file list plus implementation, verification, done-check, plan-update, and context-impact evidence, recorded directly on the completed task (`Completed`, `Files changed`, `Result`, `Verify`, `Context impact`, `Context synchronization`); the five-file root context pass remains mandatory. A later-session sync-debt retry reads that same completed task record directly from the plan by plan path and task ID, with no separate persisted synchronization handoff. `/validate` is validation-only: it runs final checks, writes the Validation Report, and reports `validated`, `failed`, or `blocked` without plan-level context synchronization. @@ -77,7 +77,7 @@ The setup service also provides repository-root install orchestration: it resolv The CLI now also applies baseline security hardening for reliability-driven automation: diagnostics/logging paths use deterministic secret redaction,`sce setup --hooks --repo ` canonicalizes and validates repository paths before execution, and setup write flows run explicit directory write-permission probes before staging/swap operations. The config service now provides deterministic runtime config resolution with explicit precedence (`flags > env > config file > defaults`), strict config-file validation (`$schema`, `log_level`, `log_format`, `log_to_file`, `log_dir`, `workos_client_id`, and nested `policies.bash`, `policies.attribution_hooks.enabled`, plus `policies.database_retry` with per-DB `connection_open`/`query` retry policy specs and Agent Trace-only `agent_trace_db.busy_timeout_ms`/`contention_deadline_ms`), deterministic default discovery/merge of global+local config files (`${config*root}/sce/config.json`then`.sce/config.json`with local override, where`config_root` comes from the shared default-path seam with XDG/`dirs::config_dir()` config-root resolution), defaults for the resolved observability value set (`log_level=error`, `log_format=text`, `log_dir=/sce/logs`), shared auth-key resolution with optional baked defaults starting at `workos_client_id`, first-class bash-policy preset/custom parsing with deterministic conflict and duplicate-prefix validation, custom-policy `satisfied_by`wrapper exemption (a policy does not fire when the matched command was unwrapped from a declared wrapper such as`nix shell nixpkgs#ripgrep`), and a canonical Pkl-authored `sce/config.json`JSON Schema generated beneath Cargo`OUT_DIR`and embedded by`cli/src/services/config/mod.rs`for both`sce config validate`and doctor-time config checks. Runtime startup config loading keeps parity with that schema by accepting its`$schema`declaration in repo-local and global config files, so startup commands such as`sce version`no longer fail before dispatch on that field; the canonical declaration is versioned as`"https://sce.crocoder.dev/v/config.json"` using the CLI release version; this schema URL is separate from the `https://sce.crocoderlab.dev` baked default used by `sce sync` for control-plane ingestion. App-runtime observability now consumes flat logging keys through the shared resolver, so env values still override config-file values while config files provide deterministic fallback for `log_dir`; positive-integer `log_file_retention_limit` uses config-file/default precedence, defaults to `10`, and controls creation-triggered cleanup for primary and v2 log files; `sce config show` reports resolved observability/auth/policy values with provenance, while `sce config validate` is now a trimmed validation surface that reports only pass/fail plus validation errors or warnings in text and JSON modes. The canonical preset catalog and matching contract live in `config/pkl/base/bash-policy-presets.pkl` and `context/sce/bash-tool-policy-enforcement-contract.md`. Invalid default-discovered config files now also degrade gracefully at startup: `sce` keeps running with degraded observability defaults, logs `sce.config.invalid_config` warnings, and reserves hard failures for explicit `--config` / `SCE_CONFIG_FILE` targets or other truly invalid runtime observability inputs. -`cli/src/services/config/mod.rs` is now a module facade that declares focused config submodules (`types`, `schema`, `policy`, `resolver`, private `render`, `command`, and `lifecycle`), re-exporting `pub use types::*`and`pub(crate) use schema::validate_config_file`. Shared config primitive ownership is delegated to `cli/src/services/config/types.rs`; schema loading and file parsing to `cli/src/services/config/schema.rs`; bash-policy semantic validation and policy-specific formatting to `cli/src/services/config/policy.rs`; runtime discovery/precedence to `cli/src/services/config/resolver.rs`; and `sce config show`/`sce config validate`text+JSON output construction to`cli/src/services/config/render.rs`. Downstream modules continue importing through `services::config`unchanged. The CLI now has a generic borrowed`AppContext`dependency view in`cli/src/app.rs`; `AppRuntime`owns concrete production logger/telemetry/fs/git dependencies, and command execution receives context views that borrow those dependencies plus an optional`repo_root: Option`. `AppContext::with_repo_root(...)`/`ContextWithRepoRoot`derives repo-root-scoped views while preserving the borrowed runtime dependencies, and command execution is generic over associated-type narrow accessor traits where practical. The broad capability seam lives in`cli/src/services/capabilities.rs`, where `FsOps`/`StdFsOps`wrap filesystem operations and`GitOps`/`ProcessGitOps`wrap git process execution plus repository-root/hooks-directory resolution. +`cli/src/services/config/mod.rs` is now a module facade that declares focused config submodules (`types`, `schema`, `policy`, `resolver`, private `render`, `command`, and `lifecycle`), re-exporting `pub use types::*`and`pub(crate) use schema::validate_config_file`. Shared config primitive ownership is delegated to `cli/src/services/config/types.rs`; schema loading and file parsing to `cli/src/services/config/schema.rs`; bash-policy semantic validation and policy-specific formatting to `cli/src/services/config/policy.rs`; runtime discovery/precedence to `cli/src/services/config/resolver.rs`; and `sce config show`/`sce config validate`text+JSON output construction to`cli/src/services/config/render.rs`. Downstream modules continue importing through `services::config`unchanged. The CLI now has a generic borrowed`AppContext`dependency view in`cli/src/app.rs`; `AppRuntime`owns concrete production logger/telemetry/fs/git dependencies, and command execution receives context views that borrow those dependencies plus an optional`repo_root: Option`. `AppContext::with_repo_root(...)`/`ContextWithRepoRoot`derives repo-root-scoped views while preserving the borrowed runtime dependencies, and command execution is generic over associated-type narrow accessor traits where practical. The broad capability seam lives in`cli/src/services/capabilities.rs`, where `FsOps`/`StdFsOps` form a method-less filesystem marker and `GitOps`/`ProcessGitOps` wrap git command execution and availability. The shared default path service in`cli/src/services/default_paths.rs`is now the canonical owner for production CLI path definitions. It resolves per-user config/state/cache roots through a dedicated internal`roots`seam, exposes the current persisted-artifact inventory (global config and auth tokens), and also defines named DB paths (auth DB, local DB, Agent Trace DB) plus the repo-relative, install, hook, and context-path accessors consumed across current CLI production code. Non-test production modules should consume this shared catalog instead of hardcoding owned path literals. No default cache-backed persisted artifact currently exists, so cache-root resolution remains available without speculative cache-path features and no legacy default-path fallback is supported. The Rust CLI also centralizes SCE-owned web URI construction in`cli/src/services/agent_trace.rs`, with `SCE_WEB_BASE_URL`as the single Rust owner for`https://sce.crocoder.dev` and helpers consumed by Agent Trace conversation URLs, Agent Trace persisted trace URLs, Agent Trace session URLs, and setup-created repo-local config schema URLs. The config resolver separately owns `control_plane_base_url` and its `https://sce.crocoderlab.dev` baked sync default; the two URL owners must not be conflated. The current user-facing synchronization entrypoint is `sce sync`; references to the former nested spelling in historical records do not describe an available command. @@ -117,7 +117,7 @@ The current supported automated release target matrix is `x86_64-unknown-linux-m The CLI now also includes an approved operator-environment doctor contract documented in `context/sce/agent-trace-hook-doctor.md`; the runtime includes `sce doctor --fix` parsing/help, stable problem/fix-result reporting, canonical hook-repair reuse, bounded doctor-owned local-DB directory bootstrap for the missing SCE-owned DB parent path, and target-scoped integration inventory: Claude reports `Plugins`, `Commands`, and `Skills`; OpenCode reports `Plugins`, `Agents`, `Commands`, and `Skills`; Pi reports `Extensions`, `Prompts`, and `Skills`; and Codex reports `Skills` and `Hooks` (see `context/sce/doctor-human-text-contract.md`). Its non-launching post-commit Agent Trace auto-sync fact reports enabled/current, explicit disabled, not-ready, and not-applicable states using canonical managed-block currency and resolved configuration; existing hook remediation and readiness semantics remain unchanged. The local DB service now provides `LocalDb` as a thin `TursoDb` alias in `cli/src/services/local_db/mod.rs`; `LocalDbSpec` resolves the canonical local DB path from the shared default-path catalog and currently declares zero migrations. Shared Turso infrastructure lives in `cli/src/services/db/mod.rs`, where `DbSpec` and generic `TursoDb` support local or remote sync-mode opens, parent-directory creation, connection setup, synchronous query helpers, embedded migration execution, and shared DB lifecycle helpers. Auth DB persistence uses encrypted `AuthDb = EncryptedTursoDb` and token storage persists credentials through the `auth_credentials` table. Agent Trace persistence uses the sole `RepositoryAgentTraceDb = TursoDb` adapter at `/sce/repos//agent-trace.db`, with a one-file repository schema for `repository_metadata`, `diff_traces`, `post_commit_patch_intersections`, `agent_traces`, `messages`, `parts`, indexes/triggers, and no `checkout_id` columns on trace rows. The checkout-scoped `AgentTraceDb = TursoDb` 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 runtime writes nullable event-local diff-trace attribution without a `session_models` API/table dependency. The hooks command surface now also supports concrete runtime subcommand routing (`pre-commit`, `commit-msg`, `post-commit`, `post-rewrite`, `diff-trace`, `conversation-trace`, and `claude-model-state`) with deterministic argument/STDIN validation; `session-model` is no longer supported. Claude settings install the model-state command for both `SessionStart` and `PostModelSwitch`; compatibility smoke against Claude Code 2.1.251 and immediately older 2.1.250 showed that the older client safely ignores the unknown event, so installation is unconditional without a raised minimum or capability gate. Current runtime behavior keeps commit-msg attribution enabled by default unless explicitly opted out: the attribution gate enables canonical trailer insertion in `commit-msg` only when the staged-diff AI-overlap preflight confirms AI/editor evidence (no trailer is appended when the preflight finds no overlap or encounters any error); `pre-commit`/`post-rewrite` remain deterministic no-ops, `post-commit` requires validated `--remote-url`, threads that URL into the Agent Trace flow, prints it to stderr, remains the active bounded recent-diff-trace intersection path, and after successful Agent Trace persistence optionally launches the detached sync-owned `sync --format json` child when config-file-only `agent_trace.auto_sync` is true; `diff-trace` is the active intake path for parsed STDIN `{ sessionID, diff, time, model_id?, tool_name, tool_version }` payload persistence with optional `model_id`, required non-empty `tool_name`, required nullable/non-empty `tool_version`, `direct > exact transcript > exact session/agent state > bridge-derived chain state > NULL` Claude `model_id` plus direct `tool_version` values (exact local state only; no generic session-model fallback or cache), required `u64` millisecond `time`, same-tool-idempotent stored `session_id` prefixing (`oc_` for OpenCode, `cc_` for Claude, `pi_` for Pi), non-lossy AgentTraceDb `time_ms` conversion, and no parsed-payload artifact fallback. This behavior is documented in `context/sce/agent-trace-hooks-command-routing.md`. The removed `sce hooks claude-capture` raw capture route is documented in `context/sce/claude-raw-hook-capture.md` as a removed feature. - The setup service now also exposes deterministic required-hook embedded asset accessors (`iter_required_hook_assets`, `get_required_hook_asset`) backed by canonical templates in `cli/assets/hooks/` for `pre-commit`, `commit-msg`, and `post-commit`; this behavior is documented in `context/sce/setup-githooks-hook-asset-packaging.md`. + The setup service now also exposes deterministic required-hook embedded asset accessor (`iter_required_hook_assets`) backed by canonical templates in `cli/assets/hooks/` for `pre-commit`, `commit-msg`, and `post-commit`; this behavior is documented in `context/sce/setup-githooks-hook-asset-packaging.md`. The setup service now also includes required-hook install orchestration (`install_required_git_hooks`) that resolves repository root and effective hooks path from git truth, computes the bytes to stage by merging the canonical hook template with any existing hook (preserving a foreign hook's content as an exact prefix with the SCE managed block appended, or bringing an SCE-owned block current in place) rather than writing canonical bytes verbatim, enforces deterministic per-hook outcomes (`Installed`/`Updated`/`Skipped`) against that merged content, surfaces a deterministic advisory when an appended block would be unreachable, and uses a unified atomic-swap policy that renames staged content directly over existing hooks without unlinking them first, with deterministic recovery guidance on swap failures; this behavior is documented in `context/sce/setup-githooks-install-flow.md`. The setup command parser/dispatch now also supports composable setup+hooks runs (`sce setup --opencode|--claude|--pi|--codex|--all --hooks`) plus hooks-only mode (`sce setup --hooks` with optional `--repo `), enforces deterministic compatibility validation (`--repo` requires `--hooks`; target flags remain mutually exclusive), and emits deterministic setup/hook outcome messaging (`installed`/`updated`/`skipped`); this behavior is documented in `context/sce/setup-githooks-cli-ux.md`. diff --git a/context/patterns.md b/context/patterns.md index 8eacf8926..ccdcec3ca 100644 --- a/context/patterns.md +++ b/context/patterns.md @@ -118,7 +118,7 @@ - Keep production CLI path ownership centralized in `cli/src/services/default_paths.rs`; new non-test path literals or path-shape definitions should be added there as named accessors/constants instead of becoming new path owners in other modules. - Keep SCE-owned web URI construction centralized in `services::agent_trace`; production Rust code should use its helpers instead of repeating `https://sce.crocoder.dev`, host-only variants, or derived path prefixes. -- Prefer localized `#[allow(dead_code)]` on intentionally shared path/setup helper items over file-level dead-code suppression so lint scope stays narrow while keeping catalog seams available to tests and future consumers. +- Dead-code exceptions are declaration-level `#[allow(dead_code, reason = "...")]` only, naming the compatibility, deferral or cross-target reason; never file-wide or module-wide. Unused helpers with no such reason are deleted, not suppressed. - For early CLI foundation tasks, keep the real top-level command catalog/help metadata centralized in one canonical seam (`cli/src/cli_schema.rs` in the current architecture) and let custom top-level help renderers consume that seam instead of maintaining a second parallel command list. - Keep top-level help intentionally curated: command visibility on `sce`, `sce help`, and `sce --help` may differ from parser availability when a command should remain directly invocable but temporarily hidden from operator-facing help. - Keep wrapper-only help rows or banner rendering logic outside the clap catalog, but do not duplicate the real command visibility/purpose metadata in those renderers. @@ -155,7 +155,7 @@ - Route local Turso access through service adapters so command handlers do not expose low-level `turso` API details. New Turso-backed services should build on `cli/src/services/db/mod.rs` (`DbSpec` + `TursoDb`, or `EncryptedTursoDb` when at-rest encryption is required) for runtime, connection, per-database `__sce_migrations` tracking, and migration infrastructure, then expose domain-specific async methods from their own service modules. - For current local DB flows, route initialization through the dedicated adapter (`cli/src/services/local_db/mod.rs`) and invoke it from approved orchestration surfaces such as setup or doctor rather than exposing a partial user command before its contract is approved. - For Turso-backed services with setup/doctor ownership, add service-owned lifecycle providers that reuse shared DB path-health and parent-bootstrap helpers, then register them through `lifecycle_providers()` instead of adding command-local database checks. -- For transient local IO/database hotspots, apply bounded resilience wrappers with explicit retry count, timeout, and capped backoff (`cli/src/services/resilience.rs`) and surface terminal failures with deterministic `Try:` remediation guidance. Use async `run_with_retry` for Turso and other async operations; use sync `run_with_retry_sync` only for pure blocking contexts where a Tokio sleep/timeout future cannot be awaited. For Turso database constructors/openers, wrap only the local open/connect operation in retry; keep migration execution outside retry because schema changes must not be replayed. Use `TursoDb::new()` for setup/lifecycle-owned schema initialization and `TursoDb::open_without_migrations()` only for hot runtime paths that verify required schema separately before query/write work. For `TursoDb` and `EncryptedTursoDb` operation retry, convert params to owned cloneable Turso params before retrying `execute()`/`query()`, retry `query_map()` query plus row-fetch failures, and keep caller row-mapping outside retry. Retry policies for both connection-open and query operations can now be configured per database via `policies.database_retry` in `sce/config.json`, parsed and resolved in `cli/src/services/config/mod.rs`, with fallback to hardcoded defaults when the config key is absent. Opt an Agent Trace write into the write-contention retry only when its whole retry unit is replay-safe: a complete `BEGIN IMMEDIATE` → `COMMIT` transaction primitive, or a single statement made idempotent by its SQL (`ON CONFLICT DO NOTHING`, a guarded `UPDATE`) routed through `TursoDb::execute_idempotent_write`; append-only or last-writer statements stay on generic `execute()`. +- For transient local IO/database hotspots, apply bounded resilience wrappers with explicit retry count, timeout, and capped backoff (`cli/src/services/resilience.rs`) and surface terminal failures with deterministic `Try:` remediation guidance. Use async `run_with_retry` (cancels the attempt when the timeout elapses) for Turso and other async operations, and `run_with_retry_elapsed` (measures elapsed time without cancelling the attempt) for operations that must lend an exclusive borrow; there is no synchronous retry variant. For Turso database constructors/openers, wrap only the local open/connect operation in retry; keep migration execution outside retry because schema changes must not be replayed. Use `TursoDb::new()` for setup/lifecycle-owned schema initialization and `TursoDb::open_without_migrations()` only for hot runtime paths that verify required schema separately before query/write work. For `TursoDb` and `EncryptedTursoDb` operation retry, convert params to owned cloneable Turso params before retrying `execute()`/`query()`, retry `query_map()` query plus row-fetch failures, and keep caller row-mapping outside retry. Retry policies for both connection-open and query operations can now be configured per database via `policies.database_retry` in `sce/config.json`, parsed and resolved in `cli/src/services/config/mod.rs`, with fallback to hardcoded defaults when the config key is absent. Opt an Agent Trace write into the write-contention retry only when its whole retry unit is replay-safe: a complete `BEGIN IMMEDIATE` → `COMMIT` transaction primitive, or a single statement made idempotent by its SQL (`ON CONFLICT DO NOTHING`, a guarded `UPDATE`) routed through `TursoDb::execute_idempotent_write`; append-only or last-writer statements stay on generic `execute()`. - For SCE operator-health commands, prefer deterministic local diagnostics over implicit pass/fail behavior: report the inspected environment scope, stable problem categories, severity/fixability classes, actionable remediation text, and any path/location facts needed to repair the issue; when repair mode exists, keep outcome vocabulary deterministic and idempotent (`cli/src/services/doctor/mod.rs`, with focused diagnosis/render/fix helpers under `cli/src/services/doctor/`). - For service-owned operator health, keep command modules as thin aggregators over `ServiceLifecycle` providers once a lifecycle slice is wired: providers own diagnosis/fix problem production through narrow capability accessors, while command-specific report builders preserve existing output facts and rendering contracts. - Keep static lifecycle provider-list construction centralized in the lifecycle service layer so doctor/setup choose provider inclusion without maintaining parallel concrete provider lists. @@ -194,7 +194,7 @@ - Pure unit tests should test in-memory logic, parsing, validation, and data transformations without external dependencies; prefer mocking/faking external dependencies over creating real filesystem or database state when a fake is available and sufficient. - In-memory database tests (e.g., `LocalDatabaseTarget::InMemory`) are acceptable for unit tests since they don't touch the filesystem. -- A filesystem-touching `#[cfg(test)] mod tests` inline in the module under test is an established, Nix-sandbox-safe pattern in this codebase when every path is a unique, process-local path under `TMPDIR` (itself a writable per-build directory inside the Nix sandbox). Prefer an RAII `tempfile::TempDir` fixture, which cleans itself up even when a test panics — as `cli/src/services/mutation_trace/store.rs`'s `RepositoryAgentTraceDb`-backed tests, `runtime/tests.rs`, `runtime/protected_worktree.rs`, `runtime/scope_runtime.rs`, `runtime/mutation_attribution.rs`'s real-store-and-`GitSnapshotService` seam test, and `cli/src/services/hooks/mod.rs`'s `mutation_attribution_e2e` real Git + repository-DB post-commit regressions do. Manually composed unique `std::env::temp_dir()` paths are the older form of the same pattern and remain in `cli/src/services/checkout/mod.rs`'s checkout-identity-lock tests and in `runtime/coordinator.rs`, `runtime/worktree_lock.rs`, and `runtime/external_taint.rs`; they leak their directory on a panicking test, so new tests should use `TempDir`. This is not integration-test-only territory in this repository. +- A filesystem-touching `#[cfg(test)] mod tests` inline in the module under test is an established, Nix-sandbox-safe pattern in this codebase when every path is a unique, process-local path under `TMPDIR` (itself a writable per-build directory inside the Nix sandbox). Prefer an RAII `tempfile::TempDir` fixture, which cleans itself up even when a test panics — as `cli/src/services/mutation_trace/store.rs`'s `RepositoryAgentTraceDb`-backed tests, `runtime/protected_worktree.rs`, `runtime/scope_runtime.rs`, `runtime/mutation_attribution.rs`'s real-store-and-`GitSnapshotService` seam test, and `cli/src/services/hooks/mod.rs`'s `mutation_attribution_e2e` real Git + repository-DB post-commit regressions do. Manually composed unique `std::env::temp_dir()` paths are the older form of the same pattern and remain in `cli/src/services/checkout/mod.rs`'s checkout-identity-lock tests and in `runtime/coordinator.rs`, `runtime/worktree_lock.rs`, and `runtime/external_taint.rs`; they leak their directory on a panicking test, so new tests should use `TempDir`. This is not integration-test-only territory in this repository. - Use integration tests instead of this inline pattern where the behavior genuinely cannot be made deterministic and isolated inside the Nix sandbox this way. - Do not depend on a shared or ambient path (`$HOME`, a fixed `/tmp` file name, the repository's own working tree) from a unit test; each test must construct its own unique, self-cleaning path. - When a unit test needs behavior that cannot be made Nix-sandbox-safe this way (for example, network access, or state shared across the whole test binary), delete it from the unit-test suite and reintroduce that coverage later as an integration test instead of keeping ignored tests in-tree. diff --git a/context/plans/cli-async-persistence-cleanup-pr3.md b/context/plans/cli-async-persistence-cleanup-pr3.md new file mode 100644 index 000000000..9bcb7c97d --- /dev/null +++ b/context/plans/cli-async-persistence-cleanup-pr3.md @@ -0,0 +1,303 @@ +# Plan: cli-async-persistence-cleanup-pr3 + +## Change summary + +PR3 is the dead-code elimination and post-async cleanup that follows PR2 (PR #301, branch `remove-tokio-db-runtimes`, still open at head `f8e9c748`, itself based on `single-tokio-runtime`). PR2 removed the database-owned Tokio runtimes and migrated persistence to native async; its Deferred cleanup ledger in `context/plans/cli-async-turso-persistence-pr2.md` hands PR3 the redundant `async` wrappers, `run_with_retry_sync`, the redundant `drop(storage)`, stale `#[allow(dead_code)]` annotations, orphaned test support, the `join_three_to_completion` re-evaluation, the large-future/`recursion_limit = "256"` measurement and the temporary-index limitation. + +An external investigation reported 114 compiler-reported unused items once dead-code suppressions are removed. That report is a seed investigation, not an authoritative inventory and not a deletion quota: its symbol-by-symbol listing is not committed to the repository, and its figures (114 items, 35 files with suppressions, 37 occurrences, 117 later `rg` matches) may use different counting conventions. **T01's reproducible, source-grounded compiler inventory, taken on the exact PR2 baseline, is the authoritative baseline for PR3.** T01 keeps the seed's classification leads (Category A retained `diff_traces` compatibility surface; Category B unwired mutation-cursor ref reconciliation and its dependencies; Category C superseded Agent Trace, repository identity, setup, sync and retry helpers; Category D unused database, path, doctor, hook, capability, configuration and mutation-trace helpers; and fields written but never read) and classifies every reproduced item. T02 deletes the verified Category C/D code and its suppressions, T03 applies the retain-and-defer disposition to the reconciliation cluster, T04 simplifies the remaining async layers, T05 audits blocking sites, future size and temporary-index cleanup, and T06 re-runs the unsuppressed audit and prepares the validation and PR4 handoff. Full validation is a separate `/validate` gate after T06. + +**Stacking.** PR3 is a stacked PR. The working branch `dead-code-blues` currently sits exactly at the PR2 head (`f8e9c748`), so every PR3 diff and review is taken against `remove-tokio-db-runtimes` (`git diff remove-tokio-db-runtimes...HEAD`), never against `main`, so the PR2 migration is not re-reviewed. If PR2 merges first, rebase onto its integration branch and keep the same commit range. + +**Reconciliation default: retain and defer.** Evidence in the current code and context: `pin_tree` is called only from `coordinator.rs`, nothing in production deletes pins, and `ref_reconciliation` has no caller (`mod ref_reconciliation;` is private; `context/cli/mutation-trace-ref-reconciliation.md` says invocation timing belongs to a later "harness-wiring PR"). The implementation and its integration path are already planned in `context/plans/mutation-cursor-ref-reconciliation.md`, and a separate `ref-rec` branch carries related reconciliation work. So pins left by crashed or failed `coordinate()` paths accumulate until a manual `git update-ref -d`; this documented consequence is accepted. PR3 therefore retains the reconciliation implementation and the helper APIs it needs, narrows suppressions to justified declarations, records that it remains unwired, and does not wire it. No new approval and no new ADR are required for this default; durable context and a reference to the existing reconciliation plan suffice. A new user or architecture decision is needed only if T01 finds material new evidence that invalidates retain-and-defer. + +## Acceptance criteria + +How this plan is proven complete. The final `/validate` gate (run after T01–T06 are complete and synchronized) executes these checks and the Full validation list below; T01–T06 prepare evidence and run only focused checks for their own changes, and none claims full repository validation. + +- [x] AC1: T01's reproduced, symbol-level compiler inventory is the authoritative dead-code baseline: it identifies the exact PR2 baseline commit, was produced from unsuppressed compiler diagnostics in an isolated worktree covering the binary and test targets and accounting for `cfg` and supported platforms, records the actual number of unique unused declarations, counts suppression attributes separately from `rg` matches, and explains differences from the seed report's 114 items where evidence exists. Every inventory row has file, category (A/B/C/D or field), usage evidence and disposition. If the original full report is located it is retained as supporting audit evidence; otherwise the plan states that the reproduced inventory supersedes it. No rows were invented to reach 114. + - Validate: inspect the inventory recorded in T01's completion record (or the file it names under `context/`); confirm the baseline commit, per-target diagnostic counts, unique-declaration count, suppression-attribute count versus `rg` match count versus files-with-suppressions count, the explanation of differences from the seed figures, and that each row has symbol, file, production/test/cross-platform/generated usage, decision/context requirement, category, disposition and risk. +- [x] AC2: Verified Category C/D dead code, written-but-never-read fields and obsolete tests are deleted together with their dead callers, constructors, imports and suppressions, and only rows classified safe-to-remove were deleted; no deletion was made on Linux-only evidence for platform-gated code. + - Validate: `git diff --stat remove-tokio-db-runtimes...HEAD -- cli/src`; each deleted symbol has an inventory row with a `delete` disposition; platform-gated symbols (macOS/Windows variants in `default_paths.rs`, `GuardError::UnsupportedPlatform`) were checked against `cfg` and supported targets rather than the Linux diagnostic. +- [x] AC3: The `diff_traces` compatibility contract from `context/decisions/2026-10-01-retire-diff-traces-remote-sync-compatibility-gate.md` is intact. These surfaces are intentionally unused during the staged compatibility window, not accidentally dead, and need no production callers: `ingest_diff_traces` and `read_diff_traces_after` still exist; `AgentTraceDiffTraceExportRow` and its SQL (`SELECT_DIFF_TRACES_AFTER_SQL`) and mapping (`diff_trace_export_row_from_turso`) dependencies remain; `IngestionStream::DiffTraces` keeps its wire representation; `/state.cursors.diffTraces` remains required; `streams.diffTraces` remains in `sce sync --format json` as a zero-upload entry (`uploaded: 0`, `batches: 0`, `initialCursor == finalCursor == state.cursors.diffTraces`); the remote sync path uploads only `messages`, `parts` and `agent_traces` and does not read local `diff_traces` rows; local `diff_traces` capture and consumers are unchanged. Any dead-code exceptions for these declarations are declaration-level and cite the decision. + - Validate: `nix shell nixpkgs#ripgrep -c rg -n "ingest_diff_traces|read_diff_traces_after|AgentTraceDiffTraceExportRow|SELECT_DIFF_TRACES_AFTER_SQL|diff_trace_export_row_from_turso|DiffTraces|diffTraces" cli/src` finds each definition and the wire-name/cursor handling, and `git diff remove-tokio-db-runtimes...HEAD -- cli/src` shows no signature, serialization or `/state` DTO change for them; the existing sync tests that assert the three-stream upload, the required `cursors.diffTraces` decode and the zero-upload `streams.diffTraces` JSON entry pass (names confirmed via `-- --list`; zero matches do not count); local hook-ingress, patch-reconstruction, post-commit intersection and attribution consumers of `diff_traces` show no diff. A symbol-existence grep alone is not sufficient. +- [x] AC4: Ref reconciliation is retained and deferred by default: the implementation and required helper APIs are preserved, suppressions are narrowed to justified declarations, it remains unwired, and PR3 introduces no new production behavior for it (no caller, hook, command or doctor wiring). Durable context records that it is unwired, that orphaned snapshot pins can accumulate, and references `context/plans/mutation-cursor-ref-reconciliation.md` as the intended integration path. A different disposition exists only if T01 recorded material new evidence and a user or architecture decision followed. + - Validate: inspect the T03 record; `rg -n "reconcile_worktree" cli/src` shows no non-test caller outside `ref_reconciliation.rs`; `context/cli/mutation-trace-ref-reconciliation.md` and `context/cli/mutation-trace-snapshot-service.md` state the retained-and-unwired status and the orphaned-pin consequence; no duplicate ADR was added to restate an already accepted decision. +- [x] AC5: No blanket or file-wide `#[allow(dead_code)]` remains that hides unrelated declarations; every remaining exception is declaration-level with a stated compatibility, deferral or cross-target reason; no `#[allow(unused)]`, unrelated warning suppression or weakened lint setting was added. + - Validate: `rg -n "allow\(dead_code\)|allow\(unused" cli/src` (via nix) lists only the justified sites recorded in T06; no `#![allow(dead_code)]` at file level; `git diff remove-tokio-db-runtimes...HEAD -- cli/Cargo.toml cli/src/main.rs flake.nix` shows no lint weakening. +- [x] AC6: Two verification modes are kept distinct. In diagnostic mode (isolated worktree, suppressions temporarily removed, binary and test targets compiled), a nonzero exit caused solely by expected dead-code diagnostics is an expected audit outcome, and any other compiler failure is investigated rather than counted as dead code. In normal verification mode (retained narrowly scoped suppressions restored), the binary and test targets compile, clippy passes with warnings denied, and the normal validation checks pass. The final diagnostic-mode audit reports only known, documented retained declarations. + - Validate: the T06 diagnostic-mode output is recorded with its build-target and platform context and every diagnostic maps to a documented exception; no claim of cross-platform safety is derived from the Linux build alone; normal-mode results come from `/validate`. +- [x] AC7: Retry, cancellation, transaction-ownership, adapter/worktree-lease and terminal-sync/credential-persistence behavior is unchanged; `run_with_retry` (cancelling timeout) and `run_with_retry_elapsed` (elapsed measurement, no cancellation) remain distinct; `run_with_retry_sync` is gone only if it had no active consumer. + - Validate: `/validate` runs the complete Rust suite, including `db::transaction_cancellation_tests`, `agent_trace_db::transaction_tests`, the sync `terminal_stream` tests and the Quint MBT suite in `cli/src/services/mutation_trace/mbt`; `git diff remove-tokio-db-runtimes...HEAD -- cli/src/services/resilience.rs` shows no policy change. +- [x] AC8: `join_three_to_completion`/`JoinSlot` is either simplified without changing cooperative stopping, in-flight batch completion, credential-persistence protection, first-observed-error selection, non-`Send` progress reporting or concurrent execution, or is retained with a recorded reason; no `try_join!` cancellation of siblings, no extra spawned tasks. + - Validate: inspect the `cli/src/services/sync/sync.rs` diff and the T04 record; `terminal_stream` tests pass in `/validate`. +- [x] AC9: No dynamic dispatch, `BoxFuture`/`dyn Future`, or nested Tokio runtime is introduced. T05 measured large-future size and `recursion_limit = "256"` pressure (a reduction is not assumed possible; "no reduction available" is an acceptable recorded outcome), and the temporary-index (`TempIndexGuard`) finding is documented; the recursion-limit override is removed only if the pinned compiler and the full checks pass without it. + - Validate: `git diff remove-tokio-db-runtimes...HEAD -- cli/src` searched for `dyn `, `BoxFuture`, `Runtime::new`, `block_on` finds nothing new; the T05 record states the measured future sizes, the measurement method and the decisions on `#![recursion_limit]` and on temporary-index cleanup (default: no automatic sweeper). +- [x] AC10: No broad test restoration occurred (no deleted ordinary test suite was restored for coverage parity) and the Rust diff is net smaller. + - Validate: `git diff --shortstat remove-tokio-db-runtimes...HEAD -- cli/src` shows deletions exceeding insertions; no previously deleted test module or fixture is re-added (`git diff --diff-filter=A --name-only remove-tokio-db-runtimes...HEAD -- cli`). +- [x] AC11: Durable context and docs match the code, including any docs that claim guarded-shell descriptor creation is atomic on every supported platform; the macOS `pipe()`/`fcntl()` race is described as an unresolved limitation, not fixed. + - Validate: inspect `context/cli/mutation-trace-external-mutation-guard.md` and `context/cli/async-persistence-guarantees.md` against `external_mutation_guard.rs` (`pipe2` on Linux, `pipe`+`fcntl` fallback elsewhere); `context/cli/default-path-catalog.md`, `capability-traits.md`, `repository-identity.md`, `agent-trace-sync-command.md` and the context map no longer describe deleted symbols. +- [x] AC12: Validation ownership is respected: T06 records which checks must run during final validation but does not claim they have passed; the plan is marked fully validated only after `/validate` records actual results, including failures. + - Validate: `/validate`'s report lists each Full validation command with its real outcome. + +### Full validation + +Owned by the separate final `/validate` gate (repository workflow `sce-validate`), run only after T01–T06 are complete and every task is `synced`. Record actual results, including failures. Do not mark PR3 fully validated because T06 is complete. + +- `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml` (complete Rust suite) +- `nix develop -c ./scripts/run-cli-cargo.sh clippy --manifest-path cli/Cargo.toml --all-targets --all-features -- -D warnings` +- `nix flake check` +- `nix build .#ci-checks` +- `nix run .#pkl-check-generated` (only if a Pkl or `config/lib` input was touched; generated-input and platform checks relevant to the diff) +- `git diff --check` + +### Verification modes + +- **Diagnostic mode (T01, T06, optionally T05):** in an isolated `git worktree`, temporarily remove dead-code suppressions, compile the non-test binary and the test targets (`cargo test --no-run`), capture warnings/errors, identify unused declarations, and record diagnostics with their build-target and platform context. A nonzero exit caused solely by expected dead-code diagnostics (the project compiles with `-D warnings`) is an expected audit outcome, not a verification failure. Other compiler failures are investigated, not counted as dead code. Discard the worktree safely (`git worktree remove`), never with destructive restore commands against the main worktree. +- **Normal verification mode (T02–T05 focused checks and `/validate`):** with the intentionally retained, narrowly scoped suppressions restored, the binary and test targets compile, clippy passes with warnings denied and the normal validation checks pass. Unrelated warnings are never suppressed and compiler lint settings are never weakened. + +### Context sync + +- `context/cli/async-persistence-guarantees.md` (PR3 staging, known limits: future size, temporary index, macOS descriptor race wording) +- `context/architecture.md`, `context/patterns.md` (only where deleted seams are described) +- `context/cli/mutation-trace-ref-reconciliation.md`, `mutation-trace-snapshot-service.md`, `mutation-trace-store.md`, `mutation-trace-runtime-coordinator.md` (retained-and-unwired status, orphaned-pin consequence, cluster ownership and link to `context/plans/mutation-cursor-ref-reconciliation.md`) +- `context/cli/default-path-catalog.md`, `capability-traits.md`, `repository-identity.md`, `agent-trace-sync-command.md`, `sync-command.md`, `cli-command-surface.md`, `service-lifecycle.md` (only for deleted symbols they name) +- `context/context-map.md`, `context/overview.md`, `context/glossary.md` when entries change +- No new ADR for the default retain-and-defer reconciliation disposition (already covered by the accepted plan and context); a new ADR only if T01 finds material new evidence that forces a different architectural decision + +## Task context synchronization lifecycle + +Persist this field in every plan; this is durable plan state, not chat state: + +- **Task context synchronization:** every task carries `pending | synced | blocked`. + A completed task must be `synced` before another task can start or the plan can + finish. +- For `blocked`, record **Blocker**, **Required action**, and **Retry condition** + beside the status. Never infer `synced` from conversation history; write every + lifecycle transition to the plan file. + +## Constraints and non-goals + +- **In scope:** `cli/src` dead-code and suppression cleanup; deleting verified-dead production code and the tests/fixtures that only exercised it; `resilience.rs`, `db/mod.rs`, `sync/sync.rs`, service forwarding and hook/lifecycle helper simplification; measurement/audit of futures, `spawn_blocking` sites and temporary Git indexes; durable context updates for the above. +- **Out of scope:** the macOS guarded-shell `pipe()`/`fcntl()` race fix; implementing or wiring ref reconciliation; new mutation-cursor semantics; database schema migrations; retry policy changes; OpenTelemetry (PR4); broad test-suite restoration; new generic execution or persistence abstractions; removal of the `diff_traces` compatibility surface (separate planned phase); full repository validation (owned by the final `/validate` gate). +- **Constraints:** the baseline is PR2 head `f8e9c748` on `remove-tokio-db-runtimes`; all diffs are measured against that branch. All tools run through Nix per `AGENTS.md` (`nix develop -c ./scripts/run-cli-cargo.sh ...`, `nix shell nixpkgs#ripgrep -c rg`); no host `rg`, `cargo`, `python3`. Reproduction and the unsuppressed build run in a disposable `git worktree`/copy, never with destructive restore commands (`git checkout .`, `git reset --hard`) against the main worktree. Repository conventions apply: no code comments except documented-exception reasons required by this plan's suppression policy (user preference is comment-free code, so an exception's reason lives in the attribute's `reason = "..."` string or the owning context doc rather than prose comments — T01 confirms the repo's lint level accepts `reason`), exact-pinned dependency versions unchanged, minimal focused tests only. +- **Constraints (preservation):** keep static dispatch and associated-type capability boundaries; do not add dynamic dispatch; preserve fail-closed recovery in mutation-scope adapters; do not delete platform-gated code on Linux-only evidence; written-but-never-read state fields are removed only after checking serialization, derives, Debug, Drop, tests and protocol transitions. +- **Non-goal:** preserving unused APIs for hypothetical future flexibility, or converting dead production code to `#[cfg(test)]` merely to keep obsolete tests alive. +- **Non-goal:** meeting a deletion count or reproducing the seed report's 114; the reproduced inventory is the baseline and no rows are invented to reach any number. +- **Non-goal:** restoring deleted ordinary test suites for coverage parity. +- **Non-goal:** deleting the reconciliation cluster to shrink the dead-code count, or wiring it into production in PR3. +- **Constraint (compatibility):** the `diff_traces` compatibility contract in `context/decisions/2026-10-01-retire-diff-traces-remote-sync-compatibility-gate.md` is preserved; intentionally unused compatibility declarations are retained with narrow exceptions rather than deleted. + +## Assumptions + +- The seed report's figures (114 items, 35 files, 37 occurrences) and the later `rg` count of 117 `allow(dead_code)` matches (35 files on the PR2 head) may use different counting conventions (attributes versus annotated declarations versus compiler-reported items). The original symbol-by-symbol listing is not committed and cannot be assumed reproducible; T01 establishes the authoritative counts and does not invent rows. +- Since PR2 is open and `dead-code-blues` equals the PR2 head, no rebase is needed now; the PR base for review is `remove-tokio-db-runtimes`. +- `reason = "..."` on `#[allow(dead_code, reason = ...)]` is acceptable under the pinned toolchain; if not, the exception is documented in the owning context file and named in T06's audit table instead of in a code comment (rests on the user's recorded no-comments-in-code preference). +- Per the user's prefs, final verification is the `/validate` gate using the commands in Full validation; T02–T06 use only narrow focused checks (`cargo test` filtered by names confirmed via `-- --list`; zero-match filters never count as passing). +- The reconciliation default (retain and defer) follows the existing context statement that invocation timing is a later harness-wiring decision, the existing plan `context/plans/mutation-cursor-ref-reconciliation.md` and the "false retention costs disk, false deletion destroys evidence" bias recorded in `mutation-trace-ref-reconciliation.md`. + +## Task stack + +- [x] T01: `Reproduce the authoritative dead-code baseline and classify it` (status:done) + - Task ID: T01 + - Scope: In — (1) identify the exact PR2 baseline commit (`git rev-parse remove-tokio-db-runtimes`; expected `f8e9c748`) and confirm `git diff remove-tokio-db-runtimes...HEAD -- cli/src` is empty; (2) in a disposable `git worktree`, in diagnostic mode, temporarily remove every dead-code suppression and reproduce the unsuppressed compiler diagnostics, repeating until the diagnostic set is stable; (3) inspect the non-test binary target and the test targets (`cargo test --no-run`) separately and record per-target diagnostics; (4) account for conditional compilation and supported platforms (`cfg(target_os)`, Windows/macOS variants, features) rather than treating the Linux build as complete; (5) produce a symbol-level inventory with file, category, usage evidence (production binary, tests, other supported target/platform, generated code or build tooling, context/ADR requiring retention) and disposition (delete, retain with reason, decision-needed) plus risk; (6) record the actual number of unique unused declarations; (7) count suppression attributes, `rg` matches and files-with-suppressions as separate figures; (8) explain differences from the seed report's 114/35/37/117 figures where evidence is available; if the original full report can be located retain it as supporting audit evidence, otherwise state explicitly that the reproduced inventory supersedes it, and do not invent rows to reach 114. The seed leads guide classification: Category A intentionally retained `diff_traces` compatibility surface (protected list per the accepted decision); Category B unwired mutation-cursor ref reconciliation and its dependencies; Category C superseded Agent Trace, repository identity, setup, sync and retry helpers; Category D unused database, path, doctor, hook, capability, configuration and mutation-trace helpers; and fields written but never read (with serialization/derive/Debug/Drop/protocol checks and a separate list of suspected logic defects). Also record the full reconciliation dependency graph (used only by `ref_reconciliation` and its tests versus also by production, and whether any material new evidence invalidates retain-and-defer), baseline measurements (test list names via `-- --list`, LOC of `cli/src`, per-category removable LOC estimate, future/`recursion_limit` baseline method, current `spawn_blocking` sites), and `run_with_retry_sync` consumers. The inventory is recorded as an appendix of this plan's T01 completion record (not under `context/tmp`). Out — any production or test modification in the real worktree, any deletion, wiring, or lint change, and counting expected dead-code diagnostics as failures. + - Dependencies: none + - Done when: the baseline commit, per-target diagnostics, unique-declaration count, separate suppression/match/file counts and the explanation of differences from the seed figures are recorded; every reproduced item has a row with category and disposition; counts of delete-now / Category A / retained-for-tests-or-cross-platform / decision-needed are recorded; the reconciliation graph, the `run_with_retry_sync` consumer check and the D-category platform variants are resolved; the disposable worktree is removed without touching unrelated changes; the main worktree is unchanged. + - Verify: in the disposable worktree, `nix develop -c ./scripts/run-cli-cargo.sh build --manifest-path cli/Cargo.toml` and `... test --manifest-path cli/Cargo.toml --no-run` with suppressions removed (nonzero exit from dead-code diagnostics alone is expected; any other failure is investigated); `git status --short` in the main worktree is unchanged; `nix shell nixpkgs#ripgrep -c rg -c "allow\(dead_code\)" cli/src` figures recorded. + - Completed: 2026-10-08 + - Files changed: `context/plans/cli-async-persistence-cleanup-pr3.md` (this record and the T01 inventory appendix); no `cli/` changes + - Result: Authoritative baseline reproduced on PR2 head `f8e9c748` (`cli/src` diff vs `remove-tokio-db-runtimes` empty; branch HEAD `2f073f98` adds only context plan commits). 147 unique unused declarations on the binary target (114 compiler warnings, several grouping multiple symbols), 127 symbol entries on the test target (100 warnings). Classified (corrected after PR review): 5 Category A, 30 Category B, 4 cross-platform retains, 7 fields retained pending defect review, 101 delete-now. See "T01 inventory appendix". + - Verify: unsuppressed `build` and `test --no-run` ran in a disposable worktree with `RUSTFLAGS=--cap-lints warn` (so the `-D warnings` lint level reports every diagnostic instead of aborting); both exited 0 with the diagnostic counts above; main worktree `git status --short` empty before and after; disposable worktree removed; `rg` suppression figures recorded in the appendix; `#[allow(dead_code, reason = "...")]` compiles under `-D warnings` on rustc 1.95.0 (open question resolved). + - Post-review correction (PR #303 review): two inventory rows were corrected without a new compiler run or any claimed test execution. (1) `run_external_mutation_guard` stays a delete of the production wrapper only; its two guarded-shell regression tests are preserved by adaptation, not deleted. (2) `WorktreeLock::acquire` moved from Category B to D (delete in T02): callers verified from source are `acquire_async` and `acquire_inner_async` (git_snapshot, mutation_attribution, protected_worktree, ref_reconciliation) while `acquire` has none; `acquire_inner` stays as the blocking implementation of `acquire_inner_async`. Totals: Category B 31 to 30, delete-now 100 to 101; 101+5+30+4+7 = 147 reconciles to the unique-declaration count. Compiler diagnostic counts (114 / 147 / 100 / 127) are unchanged. + - Context impact: none to root context; the appendix is plan-local evidence consumed by T02-T06. + - Context synchronization: synced +- [x] T02: `Delete verified Category C/D dead code and its suppressions` (status:done) + - Task ID: T02 + - Scope: In — only items T01 classified safe to remove (`delete` disposition with usage evidence), in small module-grouped commits: Agent Trace DB convenience inserts; unused `default_paths.rs` helpers/constants (only platform variants T01 proved unused on every supported target); doctor, mutation-scope adapter, capabilities (`FsOps`/`GitOps` methods and the file-wide `#![allow(dead_code)]`), `db/mod.rs` `QueryRows`/`query`/`query_values`, `HasFs::fs`, `AppContext` accessors, config/command-registry/lifecycle/storage-resolver helpers; superseded Category C items T01 verified against their originating plans/ADRs (agent-trace generator and validators, `repository_identity` dir-segment helpers, setup hook-asset and prompt helpers, `run_current_sync*`/`SyncCommand::execute`, `load_patch_from_json_bytes`, the unused `run_external_mutation_guard` production wrapper and its re-exports (its two guarded-shell regression tests are preserved, see below), the unused synchronous `WorktreeLock::acquire` wrapper, `GuardError::UnsupportedPlatform` only if no supported target needs it); genuinely dead fields (decided per T01's field table); dead tests and test support that only exercised deleted code; the `#[allow(dead_code)]` attributes those removals leave unneeded, then a second diagnostic-mode pass to catch newly exposed unreachable items. Out — Category A items (including all `diff_traces` compatibility declarations), the Category B reconciliation cluster, `run_with_retry_sync` and async wrappers (T04), any state field whose non-use may hide a missing invariant (recorded as a suspected defect instead), any row not classified safe-to-remove, restoring tests. + - Dependencies: T01 + - Done when: all T02-owned deletion candidates are removed, including their unused imports, constructors, callers and suppressions. Items explicitly assigned to later tasks, including `run_with_retry_sync` in T04, remain tracked and do not block T02 completion; broad/file-wide suppressions in the touched files are gone; the production and test targets build in normal mode with `-D warnings`; Category A symbols are untouched; each commit is one module/ownership group; suspected logic defects are listed in the completion record, not deleted; `guarded_shell_supervision_leaves_the_runtime_worker_responsive` and `guarded_shell_streams_output_and_finishes_with_exit_code` still exist in `external_mutation_guard.rs`, drive the real guarded-shell supervisor through `arm_external_mutation_guard(...).await` and `ArmedExternalMutationGuard::exec(...).await` (any `#[cfg(test)]` helper is test-only, not a production abstraction), preserve `GuardEvent::Armed` timing, output event order/contents, exit-code and external-taint marker cleanup assertions and the single-worker Tokio responsiveness assertion, use no mocks bypassing the supervisor, and pass; `acquire_inner_async` and `acquire_inner` are retained unchanged (no change to polling, timeouts or cancellation protection). + - Verify: focused normal-mode checks only: `nix develop -c ./scripts/run-cli-cargo.sh build --manifest-path cli/Cargo.toml`; `nix develop -c ./scripts/run-cli-cargo.sh clippy --manifest-path cli/Cargo.toml --all-targets --all-features -- -D warnings`; `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml ` for touched modules after confirming names with `-- --list`; `git diff --check`; explicitly run both guarded-shell tests by name (confirmed via `-- --list`; a zero-match filter never counts) after the wrapper is removed. + - Completed: 2026-10-08 + - Files changed: `cli/src/**` (about 60 source files across app, agent_trace, agent_trace_db, agent_trace_storage, capabilities, command_registry, config, db, default_paths, doctor, error, hooks, lifecycle providers, mutation_trace, observability, output_format, patch, repository_identity, setup, sync); deleted orphan fixtures `cli/src/services/agent_trace/fixtures/{average_age,file_rename,hello_world,mixed_change,poem_edit,poem_write,text_file_lifecycle}_reconstruction/`; `context/plans/cli-async-persistence-cleanup-pr3.md`. Working tree diff vs HEAD: 116 files, 111 insertions, 3343 deletions. + - Result: Deleted all T02-owned T01 `delete` rows with their callers, constructors, imports, tests and suppressions: Agent Trace DB inserts, `agent_trace.rs` validators/`classify_hunk`/`hunks_match_exactly`/`build_agent_trace` (+ seven golden tests and fixtures), `default_paths` helpers/constants, doctor chain (`inspect_repository_hooks`, `collect_global_state_health`, `collect_hook_health`, `inspect_hook_content_state`, `display_node`, `domain`/`group`/`branch`, `validate_config_file` dependency field and its generic), capabilities `FsOps`/`GitOps` methods (`FsOps` is now a marker trait), `db/mod.rs` `QueryRows`/`query`/`query_values`/encrypted `query`, `AppContext`/`HasFs` accessors, `parse_env` x2 and `from_env_lookup`, `CliError::user` (tests construct `CliError::User { .. source: None }`), `ServiceLifecycle::id` and its five impls, mutation-scope `is_subagent`/`is_clear`/`mark_recovery_pending`/`arm_recovery`/`arm_and_begin_recovery_flush`/MCP helpers, codex `tool_response` field and `resolve_codex_patch_path`, `OutputFormat::parse`, `load_patch_from_json_bytes`, repository-identity dir-segment helpers, setup hook-asset/prompt helpers, `run_current_sync`/`run_current_sync_with_progress`/`SyncCommand::execute`, `run_external_mutation_guard` (production wrapper, non-unix stub and re-exports), `WorktreeLock::acquire`, `MutationLineage::reset_file`/`tracked_paths`, `is_terminal` x2 and `scope_actor`. The two guarded-shell tests were preserved via a test-only `arm_and_exec_guard` helper driving `arm_external_mutation_guard(...).await` and `ArmedExternalMutationGuard::exec(...).await`. Blanket/file-wide dead-code allows were removed everywhere (including `capabilities.rs`, four `*_mutation_scope/mod.rs`, `generated_migrations`, `agent_trace_storage`, `agent_trace_sync`, `bash_policy`, `repository_identity`); the unsuppressed binary and test builds now warn only on retained items. Retained with declaration-level `reason` allows: Category A `diff_traces` declarations (cite the ADR), `PlatformFamily` and `GuardError::UnsupportedPlatform` (cross-platform), `run_with_retry_sync` (T04 removes). Retained under the pre-existing module-level allow on `mutation_trace` in `services/mod.rs`, now carrying a reason (T03 narrows it): the Category B reconciliation cluster, `WorktreeLockInner::path`, and the suspected-defect fields. + - Verify: `nix develop -c ./scripts/run-cli-cargo.sh clippy --manifest-path cli/Cargo.toml --all-targets --all-features -- -D warnings` passed (exit 0, normal mode, binary and test targets); unsuppressed `RUSTFLAGS="--cap-lints warn"` build and `test --no-run` (second diagnostic pass, Linux only) report only the retained items above plus a pre-existing test-only `mbt/model.rs` `IgnoredAny` field warning that also exists at baseline; `test ... guarded_shell` ran both guarded-shell tests (2 passed, names confirmed by output); a filtered run of `agent_trace::`, `mutation_trace::lineage`, `mutation_trace::tests`, `error::`, `observability::`, `repository_identity` passed (126 passed, 0 failed); `git diff --check` clean. The full suite was not run (reserved for `/validate`); test count is now 344 (baseline 354). + - Deviations and notes: tests deleted with their code — seven `build_agent_trace` golden tests (evidence-based golden tests remain), `reset_file`, deleted-file `tracked_paths` assertion test, `dir_segment` test; `scope_status_live_and_terminal_table` was reduced to `scope_status_live_table`. `AppContext` fields `fs`/`git` and the `HasFs` associated type remain because `app_support` bounds still use them. `acquire_inner_async` and `acquire_inner` are unchanged. The `hook_dir` module and related `default_paths` constants were deleted after confirming no other users. Suspected logic defects (kept, not deleted): `CoordinateOutcome` fields `worktree_id`, `observed_tree`, `revision`, `evaluation`, `mutation_event`, and the `committed`/`completed` error-payload fields. + - Context impact: yes — context docs naming deleted symbols (default-path catalog, capability traits, repository identity, agent-trace sync command, patch/agent-trace docs) need verification and update. + - Context synchronization: synced +- [x] T03: `Retain and defer ref reconciliation; narrow its dependency-cluster suppressions` (status:done) + - Task ID: T03 + - Scope: In — default disposition is **retain and defer**; no approval gate. (1) Inspect the actual dependency graph from T01. (2) Preserve the reconciliation implementation and the helper APIs it requires. (3) Replace any file-wide suppression with declaration-level exceptions limited to cluster members used solely by reconciliation (`ref_reconciliation`, `list_pins`/`delete_pins*`/`run_ref_mutation`/`assert_pins_are_unchanged_direct_refs`/`PinnedRef`/`PinInventoryError`, `load_mutation_event*`/`load_tree_roots`/`load_all_tree_roots`/`reconstruct_boundary`/`decode_boundary_kind`, and the `WorktreeLockInner::path` field decision; `acquire_inner_async` and `acquire_inner` are live and untouched, and `WorktreeLock::acquire` is no longer in this cluster), each with a `reason` naming the deferral and `context/plans/mutation-cursor-ref-reconciliation.md` as the integration path; delete only members T01 proves are not part of the reconciliation graph; resolve the unused `WorktreeLock` `path` field per T01's field analysis. (4) Record in the ref-reconciliation, snapshot-service, store and runtime-coordinator context that reconciliation remains unwired and that orphaned snapshot pins can accumulate (pins created by `pin_tree` in `coordinator.rs`, no production deleter), and reference the existing reconciliation plan and the `ref-rec` branch as the intended integration path. (5) Keep production wiring outside PR3. Only if T01 recorded material new evidence that invalidates retain-and-defer: stop, record the evidence as a blocker and require a user or architecture decision (and an ADR only then). Do not create an ADR merely to restate the already accepted decision. Correct inaccurate context found on the way (including descriptor-atomicity claims). Out — deleting the reconciliation cluster to reduce the dead-code count, wiring reconciliation into any command/hook/doctor, adding behavior, new retention/compaction design, the macOS race fix. + - Dependencies: T01, T02 + - Done when: retain-and-defer is recorded in durable context with the unwired status, orphaned-pin consequence and plan reference; no reconciliation code is deleted or wired; exceptions are declaration-level with reasons; the `WorktreeLock` `path` field is resolved per T01. + - Verify: `rg -n "reconcile_worktree" cli/src` shows no new caller; targeted tests in `ref_reconciliation`/`git_snapshot`/`worktree_lock` run (names via `-- --list`; zero matches do not count); `nix develop -c ./scripts/run-cli-cargo.sh clippy --manifest-path cli/Cargo.toml --all-targets --all-features -- -D warnings`; context inspection against the code. + - Completed: 2026-10-08 + - Files changed: `cli/src/services/mod.rs`, `cli/src/services/mutation_trace/{store.rs,mbt/model.rs}`, `cli/src/services/mutation_trace/runtime/{coordinator,git_snapshot,ref_reconciliation,scope_runtime,worktree_lock}.rs`, `context/cli/mutation-trace-{ref-reconciliation,runtime-coordinator,snapshot-service,store}.md`, `context/plans/cli-async-persistence-cleanup-pr3.md` + - Result: Removed the module-wide `#[allow(dead_code)]` on `pub mod mutation_trace`. Unsuppressed build then reported 26 warnings, all now covered by declaration-level `#[allow(dead_code, reason = ...)]`: the reconciliation cluster (`ref_reconciliation.rs` x6, `git_snapshot.rs` x10, `store.rs` x13) cites `context/plans/mutation-cursor-ref-reconciliation.md`; the suspected-defect fields (`CoordinateOutcome` x5, `committed`, `completed`) cite this plan; `mbt/model.rs` `WireModelState` (test-only, previously hidden by the module allow) is allowed because `deny_unknown_fields` requires the verification-only fields. `WorktreeLockInner::path` was never read, is outside the reconciliation graph, and was deleted (`acquire_inner_async`/`acquire_inner` untouched). No reconciliation code was deleted or wired. Context: added a "retained and unwired" status with the orphaned-pin consequence to the ref-reconciliation, snapshot-service, store and runtime-coordinator docs; corrected them where they described removed tests (`ref_reconciliation` inline tests, `runtime/tests.rs`). + - Verify: `clippy --all-targets --all-features -- -D warnings` passed (exit 0); `rg reconcile_worktree cli/src` outside `ref_reconciliation.rs` found nothing; `test mutation_trace::runtime` ran 11 passed (includes the `git_snapshot` pin tests; `ref_reconciliation` has no tests, a zero-match filter so not counted as coverage); `cargo fmt` applied; `git diff --check` clean. Full suite not run (reserved for `/validate`). + - Deviations and notes: other docs still mention the removed `runtime/tests.rs` (`mutation-trace-agent-attribution.md`, `mutation-trace-scope-abandonment.md`, `mutation-trace-external-taint.md`, `context/patterns.md`, `context/context-map.md`); left for T06 context cleanup. + - Context impact: yes — reconciliation-status docs updated above; remaining stale `runtime/tests.rs` references noted for T06. + - Context synchronization: synced +- [x] T04: `Simplify post-async wrappers and re-evaluate sync joining` (status:done) + - Task ID: T04 + - Scope: In — remove `run_with_retry_sync` (if T01 confirmed no consumer) and any duplicate helper logic it exposes, extracting shared validation/error formatting only if simpler while preserving the distinct semantics of `run_with_retry` (cancelling timeout: the attempt future is cancelled when the timeout elapses) and `run_with_retry_elapsed` (elapsed measurement, non-cancelling); remove redundant `async { ... }.await` wrappers (for example the storage scope in `sync/sync.rs`), redundant `drop(storage)`, trivial forwarding layers and unnecessary temporaries in `db/mod.rs`, sync, hook and lifecycle helpers; evaluate `join_three_to_completion`/`JoinSlot` and simplify only if cooperative stopping, in-flight completion, credential-persistence protection, first-observed-error selection, non-`Send` progress and concurrent execution are all preserved, otherwise retain and record why. Out — retry policy changes, collapsing the cancelling and non-cancelling retry variants, `try_join!` if it cancels siblings, spawned tasks, new async abstractions, dynamic dispatch, new tests unless a replacement introduces an untested invariant. + - Dependencies: T02 + - Done when: the listed leftovers are removed or recorded as retained with a reason; retry timeout/backoff behavior is unchanged in observable semantics and the cancelling versus non-cancelling distinction is intact; the join decision and rationale are recorded; no nested runtime, `BoxFuture` or `dyn` is introduced. + - Verify: focused normal-mode checks: `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml terminal_stream` (confirm names with `-- --list`; zero matches do not count), `... agent_trace_db::transaction_tests`, `... db::transaction_cancellation_tests`; clippy with `-D warnings`; `git diff --check`. + - Completed: 2026-10-08 + - Files changed: `cli/src/services/resilience.rs`, `cli/src/services/sync/sync.rs`, `cli/src/services/db/mod.rs`, `context/plans/cli-async-persistence-cleanup-pr3.md` + - Result: Deleted `run_with_retry_sync` (zero consumers) with its `std::thread` import; `run_with_retry` (cancelling timeout) and `run_with_retry_elapsed` (non-cancelling elapsed measurement) are untouched and still distinct. In `sync/sync.rs` removed the redundant `async { ... }.await` storage scope and `drop(storage)` (storage still outlives the client and drops at function end) and the three redundant `async move { ... }.await` wrappers in the ingest closures. In `db/mod.rs` flattened the redundant `async { ... }.await` wrappers in `ensure_migrations_table`, `is_migration_applied` and `apply_migration`. Net 42 insertions, 135 deletions. + - Join decision: `join_three_to_completion`/`JoinSlot` retained unchanged. `try_join!` would cancel siblings on the first error, which breaks the in-flight batch completion and credential-persistence protection; `join!` would not give first-observed-error selection after all siblings finish; the hand-rolled poller keeps cooperative stopping, non-`Send` progress reporting and concurrent execution without spawned tasks, and is covered by the three `services::sync::sync::tests` cases. No simpler combinator preserves all of those behaviors. + - Verify: `clippy --all-targets --all-features -- -D warnings` passed (exit 0); `test terminal_stream` 2 passed; `test services::sync` 3 passed (terminal-stream stop, discard-after-termination, in-flight credential save); `test agent_trace_db::transaction_tests` 2 passed; `test db::transaction_cancellation_tests` 1 passed; `cargo fmt` applied; `git diff --check` clean; added lines contain no `dyn`, `BoxFuture`, `block_on` or `Runtime::new`. Full suite not run (reserved for `/validate`). + - Deviations and notes: other `async { ... }` blocks in `db/mod.rs` (inside retry operations) were left because they scope borrows or cancellation, not mere forwarding. + - Context impact: minor — `context/cli/async-persistence-guarantees.md` may mention `run_with_retry_sync` or the retained join helper; verify. + - Context synchronization: synced +- [x] T05: `Audit blocking sites, future size and temporary Git indexes` (status:done) + - Task ID: T05 + - Scope: In — review each remaining `spawn_blocking` site against the PR2 retained-worker table in `context/architecture.md`; measure future size and `recursion_limit` pressure for `main.rs`/`app.rs` and the largest async dispatch functions (for example compiler future-size diagnostics or `size_of_val` in a disposable worktree) and record the method and numbers; determine why `#![recursion_limit = "256"]` is needed; measure large-future complexity rather than assuming a reduction is possible, and take only safe reductions of nested async composition or captured state if the measurements show one (recording "no safe reduction" is an acceptable outcome); remove the override only if the pinned compiler and the checks pass without it; assess `TempIndexGuard` (`git_snapshot.rs`) and decide: no automatic sweeper unless a safe ownership/lock protocol already exists (filename or age alone is never evidence of abandonment), otherwise document and defer to a separate PR; fix documentation that overstates guarded-shell descriptor atomicity and describe the macOS `pipe()`/`fcntl()` race as an unresolved limitation, not fixed. Out — new concurrency/ownership protocols, a temp-index sweeper without safe ownership, the macOS race fix, `BoxFuture`/`dyn Future`, test restoration. + - Dependencies: T04 + - Done when: measurements and the keep/remove decision for the recursion limit, the temporary-index decision, and any retained blocking complexity are recorded in durable context; any code change is a safe reduction with passing focused checks; the macOS race is described as an open limitation. + - Verify: `nix develop -c ./scripts/run-cli-cargo.sh build --manifest-path cli/Cargo.toml` (and the same build without the override in a disposable worktree if removal is attempted); clippy with `-D warnings`; context inspection. + - Completed: 2026-10-08 + - Files changed: `context/architecture.md`, `context/cli/async-persistence-guarantees.md`, `context/cli/mutation-trace-external-mutation-guard.md`, `context/plans/cli-async-persistence-cleanup-pr3.md`; no `cli/` changes + - Result: Audit only, no code change. The 14 `spawn_blocking` sites match the architecture retained-worker list. Future sizes measured with `RUSTC_BOOTSTRAP=1 RUSTFLAGS=-Zprint-type-sizes` on a Linux debug build in a disposable worktree: `app::run` 14000 bytes, `main` `block_on` future 14016, the dispatch chain 13952 down to `join_three_to_completion` 11832, with the dependency `DatabaseSyncEngine::open_db` future alone at 12704, so the size is dominated by a third-party future and wrappers add about 50-200 bytes each; no safe reduction without boxing. Removing `#![recursion_limit = "256"]` fails with "queries overflow the depth limit" (increased by 130 computing the layout of `app::run`), so the override is kept. `TempIndexGuard`: no automatic sweeper (no ownership or lock record; name or age is not evidence of abandonment); documented and deferred. Docs that claimed the guarded-shell pipe is created close-on-exec atomically now say atomicity holds only on Linux (`pipe2`) and the `pipe`+`fcntl` window elsewhere is an unresolved limitation. + - Verify: normal-mode `build` passed; `clippy --all-targets --all-features -- -D warnings` passed; the no-override build in the disposable worktree failed as described above and the worktree was removed; `git diff --check` clean; context inspected against `external_mutation_guard.rs` (`pipe2` on Linux, `pipe`+`fcntl` otherwise). Full suite not run (reserved for `/validate`). + - Context impact: yes — `architecture.md`, `async-persistence-guarantees.md` and the external-mutation-guard doc updated above. + - Context synchronization: synced +- [x] T06: `Final cleanup audit and validation preparation` (status:done) + - Task ID: T06 + - Scope: In — in diagnostic mode on a disposable worktree, reproduce the final unsuppressed dead-code diagnostic audit and record that each remaining diagnostic maps to a documented declaration-level exception (Category A `diff_traces`, Category B reconciliation, any cross-target cases); classify and list all remaining suppression sites with reasons; verify the `diff_traces` compatibility declarations and contract (AC3) are intact; record removed symbol and LOC counts (before/after, against T01's inventory) and the intentionally retained code; review cross-platform limitations (Linux-only diagnostics never support a cross-platform safety claim); update durable context (`context/cli/async-persistence-guarantees.md` PR3 staging/known limits, context map and overview); prepare the PR4 (OpenTelemetry) handoff and the reconciliation follow-up reference to `context/plans/mutation-cursor-ref-reconciliation.md`; record which tests and checks must run during final validation by pointing to this plan's Full validation section rather than duplicating it. T06 may run focused checks needed to verify its own changes (for example normal-mode build and the targeted regression filters) but must not run or claim the full repository validation. Out — new cleanup beyond T01's inventory, suppression changes beyond what the audit shows, any new behavior, running the Full validation suite, marking the plan validated. + - Dependencies: T03, T05 + - Done when: the audit table, LOC/declaration deltas, retained exceptions, cross-platform limitations, deferred work, PR4 handoff and the list of checks reserved for `/validate` are recorded; no new unexplained suppression exists; the completion record states that full validation has not yet run. + - Verify: focused only: diagnostic-mode build and `--no-run` test build in a disposable worktree; normal-mode `nix develop -c ./scripts/run-cli-cargo.sh build --manifest-path cli/Cargo.toml`; `nix shell nixpkgs#ripgrep -c rg -n "allow\(dead_code\)|allow\(unused" cli/src`; `git diff --check`. + - Completed: 2026-10-08 + - Files changed: `context/plans/cli-async-persistence-cleanup-pr3.md`, `context/cli/async-persistence-guarantees.md`, `context/cli/mutation-trace-{agent-attribution,external-taint,scope-abandonment}.md`, `context/context-map.md`, `context/overview.md`, `context/patterns.md`; no `cli/` changes + - Result: Final diagnostic audit (Linux x86_64, rustc 1.95.0, disposable worktree at `e45e7747`, all 44 `allow(dead_code ...)` attributes in 10 files stripped, `RUSTFLAGS=--cap-lints warn`): the binary target reports 32 warnings and the test target 27 (including a test-only `mbt/model.rs` `WireModelState` warning). Every diagnostic maps to a documented declaration-level exception. Audit table: (1) Category A `diff_traces` compatibility: `AgentTraceDiffTraceExportRow`, `SELECT_DIFF_TRACES_AFTER_SQL`, `read_diff_traces_after`, `diff_trace_export_row_from_turso` (`agent_trace_export/mod.rs`), `ingest_diff_traces` (`control_plane.rs`), all citing the ADR and all present with unchanged signatures. (2) Category B reconciliation, citing `context/plans/mutation-cursor-ref-reconciliation.md`: `ref_reconciliation.rs` x6, `git_snapshot.rs` x10 (`FOR_EACH_REF_PIN_FORMAT`, pin helpers, `PinnedRef`, `PinInventoryError`, `parse_pin_line`, `pin_ref_prefix`) and `store.rs` x13; `RECONCILIATION_LOCK_TIMEOUT` is the only one also flagged in the test target. (3) Cross-platform: `PlatformFamily::{Macos, Windows, Other}`, `GuardError::UnsupportedPlatform`. (4) Suspected-defect fields: `CoordinateOutcome::{worktree_id, observed_tree, revision, evaluation, mutation_event}`, `committed`, `completed`. No unexplained suppression exists; the remaining non-dead-code allows are clippy or `unused_imports` pre-existing exceptions. Deltas against PR2 (`remove-tokio-db-runtimes`): `cli/src` 66 files, 301 insertions, 1569 deletions; LOC 56620 to 55352 (-1268); unique unused declarations 147 to 32 binary-target diagnostics; `dead_code` allow attributes 119 matching lines in 35 files to 44 attributes in 10 files; no file-wide `#![allow(dead_code)]` remains; tests 354 to 344. No test module or fixture was added back (`git diff --diff-filter=A --name-only` over `cli` lists only already-reviewed paths: none are test modules or fixtures). No `dyn`, `BoxFuture`, `block_on` or `Runtime::new` was added in the `cli/src` diff; `Cargo.toml` and `flake.nix` carry no lint changes, and the `main.rs` change only drops a dead-code allow. Context: removed stale `runtime/tests.rs` references from the scope-abandonment, agent-attribution, external-taint, context-map and patterns docs, removed the stale module-wide dead-code claim from `overview.md`, and recorded the PR3 outcome and known limits in `async-persistence-guarantees.md`. + - Cross-platform limits: only the Linux build was compiled. macOS/Windows/non-unix code (`PlatformFamily` variants, `GuardError::UnsupportedPlatform`, the macOS `pipe()`/`fcntl()` descriptor race) was reviewed by reading and is not proven by this audit. + - Deferred work and PR4 handoff: (a) wiring ref reconciliation and a pin-retention or compaction design (plan `context/plans/mutation-cursor-ref-reconciliation.md`, `ref-rec` branch); orphaned snapshot pins accumulate until then. (b) The `TempIndexGuard` sweeper needs an ownership or lock protocol. (c) The macOS descriptor race fix. (d) Removal of the `diff_traces` compatibility surface (separate phase). (e) Resolving the suspected-defect fields. PR4 (OpenTelemetry) starts from a single application-owned Tokio runtime with directly awaited persistence; the dispatch future is about 14 KB with `recursion_limit = "256"` required, so instrumentation that nests more async layers should re-measure. + - Verify: diagnostic-mode `build` and `test --no-run` in the disposable worktree both exited 0 with the diagnostics above, and the worktree was removed; normal-mode `build` passed; `rg` listing of `allow(dead_code` and `allow(unused` recorded above; `git diff --check` clean. **Full validation has not run.** `/validate` must run the commands in this plan's Full validation section (complete Rust suite, clippy with `-D warnings`, `nix flake check`, `nix build .#ci-checks`, `git diff --check`; `pkl-check-generated` only if Pkl or `config/lib` inputs changed). + - Context impact: yes — updated as listed under Files changed. + - Context synchronization: synced + +- [x] Final gate (not an implementation task): `/validate` + - Dependencies: T01–T06 complete and `synced` + - Scope: run the repository's existing `sce-validate` workflow against this plan's acceptance criteria and the Full validation commands; record actual results, including failures, in the validation report. + - Done when: the validation report records every Full validation command outcome and each AC1–AC12 result. +## Open questions + +- None blocking. Reconciliation defaults to retain-and-defer without a gate; T01 raises a decision only if it finds material new evidence that invalidates it. +- Whether the original 114-item report can be located is unknown; T01 treats the reproduced inventory as authoritative either way. +- The no-comments-in-code preference conflicts with "every retained exception should explain its reason"; this plan carries reasons in `#[allow(..., reason = "...")]` or the owning context doc. T01 confirms the pinned toolchain accepts `reason`. + +## T01 inventory appendix + +**Baseline.** Branch `dead-code-blues` HEAD `2f073f98`; PR2 base `remove-tokio-db-runtimes` = `f8e9c748`; `git diff remove-tokio-db-runtimes...HEAD -- cli/src` is empty. Toolchain rustc 1.95.0. Platform: Linux x86_64 only; `cfg` for macOS/Windows/non-unix code was reviewed by reading, not compiled. + +**Method.** Disposable `git worktree` (since removed): deleted every `#[allow(dead_code)]`, `#![allow(dead_code)]` and `#[cfg_attr(not(test), allow(dead_code))]`, and dropped `dead_code, ` from two combined allows in `style.rs` (35 files, 119 deleted and 2 modified lines). Built with `RUSTFLAGS=--cap-lints warn` so every lint is reported in one pass. The nonzero-exit-from-lints outcome did not occur because of the cap; no non-dead-code compiler error appeared. + +**Counts.** + +| Figure | Value | +| --- | --- | +| Binary-target warnings | 114 (equals the seed's 114) | +| Binary-target unique unused declarations | 147 (warnings group multiple symbols) | +| Test-target warnings / symbol entries | 100 / 127 | +| Declarations live only through tests | 19 (9 outside reconciliation, 10 pin helpers) | +| `rg "allow\(dead_code"` matching lines | 119 (117 are `allow(dead_code)`, 2 combined in `style.rs`) | +| Files with suppressions | 35 | +| File-wide `#![allow(dead_code)]` | 5 (`capabilities.rs`, four `*_mutation_scope/mod.rs`) | + +The seed's 114 matches the binary-target warning count, not the declaration count; its 37 occurrences and 117 `rg` matches are not reproduced by any single count here (117 is the bare-attribute line count). The original symbol listing was not found, so this inventory supersedes it. No rows were invented. + +**Disposition totals.** delete-now 101 (corrected from 100); Category A retain 5; Category B retain-and-defer 30 (corrected from 31); cross-platform retain 4; fields retained pending defect review 7; decision-needed 0. Test baseline: 354 tests via `-- --list`. LOC of `cli/src`: 56620. + +**Inventory (grouped by file; all symbols listed).** + +| File | Symbols | Cat | Usage evidence | Disposition | Risk | +| --- | --- | --- | --- | --- | --- | +| app.rs | `fs`, `fs`/`git`/`repo_root` (AppContext) | D | no callers in prod or tests | delete | low | +| agent_trace.rs | `FileRead`, `InvalidJson`, `validate_agent_trace_json`, `validate_agent_trace_file`, `classify_hunk`, `hunks_match_exactly` | C | unused in prod and tests | delete | low | +| agent_trace.rs | `build_agent_trace` | C | tests only (`agent_trace/tests.rs`) | delete with its tests | low | +| agent_trace_db/mod.rs, repository.rs | `insert_message_with`, `insert_part_with`, `insert_message`, `insert_part` | D | only each other | delete | low | +| agent_trace_export/mod.rs, agent_trace_sync/control_plane.rs | `AgentTraceDiffTraceExportRow`, `SELECT_DIFF_TRACES_AFTER_SQL`, `read_diff_traces_after`, `diff_trace_export_row_from_turso`, `ingest_diff_traces` | A | protected by the 2026-10-01 diff_traces ADR | retain, declaration-level exception citing ADR | none | +| agent_trace_storage/mod.rs | `resolve_agent_trace_storage_at_state_root` | D | no callers | delete | low | +| capabilities.rs | `read_file`, `write_file`, `metadata`, `exists`, `resolve_repository_root`, `resolve_hooks_directory` | D | trait methods with no callers; file-wide allow | delete (check trait impls/fakes) | medium | +| command_registry.rs, sync/command.rs | `execute` x2 | C | no callers (T02 confirms against originating plan) | delete | low | +| config/types.rs | `parse_env` x2 | D | referenced only by unused `from_env_lookup` and tests of themselves (T02 re-verifies) | delete | low | +| db/mod.rs | `QueryRows`, `query`, `query_values`, `query` | D | no callers | delete | low | +| default_paths.rs | `PersistedArtifactRootKind`, `PersistedArtifactId`, `PersistedArtifactLocation`, `persisted_artifact_locations`, `cache_root`, `git_dir`, `git_hooks_dir`, `git_hook_file`, `git_commit_editmsg`, `opencode_plugin_target`, `skill_tile_relative_path`, `pre_commit_hook_path`, `commit_msg_hook_path`, `post_commit_hook_path`, constants `GIT`, `GIT_COMMIT_EDITMSG`, `HOOKS`, `PRE_COMMIT`, `COMMIT_MSG`, `POST_COMMIT`, `OPENCODE_DIR`, `PLUGIN_FILE`, `PLUGIN_MANIFEST_ENTRY`, `AGENTS_DIR`, `SKILL_DEFINITION` | D | defined and referenced only inside the unused helpers; constants `PRE_COMMIT`/`COMMIT_MSG`/`POST_COMMIT` also appear in `setup/mod.rs` (T02 re-verifies after setup deletion) | delete | low | +| default_paths.rs | `PlatformFamily::Macos`, `Windows`, `Other` | D | constructed only under `cfg(target_os)` in `current_platform_family`; matched in `encryption_key.rs` | retain (cross-platform, not provable on Linux) | high if deleted | +| doctor/mod.rs, inspect.rs, types.rs | field `validate_config_file`, `inspect_repository_hooks`, `collect_global_state_health`, `collect_hook_health`, `inspect_hook_content_state`, `display_node`, `domain`, `group`, `branch` | D | dead chain; the field is read only by the dead functions | delete the chain together | medium | +| error.rs | `user` | D | tests only | delete with tests | low | +| hooks/claude_mutation_scope/events.rs, codex_mutation_scope/events.rs | `is_subagent` x2, `MCP_TOOL_NAME_PREFIX`, `is_mcp_tool_name` | D | no callers | delete | low | +| hooks/*_mutation_scope/state.rs | `mark_recovery_pending`, `arm_recovery`, `arm_and_begin_recovery_flush`, `is_clear` x3 | D | no callers (T02 reviews recovery semantics before deleting) | delete (review recovery semantics) | medium | +| hooks/codex/mod.rs | field `tool_response` | field | serde payload field, never read | delete after checking serde derive and `deny_unknown_fields` | low | +| hooks/codex/apply_patch/path.rs | `resolve_codex_patch_path` | C | no callers | delete | low | +| lifecycle.rs | `id` | D | no callers | delete | low | +| mutation_trace/lineage.rs | `reset_file`, `tracked_paths` | D | tests only | delete with tests | low | +| mutation_trace/runtime/coordinator.rs | fields `worktree_id`, `observed_tree`, `revision`, `evaluation`, `mutation_event` of `CoordinateOutcome`; `committed` | field | never read; error payload carries outcome | retain, suspected defect list | medium | +| mutation_trace/runtime/scope_runtime.rs | field `completed` | field | never read; same error-payload pattern | retain, suspected defect list | medium | +| mutation_trace/runtime/external_mutation_guard.rs | `UnsupportedPlatform` | C | constructed under `cfg(not(unix))` | retain (cross-platform) | high if deleted | +| mutation_trace/runtime/external_mutation_guard.rs | `run_external_mutation_guard` | C | production wrapper with test-only callers; re-exported in `runtime/mod.rs` (post-review correction) | delete the production convenience wrapper (unix `unix_impl` and `cfg(not(unix))` stub) and its re-exports; preserve `guarded_shell_supervision_leaves_the_runtime_worker_responsive` and `guarded_shell_streams_output_and_finishes_with_exit_code` by adapting them to `arm_external_mutation_guard(...).await` and `ArmedExternalMutationGuard::exec(...).await` (a `#[cfg(test)]` helper is allowed) | low | +| mutation_trace/runtime/git_snapshot.rs | `FOR_EACH_REF_PIN_FORMAT`, `list_pins`, `delete_pins`, `delete_pins_inner`, `run_ref_mutation`, `assert_pins_are_unchanged_direct_refs`, `PinnedRef`, `PinInventoryError`, `parse_pin_line`, `pin_ref_prefix` | B | used by `ref_reconciliation` and tests only | retain, narrowed exception (T03) | none | +| mutation_trace/runtime/ref_reconciliation.rs | `RECONCILIATION_LOCK_TIMEOUT`, `ReconciliationReport`, `ReconciliationOutcome`, `ReconcileError`, `reconcile_worktree`, `reconcile_worktree_inner` | B | no caller outside the module (`mod ref_reconciliation;` private) | retain, narrowed exception (T03) | none | +| mutation_trace/runtime/worktree_lock.rs | `WorktreeLock::acquire` (sync wrapper; post-review correction) | D | no production, test or supported-platform callers; reconciliation calls `acquire_inner_async`, not `acquire` | delete in T02 (unused synchronous convenience wrapper) | low | +| mutation_trace/runtime/worktree_lock.rs | field `WorktreeLockInner::path` | B | never read; classified separately from the wrapper | retain; resolved in T03 | low | +| mutation_trace/store.rs | `decode_boundary_kind`, `SELECT_MUTATION_EVENT_SQL`, `SELECT_MUTATION_EVENT_ACTIVE_SCOPES_SQL`, `SELECT_TREE_ROOTS_BY_WORKTREE_SQL`, `SELECT_ALL_TREE_ROOTS_SQL`, `load_mutation_event`, `load_tree_roots`, `load_all_tree_roots`, `load_mutation_event_active_scopes`, `tree_root_row_from_turso`, `MutationEventRow`, `mutation_event_row_from_turso`, `reconstruct_boundary` | B | only store-internal chain plus reconciliation | retain, narrowed exception (T03) | none | +| mutation_trace/types.rs | `is_terminal` x2, `scope_actor` | D | no production callers; `is_terminal` also exercised in `mutation_trace/tests.rs` (T02 re-verifies) | delete | low | +| observability.rs | `from_env_lookup` | D | no callers | delete | low | +| output_format.rs | `parse` | D | no callers | delete | low | +| patch.rs | `load_patch_from_json_bytes` | C | no callers | delete | low | +| repository_identity/mod.rs | `dir_segment`, `DIR_SEGMENT_SHORT_HASH_LEN`, `repository_dir_segment`, `slugify`, `derive_short_hash` | C | superseded directory-segment scheme; non-method items used by tests only | delete with tests | low | +| resilience.rs | `run_with_retry_sync` | C | zero consumers anywhere in `cli/src` | delete in T04 | low | +| setup/mod.rs, prompt.rs | `RequiredHookAsset`, `get_required_hook_asset`, `setup_prompt_target_label_with_color_policy`, `setup_prompt_title_with_color_policy` x2 | C | superseded setup helpers | delete | low | +| sync/sync.rs | `run_current_sync`, `run_current_sync_with_progress` | C | no callers | delete | low | + +**Reconciliation graph and decision.** `reconcile_worktree` has no caller outside `ref_reconciliation.rs`; its dependency cluster (30 rows after the post-review correction) is used only by itself, `pin_tree` remains called from `coordinator.rs`, and no production code deletes pins. No new evidence invalidates retain-and-defer; no decision is needed. + +**Defects and checks recorded for later tasks.** +- Suspected logic defects (not for deletion): `CoordinateOutcome` fields and the `committed`/`completed` error payloads are carried but never read, so error variants may drop information callers could report. +- `run_with_retry_sync` has zero consumers; T04 owns removal. +- 19 declarations are live only via tests; T02 deletes their tests with them, and T03 keeps the pin helpers. +- `spawn_blocking` baseline (14 sites): `git_snapshot.rs` x2, `protected_worktree.rs` x2, `worktree_lock.rs`, `external_mutation_guard.rs`, `mutation_scope_lock.rs` x3, `db/mod.rs` (encryption key), and the four `*_mutation_scope/state.rs` readers. +- Future-size baseline method (for T05): `#![recursion_limit = "256"]` is in `cli/src/main.rs`; measure with compiler future-size diagnostics in a disposable worktree. +- Platform limits: only the Linux build was compiled. `PlatformFamily` variants and `UnsupportedPlatform` are cfg-gated and must not be deleted on Linux evidence. +- Stale unrelated worktree `.../f4f0ae83-.../scratchpad/wt-t01` at `f8e9c748` exists from another session; left untouched. + +## Validation Report + +**Status:** validated +**Date:** 2026-10-08 + +### Commands run + +- `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml` -> exit 0 (356 passed, 0 failed; includes `db::transaction_cancellation_tests`, `agent_trace_db::transaction_tests`, `services::sync::sync` terminal-stream tests, the new `sync::contract_tests`, and the `mutation_trace::mbt` Quint MBT suite) +- `nix develop -c ./scripts/run-cli-cargo.sh clippy --manifest-path cli/Cargo.toml --all-targets --all-features -- -D warnings` -> exit 0 +- `nix flake check` -> exit 0 (all checks passed; aarch64-darwin, aarch64-linux, x86_64-darwin omitted as incompatible systems) +- `nix build .#ci-checks` -> exit 0 +- `nix run .#pkl-check-generated` -> not run (no Pkl or `config/` input in `git diff remove-tokio-db-runtimes`) +- `git diff --check` -> exit 0 + +### Success-criteria verification + +- [x] AC1: T01 appendix records baseline `f8e9c748`, per-target counts (114/147 binary, 100/127 test), suppression/match/file counts, differences from the seed, and per-row dispositions -> inspected appendix +- [x] AC2: `git diff --shortstat remove-tokio-db-runtimes -- cli/src` -> 67 files, +665/-1575; platform-gated `PlatformFamily` and `GuardError::UnsupportedPlatform` retained +- [x] AC3: all listed declarations present with unchanged wire/DTO shapes; the working-tree diff touches only `control_plane.rs`, `render_sync.rs` and `sync.rs`, adding contract tests (required and out-of-range `cursors.diffTraces` decode, zero-upload `streams.diffTraces` JSON entry, `sync_uploads_only_three_streams_and_reports_zero_upload_diff_traces`) that pass in the full suite; the only non-test signature change is a generic credential-store parameter on private `run_sync_async`/`sync_one_stream` +- [x] AC4: no `reconcile_worktree` caller outside `ref_reconciliation.rs`; ref-reconciliation and snapshot-service docs state retained-and-unwired, the orphaned-pin consequence and the `mutation-cursor-ref-reconciliation` plan; no new ADR +- [x] AC5: no `#![allow(dead_code)]` remains; every `dead_code` allow in the diff carries a `reason`; no lint changes in `cli/Cargo.toml`, `flake.nix` +- [x] AC6: T06 records diagnostic-mode output (Linux x86_64, rustc 1.95.0; 32 binary / 27 test diagnostics mapped to documented exceptions); normal-mode build, clippy and tests pass above +- [x] AC7: full suite, transaction and terminal-stream tests pass; `resilience.rs` diff only removes `run_with_retry_sync` +- [x] AC8: `join_three_to_completion`/`JoinSlot` retained with a recorded reason (T04); sync tests pass +- [x] AC9: no added `dyn`, `BoxFuture`, `Runtime::new` or `block_on` in the `cli/src` diff (grep of added lines empty); T05 records future sizes (~14 KB), method, `recursion_limit = "256"` keep decision and temp-index deferral +- [x] AC10: deletions (1575) exceed insertions (665); `git diff --diff-filter=A --name-only remove-tokio-db-runtimes -- cli` is empty +- [x] AC11: `rg build_agent_trace context` no longer hits `context/sce/agent-trace-minimal-generator.md` (now describes `build_agent_trace_from_evidence`); macOS `pipe()`/`fcntl()` limitation documented as unresolved +- [x] AC12: T06 recorded that full validation had not run; this report lists each command with its real result + +### Failed checks and follow-ups + +- None. + +### Residual risks + +- Only the Linux build was compiled; macOS/Windows/non-unix code and the macOS descriptor race are unverified +- Orphaned snapshot pins accumulate until ref reconciliation is wired +- Fixes for the earlier AC3/AC11 failure are uncommitted in the working tree +- Historical plans under `context/plans/` still mention `build_agent_trace` diff --git a/context/sce/agent-trace-db.md b/context/sce/agent-trace-db.md index c18442627..c2d431dc2 100644 --- a/context/sce/agent-trace-db.md +++ b/context/sce/agent-trace-db.md @@ -23,12 +23,10 @@ - `MessageRole` enum: `User` / `Assistant` — maps to `messages.role` DB constraint. - `InsertMessageInsert`: owned payload struct with insertable parent `messages` columns (`session_id`, `message_id`, `role`, `generated_at_unix_ms`); message body text belongs to `parts.text`, not the parent message row. - `INSERT_MESSAGE_SQL`: parameterized single-row SQL using `INSERT ... ON CONFLICT (session_id, message_id) DO NOTHING` — leverages the unique index `idx_messages_session_message` so duplicate parent-message events remain non-failing without mutating the existing row. -- `insert_message(input)`: typed single-row helper that executes the duplicate-ignore parent-message insert; retained as part of the adapter surface. - `insert_messages(inputs)`: typed batch helper that generates and executes one parameterized multi-row `messages` insert for valid conversation-trace `message` batches while preserving duplicate-ignore semantics. - `PartType` enum: `Text` / `Reasoning` / `Patch` / `Question` — serializes known conversation part kinds for typed inserts. `parts.type` is stored as `TEXT NOT NULL` without a database-level enum `CHECK` constraint. - `InsertPartInsert`: owned payload struct with `part_type`, `text`, `session_id`, `message_id`, and `generated_at_unix_ms`. - `INSERT_PART_SQL`: parameterized single-row append-only INSERT into `parts` (no upsert; multiple rows per `(session_id, message_id)` allowed). -- `insert_part(input)`: typed single-row helper that inserts a part row without requiring a matching `messages` row (supports out-of-order writes); retained as part of the adapter surface. - `insert_parts(inputs)`: typed batch helper that generates and executes one parameterized multi-row append-only `parts` insert for valid conversation-trace `message.part` batches. - `insert_conversation_text_event(message, part)`: atomic one-message-plus-one-part write for exactly-once conversation text events (currently used only by `sce hooks codex`'s `UserPromptSubmit`/`Stop` arms; see [codex-integration-runtime.md](codex-integration-runtime.md)). Delegates to `TursoDb::execute_transactional_insert_pair_if_absent` (`cli/src/services/db/mod.rs`), a generic, schema-agnostic primitive: inside one `BEGIN IMMEDIATE` transaction it checks whether `(session_id, message_id)` already exists and, only if absent, inserts the message row then the part row and commits; an existing row causes no insert statements to run, and the no-write transaction commits, returning `Ok(false)`. `BEGIN IMMEDIATE` serializes concurrent callers against the same database file, so a replayed or concurrent duplicate delivery leaves exactly one message row and one part row — stronger than `insert_messages`/`insert_parts`' own guarantee, which dedups only the parent message row via `ON CONFLICT DO NOTHING` and leaves `parts` unguarded. `insert_messages`/`insert_parts` remain unchanged and are still what OpenCode/Claude/Pi conversation-trace intake uses. - `lifecycle.rs`: service lifecycle provider for setup/doctor integration. @@ -45,7 +43,7 @@ Repository persistence, readiness checks, transactions, and migration operations 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 the additive `002_repository_source_instance_id.sql` (adds `repository_metadata.source_instance_id`), `003_claude_model_state.sql` (adds the non-exported Claude model-state register), `004_mutation_trace_protocol.sql`, and `005_mutation_scope_provenance.sql` (the mutation-cursor protocol tables and the scope-provenance table; see [mutation-trace store](../cli/mutation-trace-store.md) and [mutation-scope provenance](../cli/mutation-scope-provenance.md)). The baseline 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) -> Result` inserts the singleton metadata row on first initialization, errors if an existing DB stores a different repository ID, and atomically claims `source_instance_id` for this physical database via `UPDATE ... WHERE source_instance_id = ''` (a losing racer's generated candidate is discarded and an already-valid stored value is never overwritten), returning the typed `RepositoryMetadata { repository_id, source_instance_id }`. `source_instance_id` is generated by application code (`generate_source_instance_id()`, UUID v4) and validated with `is_valid_source_instance_id()` (non-empty once trimmed); it is never derived from `repository_id`, remote URL, checkout ID, filesystem path, hostname, or user/workspace identity, and stays stable across reopen and repeated `sce setup` runs. `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. -`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`, `insert_parts`, and `insert_conversation_text_event`. 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. +`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_messages`, `insert_parts`, and `insert_conversation_text_event`. 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 sync`. Hook writers/readers resolve the current repository storage context before using `RepositoryAgentTraceDb`. The migration-running `new_at(path)` constructor is used by setup/lifecycle; hook runtime uses the no-migration constructor and fails open with `Run 'sce setup'.` guidance when schema readiness is not met. There is no longer a checkout-scoped adapter or trace database inspection service. @@ -57,7 +55,7 @@ to `diff_traces` or the direct intersection table. ## Non-goals -- No read/query helper for loading messages with their joined parts exists in the current runtime; the typed write helpers (`insert_message`, `insert_messages`, `insert_part`, `insert_parts`, `insert_conversation_text_event`) are the only exposed message/part API surface. Message/part query helpers are deferred to a future task. +- No read/query helper for loading messages with their joined parts exists in the current runtime; the typed write helpers (`insert_messages`, `insert_parts`, `insert_conversation_text_event`) are the only exposed message/part API surface. Message/part query helpers are deferred to a future task. - No part upsert/deduplication; `parts` uses only the internal integer `id` for row identity (append-only per the `INSERT_PART_SQL` contract). ## Database path @@ -205,7 +203,7 @@ Both triggers compare `OLD.*` vs `NEW.*` for all mutable columns (excluding `upd - AgentTraceDb open failures are logged at error level through `sce.hooks.diff_trace.agent_trace_db_open_failed`; later conversion/insert failures retain `sce.hooks.diff_trace.agent_trace_db_write_failed`. Both failure classes preserve deterministic failed-persistence success text and create no artifact fallback. Open failures use the producer-native unprefixed session and do not also emit the write-failure event. - Existing artifact files are not backfilled into the database. -Post-commit intersection rows are written by the active `post-commit` hook flow through repository-scoped Agent Trace DB access, and the same flow inserts built Agent Trace payloads into `agent_traces` via `RepositoryAgentTraceDb::insert_agent_trace()` (see [agent-trace-hooks-command-routing.md](agent-trace-hooks-command-routing.md)). The persisted `trace_json` is the schema-validated `build_agent_trace(...)` output and includes top-level `metadata.sce.version` from the compiled `sce` CLI package version, always-emitted `metadata.sce.line_changes` touched-line attribution counts, plus `content_hash` on every emitted range. Range `content_hash` values are computed from the touched-line kind/content of the post-commit hunk that produced the persisted range, not from DB IDs, paths, line positions, or runtime metadata. +Post-commit intersection rows are written by the active `post-commit` hook flow through repository-scoped Agent Trace DB access, and the same flow inserts built Agent Trace payloads into `agent_traces` via `RepositoryAgentTraceDb::insert_agent_trace()` (see [agent-trace-hooks-command-routing.md](agent-trace-hooks-command-routing.md)). The persisted `trace_json` is the schema-validated `build_agent_trace_from_evidence(...)` output and includes top-level `metadata.sce.version` from the compiled `sce` CLI package version, always-emitted `metadata.sce.line_changes` touched-line attribution counts, plus `content_hash` on every emitted range. Range `content_hash` values are computed from the touched-line kind/content of the post-commit hunk that produced the persisted range, not from DB IDs, paths, line positions, or runtime metadata. `sce hooks conversation-trace` is the current runtime writer for `messages` and `parts`. diff --git a/context/sce/agent-trace-embedded-schema-validation.md b/context/sce/agent-trace-embedded-schema-validation.md index 81cbfa6d8..516d993e8 100644 --- a/context/sce/agent-trace-embedded-schema-validation.md +++ b/context/sce/agent-trace-embedded-schema-validation.md @@ -13,7 +13,6 @@ Current internal validation seam for Agent Trace JSON in the Rust CLI. - The canonical schema remains at `config/schema/agent-trace.schema.json`. For repository builds, `cli/build.rs` copies it to `OUT_DIR/static/schema/agent-trace.schema.json`, and the CLI embeds that staged file at compile time via `include_str!`. Validation does not read the schema from disk at runtime; channel-specific package fallback preparation remains a distribution concern. - `agent_trace_schema_validator()` compiles the embedded schema once and caches the `jsonschema::Validator` in a `OnceLock`. - `validate_agent_trace_value(&serde_json::Value)` validates already-parsed JSON values against the embedded schema. -- `validate_agent_trace_json(&str)` parses a JSON string and then validates it against the embedded schema. - Top-level `version` must match strict numeric `x.y.z` (`^[0-9]+\.[0-9]+\.[0-9]+$`); two-part values like `x.y` are rejected. - Top-level `vcs` is optional at schema level; payloads without `vcs` validate, and payloads that include `vcs` must still provide both `type` and `revision`. - Validation failures use `AgentTraceValidationError` to distinguish: @@ -24,7 +23,7 @@ Current internal validation seam for Agent Trace JSON in the Rust CLI. ## Boundaries - This seam is internal-only; no user-facing `sce` command currently exposes Agent Trace validation. -- The existing minimal `build_agent_trace(...)` output remains unchanged by this validation seam. +- The `build_agent_trace_from_evidence(...)` output remains unchanged by this validation seam. - File-path loading and validation entrypoints are not part of the current implemented slice. ## Verification diff --git a/context/sce/agent-trace-export-readers.md b/context/sce/agent-trace-export-readers.md index e3bc47264..72fb87c96 100644 --- a/context/sce/agent-trace-export-readers.md +++ b/context/sce/agent-trace-export-readers.md @@ -19,7 +19,7 @@ flowchart LR `AgentTraceExportReader::new(&db)` takes a `&RepositoryAgentTraceDb` directly. The reader does not resolve storage, open a database, or generate identity itself. The existing storage resolver composes cleanly with it: ```rust -let storage = resolve_agent_trace_storage_at_state_root(&context, &state_root).await?; +let storage = resolve_agent_trace_storage(&context).await?; let reader = AgentTraceExportReader::new(&storage.db); let rows = reader.read_messages_after(cursor, limit).await?; ``` @@ -29,8 +29,6 @@ let rows = reader.read_messages_after(cursor, limit).await?; - `repository_id` / `source_instance_id` identify the *source* (the physical database). - Each stream's `table.id` (returned as `sourceRowId`) is the per-stream *progress marker* — the caller's cursor. It has no relationship to `source_instance_id` and is never used to derive or validate it. -Test coverage: `cli/src/services/agent_trace_export/mod.rs::tests::source_instance_integration` resolves storage through `resolve_agent_trace_storage_at_state_root`, asserts both metadata fields are populated, and reads one stream through a reader built from `storage.db`. - ## Reader contract Four async methods, one per capture stream, sharing one shape: `(cursor: i64, limit: usize) -> Result>`, awaiting the DB query and row materialization, running diff --git a/context/sce/agent-trace-minimal-generator.md b/context/sce/agent-trace-minimal-generator.md index 8f6631781..5c5928010 100644 --- a/context/sce/agent-trace-minimal-generator.md +++ b/context/sce/agent-trace-minimal-generator.md @@ -11,21 +11,20 @@ Given a `constructed_patch` (AI candidate) and a `post_commit_patch` (canonical - **`ai`** — every touched line in the `post_commit_patch` hunk is covered by direct evidence, mutation-derived AI evidence, or both. - **`mixed`** — a non-empty proper subset of the hunk's touched lines is covered. - **`unknown`** — no touched line in the hunk is covered. -3. With no mutation evidence this is equivalent to the direct-only slot rule, since a direct-intersection hunk always holds an ordered sub-multiset of its `post_commit_patch` hunk's touched lines: `ai` when the `intersection_patch` hunk has identical touched lines (same count, kind, `line_number`, content, order), `mixed` when that hunk exists but is a proper subset, `unknown` when no `intersection_patch` hunk shares the `old_start`. The direct-only path `build_agent_trace(...)` produces exactly this classification. +3. With no mutation evidence this is equivalent to the direct-only slot rule, since a direct-intersection hunk always holds an ordered sub-multiset of its `post_commit_patch` hunk's touched lines: `ai` when the `intersection_patch` hunk has identical touched lines (same count, kind, `line_number`, content, order), `mixed` when that hunk exists but is a proper subset, `unknown` when no `intersection_patch` hunk shares the `old_start`. `build_agent_trace_from_evidence(...)` with an empty `mutation_ai_patch` produces exactly this classification. 4. Map `Conversation.contributor.model_id` from the matched direct and mutation hunks when contributor type is `ai` or `mixed`: an absent evidence source does not count as unknown, one present model passes through, and both present models must be known and equal. Omit `model_id` when both sources are present but either model is missing or they disagree. A hunk classified `ai`/`mixed` purely through mutation-derived coverage may therefore emit the mutation model. 5. For each emitted conversation, derive optional `conversation.related` entries from non-empty `session_id` values on touched lines in both matched evidence hunks; emit related entries as `{ "type": "session", "url": "https://sce.crocoder.dev/sessions/" }`, deduplicated by session ID with deterministic ordering, and omit `related` when no included lines provide `session_id`. Structured diff-trace reconstruction supplies the persisted canonical `cc_...` row session on direct lines, while mutation reconstruction supplies scope provenance on mutation-attributed lines. 6. Emit one `Conversation` per `post_commit_patch` hunk, each carrying the trace lookup `url`, one `TraceFile` per `post_commit_patch` file, and one range per hunk with a deterministic `content_hash` computed from that hunk's touched-line kind/content. ## Separated direct/mutation evidence -`build_agent_trace_from_evidence(evidence, post_commit_patch, metadata)` is the seam that classifies from two independent AI-evidence sources: +`build_agent_trace_from_evidence(evidence, post_commit_patch, metadata)` is the only supported generator entrypoint. It handles direct-only evidence (empty `mutation_ai_patch`), mutation-only evidence, and combined evidence through one path, classifying from two independent AI-evidence sources: - `evidence.direct_patch` — the reconstructed direct patch, intersected against `post_commit_patch` internally exactly as before. It supplies direct model/session provenance and remains the sole source of the top-level `tool` object (still omitted when the direct intersection is empty). - `evidence.mutation_ai_patch` — a target-shaped set of committed touched lines with resolved scope provenance that causal mutation-lineage replay attributed to AI (an AI event's line that survived every later observed tree transition into the committed tree), produced by [../cli/mutation-trace-agent-attribution.md](../cli/mutation-trace-agent-attribution.md). It widens combined AI coverage for hunk classification and supplies mutation model/session metadata when available. Per hunk, a `post_commit_patch` touched line is covered when it pairs one-to-one on `(kind, line_number, content)` with a line in the direct intersection hunk or the mutation-AI hunk at the same `old_start`; the covered fraction drives `ai` / `mixed` / `unknown` as in the Contract above. `line_changes` buckets follow that combined classification. -`build_agent_trace(constructed_patch, post_commit_patch, metadata)` is retained unchanged and delegates to `build_agent_trace_from_evidence` with an empty `mutation_ai_patch`. Wiring the mutation-AI patch in at post-commit is owned by [agent-trace-hooks-command-routing.md](agent-trace-hooks-command-routing.md). ## Domain types @@ -58,7 +57,7 @@ the boundary that mutation protocol attribution is not itself scope provenance. Current output includes top-level metadata fields with this contract: - `version` is fixed to `"0.1.0"` and remains the Agent Trace payload/schema version -- `id` is generated per `build_agent_trace(...)` call as a UUIDv7 string derived from the same commit-time moment used for `timestamp` +- `id` is generated per `build_agent_trace_from_evidence(...)` call as a UUIDv7 string derived from the same commit-time moment used for `timestamp` - `timestamp` is sourced from explicit commit metadata input (`AgentTraceMetadataInput.commit_timestamp`) and must be RFC 3339 - `vcs` is emitted only when explicit commit metadata input includes `AgentTraceMetadataInput.vcs_type` - when `vcs` is emitted, `vcs.type` is sourced from the schema-aligned enum (`git | jj | hg | svn`) and `vcs.revision` is sourced from `AgentTraceMetadataInput.commit_revision` @@ -109,15 +108,13 @@ Current output includes top-level metadata fields with this contract: ## Public API -- `classify_hunk(post_commit_hunk, intersection_hunks) -> HunkContributor` — the direct-only slot rule, retained as a primitive; the builder itself now classifies through the internal combined direct+mutation line-coverage rule. - `range_content_hash(hunk) -> String` — internal helper that computes the serialized range-level `murmur3:` content fingerprint from `PatchHunk.lines` using versioned, length-delimited touched-line serialization in patch order. The hash input includes touched-line kind and content, and excludes hunk positions, line numbers, file paths, trace metadata, contributor/model metadata, VCS metadata, tool metadata, and database IDs. -- `build_agent_trace(constructed_patch, post_commit_patch, metadata) -> Result` — direct-only entrypoint, retained unchanged; delegates to `build_agent_trace_from_evidence` with an empty `mutation_ai_patch`. It validates `metadata.commit_timestamp` as RFC 3339, uses it as top-level `timestamp`, derives a UUIDv7 `id` from that same commit-time moment, derives one conversation URL from that `id`, conditionally emits `vcs` only when `metadata.vcs_type` is present (mapping `vcs.type` from metadata and `vcs.revision` from `metadata.commit_revision`), carries optional tool metadata inputs (`metadata.tool_name`, `metadata.tool_version`) for top-level `tool` mapping, and always emits `metadata.sce.version` from the compiled package version. When the direct `intersection_patch.files` is empty, `tool` is always `None` regardless of metadata values. - `build_agent_trace_from_evidence(evidence: AgentTraceEvidence, post_commit_patch, metadata) -> Result` — separated-evidence entrypoint (see [Separated direct/mutation evidence](#separated-directmutation-evidence)): identical top-level metadata behavior, classifies each hunk from the union of direct and mutation-derived AI coverage, unions direct/mutation related sessions, and emits a model only when the present evidence sources agree under the combined rule; top-level `tool` remains bound to the direct intersection. ## Test fixture contract - Golden fixtures under `cli/src/services/agent_trace/fixtures/**/golden.json` pin deterministic literal values for top-level `id`, `timestamp`, optional `vcs`, `metadata.sce.version`, `metadata.sce.line_changes`, per-conversation `url`, range-level `content_hash`, and expected file/conversation shapes. -- Reconstruction fixtures pair `incremental_*.patch` inputs with a `post_commit.patch` and drive `build_agent_trace`. Evidence fixtures (`direct_only`, `exclusive_without_direct`, `direct_plus_mutation`, `partial_combined`, `newer_nonexclusive_blocks`, `mutation_only_no_provenance`) instead pair `direct.patch` + `mutation_ai.patch` + `post_commit.patch` and drive `build_agent_trace_from_evidence`, pinning mutation-only coverage with and without resolved provenance, the combined model-agreement and session-union rules, the absence of fabricated metadata when provenance is unknown, and the empty-mutation path's byte identity with `build_agent_trace`. +- Evidence fixtures (`direct_only`, `exclusive_without_direct`, `direct_plus_mutation`, `partial_combined`, `newer_nonexclusive_blocks`, `mutation_only_no_provenance`) pair `direct.patch` + `mutation_ai.patch` + `post_commit.patch` and drive `build_agent_trace_from_evidence`, pinning mutation-only coverage with and without resolved provenance, the combined model-agreement and session-union rules, the absence of fabricated metadata when provenance is unknown, and the direct-only case (empty `mutation_ai_patch`), whose classification and provenance match the direct-intersection rule. - Tests validate golden fixtures and built payloads against the embedded schema, assert core runtime metadata directly (`version`, `timestamp`, optional `vcs`, and `metadata.sce.version`), and compare `vcs`, optional `tool`, `metadata.sce.line_changes`, and normalized `files` against fixture truth. Expected fixture URLs are normalized to the runtime `AgentTrace.id` before the existing file-shape comparison because UUIDv7 generation includes non-deterministic bits. ## Relationship to existing patch service diff --git a/context/sce/cli-shared-output-format-contract.md b/context/sce/cli-shared-output-format-contract.md index aa7b5ec3a..a6d13dccb 100644 --- a/context/sce/cli-shared-output-format-contract.md +++ b/context/sce/cli-shared-output-format-contract.md @@ -6,7 +6,7 @@ - Canonical type lives at `cli/src/services/output_format.rs` as `OutputFormat`. - Allowed values are `text` and `json`. -- Parsing is command-context aware via `OutputFormat::parse(raw, help_command)` so invalid values include command-specific help guidance. +- `OutputFormat` derives clap `ValueEnum` for parsing; the former `OutputFormat::parse(raw, help_command)` helper was removed as unused. ## Current command integration diff --git a/context/sce/mutation-scope-health-status.md b/context/sce/mutation-scope-health-status.md index 12e4021e6..6b657ee25 100644 --- a/context/sce/mutation-scope-health-status.md +++ b/context/sce/mutation-scope-health-status.md @@ -62,7 +62,7 @@ four-way status each adapter module already computed. Doctor is a read-only snapshot: a currently executing hook process can, in a narrow window, hold a state shape mid-transition (for example Claude's `recovery_pending = true` with non-empty `attempts` between -`mark_recovery_pending()` and `remove_attempt()` inside one still-running +`mark_recovery_pending_and_pending_abandon()` and `remove_attempt()` inside one still-running hook call). Health classifies the durable state observed at inspection time against whether normal future adapter lifecycle events can recover it, assuming the operation that produced it has stopped progressing; it is not a diff --git a/context/sce/setup-githooks-hook-asset-packaging.md b/context/sce/setup-githooks-hook-asset-packaging.md index 8a5a8e3e5..a7d12ea67 100644 --- a/context/sce/setup-githooks-hook-asset-packaging.md +++ b/context/sce/setup-githooks-hook-asset-packaging.md @@ -24,16 +24,7 @@ Available-CLI behavior remains hook-specific: ## Setup-service accessor surface -`cli/src/services/setup/mod.rs` exposes hook-template access through: - -- `iter_required_hook_assets()` for deterministic full-set iteration -- `get_required_hook_asset(RequiredHookAsset)` for stable per-hook lookup - -`RequiredHookAsset` is the canonical hook mapping enum for this packaging layer: - -- `PreCommit` -- `CommitMsg` -- `PostCommit` +`cli/src/services/setup/mod.rs` exposes hook-template access through `iter_required_hook_assets()` for deterministic full-set iteration; the per-hook lookup helper and `RequiredHookAsset` enum were removed as unused. ## Determinism and validation diff --git a/context/sce/shared-turso-db.md b/context/sce/shared-turso-db.md index 99ea18372..8b5d07143 100644 --- a/context/sce/shared-turso-db.md +++ b/context/sce/shared-turso-db.md @@ -16,21 +16,21 @@ - Agent Trace write-contention deadline, resolved by `resolve_contention_deadline::()` from `policies.database_retry.agent_trace_db.contention_deadline_ms` with fallback to `AGENT_TRACE_DB_CONTENTION_DEADLINE_MS = 2_250` (zero for every other database). It is the cutoff for starting another outer write-contention retry, not a hard operation timeout - Agent Trace write-contention retry (`write_contention_policy::()`, `run_with_write_contention_retry`), returned only for `agent_trace_db` and applied only to write units whose whole replay is safe: the two transactional primitives below (each attempt is a complete `BEGIN IMMEDIATE` → `COMMIT` unit) and `execute_idempotent_write(sql, params)`, the opt-in single-statement entrypoint used only by the repository-metadata `INSERT … ON CONFLICT DO NOTHING`, the guarded source-instance claim `UPDATE … WHERE source_instance_id = ''`, and the mutation-trace worktree/scope/scope-provenance `…_IF_ABSENT` inserts. It retries only typed `Busy`/`BusySnapshot` (classified before conversion to `anyhow`), allows at most `2` attempts, sleeps a full-jitter backoff drawn from `0..=100ms`, and admits the second attempt at two points: before the backoff sleep it requires `remaining contention deadline >= backoff + busy_timeout`, and after the sleep actually returns it re-checks `remaining >= busy_timeout`, so scheduler oversleep can never start an attempt outside the admission contract (time is measured on a monotonic clock from the operation start, so Turso's busy wait counts; nothing starts after the deadline expires). Deterministic failures return after one attempt without sleeping. On every other database these entrypoints keep the generic query retry, and `execute_idempotent_write` is plain `execute()` - config-driven connection-open retry around only the `build().await.connect()` block using async `run_with_retry` (resolved from `policies.database_retry..connection_open` via `DATABASE_RETRY_CONFIG` `OnceLock` with fallback to hardcoded defaults `3` attempts, `1s` timeout, `25ms..200ms` backoff) - - config-driven operation retry for `execute()`, `query()`, and `query_map()` using async `run_with_retry` (resolved from `policies.database_retry..query` via the same `OnceLock` with fallback to hardcoded defaults `5` attempts, `200ms` `timeout_ms`, `25ms..100ms` backoff). This is the outer retry for every operation outside the Agent Trace write-contention set, including Agent Trace reads (`query`/`query_values`/`query_map`), `passive_checkpoint`, and generic `execute()`; migration execution is deliberately outside every retry. Its retry-policy budget is not a wall-clock bound; see the retry-budget note below + - config-driven operation retry for `execute()` and `query_map()` using async `run_with_retry` (resolved from `policies.database_retry..query` via the same `OnceLock` with fallback to hardcoded defaults `5` attempts, `200ms` `timeout_ms`, `25ms..100ms` backoff). This is the outer retry for every operation outside the Agent Trace write-contention set, including Agent Trace reads (`query_map`), `passive_checkpoint`, and generic `execute()`; migration execution is deliberately outside every retry. Its retry-policy budget is not a wall-clock bound; see the retry-budget note below - parent-directory creation - - retry-backed async `execute()`, `query()`, raw-value `query_values()`, and row-mapping `query_map()` operations via the public adapter methods, with config-driven query retry resolved from `policies.database_retry..query` + - retry-backed async `execute()` and row-mapping `query_map()` operations via the public adapter methods, with config-driven query retry resolved from `policies.database_retry..query` - migration-running initialization through `new()` and generic embedded migration execution through `run_migrations()` using shared migration helpers with per-database `__sce_migrations` metadata - explicit-path migration-running initialization through `new_at(path)`, preserving the same service-specific retry/migration behavior while letting callers supply a database path outside `DbSpec::db_path()` - no-migration opening through `open_without_migrations()`, which preserves parent-directory creation and connection-open retry but does not create `__sce_migrations` or apply embedded migrations - explicit-path no-migration opening through `open_without_migrations_at(path)` for path-resolved hot runtime callers - `migration_metadata_problems(&self) -> Result>`: non-mutating readiness check that queries `__sce_migrations` metadata and compares applied migration IDs against `M::migrations()`; returns a list of problems (missing metadata table, incomplete applied migrations, unexpected extra migrations) or an empty list when the schema is ready - `ensure_schema_ready(&self, setup_guidance: &str) -> Result<()>`: non-mutating hook-readiness gate that calls `migration_metadata_problems()` and bails with a formatted error including `M::db_name()` and the caller-provided guidance string when problems are found; returns `Ok(())` when the schema is ready - - `passive_checkpoint(&self) -> Result<()>`: runs `PRAGMA wal_checkpoint(PASSIVE)` through the same async query/retry path as `execute()`/`query()` (config-driven query retry), draining the checkpoint result row without exposing its busy/log/checkpointed statistics. PASSIVE checkpoints only what is currently safe to move from the WAL into the main database file and never blocks on active readers or writers, so it does not guarantee WAL truncation; safe to call repeatedly; not a durability boundary on its own. Routine maintenance only, not exposed on `EncryptedTursoDb`. `sce hooks post-commit` is the sole current caller: it runs `RepositoryAgentTraceDb::passive_checkpoint()` exactly once after post-commit Agent Trace persistence succeeds; a failing checkpoint is logged as a warning and never fails the hook or affects already-persisted data (see [agent-trace-hooks-command-routing.md](agent-trace-hooks-command-routing.md)). + - `passive_checkpoint(&self) -> Result<()>`: runs `PRAGMA wal_checkpoint(PASSIVE)` through the same async query/retry path as `execute()` (config-driven query retry), draining the checkpoint result row without exposing its busy/log/checkpointed statistics. PASSIVE checkpoints only what is currently safe to move from the WAL into the main database file and never blocks on active readers or writers, so it does not guarantee WAL truncation; safe to call repeatedly; not a durability boundary on its own. Routine maintenance only, not exposed on `EncryptedTursoDb`. `sce hooks post-commit` is the sole current caller: it runs `RepositoryAgentTraceDb::passive_checkpoint()` exactly once after post-commit Agent Trace persistence succeeds; a failing checkpoint is logged as a warning and never fails the hook or affects already-persisted data (see [agent-trace-hooks-command-routing.md](agent-trace-hooks-command-routing.md)). - `count_write_contention(body) -> (T, WriteContentionCounts)` (`#[cfg(test)]`, `pub(crate)`): runs `body` and reports the write-contention `attempts`, `outer_retries`, and `exhaustions` it caused on the current thread. Test instrumentation only; not production telemetry and not shared across threads or processes. - `record_write_contention_timeline(body) -> (T, Vec<(Instant, WriteContentionTimelineEvent)>)` (`#[cfg(test)]`, `pub(crate)`): runs `body` and returns the `Instant`-stamped attempt start/end, backoff requested and backoff slept events of the write-contention retry on the current thread. Test-only measurement instrumentation used by the lock-contention suite; never logged in production. -- `count_read_statements(body) -> (T, usize)` (`#[cfg(test)]`, `pub(crate)`): runs `body` and reports how many `TursoDb` read statements (`query`/`query_values`/`query_map`) it issued on the current thread, each counted once in its synchronous prelude before the retry wrapper. A deterministic seam for tests that must prove an operation reads from a single DB snapshot — one statement — rather than several independent `SELECT`s a concurrent commit could tear across (first used by `MutationTraceStore::load_tree_roots` / `load_all_tree_roots`). Not compiled into production builds. +- `count_read_statements(body) -> (T, usize)` (`#[cfg(test)]`, `pub(crate)`): runs `body` and reports how many `TursoDb` read statements (`query_map`) it issued on the current thread, each counted once in its synchronous prelude before the retry wrapper. A deterministic seam for tests that must prove an operation reads from a single DB snapshot — one statement — rather than several independent `SELECT`s a concurrent commit could tear across (first used by `MutationTraceStore::load_tree_roots` / `load_all_tree_roots`). Not compiled into production builds. - `EncryptedTursoDb`: encrypted-adapter seam parallel to `TursoDb` with the same static shape (connection and spec marker). `EncryptedTursoDb::new()` resolves the encryption key via `encryption_key::get_or_create_encryption_key()` (environment variable `SCE_AUTH_DB_ENCRYPTION_KEY` with OS credential-store fallback), enables Turso experimental local encryption, applies strict `aegis256` cipher selection through `turso::EncryptionOpts` during local DB open/connect, wraps that open/connect block in the same connection-open retry policy resolved from `policies.database_retry..connection_open`, and runs embedded migrations after connect. -- `EncryptedTursoDb` exposes async `execute()`, `query()`, `query_map()`, and `run_migrations()` methods; operation methods use the same config-driven query retry policy as `TursoDb`. +- `EncryptedTursoDb` exposes async `execute()`, `query_map()`, and `run_migrations()` methods; operation methods use the same config-driven query retry policy as `TursoDb`. - Both public adapters keep concrete Turso connections and marker types; there is no shared runtime-owning core or synchronous executor bridge. - Shared lifecycle helpers: - `collect_db_path_health()` emits common parent/path health problems for DB-backed services. @@ -43,7 +43,7 @@ Both adapters expose native async operations and own no Tokio runtime. Construct ## Transactional primitives `TursoDb` offers two generic multi-statement transaction primitives beyond -the single-statement `execute()`/`query()`/`query_map()` wrappers. Both are +the single-statement `execute()`/`query_map()` wrappers. Both are public on `TursoDb` only (not `EncryptedTursoDb`). Each attempt runs the whole `BEGIN IMMEDIATE` → `COMMIT` unit and classifies every `turso::Error` as retryable (`Busy`/`BusySnapshot`) or deterministic. On the Agent Trace DB @@ -133,7 +133,7 @@ All three database areas (local DB, auth DB, Agent Trace DB) have lifecycle prov Migrations are deliberately outside the connection-open retry block. The generic constructors retry only local Turso open/connect; schema changes are not generically retried because migration SQL must not be replayed after partial execution. Service-specific resolvers may add bounded recovery around their own initialization contracts when they can prove safety; the repository-scoped Agent Trace resolver does this only for its one-file fresh schema and only repairs missing migration metadata after all required repository schema tables already exist. -`TursoDb` and `EncryptedTursoDb` operation methods use the same config-driven query retry policy, resolved from `policies.database_retry..query` via `DATABASE_RETRY_CONFIG` `OnceLock` with fallback to hardcoded defaults (`5` attempts, `200ms` `timeout_ms`, `25ms..100ms` backoff). `execute()`, `query()`, and `query_values()` convert caller parameters to owned Turso params before retry so each attempt can clone the same values. `query_values()` returns fully fetched column names plus raw `turso::Value` rows for deterministic rendering by operator-facing services. `query_map()` retries the initial query and full row-fetch loop, then runs caller-provided row mapping after retry completion so mapping failures are surfaced as logic errors and are not retried. +`TursoDb` and `EncryptedTursoDb` operation methods use the same config-driven query retry policy, resolved from `policies.database_retry..query` via `DATABASE_RETRY_CONFIG` `OnceLock` with fallback to hardcoded defaults (`5` attempts, `200ms` `timeout_ms`, `25ms..100ms` backoff). `execute()` converts caller parameters to owned Turso params before retry so each attempt can clone the same values. `query_map()` retries the initial query and full row-fetch loop, then runs caller-provided row mapping after retry completion so mapping failures are surfaced as logic errors and are not retried. Retry-policy budget versus wall-clock time: the generic query retry policy keeps its historical `5` attempts, `200ms` `timeout_ms`, and `25..100ms` backoff configuration, and its earlier `<= 2_000ms` figure describes that configured retry/budget calculation, not a hard wall-clock operation timeout. `run_with_retry` preserves post-attempt diagnostic timeout semantics and does not cancel an operation merely because its configured attempt budget elapsed: it only compares an attempt's elapsed time with `timeout_ms` after the attempt returns, to label the error. Actual failure latency is the sum of each attempt's real duration plus backoff. For the Agent Trace write-contention set, each attempt can include up to `busy_timeout_ms` (default 1000 ms) of Turso busy waiting and at most one outer retry starts, so a write blocked by a long-held lock fails after about 2–2.1 s with the defaults (about 1–1.1 s under the earlier 500 ms / 1250 ms defaults). None of `query.timeout_ms`, the `<= 2_000ms` policy figure, or `contention_deadline_ms` is a hard wall-clock deadline.