diff --git a/cli/src/services/app_support.rs b/cli/src/services/app_support.rs index 14671d7e..41f62af3 100644 --- a/cli/src/services/app_support.rs +++ b/cli/src/services/app_support.rs @@ -160,7 +160,7 @@ fn write_stdout_payload(writer: &mut W, payload: &str) -> Result<(), C }) } -fn write_error_diagnostic(writer: &mut W, error: &CliError) { +pub(crate) fn write_error_diagnostic(writer: &mut W, error: &CliError) { write_error_diagnostic_with_color_policy( writer, error, @@ -184,7 +184,7 @@ fn write_error_diagnostic_with_color_policy( } CliError::User { error: user_error, .. - } => user_error.message().to_string(), + } => user_error.message().clone(), }; let styled_message = services::style::error_text_with_color_policy( &services::security::redact_sensitive_text(&rendered), @@ -328,7 +328,7 @@ mod tests { let rendered = String::from_utf8(stderr).expect("stderr is valid utf8"); let redacted_message = - services::security::redact_sensitive_text(UserError::NotAuthenticated.message()); + services::security::redact_sensitive_text(&UserError::NotAuthenticated.message()); assert!(rendered.contains(&redacted_message)); } diff --git a/cli/src/services/command_registry.rs b/cli/src/services/command_registry.rs index b38f903e..9d184a3c 100644 --- a/cli/src/services/command_registry.rs +++ b/cli/src/services/command_registry.rs @@ -191,6 +191,7 @@ pub fn default_runtime_command(name: &str) -> Option { services::sync::NAME => Some(RuntimeCommand::Sync(services::sync::command::SyncCommand { request: services::sync::SyncRequest { format: services::output_format::OutputFormat::Text, + invocation: services::sync::SyncInvocation::Manual, }, })), _ => None, diff --git a/cli/src/services/error.rs b/cli/src/services/error.rs index 5e2b487c..c46807d3 100644 --- a/cli/src/services/error.rs +++ b/cli/src/services/error.rs @@ -48,33 +48,91 @@ impl FailureClass { } } +/// The typed origin of an automatic synchronization failure. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum AutomaticSyncFailureKind { + Authentication, + ControlPlane, + Stream, + Runtime, +} + /// Catalog of expected, deliberately-explained failures presented to the user /// as a friendly diagnostic instead of a technical error chain. -#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq)] pub enum UserError { #[allow(dead_code)] NotAuthenticated, + AutomaticSyncFailed { + failure_kind: AutomaticSyncFailureKind, + reason: String, + }, } impl UserError { - pub fn class(self) -> FailureClass { + pub fn class(&self) -> FailureClass { match self { - Self::NotAuthenticated => FailureClass::Runtime, + Self::NotAuthenticated | Self::AutomaticSyncFailed { .. } => FailureClass::Runtime, } } #[allow(dead_code)] - pub fn key(self) -> &'static str { + pub fn key(&self) -> &'static str { match self { Self::NotAuthenticated => "auth.not_authenticated", + Self::AutomaticSyncFailed { failure_kind, .. } => match failure_kind { + AutomaticSyncFailureKind::Authentication => "sync.automatic.authentication_failed", + AutomaticSyncFailureKind::ControlPlane => "sync.automatic.control_plane_failed", + AutomaticSyncFailureKind::Stream => "sync.automatic.stream_failed", + AutomaticSyncFailureKind::Runtime => "sync.automatic.runtime_failed", + }, } } - pub fn message(self) -> &'static str { + pub fn message(&self) -> String { match self { Self::NotAuthenticated => { "You are not logged in. Please log in using the `sce auth login` command." + .to_string() } + Self::AutomaticSyncFailed { + failure_kind: AutomaticSyncFailureKind::Authentication, + .. + } => "Automatic synchronization failed: authentication is required. Run `sce auth login`, then manually retry with `sce sync`.".to_string(), + Self::AutomaticSyncFailed { + failure_kind: AutomaticSyncFailureKind::ControlPlane, + reason, + } => format!( + "Automatic synchronization failed: {reason}. Check control-plane connectivity and availability, then manually retry with `sce sync`." + ), + Self::AutomaticSyncFailed { + failure_kind: AutomaticSyncFailureKind::Stream, + reason, + } => format!( + "Automatic synchronization failed: {reason}. Check Agent Trace data and connectivity, then manually retry with `sce sync`." + ), + Self::AutomaticSyncFailed { + failure_kind: AutomaticSyncFailureKind::Runtime, + reason, + } => format!( + "Automatic synchronization failed: {reason}. Check the local repository and Agent Trace configuration, then manually retry with `sce sync`." + ), + } + } + + #[allow(dead_code)] + pub fn automatic_sync_failure_kind(&self) -> Option { + match self { + Self::AutomaticSyncFailed { failure_kind, .. } => Some(*failure_kind), + Self::NotAuthenticated => None, + } + } + + #[allow(dead_code)] + pub fn reason(&self) -> Option<&str> { + match self { + Self::AutomaticSyncFailed { reason, .. } => Some(reason), + Self::NotAuthenticated => None, } } } diff --git a/cli/src/services/parse/command_runtime.rs b/cli/src/services/parse/command_runtime.rs index 31d56473..0c86165e 100644 --- a/cli/src/services/parse/command_runtime.rs +++ b/cli/src/services/parse/command_runtime.rs @@ -251,7 +251,10 @@ fn convert_clap_command(command: cli_schema::Commands) -> Result { Ok(RuntimeCommand::Sync(services::sync::command::SyncCommand { - request: services::sync::SyncRequest { format }, + request: services::sync::SyncRequest { + format, + invocation: services::sync::SyncInvocation::from_environment(), + }, })) } } diff --git a/cli/src/services/sync/auto_sync.rs b/cli/src/services/sync/auto_sync.rs index 2198f351..975afa44 100644 --- a/cli/src/services/sync/auto_sync.rs +++ b/cli/src/services/sync/auto_sync.rs @@ -4,11 +4,16 @@ use std::io; use std::path::{Path, PathBuf}; use std::process::{Command, Stdio}; +use crate::services::app_support; +use crate::services::error::{AutomaticSyncFailureKind, CliError, UserError}; +use crate::services::sync::{AUTOMATIC_SYNC_INVOCATION_ENV, AUTOMATIC_SYNC_INVOCATION_VALUE}; + const SYNC_ARGS: &[&str] = &["sync", "--format", "json"]; #[derive(Clone, Copy, Debug, Eq, PartialEq)] enum StdioMode { Null, + Inherit, } #[derive(Clone, Debug, Eq, PartialEq)] @@ -19,6 +24,7 @@ struct AutoSyncCommand { stdin: StdioMode, stdout: StdioMode, stderr: StdioMode, + environment: Vec<(String, String)>, } impl AutoSyncCommand { @@ -29,31 +35,74 @@ impl AutoSyncCommand { current_dir: repository_root.to_path_buf(), stdin: StdioMode::Null, stdout: StdioMode::Null, - stderr: StdioMode::Null, + stderr: StdioMode::Inherit, + environment: vec![( + AUTOMATIC_SYNC_INVOCATION_ENV.to_string(), + AUTOMATIC_SYNC_INVOCATION_VALUE.to_string(), + )], + } + } +} + +#[derive(Debug)] +enum AutoSyncLaunchError { + CurrentExecutable(io::Error), + Spawn(io::Error), +} + +impl std::fmt::Display for AutoSyncLaunchError { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::CurrentExecutable(error) => { + write!(formatter, "failed to resolve current executable: {error}") + } + Self::Spawn(error) => write!(formatter, "failed to spawn detached sync: {error}"), + } + } +} + +impl std::error::Error for AutoSyncLaunchError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Self::CurrentExecutable(error) | Self::Spawn(error) => Some(error), } } } +fn launcher_failure_diagnostic(error: AutoSyncLaunchError) -> CliError { + let reason = error.to_string(); + CliError::user_with_source( + UserError::AutomaticSyncFailed { + failure_kind: AutomaticSyncFailureKind::Runtime, + reason, + }, + error, + ) +} + /// Launches the current executable to synchronize the repository in the -/// background. Launcher failures are intentionally ignored by the caller. +/// background. Launcher failures are reported on stderr but remain fail-open +/// to the post-commit caller. pub fn launch(repository_root: &Path) { - let _ = launch_with(repository_root, std::env::current_exe, spawn_command); + if let Err(error) = launch_with(repository_root, std::env::current_exe, spawn_command) { + let diagnostic = launcher_failure_diagnostic(error); + let mut stderr = io::stderr(); + app_support::write_error_diagnostic(&mut stderr, &diagnostic); + } } fn launch_with( repository_root: &Path, current_exe: FCurrentExe, spawn: FSpawn, -) -> bool +) -> Result<(), AutoSyncLaunchError> where FCurrentExe: FnOnce() -> io::Result, FSpawn: FnOnce(AutoSyncCommand) -> io::Result<()>, { - let Ok(executable) = current_exe() else { - return false; - }; + let executable = current_exe().map_err(AutoSyncLaunchError::CurrentExecutable)?; - spawn(AutoSyncCommand::new(executable, repository_root)).is_ok() + spawn(AutoSyncCommand::new(executable, repository_root)).map_err(AutoSyncLaunchError::Spawn) } fn spawn_command(spec: AutoSyncCommand) -> io::Result<()> { @@ -61,9 +110,10 @@ fn spawn_command(spec: AutoSyncCommand) -> io::Result<()> { command .args(spec.args) .current_dir(spec.current_dir) + .envs(spec.environment) .stdin(Stdio::null()) .stdout(Stdio::null()) - .stderr(Stdio::null()); + .stderr(Stdio::inherit()); // Dropping Child does not wait for it; the spawned sync continues // independently of the post-commit caller. @@ -78,7 +128,10 @@ mod tests { use std::path::{Path, PathBuf}; use std::rc::Rc; - use super::{launch_with, AutoSyncCommand, StdioMode, SYNC_ARGS}; + use super::{ + launch_with, AutoSyncCommand, StdioMode, AUTOMATIC_SYNC_INVOCATION_ENV, + AUTOMATIC_SYNC_INVOCATION_VALUE, SYNC_ARGS, + }; #[test] fn launch_builds_the_expected_detached_command() { @@ -94,7 +147,7 @@ mod tests { }, ); - assert!(launched); + assert!(launched.is_ok()); assert_eq!( captured.borrow().clone(), Some(AutoSyncCommand { @@ -103,7 +156,11 @@ mod tests { current_dir: PathBuf::from("/repo/root"), stdin: StdioMode::Null, stdout: StdioMode::Null, - stderr: StdioMode::Null, + stderr: StdioMode::Inherit, + environment: vec![( + AUTOMATIC_SYNC_INVOCATION_ENV.to_string(), + AUTOMATIC_SYNC_INVOCATION_VALUE.to_string(), + )], }) ); } @@ -122,7 +179,7 @@ mod tests { }, ); - assert!(!launched); + assert!(launched.is_err()); assert!(!*spawn_called.borrow()); } @@ -134,6 +191,6 @@ mod tests { |_| Err(io::Error::other("spawn unavailable")), ); - assert!(!launched); + assert!(launched.is_err()); } } diff --git a/cli/src/services/sync/command.rs b/cli/src/services/sync/command.rs index 6120bbe6..903d8753 100644 --- a/cli/src/services/sync/command.rs +++ b/cli/src/services/sync/command.rs @@ -1,7 +1,7 @@ use std::io::Write; use crate::app::ContextWithRepoRoot; -use crate::services::error::{CliError, UserError}; +use crate::services::error::{AutomaticSyncFailureKind, CliError, UserError}; use crate::services::sync::progress::{ IndicatifProgressReporter, NoopProgressReporter, ProgressReporter, }; @@ -32,7 +32,30 @@ where } #[allow(clippy::needless_pass_by_value)] -fn classify_sync_error(err: TraceSyncError) -> CliError { +fn classify_sync_error( + err: TraceSyncError, + invocation: crate::services::sync::SyncInvocation, +) -> CliError { + if invocation == crate::services::sync::SyncInvocation::Automatic { + let failure_kind = if err.is_authentication_failure() { + AutomaticSyncFailureKind::Authentication + } else { + match &err { + TraceSyncError::ControlPlane(_) => AutomaticSyncFailureKind::ControlPlane, + TraceSyncError::Stream { .. } => AutomaticSyncFailureKind::Stream, + TraceSyncError::Runtime(_) => AutomaticSyncFailureKind::Runtime, + } + }; + let reason = err.to_string(); + return CliError::user_with_source( + UserError::AutomaticSyncFailed { + failure_kind, + reason, + }, + err, + ); + } + if err.is_authentication_failure() { CliError::user_with_source(UserError::NotAuthenticated, err) } else { @@ -87,7 +110,7 @@ impl SyncCommand { run_current_sync_with_progress_and_clock(&repo_root, &mut progress, clock) } } - .map_err(classify_sync_error)?; + .map_err(|error| classify_sync_error(error, self.request.invocation))?; render_sync::render(&report, self.request.format) .map_err(|error| CliError::runtime(anyhow::Error::msg(format!("{error:#}")))) @@ -101,9 +124,10 @@ mod tests { use crate::services::agent_trace_sync::StreamSyncError; use crate::services::error::CliError; use crate::services::sync::sync::TraceSyncError; + use crate::services::sync::SyncInvocation; fn assert_user_not_authenticated(err: TraceSyncError) { - match classify_sync_error(err) { + match classify_sync_error(err, SyncInvocation::Manual) { CliError::User { error, source } => { assert_eq!(error.key(), "auth.not_authenticated"); assert!(source.is_some()); @@ -113,7 +137,7 @@ mod tests { } fn assert_internal(err: TraceSyncError) { - match classify_sync_error(err) { + match classify_sync_error(err, SyncInvocation::Manual) { CliError::Internal { .. } => {} other @ CliError::User { .. } => panic!("expected CliError::Internal, got {other:?}"), } diff --git a/cli/src/services/sync/mod.rs b/cli/src/services/sync/mod.rs index fac25936..7058bf28 100644 --- a/cli/src/services/sync/mod.rs +++ b/cli/src/services/sync/mod.rs @@ -10,9 +10,53 @@ pub mod sync; pub const NAME: &str = "sync"; +/// Internal process-boundary marker used only by the post-commit detached +/// launcher. It is deliberately separate from the user-facing auto-sync +/// configuration setting. +pub(crate) const AUTOMATIC_SYNC_INVOCATION_ENV: &str = "SCE_INTERNAL_AUTO_SYNC"; +pub(crate) const AUTOMATIC_SYNC_INVOCATION_VALUE: &str = "1"; + use crate::services::output_format::OutputFormat; +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum SyncInvocation { + Manual, + Automatic, +} + +impl SyncInvocation { + pub(crate) fn from_environment() -> Self { + Self::from_marker(std::env::var(AUTOMATIC_SYNC_INVOCATION_ENV).ok().as_deref()) + } + + fn from_marker(value: Option<&str>) -> Self { + match value { + Some(AUTOMATIC_SYNC_INVOCATION_VALUE) => Self::Automatic, + _ => Self::Manual, + } + } +} + #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct SyncRequest { pub format: OutputFormat, + pub invocation: SyncInvocation, +} + +#[cfg(test)] +mod tests { + use super::SyncInvocation; + + #[test] + fn automatic_invocation_requires_the_internal_marker_value() { + assert_eq!( + SyncInvocation::from_marker(Some("1")), + SyncInvocation::Automatic + ); + assert_eq!( + SyncInvocation::from_marker(Some("true")), + SyncInvocation::Manual + ); + assert_eq!(SyncInvocation::from_marker(None), SyncInvocation::Manual); + } } diff --git a/context/architecture.md b/context/architecture.md index ccfdb91c..7f005c36 100644 --- a/context/architecture.md +++ b/context/architecture.md @@ -105,7 +105,7 @@ The repository includes a new placeholder Rust binary crate at `cli/`. - `cli/src/main.rs` is the executable entrypoint (`sce`) and delegates to `app::run`. - `cli/src/cli_schema.rs` defines the clap-based CLI schema using derive macros for all top-level commands and subcommands, including the top-level `sync` command, and renders command-local help text for the `auth` command tree (`auth`, `auth login`, `auth logout`, `auth whoami`). -- `cli/src/app.rs` provides the clap-based argument dispatch loop with deterministic help/setup execution, bare-command help routing for `sce auth` and `sce config`, centralized stream routing (`stdout` success payloads, `stderr` redacted diagnostics), stable class-based exit-code mapping (`2` parse, `3` validation, `4` runtime, `5` dependency), and stable class-based stderr diagnostic codes (`SCE-ERR-PARSE`, `SCE-ERR-VALIDATION`, `SCE-ERR-RUNTIME`, `SCE-ERR-DEPENDENCY`) with default `Try:` remediation injection when missing. +- `cli/src/app.rs` provides the clap-based argument dispatch loop with deterministic help/setup execution, bare-command help routing for `sce auth` and `sce config`, centralized stream routing (`stdout` success payloads, `stderr` redacted diagnostics), stable class-based exit-code mapping (`2` parse, `3` validation, `4` runtime, `5` dependency), and stable class-based stderr diagnostic codes (`SCE-ERR-PARSE`, `SCE-ERR-VALIDATION`, `SCE-ERR-RUNTIME`, `SCE-ERR-DEPENDENCY`) with default `Try:` remediation injection when missing. The typed `CliError` boundary includes payload-bearing automatic-sync user errors whose app-owned rendering uses the `Automatic synchronization failed:` prefix and preserves mode-specific recovery guidance without appending default runtime remediation; the detached post-commit child inherits stderr for that diagnostic while keeping JSON stdout silent. - The app runtime now moves through explicit startup phases in `cli/src/app.rs`: dependency bootstrapping (`perform_dependency_check`), startup context construction (`build_startup_context`), runtime initialization (`initialize_runtime`), command parse/execute inside telemetry subscriber context (`run_command_lifecycle`, `parse_command_phase` plus `services::app_support::execute_command_phase`), and final output rendering through `services::app_support::render_run_outcome`. `AppRuntime` owns the concrete production logger, no-op telemetry runtime, filesystem ops, git ops, static `CommandRegistry`, and startup-diagnostic state across those phases; `RunOutcome` carries final render data with an optional generic logger implementing `services::observability::traits::Logger`, so render support can log classified errors without production-logger type coupling. If a telemetry implementation attempts to invoke the command action more than once, dispatch returns a runtime-classified error instead of panicking or reusing consumed arguments. - `AppContext` is the CLI's borrowed dependency view in `cli/src/app.rs`: it is generic over logger, telemetry, filesystem, and git capability implementations and stores references plus an optional `repo_root: Option` instead of owning `Arc` trait objects. Because it borrows from `AppRuntime`, `AppContext` is a lightweight, short-lived view and must not be stored long-term (e.g., in structs or across await points). Startup creates a context view over `AppRuntime`'s concrete production dependencies with `repo_root` set to `None`; command paths can derive repo-root-scoped context views through the `ContextWithRepoRoot` accessor trait / `AppContext::with_repo_root(...)`, which reuses the same borrowed dependencies while attaching the resolved root. Narrow accessor traits expose associated concrete capability types for logger, telemetry, fs, and git (`&Self::...`) plus repo-root access, so call sites can express capability requirements without erasing the borrowed dependencies back to trait objects; lifecycle providers consume the repo-root accessor rather than the full context type. - Command parse-time conversion and run-time handling are separated by an internal static `RuntimeCommand` seam. `cli/src/services/command_registry.rs` defines the `RuntimeCommand` enum with variants for help/help-text, version, completion, auth, config, setup, doctor, hooks, policy, and sync, plus a deterministic `CommandRegistry` name catalog populated by `build_default_registry()`. `parse_command_phase` in `cli/src/app.rs` delegates clap-output conversion to `cli/src/services/parse/command_runtime.rs`, which owns clap error classification, help rendering bridges, and parsed-request-to-enum conversion while returning concrete enum values. Service-owned `command.rs` modules define command payload structs and generic execution methods with narrow context requirements: context-free commands accept any context, hooks requires logger access, setup/doctor require repo-root scoping, and central dispatch requires the union of logger plus repo-root-scoping capabilities. `services::app_support::execute_command_phase` emits lifecycle logs around `RuntimeCommand::execute_with_stderr(...)`; the enum performs the only central dispatch match and delegates business behavior to the service-owned command structs, with sync receiving the app-owned stderr writer for format-gated progress. diff --git a/context/cli/agent-trace-auto-sync.md b/context/cli/agent-trace-auto-sync.md index be66f21b..e2169192 100644 --- a/context/cli/agent-trace-auto-sync.md +++ b/context/cli/agent-trace-auto-sync.md @@ -24,17 +24,37 @@ start the current `sce` executable with exactly: sync --format json ``` -The child runs with the repository root as its working directory and null -stdin, stdout, and stderr. `Command::spawn()` is used without waiting for a -status; the hook returns its normal successful result immediately. A failure to -resolve the current executable or spawn the child is ignored, so launcher -failures cannot turn a successful post-commit operation into a failure. +The child runs with the repository root as its working directory, null stdin and +stdout, and inherited stderr. An internal `SCE_INTERNAL_AUTO_SYNC=1` process +marker lets the child classify this invocation as automatic without adding a +user-facing option or configuration layer. `Command::spawn()` is used without +waiting for a status; the hook returns its normal successful result immediately. +If the child sync fails, its single typed `SCE-ERR-RUNTIME` diagnostic is visible +through inherited stderr. If the current executable cannot be resolved or the +child cannot be spawned, the launcher emits the same typed automatic-sync +diagnostic with the startup reason on stderr. Both startup and child failures +remain fail-open, so they cannot turn a successful post-commit operation into a +failure. Automatic synchronization is not invoked by `pre-commit`, `diff-trace`, or `conversation-trace`. It is one post-commit launch, not a high-frequency hook, watcher, polling loop, scheduler, daemon, retry queue, persistent service, or second synchronization database. +### Failure diagnostics + +The automatic child classifies terminal sync failures into the closed +`AutomaticSyncFailureKind` set: `Authentication`, `ControlPlane`, `Stream`, or +`Runtime`. The app renders exactly one `Error [SCE-ERR-RUNTIME]` diagnostic +whose message begins `Automatic synchronization failed:`. Authentication uses +the reviewed `sce auth login`, then manual `sce sync` recovery instruction and +keeps the technical reason for observability; non-authentication failures +include their preserved display reason and actionable recovery guidance before +the manual `sce sync` retry. Automatic user-error rendering does not append the +generic runtime `Try:` sentence or render the technical source as a second +diagnostic. Launcher executable-resolution and spawn failures use the same +`Runtime` payload and preserve their startup reason. + ## Doctor readiness `sce doctor` reports the capability without invoking it. The post-commit hook's diff --git a/context/cli/agent-trace-sync-command.md b/context/cli/agent-trace-sync-command.md index 7500c311..380dd03e 100644 --- a/context/cli/agent-trace-sync-command.md +++ b/context/cli/agent-trace-sync-command.md @@ -44,7 +44,9 @@ Because every invocation starts from the control plane's authoritative `/state` ## Recovery semantics - **`401` (unexpected):** the control-plane client refreshes the WorkOS token exactly once, saves it, and retries the request exactly once. Concurrent callers that observed the same rejected token coalesce onto the first refresh and reuse its saved token; a second `401` (`ControlPlaneError::MissingCredentials`/`AuthenticationFailed`) fails the command with `sce auth login` guidance, and there is no further retry. -- **Typed authentication classification:** `ControlPlaneError::is_authentication_failure()` is `true` only for `MissingCredentials`/`AuthenticationFailed`, and the same typed traversal is exposed as `StreamSyncError::is_authentication_failure()` and `TraceSyncError::is_authentication_failure()` — no sync-stream path erases a `ControlPlaneError` into a bare `String` before it reaches the command boundary. `cli/src/services/sync/command.rs`'s `classify_sync_error` calls this traversal (never string/substring matching) to route an authentication failure from the initial `/state` call, a stream batch request, or a stream reconciliation `/state` refresh to `CliError::User { error: UserError::NotAuthenticated, .. }`; every other `ControlPlaneError` variant (`Forbidden`, `BadRequest`, `Transport`, `ServerError`, `InvalidResponse`, `Storage`, `Protocol`) stays `CliError::Internal` with its full technical chain preserved. See [CLI error-code taxonomy](../sce/cli-error-code-taxonomy.md) for the `CliError`/`UserError` architecture and [sync-command.md](sync-command.md#error-classification) for the command-level classifier. +- **Typed authentication classification:** `ControlPlaneError::is_authentication_failure()` is `true` only for `MissingCredentials`/`AuthenticationFailed`, and the same typed traversal is exposed as `StreamSyncError::is_authentication_failure()` and `TraceSyncError::is_authentication_failure()` — no sync-stream path erases a `ControlPlaneError` into a bare `String` before it reaches the command boundary. `cli/src/services/sync/command.rs`'s `classify_sync_error` calls this traversal (never string/substring matching). Manual invocations route authentication failures from the initial `/state` call, a stream batch request, or a stream reconciliation `/state` refresh to `CliError::User { error: UserError::NotAuthenticated, .. }`; every other failure stays internal with its full technical chain preserved. Automatic invocations use the payload-bearing `UserError::AutomaticSyncFailed` entry, with a typed authentication, control-plane, stream, or runtime failure kind and the display reason preserved separately from the technical source. See [CLI error-code taxonomy](../sce/cli-error-code-taxonomy.md) for the `CliError`/`UserError` architecture and [sync-command.md](sync-command.md#error-classification) for the command-level classifier. +- **Automatic invocation boundary:** Automatic execution is selected only by the detached launcher's internal `SCE_INTERNAL_AUTO_SYNC=1` process marker; manual `sce sync` remains mode-neutral. The detached child inherits stderr, so its one app-rendered runtime diagnostic is visible without exposing JSON stdout or making the post-commit hook wait. Launcher startup failures use the same typed automatic-sync payload with a preserved startup reason and remain fail-open to the hook. +- **Automatic failure rendering:** Automatic terminal failures use the closed `AutomaticSyncFailureKind` catalog (`Authentication`, `ControlPlane`, `Stream`, or `Runtime`) and render one `SCE-ERR-RUNTIME` message beginning `Automatic synchronization failed:`. Authentication renders login-plus-manual-sync guidance while retaining its technical reason only for observability; non-authentication failures render their preserved reason and actionable manual `sce sync` recovery without a duplicate generic `Try:` sentence. - **`409` (cursor conflict):** the per-stream sync engine reconciles by refetching `/state`, replacing only the affected stream's cursor, and resuming from local rows after the refreshed cursor — already-accepted rows are never resent. - **Ambiguous batch failure (`5xx`, transport failure, or an undecodable `2xx` body):** the engine reconciles via `/state` before any resend. If the refreshed cursor advanced (the batch was actually committed), sync continues from it without resending. If the cursor is unchanged (the batch was not committed), sync may resend once from the authoritative cursor. - **Reconciliation bound:** both the `409` and ambiguous-failure reconciliation paths share one bounded attempt counter per stream; exhausting it fails that stream with a "did not converge" error instead of looping unboundedly. diff --git a/context/cli/sync-command.md b/context/cli/sync-command.md index fcdfb103..7e212960 100644 --- a/context/cli/sync-command.md +++ b/context/cli/sync-command.md @@ -10,13 +10,18 @@ The Clap surface is defined in `cli/src/cli_schema.rs` and dispatched through the static `RuntimeCommand::Sync` variant. The sync-owned command boundary lives under `cli/src/services/sync/`; shared storage, export, authentication, and control-plane protocol infrastructure remains in their existing services. The -same boundary owns a best-effort one-shot launcher used by the post-commit +command request carries an internal `SyncInvocation` context so manual and +automatic executions can retain distinct error semantics without adding a public +CLI option. The same boundary owns a best-effort one-shot launcher used by the post-commit hook when `agent_trace.auto_sync` is enabled: it resolves the current `sce` -executable, starts `sync --format json` in the repository root with null standard -streams, and does not wait for the child; executable and spawn failures are -ignored. The launcher is not a daemon or retry queue; local rows remain available -for a later manual or automatic invocation through the control-plane cursor -authority. +executable, starts `sync --format json` in the repository root with null stdin and +stdout plus inherited stderr, and does not wait for the child. It passes the +internal `SCE_INTERNAL_AUTO_SYNC=1` marker so automatic failures use the typed +automatic-sync diagnostic path. Child failures are visible through inherited +stderr, while executable and spawn failures emit the same typed runtime +diagnostic with their startup reason and remain fail-open. The launcher is not a +daemon or retry queue; local rows remain available for a later manual or +automatic invocation through the control-plane cursor authority. Sync orchestration owns its `SyncProgressEvent` lifecycle, batch, and stream-completion payloads and publishes them through the consumer-typed, library-independent `services::sync::progress::ProgressReporter` contract. @@ -104,17 +109,20 @@ client. The command change does not alter those semantics. `cli/src/services/sync/command.rs`'s `classify_sync_error` maps the command's terminal `TraceSyncError` into the typed `CliError` boundary by calling `TraceSyncError::is_authentication_failure()` — a typed traversal down to -`ControlPlaneError`, never string/substring matching. An authentication -failure from the initial `/state` call, a stream batch request, or a stream -reconciliation `/state` refresh (`ControlPlaneError::MissingCredentials` or -`AuthenticationFailed`) classifies as `CliError::User { error: -UserError::NotAuthenticated, .. }`, preserving the technical error as its -source; every other `ControlPlaneError` (`Forbidden`, `BadRequest`, -`Transport`, `ServerError`, `InvalidResponse`, `Storage`, `Protocol`) -classifies as `CliError::Internal`. `sync/command.rs` builds no friendly -sentence and applies no terminal styling itself — `app_support` renders the -single `You are not logged in...` diagnostic for the user case, and the full -`anyhow`/control-plane chain for the internal case. See [CLI error-code +`ControlPlaneError`, never string/substring matching. Manual invocations retain +their existing behavior: authentication failures classify as +`UserError::NotAuthenticated`, while other failures remain `CliError::Internal` +with their technical source. Automatic invocations classify authentication, +control-plane, stream, and local runtime failures as the payload-bearing +`UserError::AutomaticSyncFailed` catalog entry, preserving the typed failure +kind and display reason while retaining the technical source through +`CliError::user_with_source`. `app_support` renders one runtime diagnostic for +the automatic case beginning `Automatic synchronization failed:`: +authentication tells the user to run `sce auth login` and then manually retry +with `sce sync` (the technical authentication reason remains observability-only); +other failures include the preserved reason and actionable recovery guidance +including manual `sce sync`, without adding the default runtime `Try:` sentence. +See [CLI error-code taxonomy](../sce/cli-error-code-taxonomy.md) for the full `CliError`/`UserError` architecture. diff --git a/context/context-map.md b/context/context-map.md index 5aaa2a05..1d2faab1 100644 --- a/context/context-map.md +++ b/context/context-map.md @@ -17,7 +17,7 @@ Feature/domain context: - `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/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` (default-enabled post-commit Agent Trace synchronization with explicit-false opt-out plus doctor readiness reporting: the existing `sce sync` command launched once through the current executable after local persistence, 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) +- `context/cli/agent-trace-auto-sync.md` (default-enabled post-commit Agent Trace synchronization with explicit-false opt-out plus doctor readiness reporting: the existing `sce sync` command launched once through the current executable after local persistence, detached null stdin/stdout with inherited stderr, an internal automatic-invocation marker, typed `Authentication`/`ControlPlane`/`Stream`/`Runtime` child and launcher-failure diagnostics beginning `Automatic synchronization failed:`, 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) - `context/cli/sync-command.md` (the top-level `sce sync` command: repository-scoped Agent Trace storage resolution, WorkOS-authenticated four-stream control-plane synchronization through the sync-owned consumer-typed `services::sync::progress` reporter contract with sync-owned events, its generic/no-op contract and `indicatif` presentation adapter for aligned stderr progress with independent stream completion, explicit successful finalization, JSON stdout silence, and rejection of the removed `sce trace` command group) - `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, 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`, default-enabled `agent_trace.auto_sync` boolean resolution with explicit-false opt-out 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) diff --git a/context/glossary.md b/context/glossary.md index e43633ce..9835e92f 100644 --- a/context/glossary.md +++ b/context/glossary.md @@ -73,7 +73,8 @@ - `command loop`: The `clap` derive-based parser + dispatcher in `cli/src/cli_schema.rs`, `cli/src/services/parse/command_runtime.rs`, and `cli/src/app.rs` that routes `help`, `config`, `setup`, `doctor`, `auth`, `hooks`, `policy`, `sync`, `version`, and `completion`, executes implemented command flows, emits command-local help payloads for supported subcommand trees, and returns deterministic actionable errors for invalid invocation. - `RuntimeCommand seam`: Internal static command-execution abstraction in `cli/src/services/command_registry.rs` where clap-parsed commands are represented as a `RuntimeCommand` enum with variants for `Help`, `HelpText`, `Version`, `Completion`, `Auth`, `Config`, `Setup`, `Doctor`, `Hooks`, `Policy`, and `Sync`. The enum owns `name()` and `execute(...)` dispatch, delegating behavior to service-owned command payload structs while avoiding boxed `dyn RuntimeCommand` handles. Parsed request construction lives in `cli/src/services/parse/command_runtime.rs` when user-provided options or subcommands are required. - `sce dependency baseline`: Current crate dependency set declared in `cli/Cargo.toml` (`anyhow`, `chrono`, `clap`, `clap_complete`, `dirs`, `hmac`, `indicatif`, `inquire`, `jsonschema`, `keyring-core`, `murmur3`, `owo-colors`, `rand`, `reqwest`, `serde`, `serde_json`, `sha2`, `tokio`, `tracing`, `turso`, `uuid`, plus target-specific keyring backends). No CLI dev-dependencies are currently declared, and the baseline is validated through normal compile/test coverage. -- `progress reporter contract`: Library-independent, consumer-typed sync seam in `cli/src/services/sync/progress.rs` where `ProgressReporter` carries an arbitrary consumer event type, supports explicit successful finalization and closure-based collectors, and provides `NoopProgressReporter` for callers without human progress output. The sync-owned `SyncProgressEvent` in `cli/src/services/sync/sync.rs` carries lifecycle, accepted-batch, and stream-completion payloads for the four concurrent Agent Trace streams; `services::sync::progress` also owns the fixed `indicatif` terminal adapter and focused contract tests, while `sync/command.rs` selects text versus JSON behavior. No top-level `services::progress` module exists. +- `progress reporter contract`: Library-independent, consumer-typed sync seam in `cli/src/services/sync/progress.rs` where `ProgressReporter` carries an arbitrary consumer event type, supports explicit successful finalization and closure-based collectors, and provides `NoopProgressReporter` for callers without human progress output. The sync-owned `SyncProgressEvent` in `cli/src/services/sync/sync.rs` carries lifecycle, accepted-batch, and stream-completion payloads for the four concurrent Agent Trace streams; `services::sync::progress` also owns the fixed `indicatif` terminal adapter and focused contract tests, while `sync/command.rs` selects text versus JSON behavior. No top-level `services::progress` module exists. The same sync request carries internal `SyncInvocation` context for manual versus automatic execution, with automatic failures represented by the closed `AutomaticSyncFailureKind` catalog (`Authentication`, `ControlPlane`, `Stream`, `Runtime`) and its reviewed `Automatic synchronization failed:` recovery guidance. +- `agent_trace.auto_sync`: Config-file-only boolean resolved by the shared config layers for the post-commit Agent Trace synchronization boundary; omitted values are `true` and explicit `false` opts out, and `sce config show` reports its winning source. Enabled post-commit runs launch `sync --format json` once through the current executable with repository-root working directory, null stdin/stdout, inherited stderr, and an internal `SCE_INTERNAL_AUTO_SYNC=1` marker; launcher and child failures remain fail-open and are visible as one typed `SCE-ERR-RUNTIME` diagnostic beginning `Automatic synchronization failed:`. Authentication directs the user through `sce auth login` and manual `sce sync`, while other kinds include their preserved reason and manual retry guidance. See [Agent Trace hook routing](sce/agent-trace-hooks-command-routing.md). - `local Turso adapter`: Module in `cli/src/services/local_db/mod.rs` that defines `LocalDbSpec` and exposes `LocalDb` as a `TursoDb` alias. It resolves the canonical local DB path with `local_db_path()`, currently declares zero migrations, and inherits retry-backed `new()`, `execute()`, `query()`, and `query_map()` behavior from the shared generic adapter. - `encrypted Turso adapter`: Generic adapter seam in `cli/src/services/db/mod.rs` exposed as `EncryptedTursoDb`, structurally parallel to `TursoDb` (connection, tokio runtime bridge, spec typing). Its constructor resolves the encryption key via `encryption_key::get_or_create_encryption_key(&db_path, db_name)`, which derives a Turso-compatible 64-character hex key from non-empty `SCE_AUTH_DB_ENCRYPTION_KEY` env-secret text before falling back to OS credential-store keyring get-or-create behavior; credential-store default registration is guarded by stable `OnceLock` plus an atomic in-progress flag so errors or panics leave initialization retryable without mutex poisoning. The adapter enables Turso local encryption with strict `aegis256` cipher selection through `turso::EncryptionOpts`, wraps encrypted local open/connect in the default DB connection-open retry policy, and runs embedded migrations after retry has produced a connection; the adapter also exposes retry-backed synchronous `execute`, `query`, `query_map`, and `run_migrations` helpers with `__sce_migrations` tracking parity. - `auth DB adapter`: Module in `cli/src/services/auth_db/mod.rs` that defines `AuthDbSpec` and exposes `AuthDb` as an `EncryptedTursoDb` alias. It resolves the canonical `/sce/auth.db` path with `auth_db_path()`, keeps encryption mandatory with `SCE_AUTH_DB_ENCRYPTION_KEY` env-secret precedence before OS keyring fallback and no plaintext mode, and embeds ordered auth migrations where baseline SQL creates `auth_credentials` without `user_id`, with `updated_at`, and a trigger that auto-refreshes `updated_at` on row updates. Auth runtime token-storage is now wired through `cli/src/services/token_storage.rs`, which persists tokens via the `auth_credentials` table in the encrypted auth DB instead of a JSON file. @@ -247,4 +248,3 @@ - `parts table (Agent Trace DB)`: Agent Trace DB table created by migration `009_create_parts.sql`; stores append-only message parts with columns `type` (typed by Rust as `text`/`reasoning`/`patch`/`question` and stored as unconstrained `TEXT NOT NULL`), `text`, `message_id`, `session_id`, `generated_at_unix_ms`, `created_at`, `updated_at`. Uses only the internal `id` for row identity (no upsert/dedup). Multiple parts can exist for the same `(session_id, message_id)`. A compound index on `(session_id, message_id, generated_at_unix_ms, id)` enables ordered joins. No foreign keys to `messages` or any other table, so parts may be inserted before their parent message exists. - `AgentTraceExportReader`: Read-only incremental export reader in `cli/src/services/agent_trace_export/mod.rs` over one `RepositoryAgentTraceDb`, exposing `read_messages_after`/`read_parts_after`/`read_diff_traces_after`/`read_agent_traces_after`, each `(cursor: i64, limit: usize) -> Result>` over `WHERE id > cursor ORDER BY id ASC LIMIT {limit}`. Holds no local cursor, performs no mutation, makes no network calls, and returns owned camelCase `serde::Serialize` export-row DTOs matching the shipped control-plane ingestion contract. See `context/sce/agent-trace-export-readers.md`. - `context synchronization lifecycle`: Durable task-level state for synchronization after successful `/next-task` execution. The task record is `pending`, `synced`, or `blocked`; blocked records carry a blocker, required action, and retry condition. Missing lifecycle state on a completed task is unresolved debt, not evidence of synchronization. `/validate` does not persist a plan-level synchronization lifecycle. See `context/sce/shared-context-code-workflow.md`. -- `agent_trace.auto_sync`: Config-file-only boolean resolved by the shared config layers for the post-commit Agent Trace synchronization boundary; omitted values are `true` and explicit `false` opts out, and `sce config show` reports its winning source. After validation and repository-DB persistence, enabled post-commit runs launch the existing sync-owned `sync --format json` command once through the current executable with detached null-standard-stream child semantics and repository-root working directory; the hook never waits, has no daemon or high-frequency trigger, and treats launcher failures as fail-open. See [Agent Trace hook routing](sce/agent-trace-hooks-command-routing.md). diff --git a/context/overview.md b/context/overview.md index 637ad8b8..aec03c10 100644 --- a/context/overview.md +++ b/context/overview.md @@ -10,7 +10,7 @@ The generated `/next-task` workflow persists task-level context-synchronization - **Exit codes:** `2` parse, `3` validation, `4` runtime, `5` dependency failure (see `context/sce/cli-exit-code-contract.md`). - **Stderr diagnostics:** stable `SCE-ERR-{PARSE,VALIDATION,RUNTIME,DEPENDENCY}` codes with class-default `Try:` remediation (see `context/sce/cli-error-code-taxonomy.md`). -- **Stdout/stderr:** command payloads on stdout only; redacted diagnostics and text-mode `sce sync` progress on stderr, while JSON sync remains silent (see `context/sce/cli-stdout-stderr-contract.md`). +- **Stdout/stderr:** command payloads on stdout only; redacted diagnostics and text-mode `sce sync` progress on stderr, while automatic JSON sync keeps stdout silent and inherits stderr for typed failures (see `context/sce/cli-stdout-stderr-contract.md`). - **Observability:** config-resolved logging with tracing, explicit config-file/default `log_to_file` control, error-specific stderr suppression when file logging is enabled, and optional dated/session-partitioned `log_dir` / `SCE_LOG_DIR` files with retention (see `context/sce/cli-observability-contract.md`). - **Config precedence:** `flags > env > config file > defaults` (see `context/cli/config-precedence-contract.md`); the config-file-only `agent_trace.auto_sync` setting defaults to `true` and is resolved with source metadata for the post-commit trigger and doctor readiness boundaries. Its asynchronous post-commit behavior and doctor capability reporting are documented in `context/cli/agent-trace-auto-sync.md`. - **Attribution hooks:** enabled by default, gated by staged-diff AI-overlap preflight; `SCE_ATTRIBUTION_HOOKS_DISABLED` opt-out (see `context/sce/agent-trace-commit-msg-coauthor-policy.md`). @@ -20,7 +20,7 @@ The CLI crate currently depends on `anyhow`, `chrono`, `clap`, `clap_complete`, Its command loop is implemented with `clap` derive-based argument parsing and `anyhow` error handling. Top-level help displays an ASCII art "SCE" banner with a per-column right-to-left color gradient (cyan to magenta when color is enabled, plain ASCII when disabled) above a slim command list without implemented/placeholder labels; `auth` is visible while `hooks` and `policy` remain directly invocable but hidden. The real top-level command catalog/help-visibility contract is centralized in `cli/src/cli_schema.rs` and consumed by `cli/src/command_surface.rs` for custom banner/help rendering plus known-command classification. The runtime includes implemented auth flows (`auth login|logout|whoami`), with authenticated whoami reading the Control Plane `GET /me` profile and rendering flat email/name/role/permissions/organization labels, optional names and missing values handled deterministically, and exact login guidance returned when logged out, alongside config inspection/validation, setup orchestration, doctor diagnosis/repair, attribution-only hooks, shell completion, and the top-level `sync` command. Parse-time command conversion plus run-time command handling flow through the internal `RuntimeCommand` seam in `cli/src/app.rs`. The current doctor presentation contract supersedes the earlier output-shape scaffolding wording above: human text uses the compact Environment/Repository/Integrations hierarchy with healthy rows collapsed and unhealthy branches expanded, while JSON retains complete path, identity, problem, and fix-result detail. See `context/sce/doctor-human-text-contract.md`. The command loop now enforces a stable exit-code contract in `cli/src/app.rs`: `2` parse failures, `3` invocation validation failures, `4` runtime failures, and `5` dependency startup failures. -The same runtime also emits stable user-facing stderr error classes (`SCE-ERR-PARSE`, `SCE-ERR-VALIDATION`, `SCE-ERR-RUNTIME`, `SCE-ERR-DEPENDENCY`) using deterministic `Error []: ...` diagnostics with class-default `Try:` remediation appended when missing. The command boundary's former flat, string-only `ClassifiedError` has been replaced by typed `CliError` in `cli/src/services/error.rs`: `CliError::User` carries a closed `UserError` catalog (currently only `NotAuthenticated`) for expected, deliberately-explained failures rendered as a friendly sentence with no `Try:` suffix, while `CliError::Internal` carries a live `anyhow::Error` source rendered as the real error chain with class-default remediation; `app_support` is the sole owner of turning either into the final styled stderr diagnostic, and `sce sync` is the first command to classify a failure (authentication) into `CliError::User`. See `context/sce/cli-error-code-taxonomy.md` for the full contract. +The same runtime also emits stable user-facing stderr error classes (`SCE-ERR-PARSE`, `SCE-ERR-VALIDATION`, `SCE-ERR-RUNTIME`, `SCE-ERR-DEPENDENCY`) using deterministic `Error []: ...` diagnostics with class-default `Try:` remediation appended when missing. The command boundary's former flat, string-only `ClassifiedError` has been replaced by typed `CliError` in `cli/src/services/error.rs`: `CliError::User` carries a closed `UserError` catalog for expected, deliberately-explained failures rendered as reviewed messages with no `Try:` suffix, while `CliError::Internal` carries a live `anyhow::Error` source rendered as the real error chain with class-default remediation; `app_support` is the sole owner of turning either into the final styled stderr diagnostic. Sync keeps manual authentication semantics and also has an internal automatic invocation context whose `AutomaticSyncFailed` payload distinguishes authentication, control-plane, stream, and local runtime failures while preserving the technical source for observability. Automatic messages use the `Automatic synchronization failed:` prefix; authentication gives login-plus-manual-sync guidance, while other kinds include their reason and manual `sce sync` recovery. See `context/sce/cli-error-code-taxonomy.md` for the full contract. The app runtime now also includes a structured observability baseline in `cli/src/services/observability.rs`: deterministic env-controlled log threshold/format (`SCE_LOG_LEVEL` defaults to `error`; `SCE_LOG_FORMAT` defaults to `text`), default-backed log-directory routing (`SCE_LOG_DIR` / config-file `log_dir` / `/sce/logs`) with per-operation machine-local dated file selection, optional session filename partitioning, and creation-triggered retention of direct regular `*.log` files to 10 entries, stable lifecycle event IDs, error-specific stderr suppression while preserving stderr for non-error records and file-write diagnostics so stdout command payloads remain pipe-safe, and `observability::traits` boundaries for logger and telemetry behavior. The app command dispatcher now enforces a centralized stdout/stderr stream contract in `cli/src/app.rs`: command success payloads are emitted on stdout only, while redacted user-facing diagnostics and text-mode sync progress are emitted on stderr; JSON sync remains silent. `cli/src/app.rs` also now runs through explicit startup phases — dependency check, observability config resolution, runtime initialization, command parse/execute, and output rendering — with the app runtime carrying logger/telemetry plus static command-catalog state across those phases while preserving the existing exit-code and degraded-startup contracts. Within that lifecycle, `parse_command_phase` delegates clap-to-runtime conversion to `cli/src/services/parse/command_runtime.rs`, which returns a static `RuntimeCommand` enum, `services::app_support::execute_command_phase` logs around enum-owned `execute_with_stderr(...)` dispatch, and generic `RunOutcome` rendering logs classified errors through the logger trait boundary without coupling render support to the production logger type. Command payload structs for `help`, `version`, `completion`, `auth`, `config`, `setup`, `doctor`, `hooks`, and `sync` live in service-owned `command.rs` files; `cli/src/services/command_registry.rs` owns the deterministic static command-name catalog and enum variants instead of boxed command trait objects. The CLI now also enforces a shared output-format parser contract in `cli/src/services/output_format.rs`, with canonical `--format ` parsing and command-specific actionable invalid-value guidance reused by `config` and `version` services. A compile-safe service lifecycle seam also exists in `cli/src/services/lifecycle.rs`: `ServiceLifecycle` exposes default no-op `diagnose`, `fix`, and `setup` methods against the narrow `HasRepoRoot` accessor, uses lifecycle-owned health/fix/setup result types, and owns the shared static `LifecycleProvider` enum catalog/factory with deterministic config → local*db → auth_db → agent_trace_db → hooks ordering and no boxed provider aggregation. Hooks has a `services/hooks/lifecycle.rs` provider for hook rollout diagnosis/fix/setup, config has a `services/config/lifecycle.rs` provider for global/repo-local config validation plus repo-local config bootstrap, local_db has a `services/local_db/lifecycle.rs` provider for canonical local DB path health, parent-directory readiness/bootstrap, and `LocalDb::new()` setup, auth_db has a `services/auth_db/lifecycle.rs` provider for canonical auth DB path health, parent-directory readiness/bootstrap, and `AuthDb::new()` setup, and agent_trace_db has a `services/agent_trace_db/lifecycle.rs` provider for repository-scoped Agent Trace DB setup and repository DB path health/parent readiness from resolved repository identity, returning an actionable "requires a Git repository" diagnostic outside repository context (no global/checkout fallback path). Doctor runtime aggregates the full shared provider catalog for `diagnose` and `fix` and adapts lifecycle records into doctor-owned output records; setup command aggregates the shared provider catalog for `setup` with hooks included only when requested and adapts lifecycle setup outcomes before rendering setup-owned messages. diff --git a/context/patterns.md b/context/patterns.md index 72ccff40..f37f35eb 100644 --- a/context/patterns.md +++ b/context/patterns.md @@ -128,7 +128,7 @@ - Keep `log_file_retention_limit` flat and config-file/default only: validate it as an integer with minimum `1`, merge global before local, default it to `10`, expose resolved source metadata without adding an environment variable or CLI flag, and pass the resolved value unchanged to primary and v2 creation-triggered logger cleanup. - For runtime CLI configuration, keep precedence deterministic and explicit (`flags > env > config file > defaults`) and expose inspect/validate command entrypoints with stable text/JSON outputs. Config-file-only Agent Trace runtime switches such as `agent_trace.auto_sync` should document their default and explicit opt-out, resolve global before local, and expose winning source metadata without adding an environment variable or CLI flag unless the contract explicitly requires one. -- For default-enabled automatic Agent Trace synchronization, keep the post-commit launcher as a one-shot reuse of `sce sync`: start the current executable after local Agent Trace persistence, use the repository root and null child streams, do not wait, and fail open on launcher errors. Keep manual sync and the control-plane cursor authority as the retry path; do not add daemons, watchers, polling, queues, or high-frequency hook triggers. +- For default-enabled automatic Agent Trace synchronization, keep the post-commit launcher as a one-shot reuse of `sce sync`: start the current executable after local Agent Trace persistence, use the repository root, null stdin/stdout, inherited stderr, and an internal automatic-invocation marker, do not wait, and fail open on launcher errors. Render child and launcher failures through one typed `SCE-ERR-RUNTIME` diagnostic beginning `Automatic synchronization failed:`; authentication must direct the user to `sce auth login` and then manual `sce sync`, while other kinds include their preserved reason and actionable manual retry guidance without duplicating generic runtime `Try:` text. Keep manual sync and the control-plane cursor authority as the retry path; do not add daemons, watchers, polling, queues, or high-frequency hook triggers. - For commands that support text/JSON dual output, centralize `--format ` parsing in one shared contract and pass command-specific `--help` guidance into invalid-value errors instead of duplicating parser logic per command. - For setup-style command contracts, keep interactive mode as the zero-flag default and enforce mutually-exclusive explicit target flags for non-interactive automation. - For durable-context bootstrap, keep create-if-missing additive semantics: ensure baseline paths on every successful setup path, offer a dedicated standalone `--bootstrap-context` mode, and never overwrite existing context content. diff --git a/context/plans/auto-sync-failure-guidance.md b/context/plans/auto-sync-failure-guidance.md new file mode 100644 index 00000000..b62922c4 --- /dev/null +++ b/context/plans/auto-sync-failure-guidance.md @@ -0,0 +1,196 @@ +# Plan: auto-sync-failure-guidance + +## Change summary + +Improve the existing post-commit automatic Agent Trace synchronization path so +that a failed detached sync produces a typed, user-facing diagnostic instead of +an opaque or invisible failure. Following the payload-bearing `UserError` +pattern used by the setup Git preflight (`NotGitRepository`/ +`NotGitRemote`), the automatic-sync error will carry its typed failure kind and +underlying reason while rendering reviewed recovery guidance; authentication +failures will explicitly direct the user to log in and then manually run +`sce sync`. + +The existing one-shot architecture remains intact: automatic sync still reuses +the `sce sync` command, does not delay the commit, and fails open. The detached +child will identify itself as an automatic invocation and expose only its +failure diagnostics through the existing stderr contract; it will not introduce +local retry state, a daemon, or a second synchronization implementation. + +## Acceptance criteria + +How this plan is proven complete. Each criterion is observable and names the +check that proves it. `/validate` runs these checks; no task in the stack +performs final validation. + +- [x] AC1: A sync failure raised by an automatic invocation maps to a payload-bearing typed `UserError`/`CliError` path and renders one runtime diagnostic that clearly says automatic synchronization failed and includes the underlying typed failure reason. + - Validate: Focused sync/error tests assert the rendered diagnostic for control-plane, stream, and local runtime failures, including the reason and `SCE-ERR-RUNTIME` classification. +- [x] AC2: An automatic authentication failure uses a distinct typed automatic-sync failure kind, tells the user that authentication is required, instructs them to run `sce auth login`, and then explicitly tells them to manually retry with `sce sync`. + - Validate: Focused authentication classification and app-rendering tests assert the complete login-plus-manual-sync guidance and ensure the technical source remains available only for observability. +- [x] AC3: Non-authentication automatic failures use the same typed payload-bearing error model, provide actionable recovery guidance, and explain that the user can manually retry with `sce sync`, without relying on substring matching or duplicating the default runtime `Try:` guidance. + - Validate: Focused tests cover representative storage, transport/server, protocol, and stream failures and assert deterministic reason/recovery text with no duplicate remediation. +- [x] AC4: Automatic child failures are visible through the existing stderr diagnostic channel while successful post-commit execution remains detached, non-blocking, JSON-stdout-silent, and fail-open to the commit; launcher startup failures retain their reason in structured auto-sync diagnostics without failing the hook. + - Validate: Launcher and post-commit seam tests assert the internal automatic-invocation marker, inherited failure stderr, unchanged `sync --format json` arguments, no wait, and fail-open behavior for executable/spawn failures. +- [x] AC5: Manual `sce sync` failures retain the existing manual-sync semantics and do not claim that automatic synchronization failed. + - Validate: Command-level tests execute/classify manual and automatic invocation modes separately and assert mode-specific rendering. +- [x] AC6: Durable context documents the typed automatic-sync failure model, stderr visibility, authentication recovery, manual retry command, and preserved no-daemon/fail-open boundaries. + - Validate: Review the listed context contracts against the final code, then run the generated-context and repository checks under `Full validation`. + +### Full validation + +Repository-wide checks `/validate` runs after the last task, regardless of +which criterion they map to. + +- `nix flake check` +- `nix run .#pkl-check-generated` + +### Context sync + +- `context/overview.md` +- `context/architecture.md` +- `context/glossary.md` +- `context/context-map.md` +- `context/patterns.md` +- `context/cli/agent-trace-auto-sync.md` +- `context/cli/sync-command.md` +- `context/cli/agent-trace-sync-command.md` +- `context/sce/cli-error-code-taxonomy.md` +- `context/sce/cli-stdout-stderr-contract.md` +- `context/sce/agent-trace-hooks-command-routing.md` + +## 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:** typed automatic-sync failure/recovery modeling; automatic-versus-manual sync invocation context; sync command/app error rendering; detached launcher stderr and startup-failure reporting; post-commit fail-open integration; focused Rust tests; the durable context files listed under Context sync. +- **Out of scope:** changes to the control-plane protocol, cursor reconciliation, Agent Trace schema/storage, manual sync success output, authentication flow implementation, generated target trees, or unrelated hook failure behavior. +- **Constraints:** preserve the exact child arguments `sync --format json`, current-executable resolution, repository-root working directory, no wait, commit fail-open semantics, stdout/stderr separation, typed authentication classification, and shared sensitive-text redaction; add no dependency or persistent retry state. +- **Non-goal:** making Git wait for network synchronization or adding a daemon, watcher, scheduler, queue, status file, or local retry cursor. + +## Assumptions + +- Automatic sync remains a detached child and reports completion failures through its inherited stderr rather than waiting for the child or persisting a new failure record; this preserves the existing one-shot/fail-open contract while making the diagnostic observable. +- The automatic invocation marker is an internal process-boundary detail, not a new user configuration key or public CLI option; manual `sce sync` remains mode-neutral and keeps its existing error wording. +- The typed failure model will preserve the technical source for structured logging while rendering a reviewed, deterministic recovery sentence at the app boundary, following `CliError` and `UserError` ownership patterns. + +## Task stack + +- [x] T01: `Add payload-bearing typed automatic-sync user errors` (status:complete) + - Task ID: T01 + - Scope: In — `cli/src/services/error.rs`, sync invocation context/classification, app-level rendering support, and focused tests for automatic authentication, runtime, stream, and control-plane failures. Model the new error after the payload-bearing `UserError` entries added by setup remote preflight: keep a closed catalog, use a typed failure-kind payload, retain the underlying reason, and preserve the technical source through `CliError::user_with_source`. Out — child process stdio changes, hook wiring, and durable context edits. + - Dependencies: none + - Done when: automatic failures have a payload-bearing typed user-error representation that distinguishes authentication from other sync failures, includes the underlying reason without adding an arbitrary-message escape hatch, renders deterministic automatic-sync wording plus actionable manual retry guidance, preserves the technical source for logging, and leaves manual sync classification/rendering unchanged. + - Verify: `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml error::`; `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml sync::command`. + - Completed: 2026-08-27 + - Files changed: + - `cli/src/services/app_support.rs` + - `cli/src/services/command_registry.rs` + - `cli/src/services/error.rs` + - `cli/src/services/parse/command_runtime.rs` + - `cli/src/services/sync/command.rs` + - `cli/src/services/sync/mod.rs` + - Result: Added typed automatic-sync failure kinds and payload-bearing user errors with preserved technical sources, deterministic authentication and recovery guidance, and an explicit manual-versus-automatic sync invocation context. Manual sync classification remains unchanged. + - Verify: + - `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml error::` — passed (6 tests). + - `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml sync::command` — passed (8 tests). + - Context impact: root — changed the typed CLI error contract, sync command invocation classification, and app-level diagnostic rendering; durable context synchronization is required before another task starts. + - Context synchronization: synced + +- [x] T02: `Surface detached automatic-sync failures without blocking post-commit` (status:complete) + - Task ID: T02 + - Scope: In — `cli/src/services/sync/auto_sync.rs`, post-commit launcher seam in `cli/src/services/hooks/mod.rs`, internal child invocation marker/stdio configuration, structured launcher-failure reporting using the same typed automatic-sync error payload, and focused launcher/hook tests. Out — sync protocol behavior, waiting for child completion, retry queues, and high-frequency hook triggers. + - Dependencies: T01 + - Done when: the detached child keeps the exact `sync --format json` command and repository-root/no-wait behavior, identifies automatic mode, exposes only typed failure diagnostics through stderr, and launcher executable/spawn errors retain actionable reasons through structured auto-sync reporting while remaining fail-open to the successful hook result. + - Verify: `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml auto_sync`; `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml hooks::`. + - Completed: 2026-08-27 + - Files changed: + - `cli/src/services/app_support.rs` + - `cli/src/services/error.rs` + - `cli/src/services/parse/command_runtime.rs` + - `cli/src/services/sync/auto_sync.rs` + - `cli/src/services/sync/command.rs` + - `cli/src/services/sync/mod.rs` + - Result: Preserved detached `sync --format json` execution while passing an internal automatic-invocation marker, inheriting child stderr for typed failure diagnostics, and rendering structured fail-open launcher errors with actionable reasons. + - Verify: + - `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml auto_sync` — passed (14 tests). + - `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml hooks::` — passed (163 tests). + - Context impact: root — changed the automatic sync process boundary, stderr visibility, invocation classification, and fail-open launcher diagnostic contract; durable context synchronization is required before another task starts. + - Context synchronization: synced + +- [x] T03: `Document typed automatic-sync failure recovery contract` (status:complete) + - Task ID: T03 + - Scope: Update the auto-sync, sync, CLI error, stdout/stderr, hook-routing, and required root context contracts listed under Context sync to describe the final typed error and recovery behavior. Out — generated configuration artifacts, historical plans/decisions, and code/test changes. + - Dependencies: T02 + - Done when: durable context states the payload-bearing typed error model, automatic-failure prefix, reason preservation, authentication login flow, manual `sce sync` retry, stderr visibility, mode distinction, and unchanged detached/fail-open/no-daemon boundaries without stale null-output claims. + - Verify: Manual code/context review against `cli/src/services/error.rs`, `cli/src/services/app_support.rs`, `cli/src/services/sync/command.rs`, `cli/src/services/sync/auto_sync.rs`, and `cli/src/services/hooks/mod.rs`. + - Completed: 2026-08-27 + - Files changed: + - `context/architecture.md` + - `context/cli/agent-trace-auto-sync.md` + - `context/cli/agent-trace-sync-command.md` + - `context/cli/sync-command.md` + - `context/context-map.md` + - `context/glossary.md` + - `context/overview.md` + - `context/patterns.md` + - `context/sce/agent-trace-hooks-command-routing.md` + - `context/sce/cli-error-code-taxonomy.md` + - `context/sce/cli-stdout-stderr-contract.md` + - Result: Updated durable root and domain context to describe the closed typed automatic-sync failure catalog, the stable automatic-failure diagnostic prefix, authentication login-plus-manual-sync recovery, preserved non-authentication reasons, stderr visibility, manual-mode distinction, and unchanged detached/fail-open/no-daemon boundaries. + - Verify: + - `Manual code/context review against cli/src/services/error.rs, cli/src/services/app_support.rs, cli/src/services/sync/command.rs, cli/src/services/sync/auto_sync.rs, and cli/src/services/hooks/mod.rs` — passed. + - `git diff --check` — passed. + - Context impact: root — clarified the durable CLI error, stream, synchronization, hook-routing, and recovery contracts to match the implemented automatic-sync behavior. + - Context synchronization: synced + +## Open questions + +None. The existing detached/fail-open contract determines that reporting must +travel through the child diagnostic stream rather than a completion wait or new +persistent retry mechanism, and the setup preflight change establishes the +payload-bearing `UserError` pattern to reuse; the remaining wording and +internal marker choices are local implementation details covered by the existing +CLI error/rendering patterns. + +## Validation Report + +**Status:** validated +**Date:** 2026-08-27 + +### Commands run + +- `nix flake check` -> exit 0 (all flake checks passed) +- `nix run .#pkl-check-generated` -> exit 0 (ephemeral Pkl generation passed: 141 files) +- `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml error::` -> terminated by the 120-second tool timeout while concurrent Cargo invocations waited on locks (no exit code reported; rerun completed successfully) +- `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml error::` -> exit 0 (6 focused error tests passed) +- `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml sync::command` -> exit 0 (6 focused sync classification tests passed) +- `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml app_support` -> exit 0 (5 focused app rendering tests passed) +- `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml parse::command_runtime` -> exit 0 (5 focused command-runtime tests passed) +- `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml auto_sync` -> exit 0 (13 focused auto-sync and hook tests passed) +- `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml hooks::` -> exit 0 (163 focused hook tests passed) +- `git diff --check` -> exit 0 (no whitespace errors) + +### Success-criteria verification + +- [x] AC1: A sync failure raised by an automatic invocation maps to a payload-bearing typed `UserError`/`CliError` path and renders one runtime diagnostic that clearly says automatic synchronization failed and includes the underlying typed failure reason. -> Error, app-rendering, and sync classification tests passed; typed runtime code and reason-preserving paths were inspected. +- [x] AC2: An automatic authentication failure uses a distinct typed automatic-sync failure kind, tells the user that authentication is required, instructs them to run `sce auth login`, and then explicitly tells them to manually retry with `sce sync`. -> Sync classification and app-rendering tests passed; authentication guidance and observability-only source handling were inspected. +- [x] AC3: Non-authentication automatic failures use the same typed payload-bearing error model, provide actionable recovery guidance, and explain that the user can manually retry with `sce sync`, without relying on substring matching or duplicating the default runtime `Try:` guidance. -> Sync, error, and app-rendering tests passed; representative typed control-plane, stream, runtime, storage, transport, server, and protocol paths were inspected. +- [x] AC4: Automatic child failures are visible through the existing stderr diagnostic channel while successful post-commit execution remains detached, non-blocking, JSON-stdout-silent, and fail-open to the commit; launcher startup failures retain their reason in structured auto-sync diagnostics without failing the hook. -> Auto-sync and hook seam tests passed; launcher arguments, marker, inherited stderr, null stdout, no-wait, and fail-open behavior were inspected. +- [x] AC5: Manual `sce sync` failures retain the existing manual-sync semantics and do not claim that automatic synchronization failed. -> Manual and automatic classification branches were inspected and focused sync tests passed. +- [x] AC6: Durable context documents the typed automatic-sync failure model, stderr visibility, authentication recovery, manual retry command, and preserved no-daemon/fail-open boundaries. -> Listed context contracts were reviewed against the final code; generated-context and repository checks passed. + +### Failed checks and follow-ups + +- None. + +### Residual risks + +- None identified. diff --git a/context/sce/agent-trace-hooks-command-routing.md b/context/sce/agent-trace-hooks-command-routing.md index 8fe4d536..74758854 100644 --- a/context/sce/agent-trace-hooks-command-routing.md +++ b/context/sce/agent-trace-hooks-command-routing.md @@ -63,7 +63,7 @@ - Post-commit Agent Trace success requires both schema validation and Agent Trace DB `agent_traces` persistence to succeed. - Current command-surface success output is: `post-commit hook processed intersection: commit=, intersection_files=`. - After Agent Trace validation and `agent_traces` persistence succeed, post-commit runs exactly one passive WAL checkpoint through `RepositoryAgentTraceDb::passive_checkpoint()` (see [shared-turso-db.md](shared-turso-db.md)) before resolving auto-sync. This is routine maintenance, not a durability boundary: a checkpoint failure is logged as a warning via `Logger::warn` with event `sce.agent_trace_db.passive_checkpoint_failed` and does not fail the hook or affect already-persisted Agent Trace data. `diff-trace` and `conversation-trace` do not checkpoint per write; only this one post-commit call site does. -- After Agent Trace validation and `agent_traces` persistence succeed, post-commit resolves the config-file-only `agent_trace.auto_sync` gate. When it is `true`, the hook invokes the sync-owned one-shot launcher exactly once with the repository root; the launcher starts the current `sce` executable as detached `sync --format json` work and is not awaited. Explicit `false` configuration does not launch; omitted configuration launches, and validation or persistence failure reaches the existing error path before the gate. Launcher/current-executable/spawn failures are fail-open and do not change the successful post-commit result. No `pre-commit`, `diff-trace`, or `conversation-trace` path invokes automatic synchronization. + - After Agent Trace validation and `agent_traces` persistence succeed, post-commit resolves the config-file-only `agent_trace.auto_sync` gate. When it is `true`, the hook invokes the sync-owned one-shot launcher exactly once with the repository root; the launcher starts the current `sce` executable as detached `sync --format json` work, passes the internal `SCE_INTERNAL_AUTO_SYNC=1` marker, inherits stderr, and is not awaited. The child keeps stdin/stdout null, so successful JSON execution is silent, while automatic failures render one `Error [SCE-ERR-RUNTIME]` diagnostic beginning `Automatic synchronization failed:` through inherited stderr. Authentication tells the user to run `sce auth login` and then manually retry with `sce sync`; other typed failure kinds include their preserved reason and actionable manual retry guidance without a duplicate generic `Try:` sentence. Explicit `false` configuration does not launch; omitted configuration launches, and validation or persistence failure reaches the existing error path before the gate. Launcher/current-executable/spawn failures use the typed automatic-sync runtime payload with their reasons on stderr but remain fail-open and do not change the successful post-commit result. No `pre-commit`, `diff-trace`, or `conversation-trace` path invokes automatic synchronization. - `post-rewrite` is a deterministic no-op entrypoint. - `diff-trace` reads STDIN JSON and classifies the payload: - **Claude structured payloads** (detected by presence of top-level `hook_event_name`): the STDIN JSON is validated through `derive_claude_structured_patch`. Supported `PostToolUse` `Write` create and `Edit` structured-patch events produce a `DiffTracePayload` with `payload_type="structured"` and the raw event JSON stored as the `diff` column without conversion to unified-diff text. Model attribution is resolved event-locally and direct-first: top-level `model`, `model_id`, or `modelId`, or nested `model.id`, `model.model`, or `model.name`, wins when present. Otherwise, when the event provides both `transcript_path` and `tool_use_id`, Rust scans that Claude JSONL transcript for the assistant-message envelope whose `tool_use.id` matches, skipping malformed unrelated records. Either source is normalized once with the `claude/` prefix. Missing/unreadable transcripts, unmatched tool calls, missing models, or absent lookup fields leave `model_id` nullable without rejecting the hook, and downstream Agent Trace JSON omits contributor `model_id`. No session-level cache or lookup participates. Unsupported Claude events (non-`PostToolUse`, unsupported tools, invalid payloads) produce a deterministic `NoOp` success result. diff --git a/context/sce/cli-error-code-taxonomy.md b/context/sce/cli-error-code-taxonomy.md index b43c0da4..6586a926 100644 --- a/context/sce/cli-error-code-taxonomy.md +++ b/context/sce/cli-error-code-taxonomy.md @@ -31,8 +31,11 @@ It complements the numeric process exit-code classes documented in `context/sce/ ## Ownership - `FailureClass` in `cli/src/services/error.rs` owns class selection and stable code assignment (`FailureClass::code()`). -- `CliError::{User,Internal}` in `cli/src/services/error.rs` is the typed CLI-boundary error type; `CliError::code()`/`CliError::class()` delegate to the failure class. `CliError::User` carries a catalog `UserError` (currently only `NotAuthenticated`) for expected, deliberately-explained failures; `CliError::Internal` carries a live `anyhow::Error` source for every other failure. `CliError::User` may also carry an optional preserved technical `source`, kept for observability only and never rendered to the terminal. -- `UserError` in `cli/src/services/error.rs` is the closed catalog of deliberately presented terminal failures. It has no arbitrary-message variant (no `Message(String)`/`Custom(...)` escape hatch): every entry is a fixed, reviewed sentence returned by `UserError::message()`, keyed for structured logging by `UserError::key()`. +- `CliError::{User,Internal}` in `cli/src/services/error.rs` is the typed CLI-boundary error type; `CliError::code()`/`CliError::class()` delegate to the failure class. `CliError::User` carries a closed catalog `UserError` for expected, deliberately-explained failures; `CliError::Internal` carries a live `anyhow::Error` source for every other failure. `CliError::User` may also carry an optional preserved technical `source`, kept for observability only and never rendered to the terminal. +- `UserError` in `cli/src/services/error.rs` is the closed catalog of deliberately presented terminal failures. It has no arbitrary-message variant (no `Message(String)`/`Custom(...)` escape hatch): `AutomaticSyncFailed` carries only the typed `AutomaticSyncFailureKind` and underlying display reason needed by its reviewed templates, and every entry is keyed for structured logging by `UserError::key()`. +- `AutomaticSyncFailureKind` distinguishes authentication, control-plane, stream, and local runtime automatic-sync failures. Automatic authentication renders login-plus-manual-sync guidance; the other kinds render their preserved reason with actionable manual `sce sync` recovery guidance. The technical `anyhow` source remains available to observability and is not rendered as a second diagnostic. +- Automatic `UserError` messages begin with the stable `Automatic synchronization failed:` prefix. Authentication deliberately keeps its technical reason out of the user-facing sentence while preserving it in the typed payload/source for observability; the other failure kinds include the payload reason in that single rendered sentence. +- The post-commit launcher reports executable-resolution and spawn failures as the local runtime kind through the same payload-bearing `UserError::AutomaticSyncFailed` path. Those startup diagnostics retain the launcher reason, use `SCE-ERR-RUNTIME`, and remain fail-open to the successful hook result. - Command and domain layers construct and return a `CliError`; they do not format terminal text, apply styling, or decide authentication/user-error semantics from string matching. `app_support` is the sole owner of turning a `CliError` into the final stderr sentence. - `Logger::log_cli_error` in `cli/src/services/observability.rs` owns structured error logging with `sce.error.{code}` event IDs. - `write_error_diagnostic` in `cli/src/services/app_support.rs` owns final code-bearing stderr rendering, including styling `CliError::User`'s catalog message and `CliError::Internal`'s rendered chain through `services::style::error_text_with_color_policy` under the stderr TTY/`NO_COLOR` policy (`services::style::supports_color_stderr()`), independent of stdout's TTY state. diff --git a/context/sce/cli-stdout-stderr-contract.md b/context/sce/cli-stdout-stderr-contract.md index cb82148c..104dc398 100644 --- a/context/sce/cli-stdout-stderr-contract.md +++ b/context/sce/cli-stdout-stderr-contract.md @@ -9,8 +9,10 @@ This document defines the implemented stream contract for CLI command payload an - Command success payloads are emitted to `stdout` only through app-level stream handling. - User-facing diagnostics and failures are emitted to `stderr` only. - Failure diagnostics are emitted as `Error []: ...` on `stderr`, where `` is the stable class-based `SCE-ERR-*` identifier from `CliError` in `cli/src/services/error.rs`; diagnostics are passed through shared redaction (`services::security::redact_sensitive_text`) before emission. -- The diagnostic body differs by `CliError` variant: `CliError::Internal` renders the real `anyhow` source chain (`format!("{source:#}")`) plus class-default `Try:` remediation; `CliError::User` renders its catalog `UserError` message verbatim, with no low-level technical text and no `Try:` suffix. Both bodies are styled through the same stderr TTY/`NO_COLOR` policy before redaction and emission. +- The diagnostic body differs by `CliError` variant: `CliError::Internal` renders the real `anyhow` source chain (`format!("{source:#}")`) plus class-default `Try:` remediation; `CliError::User` renders its catalog `UserError` template, with non-authentication automatic-sync payload reasons included only in that reviewed message and no technical source chain or `Try:` suffix. Both bodies are styled through the same stderr TTY/`NO_COLOR` policy before redaction and emission. - Command handlers now return payload strings to the app dispatcher; the app owns stream selection and final emission. +- The detached post-commit `sce sync --format json` child keeps stdout null so a successful automatic run produces no hook payload, while inheriting stderr so its single app-rendered typed failure diagnostic remains observable. Launcher executable/spawn failures are rendered through the same stderr diagnostic writer in the parent and remain fail-open. +- Automatic child failures render exactly one `Error [SCE-ERR-RUNTIME]: Automatic synchronization failed: ...` diagnostic on inherited stderr. Authentication exposes login-plus-manual-sync recovery while its technical reason stays in observability; non-authentication reasons and recovery guidance are included in that one diagnostic, with no generic duplicate `Try:` suffix. ## Implementation surface