diff --git a/cli/src/services/app_support.rs b/cli/src/services/app_support.rs index cf9c20879..6e4dde796 100644 --- a/cli/src/services/app_support.rs +++ b/cli/src/services/app_support.rs @@ -143,7 +143,7 @@ where W: Write, { if let Some(log) = logger { - log.log_classified_error(error, None); + log.log_classified_error(error, None, error.user_facing_presentation().is_none()); } write_error_diagnostic(stderr, error); ExitCode::from(error.class().exit_code()) @@ -159,6 +159,12 @@ fn write_stdout_payload(writer: &mut W, payload: &str) -> Result<(), C } fn write_error_diagnostic(writer: &mut W, error: &ClassifiedError) { + if let Some(presentation) = error.user_facing_presentation() { + let message = services::security::redact_sensitive_text(presentation.message()); + writeln!(writer, "{message}").expect("writing error diagnostic to writer should not fail"); + return; + } + let rendered = if error.message().contains("Try:") { error.message().to_string() } else { diff --git a/cli/src/services/error.rs b/cli/src/services/error.rs index 0e8fb5694..71bfbac62 100644 --- a/cli/src/services/error.rs +++ b/cli/src/services/error.rs @@ -39,11 +39,42 @@ impl FailureClass { } } +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct UserFacingPresentation { + message: String, + reason: Option, +} + +impl UserFacingPresentation { + pub fn new(message: impl Into) -> Self { + Self { + message: message.into(), + reason: None, + } + } + + #[allow(dead_code)] + pub fn with_reason(mut self, reason: impl Into) -> Self { + self.reason = Some(reason.into()); + self + } + + pub fn message(&self) -> &str { + &self.message + } + + #[allow(dead_code)] + pub fn reason(&self) -> Option<&str> { + self.reason.as_deref() + } +} + #[derive(Debug)] pub struct ClassifiedError { class: FailureClass, code: &'static str, message: String, + user_facing_presentation: Option, } impl ClassifiedError { @@ -52,6 +83,7 @@ impl ClassifiedError { class: FailureClass::Parse, code: "SCE-ERR-PARSE", message: message.into(), + user_facing_presentation: None, } } @@ -60,6 +92,7 @@ impl ClassifiedError { class: FailureClass::Validation, code: "SCE-ERR-VALIDATION", message: message.into(), + user_facing_presentation: None, } } @@ -68,6 +101,7 @@ impl ClassifiedError { class: FailureClass::Runtime, code: "SCE-ERR-RUNTIME", message: message.into(), + user_facing_presentation: None, } } @@ -76,9 +110,21 @@ impl ClassifiedError { class: FailureClass::Dependency, code: "SCE-ERR-DEPENDENCY", message: message.into(), + user_facing_presentation: None, } } + #[allow(dead_code)] + pub fn with_user_facing_message(mut self, message: impl Into) -> Self { + self.user_facing_presentation = Some(UserFacingPresentation::new(message)); + self + } + + pub fn with_user_facing_presentation(mut self, presentation: UserFacingPresentation) -> Self { + self.user_facing_presentation = Some(presentation); + self + } + pub fn class(&self) -> FailureClass { self.class } @@ -90,6 +136,10 @@ impl ClassifiedError { pub fn message(&self) -> &str { &self.message } + + pub fn user_facing_presentation(&self) -> Option<&UserFacingPresentation> { + self.user_facing_presentation.as_ref() + } } impl std::fmt::Display for ClassifiedError { diff --git a/cli/src/services/observability.rs b/cli/src/services/observability.rs index 4e293bcad..ae5ebed00 100644 --- a/cli/src/services/observability.rs +++ b/cli/src/services/observability.rs @@ -122,7 +122,7 @@ impl Logger { fields: &[(&str, &str)], session_id: Option<&str>, ) { - self.log_forced(LogLevel::Warn, event_id, message, fields, session_id); + self.log_forced(LogLevel::Warn, event_id, message, fields, session_id, true); } #[cfg_attr(not(test), allow(dead_code))] @@ -136,9 +136,14 @@ impl Logger { self.log(LogLevel::Error, event_id, message, fields, session_id); } - pub fn log_classified_error(&self, error: &ClassifiedError, session_id: Option<&str>) { + pub(crate) fn log_classified_error( + &self, + error: &ClassifiedError, + session_id: Option<&str>, + emit_stderr: bool, + ) { let event_id = format!("sce.error.{}", error.code()); - self.log( + self.log_forced( LogLevel::Error, &event_id, error.message(), @@ -147,6 +152,7 @@ impl Logger { ("error_class", error.class().as_str()), ], session_id, + emit_stderr, ); } @@ -162,7 +168,7 @@ impl Logger { return; } - self.log_forced(level, event_id, message, fields, session_id); + self.log_forced(level, event_id, message, fields, session_id, true); } fn log_forced( @@ -172,19 +178,18 @@ impl Logger { message: &str, fields: &[(&str, &str)], session_id: Option<&str>, + emit_stderr: bool, ) { emit_tracing_event(level, event_id, message, fields); let line = self.render_line(level, event_id, message, fields); let redacted_line = redact_sensitive_text(&line); - emit_stderr_line(&redacted_line); - if let Err(error) = self.write_log_line(&redacted_line, session_id) { - let diagnostic = redact_sensitive_text(&format!( - "Failed to write SCE log file: {error}. Logging continues on stderr." - )); - emit_stderr_line(&diagnostic); + if emit_stderr { + emit_stderr_line(&redacted_line); } + + let _ = self.write_log_line(&redacted_line, session_id); } fn write_log_line(&self, redacted_line: &str, session_id: Option<&str>) -> Result<()> { @@ -388,12 +393,7 @@ where { if write_target == LogWriteTarget::Created { if let Some(parent) = path.parent() { - if let Err(error) = cleanup(parent) { - let diagnostic = redact_sensitive_text(&format!( - "Failed to clean up SCE log files: {error}. Logging continues on stderr." - )); - emit_stderr_line(&diagnostic); - } + let _ = cleanup(parent); } } } diff --git a/cli/src/services/observability/traits.rs b/cli/src/services/observability/traits.rs index d715f1f3a..d276c658c 100644 --- a/cli/src/services/observability/traits.rs +++ b/cli/src/services/observability/traits.rs @@ -33,7 +33,12 @@ pub trait Logger: Send + Sync { session_id: Option<&str>, ); - fn log_classified_error(&self, error: &ClassifiedError, session_id: Option<&str>); + fn log_classified_error( + &self, + error: &ClassifiedError, + session_id: Option<&str>, + emit_stderr: bool, + ); } pub trait Telemetry: Send + Sync { @@ -84,7 +89,13 @@ impl Logger for NoopLogger { ) { } - fn log_classified_error(&self, _error: &ClassifiedError, _session_id: Option<&str>) {} + fn log_classified_error( + &self, + _error: &ClassifiedError, + _session_id: Option<&str>, + _emit_stderr: bool, + ) { + } } impl Logger for super::Logger { @@ -128,8 +139,13 @@ impl Logger for super::Logger { super::Logger::error(self, event_id, message, fields, session_id); } - fn log_classified_error(&self, error: &ClassifiedError, session_id: Option<&str>) { - super::Logger::log_classified_error(self, error, session_id); + fn log_classified_error( + &self, + error: &ClassifiedError, + session_id: Option<&str>, + emit_stderr: bool, + ) { + super::Logger::log_classified_error(self, error, session_id, emit_stderr); } } diff --git a/cli/src/services/sync/command.rs b/cli/src/services/sync/command.rs index 3ff9845e6..1b5776a23 100644 --- a/cli/src/services/sync/command.rs +++ b/cli/src/services/sync/command.rs @@ -1,7 +1,8 @@ use std::io::Write; use crate::app::ContextWithRepoRoot; -use crate::services::error::ClassifiedError; +use crate::services::agent_trace_sync::control_plane::ControlPlaneError; +use crate::services::error::{ClassifiedError, UserFacingPresentation}; use crate::services::sync::progress::{ IndicatifProgressReporter, NoopProgressReporter, ProgressReporter, }; @@ -31,7 +32,22 @@ where #[allow(clippy::needless_pass_by_value)] fn classify_sync_error(err: TraceSyncError) -> ClassifiedError { - ClassifiedError::runtime(format!("{err}")) + let is_unauthenticated = matches!( + &err, + TraceSyncError::ControlPlane( + ControlPlaneError::MissingCredentials | ControlPlaneError::AuthenticationFailed(_) + ) + ); + let classified = ClassifiedError::runtime(format!("{err}")); + + if is_unauthenticated { + classified.with_user_facing_presentation(UserFacingPresentation::new(format!( + "You are not logged in. Please log in using the {} command.", + crate::services::style::success("sce auth login") + ))) + } else { + classified + } } impl SyncCommand { diff --git a/context/architecture.md b/context/architecture.md index 19b34c1a5..92e3dd0b5 100644 --- a/context/architecture.md +++ b/context/architecture.md @@ -102,12 +102,12 @@ 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. `cli/src/services/error.rs` owns the `FailureClass`/`ClassifiedError` model and its optional `UserFacingPresentation` metadata; `services::app_support::render_run_outcome` selects an attached presentation as a redacted stderr message without applying renderer-owned styling, the classified fallback header, or guidance, while preserving caller-provided presentation styling and existing command fallback behavior. - 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. - Startup observability bootstrapping in `cli/src/app.rs` still tolerates invalid default-discovered config files by continuing with degraded defaults plus `sce.config.invalid_config` warn-level logs, but the warning/logging work is now isolated behind the startup-context and runtime-initialization phases rather than one inline startup function. -- `cli/src/services/observability.rs` provides deterministic runtime observability controls and rendering for app lifecycle logs, including shared config-resolved threshold/format and `log_dir` inputs with precedence `env > config file > defaults` for non-flag observability keys, stable event identifiers, severity filtering, the forced-emission warning path used for invalid discovered config startup diagnostics, stderr primary emission, redaction-safe emission through the shared security helper, and log-directory writes with bounded retention. Config resolution also carries a positive config-file/default-only `log_file_retention_limit` (`10` by default) into startup observability config and `sce config show`; the concrete logger stores that resolved value and threads it through primary and v2 cleanup. When `log_dir` resolves from `SCE_LOG_DIR`, config, or the `/sce/logs` default, each enabled or forced log operation selects `/sce-.log` or `/sce--.log` using the machine-local date and optional logger session context, with deterministic percent-encoding for unsafe session filename bytes; after successfully writing a newly created selected file, retention keeps the configured number of newest direct regular `*.log` files by mtime plus path/name tie-break and fails open on cleanup errors. Its `observability::traits` submodule exposes the current `Logger` API with `Option<&str>` session context plus object-safe `Telemetry` trait boundaries and `NoopLogger`; the concrete observability logger and telemetry runtime still own behavior and implement those traits. `services::app_support::render_run_outcome` consumes the logger through that trait boundary when logging classified errors and stdout-write failures. +- `cli/src/services/observability.rs` provides deterministic runtime observability controls and rendering for app lifecycle logs, including shared config-resolved threshold/format and `log_dir` inputs with precedence `env > config file > defaults` for non-flag observability keys, stable event identifiers, severity filtering, the forced-emission warning path used for invalid discovered config startup diagnostics, always-active tracing/file sinks, and a configured stderr sink with per-call classified-error suppression, redaction-safe emission through the shared security helper, and log-directory writes with bounded retention. Config resolution also carries a positive config-file/default-only `log_file_retention_limit` (`10` by default) into startup observability config and `sce config show`; the concrete logger stores that resolved value and threads it through primary and v2 cleanup. When `log_dir` resolves from `SCE_LOG_DIR`, config, or the `/sce/logs` default, each enabled or forced log operation selects `/sce-.log` or `/sce--.log` using the machine-local date and optional logger session context, with deterministic percent-encoding for unsafe session filename bytes; after successfully writing a newly created selected file, retention keeps the configured number of newest direct regular `*.log` files by mtime plus path/name tie-break and fails open on cleanup errors. Its `observability::traits` submodule exposes the current `Logger` API with `Option<&str>` session context plus an `emit_stderr` boolean classified-error argument, object-safe `Telemetry` trait boundaries, and `NoopLogger`; the concrete observability logger and telemetry runtime still own behavior and implement those traits. `services::app_support::render_run_outcome` consumes the logger through that trait boundary when logging classified errors and stdout-write failures, while retaining ownership of intentional user-facing stderr diagnostics. - `cli/src/services/observability.rs` no longer owns duplicate log enums or parsing helpers; it consumes the canonical primitive seam from `cli/src/services/config/mod.rs` and stays focused on logger and telemetry runtime behavior. - `cli/src/cli_schema.rs` is now the canonical owner for top-level command metadata for the real clap-backed command set (`auth`, `config`, `setup`, `doctor`, `hooks`, `policy`, `sync`, `version`, `completion`), including the slim top-level help purpose text and per-command visibility on `sce`, `sce help`, and `sce --help`; `cli/src/command_surface.rs` remains the custom top-level help renderer and known-command classifier, adding the synthetic `help` row plus the ASCII banner while consuming that shared metadata instead of maintaining a parallel command catalog. - `cli/src/services/default_paths.rs` is the canonical production path catalog for the CLI: it resolves config/state/cache roots with platform-aware XDG or `dirs` fallbacks through an internal `roots` seam, exposes named default paths for current persisted artifacts and database/log files (global config, auth tokens, auth DB, local DB, default observability log directory, and the sole Agent Trace DB path helper `agent_trace_db_path_for_repository` under `repos//agent-trace.db`; the former global-sentinel and per-checkout Agent Trace path helpers were removed by the `retire-legacy-agent-trace-db` plan), and owns canonical repo-relative, embedded-asset, install, hook, and context-path accessors so non-test production path definitions have one shared owner. Compile-time generated payload paths are owned by `build.rs` under `OUT_DIR`, not by the default-path catalog. Current production consumers such as config discovery, observability config resolution, doctor reporting, setup/install flows, database adapters, checkout identity, Agent Trace storage resolution, and local hook runtime path resolution consume this shared catalog rather than defining owned path literals in their own modules. diff --git a/context/cli/cli-command-surface.md b/context/cli/cli-command-surface.md index 9443425d1..e97dc0eb7 100644 --- a/context/cli/cli-command-surface.md +++ b/context/cli/cli-command-surface.md @@ -73,7 +73,7 @@ An interactive `setup` run instead resolves the selection through an `inquire` m - `cli/src/app.rs` now runs commands through explicit phases with `StartupContext`, `AppRuntime`, and generic `RunOutcome` carrying startup-derived observability config, logger/telemetry state, registry state, and final render data across the lifecycle without hardcoding render support to the production logger type. - `parse_command_phase` delegates clap output conversion to `cli/src/services/parse/command_runtime.rs`, which returns concrete `RuntimeCommand` enum variants; `services::app_support::execute_command_phase` emits lifecycle logs around `RuntimeCommand::execute(...)`, and the enum delegates behavior to service-owned command payload structs. - Top-level failures are classified into stable exit-code classes owned by `cli/src/app.rs`: `2` parse, `3` validation, `4` runtime, and `5` dependency. -- User-facing diagnostics are rendered on `stderr` as `Error [SCE-ERR-]: ...` with class-default `Try:` remediation appended only when missing; when stderr color is enabled the heading, error code, and diagnostic body all render through shared stderr styling helpers. +- Classified fallback diagnostics are rendered on `stderr` as `Error [SCE-ERR-]: ...` with class-default `Try:` remediation appended only when missing; an explicit `ClassifiedError::UserFacingPresentation` renders only its redacted configured message without renderer-owned styling, the header, or automatic guidance, preserving caller-provided styling and structure. Fallback errors retain the configured logger stderr record, while explicit presentations suppress only that per-call logger record; both retain tracing/file observability and the shared stderr styling policy for app-owned diagnostics. - Unknown commands/options and extra positional arguments return deterministic, actionable guidance to run `sce --help`. - `sce setup --help` returns setup-specific usage output with target-flag contract details and deterministic examples, including one-run non-interactive setup+hooks and composable follow-up validation/repair-intent flows (`sce doctor --format json`, `sce doctor --fix`). - `sce auth` and `sce auth --help` return auth-specific usage output with available subcommands and deterministic examples, while `sce auth --help` stays scoped to the selected auth subcommand. The removed `sce auth renew` and `sce auth status` routes are rejected as invalid commands. diff --git a/context/cli/styling-service.md b/context/cli/styling-service.md index 903157609..c46c4b200 100644 --- a/context/cli/styling-service.md +++ b/context/cli/styling-service.md @@ -54,7 +54,7 @@ The CLI styling service in `cli/src/services/style.rs` provides deterministic te - Help output uses `supports_color()` for stdout TTY detection - Command-local help styling is applied after clap renders plain help text, covering `Usage:`, section headings, command rows, and placeholder tokens on stdout surfaces - Error diagnostics use `supports_color_stderr()` for stderr TTY detection -- Top-level app diagnostics and observability log-file write failures both render through the shared stderr styling helpers when stderr color is enabled. +- Classified fallback diagnostics render through the shared stderr styling helpers when stderr color is enabled; explicit user-facing presentations are redacted without a second renderer styling layer so caller-owned styling and structure survive. Observability log-file write failures remain fail-open without terminal styling or output. ## Sync progress styling diff --git a/context/cli/sync-command.md b/context/cli/sync-command.md index 5f5070cba..9dc78ce72 100644 --- a/context/cli/sync-command.md +++ b/context/cli/sync-command.md @@ -87,6 +87,21 @@ sink, emits no progress on stderr, and emits this JSON-only stdout shape: } ``` +## Authentication failure presentation + +When sync classification receives the typed `ControlPlaneError::MissingCredentials` +or `ControlPlaneError::AuthenticationFailed` failure, it keeps the classified +runtime error and attaches the shared `UserFacingPresentation` seam. Text-mode +stderr then renders the concise message: + +`You are not logged in. Please log in using the sce auth login command.` + +The `sce auth login` segment is caller-styled through the shared `success` +helper when styling is enabled. The app boundary does not add the classified +error header, technical diagnostic, or automatic `Try:` guidance for this +presentation; exit code `4`, technical logging, and empty stdout remain +unchanged. Other sync failures retain the classified fallback diagnostic. + Authentication refresh, conflict reconciliation, ambiguous batch recovery, terminal protocol failures, ownership rejection, and sanitized control-plane errors remain owned by `services::agent_trace_sync` and its control-plane diff --git a/context/context-map.md b/context/context-map.md index 167420d8c..142c41221 100644 --- a/context/context-map.md +++ b/context/context-map.md @@ -16,20 +16,20 @@ Feature/domain context: - `context/cli/checkout-identity.md` (current checkout identity infrastructure in `cli/src/services/checkout/`, including `/sce/checkout-id` UUIDv7 storage, setup/hook integration that creates/reuses checkout identity as repository-scoped Agent Trace diagnostic metadata, the removed per-checkout DB opener/path helper, `sce doctor` checkout identity display, and the never-touch on-disk handling of pre-migration checkout-scoped DB files that are no longer inspectable via the CLI) - `context/cli/patch-service.md` (standalone patch domain model, parser, JSON load helpers, and set operations in `cli/src/services/patch.rs` for in-memory parsed unified-diff representation, capturing only touched lines plus minimal per-file/per-hunk metadata, supporting both `Index:` SVN-style and `diff --git` git-style formats, with `ParseError` for actionable malformed-input diagnostics, `PatchLoadError`/`load_patch_from_json`/`load_patch_from_json_bytes` for storage-agnostic JSON reconstruction, `intersect_patches` for target-shaped overlap with exact-match-first and historical `kind`+`content` fallback semantics plus matched-constructed-line `session_id` and matched-constructed-hunk `model_id` provenance inheritance, and `combine_patches` for ordered patch combination with later-wins conflict resolution plus winning-hunk `model_id` provenance inheritance; 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/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, caller-owned styling preservation for explicit user-facing presentations, and per-column right-to-left RGB gradient banner rendering) - `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 `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`, 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 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 compile-safe 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/sce/cli-exit-code-contract.md` (stable class-based `sce` process exit-code contract in `cli/src/app.rs`, so automation can branch on failure category without parsing error text) -- `context/sce/cli-error-code-taxonomy.md` (stable user-facing `SCE-ERR-*` diagnostic code classes rendered by `cli/src/app.rs`, complementing the numeric exit-code classes) -- `context/sce/cli-stdout-stderr-contract.md` (implemented stream contract in `cli/src/app.rs`: command payloads on stdout only, redacted diagnostics on stderr) +- `context/sce/cli-error-code-taxonomy.md` (stable user-facing `SCE-ERR-*` diagnostic code classes, classified fallback guidance, and the app-boundary `ClassifiedError` optional `UserFacingPresentation` rendering seam owned by `cli/src/services/error.rs` and `services::app_support`) +- `context/sce/cli-stdout-stderr-contract.md` (implemented stream contract in `cli/src/app.rs`: command payloads on stdout only, redacted fallback diagnostics or exact optional user-facing presentations on stderr) - `context/sce/cli-shared-output-format-contract.md` (canonical `OutputFormat` `--format ` contract in `cli/src/services/output_format.rs` with command-specific invalid-value guidance) - `context/sce/cli-version-command-contract.md` (implemented `sce version` contract for deterministic human and machine-readable runtime identification) - `context/sce/cli-shell-completion-contract.md` (implemented `sce completion` contract for deterministic Bash/Zsh/Fish completion script generation) - `context/sce/claude-raw-hook-capture.md` (removed feature: the former `sce hooks claude-capture` raw-capture route and its supporting types, replaced by the active `diff-trace` and `conversation-trace` intakes) -- `context/sce/cli-observability-contract.md` (implemented config-backed runtime observability contract for the flat logging config-file shape with `log_dir` / `SCE_LOG_DIR` env-over-config-over-`/sce/logs` fallback, append-only local-date/session log file routing with a one-time complete-record `-v2.log` fallback on primary open/append/flush failure, creation-triggered retention of direct regular `*.log` files to 10 entries, reliable producer-native diff-trace/conversation-trace session routing and hook-specific non-duplicated Agent Trace DB-open error events, deterministic session filename sanitization, concrete logger/telemetry runtime behavior plus logger and object-safe telemetry trait boundaries, AppContext observability wiring, generic `RunOutcome` final rendering, runtime-classified repeated telemetry action protection, operator-facing `sce config show` observability reporting, and the trimmed `sce config validate` status-only validation surface) +- `context/sce/cli-observability-contract.md` (implemented config-backed runtime observability contract for the flat logging config-file shape with always-active tracing/file sinks plus configured stderr emission and per-call classified-error stderr suppression, `log_dir` / `SCE_LOG_DIR` env-over-config-over-`/sce/logs` fallback, append-only local-date/session log file routing with a one-time complete-record `-v2.log` fallback on primary open/append/flush failure, creation-triggered retention of direct regular `*.log` files to 10 entries, reliable producer-native diff-trace/conversation-trace session routing and hook-specific non-duplicated Agent Trace DB-open error events, deterministic session filename sanitization, concrete logger/telemetry runtime behavior plus logger and object-safe telemetry trait boundaries, AppContext observability wiring, generic `RunOutcome` final rendering, runtime-classified repeated telemetry action protection, operator-facing `sce config show` observability reporting, and the trimmed `sce config validate` status-only validation surface) - `context/sce/shared-context-code-workflow.md` (canonical `/next-task` task-synchronization lifecycle and validation-only `/validate` lifecycle, package-local phase references with single-skill control flow, and the task-synchronization-scoped `sce-decision` sibling invocation with ADR reuse/blocker propagation) - `context/sce/shared-context-plan-workflow.md` (canonical `/change-to-plan` workflow, package-local context-load/plan-authoring/template references, clarification/readiness gate contract, and one-task/one-atomic-commit task slicing) - [Context workflow rules](sce/context-workflow-rules.md) (canonical bootstrap, ongoing context maintenance, task synchronization, hygiene, discoverability, and feature-existence rules) @@ -122,4 +122,6 @@ Recent decision records: - `context/decisions/2026-08-13-trace-sync-progress-stream-contract.md` (keeps trace-sync progress and lifecycle timestamps on stderr while preserving stdout payload and JSON silence) - `context/decisions/2026-08-18-consumer-typed-progress-reporter-boundary.md` (keeps the reusable reporter contract generic over consumer event types while sync owns `SyncProgressEvent`) - `context/decisions/2026-08-18-sync-owned-progress-reporter-contract.md` (makes `services::sync::progress` the sole owner of the generic progress contract, no-op reporter, sync adapter, and focused tests; no top-level progress service remains) +- `context/decisions/2026-08-19-file-only-observability-sinks.md` (separates structured tracing/file observability sinks from app-owned terminal diagnostics and keeps logger persistence failures fail-open) +- `context/decisions/2026-08-19-per-call-classified-error-logging-sinks.md` (restores configured logger stderr emission by default and makes explicit user-facing errors suppress only that logger sink per call while retaining tracing/file records; supersedes the T03 file-only sink decision) - `context/decisions/2026-08-07-git-hook-managed-block-cooperation.md` (SCE-installed git hooks are a bounded in-place editor, not an exclusive owner: hook ownership is decided structurally by the SCE managed-block marker pair or a legacy guidance-URL marker, a foreign hook's bytes are preserved as an exact prefix with the block appended after them, and coexistence with third-party hook managers is cooperative, not authoritative) diff --git a/context/glossary.md b/context/glossary.md index 5661f1d45..c590e6282 100644 --- a/context/glossary.md +++ b/context/glossary.md @@ -1,5 +1,4 @@ # Glossary - - `pkl-check-generated`: Flake app exposed as `nix run .#pkl-check-generated`; canonical ephemeral-generation check that rejects committed target/schema/mirror outputs, evaluates exact workflow metadata, the generated artifact contract, semantic layout/path/inventory/content/parity/observational checks, and the optional-workflow manifest's content against the catalog, requires the shared helper-composition rule and SCE-scoped workflow prohibitions, enforces ordered catalog-derived OpenCode skill permissions plus explicit-permission artifact integrity, rejects stale sibling-package references or unresolved internalization tokens in workflow entrypoint `SKILL.md` documents, proves contract failures through checked-in negative fixtures, and delegates deterministic generation plus payload/input inventories to the generated-input producer while preserving its established inventory report. - `repo-level verification preference`: Current repository guidance that contributor-facing validation/check flows should prefer `nix flake check`; direct Cargo verification commands are secondary and used only when explicitly requested or for narrow targeted debugging, while `cargo fmt` remains the explicit autofix path. - lightweight post-task verification baseline: Required quick checks after each completed task in this repo: `nix run .#pkl-check-generated` and `nix flake check`. @@ -66,6 +65,7 @@ - `cli crates.io publication posture`: Current Cargo package posture in `cli/Cargo.toml` where crates.io-facing metadata is publication-ready for the `shared-context-engineering` crate, with crate-facing install guidance owned by `cli/README.md`. - `Nix performance recommendations`: Repo-local operator guidance in `AGENTS.md` covering optional user-level `~/.config/nix/nix.conf` tuning (`max-jobs = auto`, `cores = 0`) and the explicit root/admin-only boundary for `/etc/nix/nix.conf` `auto-optimise-store = true`. - `sce` (CLI foundation): Rust binary crate at `cli/` with implemented auth command flows (`auth login|logout|whoami`) plus auth-local bare-command guidance (`sce auth`, `sce auth --help`), Control Plane `/me`-backed whoami profile output using flat email/name/role/permissions/organization labels, exact logged-out login guidance, implemented setup installation flow including lifecycle-aggregated local DB and Agent Trace DB bootstrap, implemented attribution-only `hooks` subcommand routing/validation entrypoints, and a fully implemented top-level `sce sync` command that synchronizes the current repository's Agent Trace DB with the control plane and renders the documented text/JSON output (see `context/cli/sync-command.md`). +- `ClassifiedError` / `UserFacingPresentation`: `ClassifiedError` in `cli/src/services/error.rs` carries a `FailureClass`, stable `SCE-ERR-*` code, technical diagnostic message, and optional `UserFacingPresentation`. The presentation contains a caller-owned user-facing message that may already be styled plus an optional separate semantic reason key for structured logging or later routing; the app redacts it without applying a second style layer, and it does not replace the technical diagnostic, failure class, stable code, or numeric exit code. Top-level `sce sync` selects it for typed unauthenticated control-plane failures; other command mappings retain the fallback. The classified-error logger receives an `emit_stderr` boolean controlling only its stderr record per call: ordinary/fallback errors pass `true`, explicit presentations pass `false`, while tracing/file persistence remains active. See [CLI observability contract](sce/cli-observability-contract.md). - `auth login stored-credential renewal`: The `sce auth login` behavior that first validates every stored credential through the existing non-forced token path, preserves valid credentials, refreshes expired credentials, and falls back to device authorization when credentials are absent or renewal fails. Renewal reports retain `login` labels in text and JSON; credential renewal is not exposed as a public subcommand. - `command surface contract`: The current top-level command/help catalog split where `cli/src/cli_schema.rs` owns the real clap-backed command metadata (top-level purpose text plus help visibility for `auth`, `config`, `setup`, `doctor`, `hooks`, `policy`, `sync`, `version`, and `completion`) and `cli/src/command_surface.rs` consumes that catalog for the custom banner/help surface plus known-command classification, while still adding the synthetic `help` row. - `top-level help visibility metadata`: Per-command `show_in_top_level_help` metadata in `cli/src/cli_schema.rs` that controls whether a known command appears in `sce`, `sce help`, and `sce --help` without affecting direct invocation; the current hidden top-level commands are `hooks` and `policy`, while `auth` is visible, and `cli/src/command_surface.rs` renders the curated top-level help list from that shared metadata. @@ -119,7 +119,7 @@ - `sce exit-code class contract`: Stable top-level process exit-code mapping owned by `cli/src/app.rs` (`0` success, `2` parse failure, `3` validation failure, `4` runtime failure, `5` dependency failure) so automation can branch on failure class without parsing text errors. - `sce stderr error-code taxonomy`: Stable user-facing diagnostic code classes emitted by `cli/src/app.rs` (`SCE-ERR-PARSE`, `SCE-ERR-VALIDATION`, `SCE-ERR-RUNTIME`, `SCE-ERR-DEPENDENCY`) via `Error []: ...` stderr formatting. - `class-default Try guidance injection`: `cli/src/app.rs` diagnostic behavior that appends `Try:` remediation text by failure class only when an error message does not already include `Try:` guidance. -- `sce observability baseline`: App-runtime logging contract in `cli/src/services/observability.rs` and `cli/src/app.rs` with config-resolved observability inputs, deterministic env-over-config-file-over-default precedence for non-flag logging keys, default-backed `log_dir` / `SCE_LOG_DIR` file routing by machine-local date plus optional session filename partitioning, creation-triggered retention of direct regular `*.log` files to 10 entries, stable lifecycle `event_id` values, and stderr primary emission. +- `sce observability baseline`: App-runtime logging contract in `cli/src/services/observability.rs` and `cli/src/app.rs` with config-resolved observability inputs, deterministic env-over-config-file-over-default precedence for non-flag logging keys, always-active tracing/file sinks plus configured stderr emission, per-call classified-error stderr suppression for explicit user-facing presentations, default-backed `log_dir` / `SCE_LOG_DIR` file routing by machine-local date plus optional session filename partitioning, creation-triggered retention of direct regular `*.log` files to 10 entries, and stable lifecycle `event_id` values. - `sce stdout/stderr contract`: App-level stream routing contract in `cli/src/app.rs` where command success payloads are emitted on stdout only, while redacted user-facing diagnostics and text-mode `sce sync` progress are emitted on stderr; JSON sync emits no human progress. - `SCE_LOG_LEVEL`: Optional runtime env key for `sce` observability threshold; allowed values are `error`, `warn`, `info`, and `debug`, defaulting to `error` when unset. - `SCE_LOG_FORMAT`: Optional runtime env key for `sce` observability record format; allowed values are `text` and `json`, defaulting to `text` when unset. diff --git a/context/overview.md b/context/overview.md index 6e524c6eb..017d5ac6a 100644 --- a/context/overview.md +++ b/context/overview.md @@ -9,9 +9,9 @@ The generated `/next-task` workflow persists task-level context-synchronization ## Key cross-cutting contracts - **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`). +- **Stderr diagnostics:** classified errors use stable `SCE-ERR-{PARSE,VALIDATION,RUNTIME,DEPENDENCY}` codes with class-default `Try:` remediation, while an attached `UserFacingPresentation` renders its redacted caller-owned message without renderer-added styling, the classified header, or automatic guidance; `sce sync` adopts that seam for typed unauthenticated control-plane failures and other mappings retain the fallback (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`). -- **Observability:** config-resolved logging to stderr, optional dated/session-partitioned `log_dir` / `SCE_LOG_DIR` files with retention (see `context/sce/cli-observability-contract.md`). +- **Observability:** config-resolved structured logging uses tracing and optional redacted dated/session-partitioned `log_dir` / `SCE_LOG_DIR` files with retention; logger records use the configured stderr sink by default, while explicit user-facing errors suppress only that per-call logger sink (see `context/sce/cli-observability-contract.md`). - **Config precedence:** `flags > env > config file > defaults` (see `context/cli/config-precedence-contract.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`). - **Install channels:** repo-flake Nix, Cargo, npm, and source-built Flatpak (`dev.crocoder.sce`); Homebrew deferred (see `context/sce/cli-first-install-channels-contract.md`). @@ -19,8 +19,8 @@ The generated `/next-task` workflow persists task-level context-synchronization The CLI crate currently depends on `anyhow`, `chrono`, `clap`, `clap_complete`, `dirs`, `hmac`, `indicatif`, `inquire`, `jsonschema`, `keyring-core`, `murmur3`, `owo-colors`, `rand`, `reqwest`, `serde`, `serde_json`, `sha2`, `tokio`, `tracing`, `turso`, and `uuid`, with target-specific keyring backend dependencies for Linux/FreeBSD, macOS, and Windows. No CLI dev-dependencies are currently declared. 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 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 app runtime now also includes a structured observability baseline in `cli/src/services/observability.rs`: deterministic env-controlled log threshold/format (`SCE_LOG_LEVEL` defaults to `error`; `SCE_LOG_FORMAT` defaults to `text`), default-backed log-directory routing (`SCE_LOG_DIR` / config-file `log_dir` / `/sce/logs`) with per-operation machine-local dated file selection, optional session filename partitioning, and creation-triggered retention of direct regular `*.log` files to 10 entries, stable lifecycle event IDs, stderr primary emission so stdout command payloads remain pipe-safe, and `observability::traits` boundaries for logger and telemetry behavior. +The 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. `ClassifiedError` can additionally carry a caller-owned `UserFacingPresentation` message and optional semantic reason; the app renderer redacts that message without adding a second styling layer, the classified header, or automatic guidance when present. The top-level `sync` command uses it for typed unauthenticated control-plane failures; other command mappings still use the classified fallback. +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, tracing/file sinks that remain active for every record, a configured stderr sink that can be suppressed per classified-error call when the app renders an explicit presentation, 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. Agent Trace lifecycle setup now resolves repository storage, creates/reuses checkout identity for diagnostics, and initializes `/sce/repos//agent-trace.db` via `RepositoryAgentTraceDb`; hook runtime lazy initialization uses the same repository storage resolver when setup has not prepared the DB or schema metadata is incomplete. diff --git a/context/patterns.md b/context/patterns.md index 07dc6eded..eddb3bc10 100644 --- a/context/patterns.md +++ b/context/patterns.md @@ -121,8 +121,8 @@ - Keep placeholder or deferred state explicit in runtime responses and command-local docs rather than relying on top-level help status badges. - Parse CLI args with `clap` derive macros, classify top-level failures into stable exit-code classes (`parse`, `validation`, `runtime`, `dependency`), and keep user-facing failures deterministic/actionable. - Keep command payload structs and execution methods in service-owned `command.rs` modules; keep the static `RuntimeCommand` enum and deterministic command-name catalog in `services/command_registry.rs`; keep clap-to-runtime conversion in `services/parse/command_runtime.rs`; `app.rs` should stay focused on startup lifecycle and thin parse/execute/render orchestration rather than owning command-specific runtime handlers or parse conversion details. The top-level `sce sync` command keeps its format-gated stderr progress adapter and report rendering inside the sync-owned service boundary. -- Emit user-facing CLI diagnostics with stable class-based error IDs (`SCE-ERR-PARSE`, `SCE-ERR-VALIDATION`, `SCE-ERR-RUNTIME`, `SCE-ERR-DEPENDENCY`) using deterministic `Error []: ...` stderr formatting, and auto-append class-default `Try:` remediation only when the message does not already provide one. -- Keep CLI observability separate from command payloads: emit deterministic lifecycle logs to `stderr` only with stable `event_id` values, and preserve `stdout` for command result payloads. +- Emit classified fallback CLI diagnostics with stable class-based error IDs (`SCE-ERR-PARSE`, `SCE-ERR-VALIDATION`, `SCE-ERR-RUNTIME`, `SCE-ERR-DEPENDENCY`) using deterministic `Error []: ...` stderr formatting, auto-appending class-default `Try:` remediation only when the fallback message does not already provide one; an explicit `UserFacingPresentation` is redacted and emitted without the fallback header, guidance, or renderer-owned styling so caller-provided styling and structure are preserved. +- Keep CLI observability separate from command payloads and user diagnostics: route deterministic lifecycle logs through tracing/file sinks plus the configured stderr sink with stable `event_id` values, suppress only the classified-error logger stderr record when an explicit user-facing presentation owns that invocation's diagnostic, and preserve `stdout` for command result payloads. - For baseline runtime observability controls, resolve logging settings through the shared config resolver first, preserving deterministic precedence (`flags > env > config file > defaults`) and fail-fast validation on invalid env/config inputs. - For observability log-directory configuration, resolve `log_dir` through `SCE_LOG_DIR` > config-file `log_dir` > `default_paths::observability_log_dir()` (`/sce/logs`; Linux `${XDG_STATE_HOME:-~/.local/state}/sce/logs`); select log files per emission from the machine-local date and optional logger session context, append rendered records to the selected file, run retention only after successfully creating a selected file, and keep session IDs out of rendered log schemas unless a caller explicitly passes them as normal fields. - 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. diff --git a/context/sce/cli-error-code-taxonomy.md b/context/sce/cli-error-code-taxonomy.md index 29b4dc7a8..85fc1eb8b 100644 --- a/context/sce/cli-error-code-taxonomy.md +++ b/context/sce/cli-error-code-taxonomy.md @@ -12,13 +12,21 @@ It complements the numeric process exit-code classes documented in `context/sce/ - `SCE-ERR-RUNTIME`: runtime execution failures after successful parse + validation. - `SCE-ERR-DEPENDENCY`: startup dependency failures before parsing/dispatch. +## Optional user-facing presentation + +- `ClassifiedError` may carry a `UserFacingPresentation` containing a caller-provided message, including any presentation styling, and an optional separate semantic reason key. +- The presentation is distinct from the technical diagnostic message, `FailureClass`, stable `SCE-ERR-*` code, and numeric exit code. +- Existing constructors leave the presentation absent. The top-level `sync` command is the current command-specific adoption: typed `MissingCredentials` and `AuthenticationFailed` control-plane failures attach a concise login presentation, while other command mappings retain the classified fallback. + ## Rendering contract -- User-facing diagnostics are emitted on `stderr` as: `Error []: `. -- Before stderr emission, all `ClassifiedError` instances are logged via `Logger::log_classified_error()` with event ID `sce.error.{code}` and fields `error_code`, `error_class`. -- If a diagnostic message does not already include `Try:`, runtime appends class-default remediation guidance. +- Errors with no `UserFacingPresentation` are emitted on `stderr` as: `Error []: `. +- When a `UserFacingPresentation` is present, the app emits its redacted message on `stderr` without applying renderer-owned styling, and without the `Error []` header, technical diagnostic, or automatic class-default `Try:` guidance; any caller-provided styling and message structure are preserved. +- The presentation does not change the classified exit code or structured error logging. For `sce sync` authentication failures, it renders `You are not logged in. Please log in using the sce auth login command.` in color-disabled output, with only the `sce auth login` segment caller-styled when styling is enabled; other command mappings retain the classified fallback. +- Before stderr emission, all `ClassifiedError` instances are logged via `Logger::log_classified_error()` with event ID `sce.error.{code}` and fields `error_code`, `error_class`. The app passes `true` for fallback errors so the configured logger stderr record remains visible, and `false` for an explicit presentation so only that logger stderr record is suppressed; tracing/file observability remains active. +- If a fallback diagnostic message does not already include `Try:`, runtime appends class-default remediation guidance. - If the message already contains `Try:`, runtime preserves the original remediation text and does not append a second one. -- Diagnostic text is still redaction-filtered through `services::security::redact_sensitive_text` before emission. +- Both presentation and fallback diagnostic text are redaction-filtered through `services::security::redact_sensitive_text` before emission; only fallback diagnostics receive renderer-owned stderr styling. ## Actionable parser/invocation guidance contract @@ -31,12 +39,13 @@ It complements the numeric process exit-code classes documented in `context/sce/ ## Ownership - `FailureClass` in `cli/src/services/error.rs` owns class selection. -- `ClassifiedError` in `cli/src/services/error.rs` owns stable code assignment. +- `ClassifiedError` in `cli/src/services/error.rs` owns stable code assignment, technical diagnostics, and optional `UserFacingPresentation` metadata. - `Logger::log_classified_error` in `cli/src/services/observability.rs` owns structured error logging with `sce.error.{code}` event IDs. -- `write_error_diagnostic` in `cli/src/app.rs` owns final code-bearing stderr rendering. +- `write_error_diagnostic` in `cli/src/services/app_support.rs` owns final stderr rendering, selecting either the exact optional presentation or the code-bearing fallback. - `run_with_dependency_check_and_streams` in `cli/src/app.rs` owns error logging before stderr emission. ## Determinism and testing - Error code value is derived from failure class and is stable for a given class. -- Code-bearing stderr output and remediation presence are locked by `app::tests`. +- Code-bearing stderr output, exact presentation rendering, stdout isolation, and remediation presence are locked by `services::app_support::tests`. +- Technical classified-error logging remains independently covered by `services::observability::tests::classified_error_logging_keeps_technical_data_separate_from_presentation`. diff --git a/context/sce/cli-observability-contract.md b/context/sce/cli-observability-contract.md index dba7ee76a..756a35339 100644 --- a/context/sce/cli-observability-contract.md +++ b/context/sce/cli-observability-contract.md @@ -3,7 +3,7 @@ ## Scope This document defines the implemented structured observability baseline for `sce` runtime execution. -It covers deterministic stderr logger controls, default-backed log-directory routing with a one-time file fallback and bounded retention, the current logger and telemetry trait boundaries, config-backed runtime resolution, startup degradation behavior for invalid discovered config, and event emission boundaries in `cli/src/services/observability.rs`, `cli/src/services/config/mod.rs`, and `cli/src/app.rs`. +It covers deterministic logger controls with an explicit tracing/file sink policy, default-backed log-directory routing with a one-time file fallback and bounded retention, the current logger and telemetry trait boundaries, config-backed runtime resolution, startup degradation behavior for invalid discovered config, and event emission boundaries in `cli/src/services/observability.rs`, `cli/src/services/config/mod.rs`, and `cli/src/app.rs`. Runtime observability consumes the shared resolved observability config from `cli/src/services/config/mod.rs`: env values still win where supported, config-file values act as fallback, and defaults apply when higher-precedence layers are absent. The concrete logger stores the resolved `log_file_retention_limit` and uses it for creation-triggered primary and v2 cleanup. When default-discovered config files are invalid JSON, fail schema validation, or are not top-level JSON objects, observability resolution skips those files, collects the failure text in `validation_errors`, and continues with defaults; explicit `--config` / `SCE_CONFIG_FILE` selections remain fatal. Startup therefore keeps running with degraded observability defaults instead of turning discovered invalid config into a startup failure. Those resolved values are surfaced to operators through `sce config show`; `sce config validate` uses the same validation path but reports only validation status plus any errors or warnings. @@ -26,17 +26,17 @@ Runtime observability consumes the shared resolved observability config from `cl ## Emission contract -- Log output is always emitted to `stderr`; command result payloads remain on `stdout`. +- Structured logger records are emitted through tracing and the configured redacted file sink, and ordinary logger calls emit a redacted record to the configured terminal `stderr` sink. When the app renders an explicit `UserFacingPresentation`, its classified-error logging call suppresses only that logger stderr record; tracing and file persistence still run. Logger persistence-failure diagnostics remain fail-open and are not emitted to terminal `stderr`. Intentional user-facing diagnostics and command-owned progress remain on `stderr`, while command result payloads remain on `stdout`. - Each enabled or forced log operation appends the redacted rendered record to a file selected at emit time from the resolved `log_dir`, machine-local date, and optional caller-provided session ID. - Sessionless file logs route to `/sce-.log`; session-aware file logs route to `/sce--.log`. - Session filename sanitization preserves ASCII letters, digits, `-`, and `_`; percent-encodes every other UTF-8 byte as uppercase `%HH`; and represents an explicitly empty `Some("")` session ID with the reserved `%EMPTY` token. - `sce hooks diff-trace` and `conversation-trace` pass producer-native session context into this existing routing argument when available. Diff-trace logging never uses the AgentTraceDb-only `oc_`/`cc_`/`pi_` prefix; skipped conversation items use their own session; batch-wide conversation insert failures use the first valid insert's session. Agent Trace DB open failures use hook-specific error events (`sce.hooks.diff_trace.agent_trace_db_open_failed` and `sce.hooks.conversation_trace.agent_trace_db_open_failed`) and do not also emit their broader write/intake events for the same failure. Session IDs remain absent from rendered record fields unless separately supplied as fields. - File routing creates the configured directory when needed, uses owner-only create permissions on Unix, and serializes writes independently per path. - If the selected primary file cannot be opened, appended to, or flushed, the logger retries the complete rendered record exactly once at a sibling path with `-v2` inserted before `.log`: `sce--v2.log` or `sce---v2.log`. Existing v2 files use the same create-or-append and per-path serialization behavior. -- Successful v2 persistence suppresses the terminal `Failed to write SCE log file` diagnostic and leaves the CLI command result unchanged. If both persistence attempts fail, the logger emits one redacted terminal file-write diagnostic to stderr and continues fail-open; a partial primary append may therefore coexist with the complete fallback record. +- Successful v2 persistence suppresses any file-write diagnostic and leaves the CLI command result unchanged. If both persistence attempts fail, the logger continues fail-open without terminal logger output; a partial primary append may therefore coexist with the complete fallback record. - Directory creation, primary lock acquisition, and retention cleanup failures do not trigger alternate-name generation. The fallback attempt is non-recursive: no v3, timestamped, random, or unbounded variants are tried. - After a successful write to a newly created primary or v2 SCE log file, the logger runs one best-effort retention pass over direct regular `*.log` children of `log_dir`. Existing-file appends do not scan or delete files. Cleanup keeps the resolved `log_file_retention_limit` newest files (default `10`). Files are ordered newest-first by filesystem modification time with path/name ordering as the deterministic tie-break, and older `.log` files are removed regardless of whether their names are SCE-owned. -- Retention is non-recursive and ignores non-regular entries plus non-`.log` files such as directories, symlinks, database files, and extensionless artifacts. Directory scan, metadata, or deletion failures do not fail the completed write; cleanup emits a redacted direct-stderr diagnostic without re-entering `Logger::log` and leaves entries it cannot safely process intact. +- Retention is non-recursive and ignores non-regular entries plus non-`.log` files such as directories, symlinks, database files, and extensionless artifacts. Directory scan, metadata, or deletion failures do not fail the completed write; cleanup remains fail-open without terminal logger output and leaves entries it cannot safely process intact. - Each emitted record includes a stable `event_id`. - Current app-level event identifiers: - `sce.app.start` @@ -48,7 +48,7 @@ Runtime observability consumes the shared resolved observability config from `cl - `sce.command.dispatch_end` (debug level - logged after successful dispatch) - `sce.command.completed` - Error logging uses the pattern `sce.error.{code}` where `{code}` is the classified error code (e.g., `sce.error.SCE-ERR-RUNTIME`). -- All `ClassifiedError` instances are logged via `Logger::log_classified_error()` before user-facing stderr diagnostics are written. +- All `ClassifiedError` instances are logged via `Logger::log_classified_error()` before user-facing stderr diagnostics are written; its `emit_stderr` boolean argument is `true` for fallback errors and `false` only when an explicit presentation owns the diagnostic. - Event records include deterministic metadata keys used by automation (`command`, `failure_class`, `component` when applicable). - Error log records include `error_code` and `error_class` fields for structured observability. - App runtime initializes tracing subscriber context before parse/dispatch and shuts down tracer provider on process exit. @@ -61,11 +61,11 @@ Runtime observability consumes the shared resolved observability config from `cl - Timestamps are UTC ISO8601 with millisecond precision (e.g., `2026-03-20T14:30:00.123Z`) generated via `chrono::Utc::now()`. - Logger threshold behavior is deterministic and severity-based (`error < warn < info < debug`). - Startup invalid-config diagnostics use an explicit warn-emission path so the warning is still rendered even when degraded defaults resolve to `log_level=error`. -- Rendered records remain deterministic line-based strings on `stderr`; log-directory files contain the same redacted rendered lines, do not add session IDs to the record schema automatically, and are bounded by creation-triggered `*.log` retention. +- Rendered records remain deterministic line-based redacted strings in the file sink; logger output does not add session IDs to the record schema automatically, and files are bounded by creation-triggered `*.log` retention. User-facing diagnostics remain separately rendered on `stderr`. ## Observability trait boundaries -- `cli/src/services/observability/traits.rs` exposes the `services::observability::traits::Logger` trait with the current logging API: `info`, `debug`, `warn`, `error`, and `log_classified_error`, each accepting `Option<&str>` session context used only for file routing. +- `cli/src/services/observability/traits.rs` exposes the `services::observability::traits::Logger` trait with the current logging API: `info`, `debug`, `warn`, `error`, and `log_classified_error`; ordinary methods accept `Option<&str>` session context used only for file routing, while classified-error logging also accepts an `emit_stderr` boolean controlling only that call's stderr sink. - The concrete `services::observability::Logger` implements the trait while retaining the existing inherent methods and behavior. - `NoopLogger` is available from the same traits module for tests and future dependency-injected services that need a logger without side effects. - The same traits module exposes object-safe `services::observability::traits::Telemetry` with the current app subscriber boundary: `with_default_subscriber` for command-lifecycle execution. @@ -83,6 +83,7 @@ Runtime observability consumes the shared resolved observability config from `cl ## Ownership and verification - `cli/src/services/config/resolver.rs` owns shared observability value resolution, config-file discovery/merge, env-over-config/default precedence for supported runtime inputs, default `log_dir` resolution through `default_paths::observability_log_dir()`, and config-file/default-only `log_file_retention_limit` resolution. -- `cli/src/services/observability.rs` owns runtime logger construction from resolved values, storage and application of `log_file_retention_limit`, `log_dir` non-empty validation, level filtering, tracing-event enablement checks, record rendering, local-date/session file-name selection, session filename sanitization, primary append plus one-time v2 fallback persistence, and best-effort `.log` retention; `cli/src/services/observability/traits.rs` owns the logger and telemetry trait boundaries plus the no-op logger implementation. +- `cli/src/services/observability.rs` owns runtime logger construction from resolved values, always-active tracing/file sinks, configured stderr emission with per-call classified-error suppression, storage and application of `log_file_retention_limit`, `log_dir` non-empty validation, level filtering, tracing-event enablement checks, record rendering, local-date/session file-name selection, session filename sanitization, primary append plus one-time v2 fallback persistence, and best-effort `.log` retention without persistence-failure diagnostics; `cli/src/services/observability/traits.rs` owns the logger and telemetry trait boundaries plus the no-op logger implementation. - `cli/src/app.rs` owns lifecycle event emission around parse/dispatch success and failure paths, resolves observability config before command dispatch, emits startup invalid-config warning events for skipped discovered config files, wraps dispatch inside the observability subscriber context, and guards the single-use command-dispatch action against repeated telemetry invocation with a runtime-classified error. `cli/src/services/app_support.rs` owns final stdout/stderr rendering and generic logger-backed classified-error logging. +- `services::observability::tests` lock default classified-error stderr emission, redacted file records, file retention when stderr is suppressed, and separation of technical classified-error logs from optional user-facing presentation text; `services::app_support::tests` lock exact friendly stderr output, classified fallback redaction/exit behavior, stdout isolation, and fail-open logger-write routing. - Retention-specific validation uses packaged CLI smoke checks for config/schema behavior and direct review of the primary/v2 logger cleanup plumbing. The root flake check suite validates the build, lint, formatting, generated parity, and remaining repository tests; no retention-specific Rust test module is currently kept in `observability.rs`, `config/resolver.rs`, or `config/schema.rs`. diff --git a/context/sce/cli-stdout-stderr-contract.md b/context/sce/cli-stdout-stderr-contract.md index 4c810737d..cfed66335 100644 --- a/context/sce/cli-stdout-stderr-contract.md +++ b/context/sce/cli-stdout-stderr-contract.md @@ -8,7 +8,7 @@ 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 `ClassifiedError` in `cli/src/app.rs`; diagnostics are passed through shared redaction (`services::security::redact_sensitive_text`) before emission. +- Classified fallback diagnostics are emitted as `Error []: ...` on `stderr`, where `` is the stable class-based `SCE-ERR-*` identifier from `ClassifiedError`; an explicit `UserFacingPresentation` instead emits only its redacted configured message without renderer-owned styling. Both paths pass through shared redaction (`services::security::redact_sensitive_text`) before emission, while caller-provided presentation styling is preserved. - Command handlers now return payload strings to the app dispatcher; the app owns stream selection and final emission. ## Implementation surface @@ -17,7 +17,7 @@ This document defines the implemented stream contract for CLI command payload an - `try_run_with_dependency_check(...)` performs parse + dispatch and returns payload text or classified errors. - `dispatch(...)` returns payload text for each command path rather than writing directly to process streams. - `write_stdout_payload(...)` handles success payload writes. -- `write_error_diagnostic(...)` handles redacted error writes. +- `write_error_diagnostic(...)` handles redacted fallback or explicit user-facing error writes. See also: `context/sce/cli-error-code-taxonomy.md` for the canonical error-code classes and `Try:` remediation injection rules. @@ -25,7 +25,7 @@ See also: `context/sce/cli-error-code-taxonomy.md` for the canonical error-code - Stream routing is centralized in one app-level path to avoid per-command stream drift. - Exit code class mapping remains unchanged (`parse`, `validation`, `runtime`, `dependency`). -- Observability lifecycle logs remain on `stderr` by contract and are independent from command payload output. +- Observability lifecycle logs use the logger's tracing/file sinks and configured stderr sink. Classified fallback errors retain the logger stderr record, while explicit user-facing presentations suppress only that per-call logger record; intentional user-facing diagnostics and command-owned progress remain on `stderr` independently from command payload output. - Text-mode `sce sync` emits its aligned four-row `indicatif` progress display on `stderr` before accepted batches begin: rows start at zero with independent steady spinners, accepted batches update only the corresponding cumulative count, and each stream receives a styled completion check at its own future boundary. Redirected/non-TTY output stays plain and free of terminal-control sequences, while `NO_COLOR` disables styling. The final text report remains the command result without repository or source-instance identifiers; JSON-mode sync emits no human progress text and keeps its JSON-only payload on `stdout`, also without those identifiers. The durable trace-sync stream choice is recorded in [Trace-sync progress stream contract](../decisions/2026-08-13-trace-sync-progress-stream-contract.md).