From 3d148061185d2701a72147d4607abdb9d1272a31 Mon Sep 17 00:00:00 2001 From: David Abram Date: Tue, 16 Jun 2026 01:26:55 +0200 Subject: [PATCH 1/8] plan Co-authored-by: SCE --- .../plans/agent-trace-checkout-identity.md | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 context/plans/agent-trace-checkout-identity.md diff --git a/context/plans/agent-trace-checkout-identity.md b/context/plans/agent-trace-checkout-identity.md new file mode 100644 index 00000000..f9c88389 --- /dev/null +++ b/context/plans/agent-trace-checkout-identity.md @@ -0,0 +1,99 @@ +# agent-trace-checkout-identity + +## Change summary + +Give each cloned repository (and linked Git worktree) its own `agent-trace` checkout identity and its own database. Currently every checkout shares one global database at `/sce/agent-trace.db`. After this plan, `sce setup` detects the current checkout, assigns a stable checkout ID stored in `.git/sce/checkout-id`, registers it in a central JSON registry, and resolves a per-checkout database at `/sce/agent-trace-{checkout_id}.db` so each local checkout gets isolated storage. + +## Success criteria + +- `sce setup` run inside a cloned repository creates a stable UUIDv7 checkout ID in `/sce/checkout-id` and registers the checkout in `/sce/checkout-registry.json` — but does **not** eagerly initialize the per-checkout database. +- A second `sce setup` in the same checkout reuses the existing ID (idempotent). +- A linked Git worktree gets its own checkout ID distinct from the main checkout. +- The per-checkout database (`agent-trace-{checkout_id}.db`) is created lazily on the first write — when a hook fires and finds a checkout ID but no database (or an uninitialized database), it auto-creates the DB, runs migrations, and updates the registry `database_path`. +- If no checkout ID exists yet (cloned but never ran `sce setup`), the first hook invocation creates the checkout ID, registers it, and creates the DB in one pass. +- Each checkout resolves its own `agent-trace-{checkout_id}.db`; data is isolated per local checkout. +- Per-checkout database isolation means every row in `agent-trace-{checkout_id}.db` inherently belongs to that checkout — no `checkout_id` column is needed in any table. +- `sce doctor` shows the current checkout identity (checkout ID + per-checkout DB status) instead of a generic global Agent Trace DB health row. +- `sce doctor dbs` lists all registered checkouts from the registry with `checkout_id`, `path`, `database_path`, `last_seen`, and `remote_url`. +- All existing hook flows (`post-commit`, `diff-trace`, `session-model`, `conversation-trace`) continue to work, resolving their database from the current checkout with lazy initialization. +- Global lifecycle operations (`sce doctor`, `sce setup` on a fresh machine) still work. +- `nix flake check` and `nix run .#pkl-check-generated` pass. + +## Constraints and non-goals + +- In scope: checkout ID creation/reuse during `sce setup`, central JSON registry, per-checkout DB path, hook and lifecycle refactoring to use the per-checkout DB. +- In scope: `git rev-parse --git-dir` is the canonical way to locate the checkout metadata directory; the checkout ID file lives at `/sce/checkout-id` and is never committed. +- In scope: the checkout ID is a UUIDv7 (consistent with the existing `agent_trace_id` convention in this codebase). +- In scope: hooks resolve the checkout by calling `git rev-parse --git-dir` from the current working directory (or passed repository root) and reading `/sce/checkout-id`. +- Out of scope: a separate `DatabaseResolver` abstraction layer — the DB path is derived directly from the checkout ID. +- Out of scope: purging or deleting old checkout DBs; checkout migration/rename handling; `sce sync` command changes. +- Out of scope: adding `checkout_id` columns to any table — per-checkout DB files provide isolation without needing a discriminator column. +- Out of scope: changing OpenCode or Claude agent-trace plugin behavior. +- Out of scope: new external crate dependencies. +- Use `git rev-parse --git-dir` resolved from the repository root. For a normal clone this returns `.git`; for a worktree it returns `/.git/worktrees/`. Store the checkout ID file relative to that resolved path. + +## Assumptions + +- `git rev-parse --git-dir` is available in all Git environments where `sce setup` already works (the setup preflight already requires a Git repository). +- The checkout registry file (`/sce/checkout-registry.json`) can be safely read/written with standard filesystem operations; it does not require locking beyond atomic write-and-rename. +- Per-checkout databases use the same `AgentTraceDbSpec` migration set as the global DB; the migration runner is idempotent. +- Hooks resolve the checkout ID from the current working directory (which is always the repository root when invoked by Git) or from an explicitly passed repository root. Hook entrypoints that don't currently receive a repository root (`diff-trace`, `session-model`, `conversation-trace`) will resolve it from `std::env::current_dir()`. +- The global `agent-trace.db` is no longer created or used for new setups after this plan; existing global DB files are left in place but not accessed by the new code paths. +- Per-checkout database creation is **lazy**: `sce setup` only establishes the checkout identity (ID + registry). The actual `agent-trace-{checkout_id}.db` file is created on the first hook invocation that needs it. If a hook fires in a checkout that has no checkout ID at all, the hook auto-creates the ID, registers it, and creates the DB in one pass. +- `TursoDb` currently resolves its path exclusively through `AgentTraceDbSpec::db_path()` → `agent_trace_db_path()`. T03 must add an `open_at(path)` constructor (or path-override mechanism) so the per-checkout DB can be opened at an arbitrary path while still running migrations and retry logic from the shared adapter. +- The hook hot path currently opens with `open_for_hooks_without_migrations()` + `ensure_schema_ready_for_hooks()`. The lazy resolution helper must handle three scenarios: (a) **brand-new DB** — no file exists, or file exists but `__sce_migrations` table is absent → fall back to full `new()` (run all migrations); (b) **existing DB, migrations current** — `ensure_schema_ready_for_hooks()` passes → use fast no-migration path; (c) **existing DB, pending migrations** (e.g. after a `sce` version bump that ships new migrations) — `ensure_schema_ready_for_hooks()` fails readiness → auto-run `new()` to apply pending migrations, then proceed. This ensures hook upgrades are seamless; no manual `sce setup` is needed after a version bump. +- `AgentTraceDbLifecycle::fix()` currently bootstraps the global DB parent directory. After T03, when running in a checkout context, `fix()` targets the per-checkout DB path instead. T03 already covers this refactor but it applies to the fix/diagnose paths, not just setup. +- Existing tests in `agent_trace_db/` and `hooks/` that reference `agent_trace_db_path()` or assert on the global DB path will need updating. Verification through `nix flake check` will surface these. + +## Task stack + +- [ ] T01: `Checkout identity infrastructure` (status:todo) + - Task ID: T01 + - Goal: Add a `cli/src/services/checkout/` module with checkout ID storage (`/sce/checkout-id`) and a central JSON registry (`/sce/checkout-registry.json`). + - Boundaries (in/out of scope): In — new `checkout/mod.rs` with `resolve_git_dir(repo_root)`, `get_or_create_checkout_id(git_dir)`, `read_checkout_id(git_dir)`, and `resolve_checkout_id_for_repo(repo_root)` (convenience combining the two); new `checkout/registry.rs` with `CheckoutRecord` struct carrying `checkout_id`, `path`, `last_seen` (RFC 3339), `remote_url` (optional), and `database_path` (optional); `CheckoutRegistry` with `register_checkout()`, `update_checkout_last_seen()`, `list_checkouts()`, and `remove_checkout()`; atomic write-through-rename for persistence. Checkout ID is a UUIDv7 generated with the `uuid` crate (already a dependency). Out — setup/hook integration, DB path changes, schema changes, doctor changes. + - Done when: `cargo build` compiles the new modules; focused unit tests for checkout ID create/read/idempotent-reuse, `resolve_git_dir` against a real git repo, and registry register/list/update pass. + - Verification notes (commands or checks): `nix develop -c sh -c 'cd cli && cargo test checkout'` during development; final validation via `nix flake check`. + +- [ ] T02: `Integrate checkout detection into setup lifecycle` (status:todo) + - Task ID: T02 + - Goal: `AgentTraceDbLifecycle::setup()` resolves the checkout ID from the repo root, creates or reuses it, and registers the checkout in the central registry. The per-checkout database is **not** eagerly initialized — it will be created lazily on first write (T03). + - Boundaries (in/out of scope): In — `AgentTraceDbLifecycle::setup()` calls `checkout::resolve_git_dir()` and `checkout::get_or_create_checkout_id()` using `ctx.repo_root()`, then `checkout::registry::register_checkout()`. Setup outcome messaging reports the resolved checkout ID and notes that the DB will be created on first write. The global `agent_trace_db_path()` DB is still opened for now (backward compat) — the switch to per-checkout paths comes in T03. Lifecycle `diagnose()` and `fix()` continue to target the global DB until T03. Out — hook flow changes, per-checkout DB path changes, registry query from doctor. + - Done when: `sce setup` in a cloned repo prints the checkout ID; a second run reuses the same ID; `cat .git/sce/checkout-id` shows the same UUID; `/sce/checkout-registry.json` contains the registered record (with `database_path` still absent/`null` since the DB hasn't been created yet); existing setup tests pass. + - Verification notes (commands or checks): Manual end-to-end: run `sce setup` in a test repo, inspect `.git/sce/checkout-id` and registry file, re-run `sce setup` and confirm idempotent ID reuse. Run existing setup tests through `nix flake check`. + +- [ ] T03: `Enable per-checkout database resolution with lazy initialization` (status:todo) + - Task ID: T03 + - Goal: Switch all Agent Trace DB consumers from the shared global path to per-checkout paths. Each checkout gets its own `agent-trace-{checkout_id}.db`, created lazily on first write when a hook fires — no explicit setup step needed for the DB itself. Per-checkout DB files provide isolation without any `checkout_id` column in the schema. + - Boundaries (in/out of scope): In — + - Add `agent_trace_db_path_for_checkout(checkout_id: &str) -> Result` to `cli/src/services/default_paths.rs`, resolving to `/sce/agent-trace-{checkout_id}.db`. + - Add a path-override constructor to `TursoDb` (or `AgentTraceDb`) so the per-checkout DB can be opened at an arbitrary path while still running migrations and retry logic. The existing `AgentTraceDbSpec::db_path()` + `TursoDb::new()` path is not parameterizable — it always resolves the global path. + - Add a lazy DB resolution helper in `checkout/mod.rs`: `resolve_or_create_agent_trace_db_for_current_checkout() -> Result<(AgentTraceDb, String)>` that: (1) resolves the current checkout via `git rev-parse --git-dir`, (2) reads or creates the checkout ID, (3) registers the checkout if newly created, (4) computes the per-checkout DB path, (5) attempts the fast no-migration open path; if the DB file doesn't exist yet or `ensure_schema_ready_for_hooks()` fails (either brand-new DB with no `__sce_migrations` table, or existing DB with pending migrations after a version bump), falls back to the path-override constructor to run migrations, (6) on success updates the registry `database_path` if the DB was just created, (7) returns the `AgentTraceDb` handle and checkout ID. + - Refactor `AgentTraceDbLifecycle::setup()` to only handle identity (checkout ID + registration) — no longer opens the global DB. `diagnose()` checks per-checkout DB path/health when a checkout ID is available. `fix()` bootstraps the per-checkout DB parent directory (not the global one) when running in a checkout context. Fall back to global path only when no checkout context exists (e.g. bare `sce doctor` outside any repo). + - Refactor `hooks/mod.rs`: all hook entrypoints call the lazy resolution helper to get their `AgentTraceDb` handle + checkout ID. Replace direct `AgentTraceDb::new()` and `open_agent_trace_db_for_hook_runtime()` calls. + - Update existing tests that reference `agent_trace_db_path()` or assert on the global DB path to use the per-checkout path or the test harness temp directories. + Out — schema migrations, `checkout_id` columns, data migration from old global DB, doctor checkout identity display. + - Done when: A fresh clone with `sce setup` run (no hooks yet) has a checkout ID and registry entry but no `agent-trace-{checkout_id}.db` file. The first hook invocation (e.g. post-commit) auto-creates the DB, runs migrations, and updates the registry `database_path`. A `git worktree add` followed by a hook invocation auto-creates a new checkout ID and DB for the worktree. Two checkouts have independent databases with no data leakage. `nix flake check` passes. + - Verification notes (commands or checks): Manual end-to-end: run `sce setup` in a test repo, verify no per-checkout DB file exists yet, trigger a commit (post-commit hook), verify `agent-trace-{checkout_id}.db` now exists and registry `database_path` is populated. Create a worktree, trigger a hook there, verify a second DB exists independently. Run `nix flake check` for full validation. + +- [ ] T04: `Surface checkout identity in doctor and add 'sce doctor dbs'` (status:todo) + - Task ID: T04 + - Goal: Extend `sce doctor` to report the current checkout identity, and add a new `sce doctor dbs` subcommand that lists all registered checkouts from the central registry. + - Boundaries (in/out of scope): In — + - Normal `sce doctor` output (both text and JSON) gains a checkout identity section/row showing the resolved checkout ID and whether the per-checkout DB exists (path and health status). This replaces the current generic Agent Trace DB health row in the Configuration section — the checkout ID + its DB status is the meaningful information now. + - New `sce doctor dbs` subcommand (text and JSON via `--format`) reads `/sce/checkout-registry.json` and lists all registered checkouts with: `checkout_id`, `path`, `database_path`, `last_seen`, and `remote_url` (if available). Sorted by `last_seen` descending. When the registry file doesn't exist or is empty, reports "no registered checkouts" gracefully. + - CLI schema: add `dbs` variant to the doctor subcommand enum, with `--format text|json` support. + - `AgentTraceDbLifecycle::diagnose()` is updated to report checkout identity health instead of/in addition to the generic global DB path health. + Out — modifying checkout rows, purging/cleanup commands, changing the existing Environment/Repository/Git Hooks/Integrations sections. + - Done when: `sce doctor` in a repo with a checkout ID shows the checkout ID and DB status; `sce doctor dbs` lists all registered checkouts; `sce doctor dbs --format json` outputs stable machine-readable fields; `nix flake check` passes. + - Verification notes (commands or checks): Run `sce doctor` inside a setup repo, verify checkout ID appears in output. Run `sce doctor dbs` and `sce doctor dbs --format json`, verify output shape. Run `nix flake check` for full validation. + +- [ ] T05: `Validation and context sync` (status:todo) + - Task ID: T05 + - Goal: Run full repository validation, verify all success criteria, and update durable context files to reflect the new checkout identity and per-checkout database architecture. + - Boundaries (in/out of scope): In — `nix run .#pkl-check-generated`, `nix flake check`, context sync for `context/sce/agent-trace-db.md`, `context/sce/agent-trace-hook-doctor.md`, `context/cli/default-path-catalog.md`, `context/cli/cli-command-surface.md`, `context/overview.md`, `context/architecture.md`, `context/glossary.md`, `context/context-map.md`. Add a new `context/cli/checkout-identity.md` context file describing the checkout identity service. Remove any temporary/debug artifacts. Out — new behavior beyond what was implemented in T01-T04. + - Done when: All verifications pass; context files accurately describe the current checkout identity architecture including the new `checkout/` service module, `.git/sce/checkout-id` storage, `checkout-registry.json` format, per-checkout DB naming convention (`agent-trace-{checkout_id}.db`), lazy initialization flow, `sce doctor` checkout identity reporting, and `sce doctor dbs` registry listing. + - Verification notes (commands or checks): `nix run .#pkl-check-generated`; `nix flake check`; review context files for stale references to a shared global database path or outdated architecture descriptions. + +## Open questions + +- None blocking. The lazy initialization model means `git worktree add` followed by a hook invocation just works — the hook creates the checkout ID and DB on first write without requiring `sce setup` in the worktree. `sce setup` remains the canonical way to pre-establish the checkout identity, but hooks are self-sufficient when it hasn't been run. From b25ac1597fd4c82e3fec664d3d6f57311540fbba Mon Sep 17 00:00:00 2001 From: David Abram Date: Tue, 16 Jun 2026 10:43:11 +0200 Subject: [PATCH 2/8] cli: Implement checkout identity infrastructure Add services/checkout/mod.rs with resolve_git_dir, read_checkout_id, get_or_create_checkout_id, and resolve_checkout_id_for_repo using UUIDv7 generated via the uuid crate. Add services/checkout/registry.rs with CheckoutRecord, CheckoutRegistry, and CRUD operations (register, update_last_seen, list, remove) backed by atomic write-through-rename JSON persistence at /sce/checkout-registry.json. Co-authored-by: SCE --- cli/src/services/checkout/mod.rs | 154 ++++++++++++++ cli/src/services/checkout/registry.rs | 190 ++++++++++++++++++ cli/src/services/mod.rs | 1 + context/architecture.md | 2 +- context/cli/checkout-identity.md | 30 +++ context/context-map.md | 1 + .../plans/agent-trace-checkout-identity.md | 6 +- 7 files changed, 382 insertions(+), 2 deletions(-) create mode 100644 cli/src/services/checkout/mod.rs create mode 100644 cli/src/services/checkout/registry.rs create mode 100644 context/cli/checkout-identity.md diff --git a/cli/src/services/checkout/mod.rs b/cli/src/services/checkout/mod.rs new file mode 100644 index 00000000..ba320d47 --- /dev/null +++ b/cli/src/services/checkout/mod.rs @@ -0,0 +1,154 @@ +//! Checkout identity service. +//! +//! Each cloned repository (and linked Git worktree) gets its own stable checkout +//! identity stored in `/sce/checkout-id`. The checkout ID is a `UUIDv7` +//! string, consistent with the existing `agent_trace_id` convention in this +//! codebase. +//! +//! The central JSON registry at `/sce/checkout-registry.json` tracks +//! all known checkouts with metadata like path, last-seen timestamp, remote URL, +//! and per-checkout database path. + +#![allow(dead_code)] + +pub mod registry; + +use std::path::{Path, PathBuf}; +use std::process::Command; + +use anyhow::{anyhow, Context, Result}; +use uuid::{NoContext, Timestamp, Uuid}; + +/// Subdirectory inside `/` where SCE checkout metadata lives. +const SCE_CHECKOUT_DIR: &str = "sce"; + +/// File name for the checkout ID inside `/sce/`. +const CHECKOUT_ID_FILE: &str = "checkout-id"; + +/// Resolves the Git directory (`.git` for normal clones, or the worktree-specific +/// path for linked worktrees) by running `git rev-parse --git-dir` from the +/// given repository root. +/// +/// For a normal clone this returns `/.git`. +/// For a linked worktree it returns the worktree-specific Git directory +/// (e.g. `/.git/worktrees/`). +pub fn resolve_git_dir(repo_root: &Path) -> Result { + let output = Command::new("git") + .args(["rev-parse", "--git-dir"]) + .current_dir(repo_root) + .output() + .with_context(|| { + format!( + "Failed to run git rev-parse --git-dir in '{}'", + repo_root.display() + ) + })?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string(); + return Err(anyhow!( + "git rev-parse --git-dir failed in '{}': {}", + repo_root.display(), + stderr + )); + } + + let git_dir_relative = String::from_utf8(output.stdout) + .with_context(|| "git rev-parse --git-dir emitted invalid UTF-8")? + .trim() + .to_string(); + + // `git rev-parse --git-dir` returns a path relative to the repo root + // (or an absolute path). Resolve it against the repo root. + let git_dir = PathBuf::from(&git_dir_relative); + if git_dir.is_absolute() { + Ok(git_dir) + } else { + Ok(repo_root.join(git_dir)) + } +} + +/// Reads an existing checkout ID from `/sce/checkout-id`. +/// +/// Returns `Ok(Some(id))` if the file exists and contains a valid checkout ID. +/// Returns `Ok(None)` if the file does not exist. +/// Returns an error if the file exists but cannot be read or contains invalid data. +pub fn read_checkout_id(git_dir: &Path) -> Result> { + let checkout_id_path = git_dir.join(SCE_CHECKOUT_DIR).join(CHECKOUT_ID_FILE); + + if !checkout_id_path.exists() { + return Ok(None); + } + + let content = std::fs::read_to_string(&checkout_id_path).with_context(|| { + format!( + "Failed to read checkout ID from '{}'", + checkout_id_path.display() + ) + })?; + + let id = content.trim().to_string(); + + if id.is_empty() { + return Err(anyhow!( + "Checkout ID file '{}' is empty", + checkout_id_path.display() + )); + } + + // Validate that the stored value is a valid UUIDv7. + Uuid::parse_str(&id).with_context(|| { + format!( + "Invalid checkout ID '{}' in '{}'", + id, + checkout_id_path.display() + ) + })?; + + Ok(Some(id)) +} + +/// Gets the existing checkout ID or creates a new one. +/// +/// If `/sce/checkout-id` already exists, returns the stored ID (idempotent). +/// If it does not exist, generates a new `UUIDv7`, writes it to the file, and returns it. +pub fn get_or_create_checkout_id(git_dir: &Path) -> Result { + if let Some(existing_id) = read_checkout_id(git_dir)? { + return Ok(existing_id); + } + + // Generate a new UUIDv7 using the current timestamp. + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default(); + let timestamp = Timestamp::from_unix(NoContext, now.as_secs(), now.subsec_nanos()); + let checkout_id = Uuid::new_v7(timestamp).to_string(); + + let checkout_dir = git_dir.join(SCE_CHECKOUT_DIR); + std::fs::create_dir_all(&checkout_dir).with_context(|| { + format!( + "Failed to create checkout directory '{}'", + checkout_dir.display() + ) + })?; + + let checkout_id_path = checkout_dir.join(CHECKOUT_ID_FILE); + std::fs::write(&checkout_id_path, &checkout_id).with_context(|| { + format!( + "Failed to write checkout ID to '{}'", + checkout_id_path.display() + ) + })?; + + Ok(checkout_id) +} + +/// Convenience function that resolves the git directory from a repository root +/// and then gets or creates the checkout ID. +/// +/// This combines `resolve_git_dir` and `get_or_create_checkout_id` into a +/// single call for callers that have a repository root but not a git directory. +pub fn resolve_checkout_id_for_repo(repo_root: &Path) -> Result { + let git_dir = resolve_git_dir(repo_root)?; + get_or_create_checkout_id(&git_dir) +} diff --git a/cli/src/services/checkout/registry.rs b/cli/src/services/checkout/registry.rs new file mode 100644 index 00000000..e410d115 --- /dev/null +++ b/cli/src/services/checkout/registry.rs @@ -0,0 +1,190 @@ +//! Central checkout registry. +//! +//! The checkout registry is a JSON file at `/sce/checkout-registry.json` +//! that tracks all known checkouts with metadata like path, last-seen timestamp, +//! remote URL, and per-checkout database path. + +#![allow(dead_code)] + +use std::path::PathBuf; + +use anyhow::{Context, Result}; +use serde::{Deserialize, Serialize}; + +use crate::services::default_paths::resolve_state_data_root; + +/// File name for the checkout registry inside `/sce/`. +const CHECKOUT_REGISTRY_FILE: &str = "checkout-registry.json"; + +/// A single checkout record in the central registry. +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub struct CheckoutRecord { + /// Stable `UUIDv7` checkout identity. + pub checkout_id: String, + /// Absolute path to the repository root (or worktree root). + pub path: String, + /// ISO 8601 / RFC 3339 timestamp of the last time this checkout was seen. + pub last_seen: String, + /// Remote URL if available (e.g. the `origin` URL). + pub remote_url: Option, + /// Absolute path to the per-checkout database file, if it has been created. + pub database_path: Option, +} + +/// The central checkout registry, persisted as a JSON file. +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] +pub struct CheckoutRegistry { + /// Registered checkouts, keyed by checkout ID for efficient lookup. + /// Serialized as a JSON array for human readability. + #[serde(default)] + pub checkouts: Vec, +} + +/// Returns the canonical path to the checkout registry file. +/// +/// The path is `/sce/checkout-registry.json`, where `state_root` +/// comes from the shared default-path catalog. +pub fn checkout_registry_path() -> Result { + Ok(resolve_state_data_root()? + .join("sce") + .join(CHECKOUT_REGISTRY_FILE)) +} + +/// Reads the checkout registry from disk. +/// +/// Returns an empty registry if the file does not exist. +/// Returns an error if the file exists but cannot be parsed. +pub fn read_registry() -> Result { + let path = checkout_registry_path()?; + + if !path.exists() { + return Ok(CheckoutRegistry::default()); + } + + let content = std::fs::read_to_string(&path) + .with_context(|| format!("Failed to read checkout registry from '{}'", path.display()))?; + + let registry: CheckoutRegistry = serde_json::from_str(&content).with_context(|| { + format!( + "Failed to parse checkout registry from '{}'", + path.display() + ) + })?; + + Ok(registry) +} + +/// Writes the checkout registry to disk using atomic write-through-rename. +/// +/// This creates the parent directory if it doesn't exist, writes to a temporary +/// file, and then renames the temporary file to the target path. This ensures +/// that concurrent readers never see a partially-written registry. +pub fn write_registry(registry: &CheckoutRegistry) -> Result<()> { + let path = checkout_registry_path()?; + + // Ensure the parent directory exists. + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent).with_context(|| { + format!("Failed to create registry directory '{}'", parent.display()) + })?; + } + + // Serialize the registry. + let content = serde_json::to_string_pretty(registry) + .with_context(|| "Failed to serialize checkout registry")?; + + // Write to a temporary file first, then rename for atomicity. + let temp_path = path.with_extension("json.tmp"); + + std::fs::write(&temp_path, &content).with_context(|| { + format!( + "Failed to write temporary registry to '{}'", + temp_path.display() + ) + })?; + + std::fs::rename(&temp_path, &path).with_context(|| { + format!( + "Failed to rename temporary registry from '{}' to '{}'", + temp_path.display(), + path.display() + ) + })?; + + Ok(()) +} + +/// Registers a checkout in the central registry. +/// +/// If a checkout with the same `checkout_id` already exists, its `path`, +/// `last_seen`, and `remote_url` fields are updated. The `database_path` field +/// is preserved from the existing record if present. +/// +/// After registration, the registry is persisted to disk. +pub fn register_checkout(record: CheckoutRecord) -> Result<()> { + let mut registry = read_registry()?; + + if let Some(existing) = registry + .checkouts + .iter_mut() + .find(|r| r.checkout_id == record.checkout_id) + { + // Update existing record, preserving database_path if the new record + // doesn't have one. + existing.path = record.path; + existing.last_seen = record.last_seen; + existing.remote_url = record.remote_url; + if record.database_path.is_some() { + existing.database_path = record.database_path; + } + } else { + registry.checkouts.push(record); + } + + write_registry(®istry) +} + +/// Updates the `last_seen` timestamp for a checkout in the registry. +/// +/// Returns `Ok(())` if the checkout was found and updated, or an error if the +/// checkout ID is not found in the registry. +pub fn update_checkout_last_seen(checkout_id: &str, last_seen: &str) -> Result<()> { + let mut registry = read_registry()?; + + let record = registry + .checkouts + .iter_mut() + .find(|r| r.checkout_id == checkout_id) + .ok_or_else(|| anyhow::anyhow!("Checkout ID '{checkout_id}' not found in registry"))?; + + record.last_seen = last_seen.to_string(); + write_registry(®istry) +} + +/// Lists all registered checkouts from the central registry. +/// +/// Returns an empty list if the registry file does not exist. +pub fn list_checkouts() -> Result> { + let registry = read_registry()?; + Ok(registry.checkouts) +} + +/// Removes a checkout from the central registry by checkout ID. +/// +/// Returns `Ok(true)` if the checkout was found and removed, `Ok(false)` if +/// the checkout was not found, or an error if the registry could not be +/// read or written. +pub fn remove_checkout(checkout_id: &str) -> Result { + let mut registry = read_registry()?; + + let original_len = registry.checkouts.len(); + registry.checkouts.retain(|r| r.checkout_id != checkout_id); + + let removed = registry.checkouts.len() < original_len; + + if removed { + write_registry(®istry)?; + } + + Ok(removed) +} diff --git a/cli/src/services/mod.rs b/cli/src/services/mod.rs index bfc00206..f7ca8fcd 100644 --- a/cli/src/services/mod.rs +++ b/cli/src/services/mod.rs @@ -7,6 +7,7 @@ pub mod auth_db; #[allow(dead_code)] pub mod bash_policy; pub mod capabilities; +pub mod checkout; pub mod command_registry; pub mod completion; pub mod config; diff --git a/context/architecture.md b/context/architecture.md index 099715bd..17b38c0d 100644 --- a/context/architecture.md +++ b/context/architecture.md @@ -125,7 +125,7 @@ The repository includes a new placeholder Rust binary crate at `cli/`. - No user-invocable `sce sync` command is wired in the current runtime; local DB and Agent Trace DB bootstrap flows through lifecycle providers aggregated by setup, and DB health/repair flows through the doctor surface. - `cli/src/services/patch.rs` defines the standalone patch domain model (`ParsedPatch`, `PatchFileChange`, `FileChangeKind`, `PatchHunk`, `TouchedLine`, `TouchedLineKind`) for in-memory parsed unified-diff representation, capturing only touched lines (added/removed) plus minimal per-file/per-hunk metadata while excluding non-hunk headers and unchanged context lines. All types are `serde`-serializable/deserializable with `snake_case` JSON field naming. The module also provides `parse_patch`, a public parser function that converts raw unified-diff text (both `Index:` SVN-style and `diff --git` git-style formats) into `ParsedPatch` structs, with `ParseError` for actionable malformed-input diagnostics. Storage-agnostic JSON load helpers (`load_patch_from_json` for string input, `load_patch_from_json_bytes` for byte input) reconstruct `ParsedPatch` from serialized JSON content with `PatchLoadError` for actionable deserialization diagnostics. Its patch-set operations now include deterministic ordered combination plus target-shaped intersection that prefers exact touched-line matches and falls back to historical `kind`+`content` matching when incremental diffs and canonical post-commit diffs have drifted line numbers; `parse_patch`, `combine_patches`, and `intersect_patches` are consumed by the active post-commit hook runtime. - `cli/src/services/structured_patch.rs` defines the synchronous structured editor-hook derivation seam. It derives Claude `PostToolUse` `Write` create and `Edit` structured-patch payloads into canonical `ParsedPatch` values plus Claude session/tool metadata, returning deterministic skip reasons for unsupported events/tools/payload shapes. The module is pure and side-effect-free. It is wired into `sce hooks diff-trace` for Claude payload classification at intake (T04) and into `AgentTraceDb::recent_diff_trace_patches` for post-commit structured payload parsing dispatch at read time (T05). -- `cli/src/services/` contains module boundaries for command_registry, lifecycle, auth_command, config, setup, doctor, hooks, bash_policy, version, completion, help, patch, SCE web URL helpers, shared database infrastructure, local DB adapters, encrypted auth DB adapters, and Agent Trace DB adapters with explicit trait seams for future implementations. `cli/src/services/bash_policy.rs` owns both the CLI-agnostic evaluator logic and the hidden `sce policy bash` command adapter used by OpenCode and Claude hook callers. `cli/src/services/command_registry.rs` defines the static `RuntimeCommand` enum, deterministic `CommandRegistry` name catalog, and `build_default_registry()` function for command dispatch metadata. Service-owned command modules own the runtime command payload structs for help/help-text, version, completion, auth, config, setup, doctor, hooks, and policy. +- `cli/src/services/` contains module boundaries for command_registry, lifecycle, auth_command, config, setup, doctor, hooks, checkout identity, bash_policy, version, completion, help, patch, SCE web URL helpers, shared database infrastructure, local DB adapters, encrypted auth DB adapters, and Agent Trace DB adapters with explicit trait seams for future implementations. `cli/src/services/checkout/` currently owns checkout ID file infrastructure plus central checkout-registry JSON persistence, but setup/hooks/doctor/Agent Trace DB consumers are not yet integrated with it. `cli/src/services/bash_policy.rs` owns both the CLI-agnostic evaluator logic and the hidden `sce policy bash` command adapter used by OpenCode and Claude hook callers. `cli/src/services/command_registry.rs` defines the static `RuntimeCommand` enum, deterministic `CommandRegistry` name catalog, and `build_default_registry()` function for command dispatch metadata. Service-owned command modules own the runtime command payload structs for help/help-text, version, completion, auth, config, setup, doctor, hooks, and policy. - `cli/README.md` is the crate-local onboarding and usage source of truth for placeholder behavior, safety limitations, and roadmap mapping back to service contracts. - `flake.nix` applies `rust-overlay` (`oxalica/rust-overlay`) to nixpkgs, pins `rust-bin.stable.1.93.1.default` with `rustfmt` + `clippy`, reads the package/check version from repo-root `.version`, builds `packages.sce` through Crane (`buildDepsOnly` -> `buildPackage`) with a filtered repo-root source that preserves the Cargo tree plus `cli/assets/hooks`, then injects generated OpenCode/Claude config payloads and schema inputs into a temporary `cli/assets/generated/` mirror during derivation unpack so `cli/build.rs` can package the crate without requiring committed generated crate assets. The same `cli/build.rs` now scans `cli/migrations/*/*.sql` and writes `cli/src/generated_migrations.rs` with deterministic migration constants sorted by numeric filename prefix. The flake runs `cli-tests`, `cli-clippy`, and `cli-fmt` plus the dedicated `integrations-install-tests`, `integrations-install-clippy`, and `integrations-install-fmt` derivations through Crane-backed paths so both Rust crates have first-class default-flake verification, exposes directory-scoped JS validation derivations for both `npm/` and the shared `config/lib/` plugin package root, and also exposes the non-default `apps.install-channel-integration-tests` flake app for install-channel integration coverage outside the default check set. The shared config-lib source set is rooted at `config/lib/` and includes the shared `package.json`, `bun.lock`, and `tsconfig.json` plus `agent-trace-plugin/` and `bash-policy-plugin/`; `config-lib-bun-tests` runs the bash-policy plugin wrapper tests from that shared root, while `config-lib-biome-check` and `config-lib-biome-format` run Biome over the copied shared package source. `.github/workflows/publish-crates.yml` follows the same asset-preparation rule but runs Cargo packaging from a temporary clean repository copy so crates.io publish no longer needs `--allow-dirty`. - The config-lib check source preserves repo-relative access to shared CLI patch fixtures: Nix copies a filtered repo-shaped source containing `config/lib/**` plus `cli/src/services/structured_patch/fixtures`, then runs Bun/Biome from `config/lib/`. diff --git a/context/cli/checkout-identity.md b/context/cli/checkout-identity.md new file mode 100644 index 00000000..b7c27b43 --- /dev/null +++ b/context/cli/checkout-identity.md @@ -0,0 +1,30 @@ +# Checkout Identity Service + +The checkout identity service lives in `cli/src/services/checkout/`. + +It provides the initial infrastructure for assigning a stable identity to a local Git checkout or linked Git worktree. Integration with setup, hooks, per-checkout Agent Trace database resolution, and doctor reporting is deferred to later tasks in `context/plans/agent-trace-checkout-identity.md`. + +## Current code surface + +- `cli/src/services/checkout/mod.rs` + - `resolve_git_dir(repo_root)` runs `git rev-parse --git-dir` from the supplied repository root. + - `read_checkout_id(git_dir)` reads `/sce/checkout-id` and validates non-empty UUID syntax. + - `get_or_create_checkout_id(git_dir)` reuses an existing ID or writes a new UUIDv7 checkout ID to `/sce/checkout-id`. + - `resolve_checkout_id_for_repo(repo_root)` combines Git directory resolution with get-or-create checkout ID behavior. +- `cli/src/services/checkout/registry.rs` + - `CheckoutRecord` serializes `checkout_id`, `path`, `last_seen`, optional `remote_url`, and optional `database_path`. + - `CheckoutRegistry` serializes the registry as `{ "checkouts": [...] }`. + - Registry persistence uses `/sce/checkout-registry.json` and atomic write-through-rename. + - Registry operations include register, update last seen, list, and remove. + +## Current integration state + +The module is registered through `cli/src/services/mod.rs` but is not yet called by setup, hooks, doctor, or Agent Trace DB resolution. + +The existing global Agent Trace database path remains the active runtime path until later plan tasks switch consumers to per-checkout database resolution. + +## Testing boundary + +No unit tests are currently included for this filesystem/Git-facing service. Filesystem, Git repository, and database behaviors should be covered in integration tests rather than unit tests per `context/patterns.md`. + +See also: `context/cli/default-path-catalog.md`, `context/sce/agent-trace-db.md`, `context/sce/agent-trace-hooks-command-routing.md`. diff --git a/context/context-map.md b/context/context-map.md index f37ed63e..f95bc0be 100644 --- a/context/context-map.md +++ b/context/context-map.md @@ -11,6 +11,7 @@ Feature/domain context: - `context/cli/cli-command-surface.md` (CLI command surface including top-level help with ASCII art banner and gradient rendering, setup install flow, WorkOS device authorization flow + token storage behavior, attribution-only hook routing with validated post-commit `--remote-url` plumbing plus DB-backed `diff-trace` dual persistence and post-commit Agent Trace payload persistence including range `content_hash`, setup-owned local DB + Agent Trace DB bootstrap plus doctor DB health coverage, centralized Rust SCE web URL helpers in `services::agent_trace`, nested flake release package/app installability, Cargo local install + crates.io readiness policy, and hidden `sce policy bash` command adapter for bash-policy hook callers; `sce sync` command wiring is deferred to `0.4.0`; static `RuntimeCommand` enum dispatch lives in `services/command_registry.rs`, command payload structs for help/version/completion/auth/config/setup/doctor/hooks/policy are owned by their respective `services/{name}/command.rs` files, and clap-to-runtime conversion lives in `services/parse/command_runtime.rs`) - `context/cli/default-path-catalog.md` (canonical production CLI path-ownership contract centered on `cli/src/services/default_paths.rs`, including persisted auth/config files, named DB paths for auth/local/Agent Trace databases, repo-relative, embedded-asset, install, hook, and context-path families plus the regression guard that keeps production path ownership centralized) +- `context/cli/checkout-identity.md` (current checkout identity infrastructure in `cli/src/services/checkout/`, including `/sce/checkout-id` UUIDv7 storage, central `/sce/checkout-registry.json` records, atomic registry persistence, and the current no-integration boundary before setup/hooks/doctor/Agent Trace DB consumers are switched) - `context/cli/patch-service.md` (standalone patch domain model, parser, JSON load helpers, and set operations in `cli/src/services/patch.rs` for in-memory parsed unified-diff representation, capturing only touched lines plus minimal per-file/per-hunk metadata, supporting both `Index:` SVN-style and `diff --git` git-style formats, with `ParseError` for actionable malformed-input diagnostics, `PatchLoadError`/`load_patch_from_json`/`load_patch_from_json_bytes` for storage-agnostic JSON reconstruction, `intersect_patches` for target-shaped overlap with exact-match-first and historical `kind`+`content` fallback semantics plus matched-constructed-line `session_id` and matched-constructed-hunk `model_id` provenance inheritance, and `combine_patches` for ordered patch combination with later-wins conflict resolution plus winning-hunk `model_id` provenance inheritance; `parse_patch`, `intersect_patches`, and `combine_patches` are consumed by the active post-commit hook runtime) - `context/cli/styling-service.md` (CLI text-mode output styling with `owo-colors` and `comfy-table`, TTY/`NO_COLOR` policy, shared helper API for human-facing surfaces, and per-column right-to-left RGB gradient banner rendering) - `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, canonical Pkl-generated `sce/config.json` schema ownership plus CLI embedding/reuse contract, config-file selection order, `show` provenance output, and trimmed `validate` output contract) diff --git a/context/plans/agent-trace-checkout-identity.md b/context/plans/agent-trace-checkout-identity.md index f9c88389..47418973 100644 --- a/context/plans/agent-trace-checkout-identity.md +++ b/context/plans/agent-trace-checkout-identity.md @@ -47,12 +47,16 @@ Give each cloned repository (and linked Git worktree) its own `agent-trace` chec ## Task stack -- [ ] T01: `Checkout identity infrastructure` (status:todo) +- [x] T01: `Checkout identity infrastructure` (status:done) - Task ID: T01 - Goal: Add a `cli/src/services/checkout/` module with checkout ID storage (`/sce/checkout-id`) and a central JSON registry (`/sce/checkout-registry.json`). - Boundaries (in/out of scope): In — new `checkout/mod.rs` with `resolve_git_dir(repo_root)`, `get_or_create_checkout_id(git_dir)`, `read_checkout_id(git_dir)`, and `resolve_checkout_id_for_repo(repo_root)` (convenience combining the two); new `checkout/registry.rs` with `CheckoutRecord` struct carrying `checkout_id`, `path`, `last_seen` (RFC 3339), `remote_url` (optional), and `database_path` (optional); `CheckoutRegistry` with `register_checkout()`, `update_checkout_last_seen()`, `list_checkouts()`, and `remove_checkout()`; atomic write-through-rename for persistence. Checkout ID is a UUIDv7 generated with the `uuid` crate (already a dependency). Out — setup/hook integration, DB path changes, schema changes, doctor changes. - Done when: `cargo build` compiles the new modules; focused unit tests for checkout ID create/read/idempotent-reuse, `resolve_git_dir` against a real git repo, and registry register/list/update pass. - Verification notes (commands or checks): `nix develop -c sh -c 'cd cli && cargo test checkout'` during development; final validation via `nix flake check`. + - Completed: 2026-06-16 + - Files changed: `cli/src/services/checkout/mod.rs`, `cli/src/services/checkout/registry.rs`, `cli/src/services/mod.rs` + - Evidence: `nix flake check` passed. A local `cargo build` reached final `rustc`/link and was terminated by SIGTERM in the environment without Rust diagnostics; the Crane-backed flake validation completed successfully. + - Notes: Generated unit tests and their temporary helper/dependency were removed after review feedback; T01 now lands the checkout identity infrastructure surface only. - [ ] T02: `Integrate checkout detection into setup lifecycle` (status:todo) - Task ID: T02 From b65687144a33a000bef9e8c86c342a7652070b67 Mon Sep 17 00:00:00 2001 From: David Abram Date: Tue, 16 Jun 2026 14:02:33 +0200 Subject: [PATCH 3/8] agent_trace_db: Integrate checkout identity into setup lifecycle AgentTraceDbLifecycle::setup() now resolves the checkout identity from ctx.repo_root() when available, creates or reuses /sce/checkout-id, and registers the checkout in the central registry with database_path: null. SetupOutcome gains a generic messages Vec so lifecycle providers can emit setup-time diagnostics Co-authored-by: SCE --- cli/src/generated_migrations.rs | 1 - cli/src/services/agent_trace_db/lifecycle.rs | 57 ++++++++++++++++++- cli/src/services/hooks/lifecycle.rs | 1 + cli/src/services/lifecycle.rs | 1 + cli/src/services/setup/command.rs | 2 + context/architecture.md | 6 +- context/cli/checkout-identity.md | 11 +++- context/cli/service-lifecycle.md | 4 +- context/context-map.md | 4 +- context/overview.md | 2 +- .../plans/agent-trace-checkout-identity.md | 6 +- context/sce/agent-trace-db.md | 2 +- 12 files changed, 83 insertions(+), 14 deletions(-) diff --git a/cli/src/generated_migrations.rs b/cli/src/generated_migrations.rs index a149c01f..40ee4959 100644 --- a/cli/src/generated_migrations.rs +++ b/cli/src/generated_migrations.rs @@ -26,4 +26,3 @@ pub static AUTH_MIGRATIONS: &[(&str, &str)] = &[ ("001_create_auth_tokens", include_str!("../migrations/auth/001_create_auth_tokens.sql")), ("002_create_auth_credentials_updated_at_trigger", include_str!("../migrations/auth/002_create_auth_credentials_updated_at_trigger.sql")), ]; - diff --git a/cli/src/services/agent_trace_db/lifecycle.rs b/cli/src/services/agent_trace_db/lifecycle.rs index cbdab96a..e2783e6e 100644 --- a/cli/src/services/agent_trace_db/lifecycle.rs +++ b/cli/src/services/agent_trace_db/lifecycle.rs @@ -1,6 +1,8 @@ use anyhow::{Context, Result}; +use chrono::Utc; use crate::app::HasRepoRoot; +use crate::services::checkout::{self, registry}; use crate::services::db::{bootstrap_db_parent, collect_db_path_health, DbSpec}; use crate::services::default_paths::agent_trace_db_path; use crate::services::lifecycle::{ @@ -50,13 +52,64 @@ impl ServiceLifecycle for AgentTraceDbLifecycle { } } - fn setup(&self, _ctx: &C) -> Result { + fn setup(&self, ctx: &C) -> Result { + let checkout_setup = ctx + .repo_root() + .map(setup_checkout_identity) + .transpose() + .context("Agent trace DB lifecycle setup failed while resolving checkout identity")?; + AgentTraceDb::new() .context("Agent trace DB lifecycle setup failed while initializing agent trace DB")?; - Ok(SetupOutcome::default()) + + Ok(SetupOutcome { + messages: checkout_setup + .iter() + .map(format_checkout_identity_setup_message) + .collect(), + ..SetupOutcome::default() + }) } } +#[derive(Clone, Debug, Eq, PartialEq)] +struct CheckoutIdentitySetup { + checkout_id: String, +} + +fn setup_checkout_identity(repo_root: &std::path::Path) -> Result { + let git_dir = checkout::resolve_git_dir(repo_root).with_context(|| { + format!( + "failed to resolve git directory for checkout identity from '{}'", + repo_root.display() + ) + })?; + let checkout_id = checkout::get_or_create_checkout_id(&git_dir).with_context(|| { + format!( + "failed to get or create checkout identity under '{}'", + git_dir.display() + ) + })?; + + registry::register_checkout(registry::CheckoutRecord { + checkout_id: checkout_id.clone(), + path: repo_root.display().to_string(), + last_seen: Utc::now().to_rfc3339(), + remote_url: None, + database_path: None, + }) + .context("failed to register checkout identity")?; + + Ok(CheckoutIdentitySetup { checkout_id }) +} + +fn format_checkout_identity_setup_message(setup: &CheckoutIdentitySetup) -> String { + format!( + "Agent Trace checkout identity: {}\nAgent Trace database will be created on first write.", + setup.checkout_id + ) +} + pub fn diagnose_agent_trace_db_health() -> Vec { let mut problems = Vec::new(); diff --git a/cli/src/services/hooks/lifecycle.rs b/cli/src/services/hooks/lifecycle.rs index a46f7108..8672fbf0 100644 --- a/cli/src/services/hooks/lifecycle.rs +++ b/cli/src/services/hooks/lifecycle.rs @@ -106,6 +106,7 @@ impl ServiceLifecycle for HooksLifecycle { Ok(SetupOutcome { required_hooks_install: Some(required_hooks_outcome_from_setup(outcome)), + ..SetupOutcome::default() }) } } diff --git a/cli/src/services/lifecycle.rs b/cli/src/services/lifecycle.rs index 5ea32311..8eb31e0b 100644 --- a/cli/src/services/lifecycle.rs +++ b/cli/src/services/lifecycle.rs @@ -105,6 +105,7 @@ pub struct RequiredHooksInstallOutcome { #[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct SetupOutcome { + pub messages: Vec, pub required_hooks_install: Option, } diff --git a/cli/src/services/setup/command.rs b/cli/src/services/setup/command.rs index 0194a987..e0d19a48 100644 --- a/cli/src/services/setup/command.rs +++ b/cli/src/services/setup/command.rs @@ -49,6 +49,8 @@ impl SetupCommand { .setup(&ctx) .map_err(|error| ClassifiedError::runtime(format!("{error:#}")))?; + sections.extend(outcome.messages); + if let Some(ref hooks_outcome) = outcome.required_hooks_install { sections.push(setup::format_required_hook_install_success_message( &setup_required_hooks_outcome_from_lifecycle(hooks_outcome), diff --git a/context/architecture.md b/context/architecture.md index 17b38c0d..052129b5 100644 --- a/context/architecture.md +++ b/context/architecture.md @@ -105,12 +105,12 @@ The repository includes a new placeholder Rust binary crate at `cli/`. - `cli/src/services/output_format.rs` defines the canonical shared CLI output-format contract (`OutputFormat`) for supporting commands, with deterministic `text|json` parsing and command-scoped actionable invalid-value guidance. - `cli/src/services/config/types.rs` is the canonical owner for the shared runtime/config primitive seam used by the CLI: `LogLevel`, `LogFormat`, `LogFileMode`, the observability env-key constants, and the shared bool parsing helpers used by both config resolution and observability bootstrap; `cli/src/services/config/mod.rs` re-exports those primitives through the facade. - `cli/src/services/capabilities.rs` defines the current broad CLI capability traits consumed by the borrowed, compile-time-typed `AppContext`: `FsOps` with `StdFsOps` for filesystem operations and `GitOps` with `ProcessGitOps` for git command execution plus repository-root/hooks-directory resolution. Existing service internals do not consume these traits directly yet; command execution uses narrow accessors and repo-root-scoped context derivation. -- `cli/src/services/lifecycle.rs` defines the current compile-safe lifecycle seam. `ServiceLifecycle` has default no-op generic `diagnose`, `fix`, and `setup` methods over `C: HasRepoRoot`, with lifecycle-owned health, fix, and setup result types so the trait contract is not publicly anchored to doctor/setup module types or the full `AppContext` shape. The same module owns the static `LifecycleProvider` enum and shared `lifecycle_providers(include_hooks)` catalog/factory, returning providers in deterministic order (config → local_db → auth_db → agent_trace_db → hooks when requested); enum dispatch calls each concrete provider through generic context methods without boxed lifecycle-provider allocation or repo-root trait-object context erasure. Hooks exposes a `HooksLifecycle` provider in `cli/src/services/hooks/lifecycle.rs` for hook rollout diagnosis/fix/setup using lifecycle-owned health records plus the canonical required-hook installer. Config exposes a `ConfigLifecycle` provider in `cli/src/services/config/lifecycle.rs` for global/repo-local config validation and repo-local `.sce/config.json` bootstrap. local_db exposes a `LocalDbLifecycle` provider in `cli/src/services/local_db/lifecycle.rs` for canonical local DB path health, parent-directory readiness/bootstrap, and `LocalDb::new()` setup. auth_db exposes an `AuthDbLifecycle` provider in `cli/src/services/auth_db/lifecycle.rs` for canonical auth DB path health, parent-directory readiness/bootstrap, and `AuthDb::new()` setup. agent_trace_db exposes an `AgentTraceDbLifecycle` provider in `cli/src/services/agent_trace_db/lifecycle.rs` for canonical Agent Trace DB path health, parent-directory readiness/bootstrap, and `AgentTraceDb::new()` setup. Doctor runtime aggregates the full provider catalog for `diagnose` and `fix` and adapts lifecycle records into doctor report/fix records at the orchestration boundary; setup command aggregates the shared catalog for `setup` with hooks included only when requested and adapts hook setup outcomes before rendering setup-owned messages. +- `cli/src/services/lifecycle.rs` defines the current compile-safe lifecycle seam. `ServiceLifecycle` has default no-op generic `diagnose`, `fix`, and `setup` methods over `C: HasRepoRoot`, with lifecycle-owned health, fix, and setup result types so the trait contract is not publicly anchored to doctor/setup module types or the full `AppContext` shape. The same module owns the static `LifecycleProvider` enum and shared `lifecycle_providers(include_hooks)` catalog/factory, returning providers in deterministic order (config → local_db → auth_db → agent_trace_db → hooks when requested); enum dispatch calls each concrete provider through generic context methods without boxed lifecycle-provider allocation or repo-root trait-object context erasure. Hooks exposes a `HooksLifecycle` provider in `cli/src/services/hooks/lifecycle.rs` for hook rollout diagnosis/fix/setup using lifecycle-owned health records plus the canonical required-hook installer. Config exposes a `ConfigLifecycle` provider in `cli/src/services/config/lifecycle.rs` for global/repo-local config validation and repo-local `.sce/config.json` bootstrap. local_db exposes a `LocalDbLifecycle` provider in `cli/src/services/local_db/lifecycle.rs` for canonical local DB path health, parent-directory readiness/bootstrap, and `LocalDb::new()` setup. auth_db exposes an `AuthDbLifecycle` provider in `cli/src/services/auth_db/lifecycle.rs` for canonical auth DB path health, parent-directory readiness/bootstrap, and `AuthDb::new()` setup. agent_trace_db exposes an `AgentTraceDbLifecycle` provider in `cli/src/services/agent_trace_db/lifecycle.rs` for canonical Agent Trace DB path health, parent-directory readiness/bootstrap, setup-time checkout identity creation/registry registration when a repo root is available, and current global `AgentTraceDb::new()` setup until per-checkout DB resolution lands. Doctor runtime aggregates the full provider catalog for `diagnose` and `fix` and adapts lifecycle records into doctor report/fix records at the orchestration boundary; setup command aggregates the shared catalog for `setup` with hooks included only when requested and adapts hook setup outcomes before rendering setup-owned messages. - `cli/src/services/auth_command/mod.rs` defines the implemented auth command surface for `sce auth login|renew|logout|status`, including device-flow login, stored-token renewal (`--force` supported for renew), logout, and status rendering in text/JSON formats; `cli/src/services/auth_command/command.rs` owns the `AuthCommand` payload used by the static `RuntimeCommand` enum. - `cli/src/services/db/mod.rs` provides the shared generic Turso infrastructure seam: `DbSpec` supplies a service-specific name, path, ordered embedded migrations, and config-file lookup key (`db_config_key()`), while `TursoDb` owns parent-directory creation, `Builder::new_local(...)` initialization (with `experimental_multiprocess_wal(true)` for safe concurrent access), Turso connection setup, tokio current-thread runtime bridging, retry-backed blocking `execute`/`query`/`query_map` wrappers, and generic migration execution with per-database `__sce_migrations` metadata. `TursoDb::new()` and `EncryptedTursoDb::new()` wrap only their local open/connect block in `run_with_retry_sync` using a config-driven connection-open policy resolved from the `DATABASE_RETRY_CONFIG` `OnceLock` with fallback to hardcoded defaults, while both adapters' `execute()`, `query()`, and `query_map()` methods use a config-driven operation policy from the same source. Both adapters' `query_map()` methods retry the initial query and row-fetch loop, then apply caller row mapping after retry completion. Migration execution is not retried. The same module also provides `EncryptedTursoDb`, a structurally parallel encrypted adapter that resolves the encryption key through `encryption_key::get_or_create_encryption_key()`, enables Turso local encryption with strict `aegis256` cipher selection, and exposes retry-backed synchronous wrappers plus migration execution. `cli/src/services/db/encryption_key.rs` first derives a Turso-compatible 64-character hex key from non-empty `SCE_AUTH_DB_ENCRYPTION_KEY` env-secret text when present, otherwise falls back to keyring-backed credential-store get-or-create behavior; no plaintext auth DB fallback exists. - `cli/src/services/local_db/mod.rs` provides the concrete local DB spec and `LocalDb` type alias over the shared generic `TursoDb` adapter. `LocalDbSpec` resolves the deterministic persistent runtime DB target through the shared default-path seam and declares no local migrations; `TursoDb` supplies retry-backed blocking `execute`/`query`, parent-directory creation, Turso connection setup, tokio current-thread runtime bridging, and generic migration execution. - `cli/src/services/auth_db/mod.rs` provides the encrypted auth DB spec and `AuthDb` type alias over `EncryptedTursoDb`. `AuthDbSpec` resolves `/sce/auth.db` through the shared default-path seam and embeds ordered auth migrations. Auth DB lifecycle setup/doctor integration is wired through `AuthDbLifecycle`; auth command/token-storage reads/writes are directed through `token_storage.rs`. -- `cli/src/services/agent_trace_db/mod.rs` provides the Agent Trace DB spec and `AgentTraceDb` type alias over `TursoDb`. `AgentTraceDbSpec` resolves `/sce/agent-trace.db` through the shared default-path seam and embeds ordered fresh-start migrations `001..016` for `diff_traces`, `post_commit_patch_intersections`, `agent_traces`, `messages`, `parts`, `session_models`, `diff_traces.payload_type`, indexes, and triggers. `agent_traces.agent_trace_id` is `NOT NULL UNIQUE`. The module provides `DiffTraceInsert<'_>`/`insert_diff_trace()` including the `payload_type` discriminator, `PostCommitPatchIntersectionInsert<'_>`/`insert_post_commit_patch_intersection()`, `AgentTraceInsert<'_>`/`insert_agent_trace()`, `InsertMessageInsert`/`insert_message()` plus `insert_messages()` for parent message inserts with duplicate `(session_id, message_id)` writes ignored, `InsertPartInsert`/`insert_part()` plus `insert_parts()` for append-only part text writes, `SessionModelUpsert<'_>`/`upsert_session_model()` plus lookup helpers for durable `session_models` attribution, and `recent_diff_trace_patches(cutoff_time_ms, end_time_ms)` for inclusive chronological `diff_traces` reads that dispatch on `payload_type`, parse valid raw patch or structured payload rows, and return skipped malformed-row reports. `cli/src/services/agent_trace_db/lifecycle.rs` registers Agent Trace DB setup/doctor lifecycle behavior through migration-running `AgentTraceDb::new()`; active hook runtime writes/read paths use `AgentTraceDb::open_for_hooks_without_migrations()` followed by `ensure_schema_ready_for_hooks()` and fail with setup guidance when migration metadata is missing or incomplete. +- `cli/src/services/agent_trace_db/mod.rs` provides the Agent Trace DB spec and `AgentTraceDb` type alias over `TursoDb`. `AgentTraceDbSpec` resolves `/sce/agent-trace.db` through the shared default-path seam and embeds ordered fresh-start migrations `001..016` for `diff_traces`, `post_commit_patch_intersections`, `agent_traces`, `messages`, `parts`, `session_models`, `diff_traces.payload_type`, indexes, and triggers. `agent_traces.agent_trace_id` is `NOT NULL UNIQUE`. The module provides `DiffTraceInsert<'_>`/`insert_diff_trace()` including the `payload_type` discriminator, `PostCommitPatchIntersectionInsert<'_>`/`insert_post_commit_patch_intersection()`, `AgentTraceInsert<'_>`/`insert_agent_trace()`, `InsertMessageInsert`/`insert_message()` plus `insert_messages()` for parent message inserts with duplicate `(session_id, message_id)` writes ignored, `InsertPartInsert`/`insert_part()` plus `insert_parts()` for append-only part text writes, `SessionModelUpsert<'_>`/`upsert_session_model()` plus lookup helpers for durable `session_models` attribution, and `recent_diff_trace_patches(cutoff_time_ms, end_time_ms)` for inclusive chronological `diff_traces` reads that dispatch on `payload_type`, parse valid raw patch or structured payload rows, and return skipped malformed-row reports. `cli/src/services/agent_trace_db/lifecycle.rs` registers Agent Trace DB setup/doctor lifecycle behavior: setup creates/reuses and registers checkout identity when scoped to a repo root, then still runs migration-backed global `AgentTraceDb::new()` until the per-checkout DB switch lands; active hook runtime writes/read paths use `AgentTraceDb::open_for_hooks_without_migrations()` followed by `ensure_schema_ready_for_hooks()` and fail with setup guidance when migration metadata is missing or incomplete. - `cli/src/test_support.rs` provides a shared test-only temp-directory helper (`TestTempDir`) used by service tests that need filesystem fixtures. - `cli/src/services/setup/mod.rs` defines the setup command contract (`SetupMode`, `SetupTarget`, `SetupRequest`, CLI flag parser/validator), an `inquire`-backed interactive target prompter (`InquireSetupTargetPrompter`), setup dispatch outcomes (proceed/cancelled), compile-time embedded asset access (`EmbeddedAsset`, target-scoped iterators, required-hook asset iterators/lookups) generated by `cli/build.rs` from the ephemeral crate-local `cli/assets/generated/config/{opencode,claude}/**` mirror plus `cli/assets/hooks/**`, and focused internal support seams for install-flow vs prompt-flow logic; `cli/src/services/setup/command.rs` owns the `SetupCommand` payload used by the static `RuntimeCommand` enum and executes against any context implementing repo-root scoping. Its install engine/orchestrator stages embedded files and uses a unified remove-and-replace policy (removing existing targets before swapping staged content, with deterministic recovery guidance on swap failure and no backup artifact creation), and formats deterministic completion messaging; required-hook install orchestration (`install_required_git_hooks`) follows the same remove-and-replace policy (removing existing hooks before swapping staged content, with deterministic recovery guidance on swap failure). The setup command derives a repo-root-scoped context before aggregating static lifecycle provider `setup` dispatch across providers (config → local_db → auth_db → agent_trace_db → hooks when requested), so setup providers consume only repo-root access from the scoped context. - `cli/src/services/setup/mod.rs` keeps those responsibilities inside one file for now, but the current ownership split is explicit: the inline `install` module owns repository-path normalization, staging/swap install behavior, required-hook installation, and filesystem safety guards, while the inline `prompt` module owns interactive target selection and prompt styling. @@ -125,7 +125,7 @@ The repository includes a new placeholder Rust binary crate at `cli/`. - No user-invocable `sce sync` command is wired in the current runtime; local DB and Agent Trace DB bootstrap flows through lifecycle providers aggregated by setup, and DB health/repair flows through the doctor surface. - `cli/src/services/patch.rs` defines the standalone patch domain model (`ParsedPatch`, `PatchFileChange`, `FileChangeKind`, `PatchHunk`, `TouchedLine`, `TouchedLineKind`) for in-memory parsed unified-diff representation, capturing only touched lines (added/removed) plus minimal per-file/per-hunk metadata while excluding non-hunk headers and unchanged context lines. All types are `serde`-serializable/deserializable with `snake_case` JSON field naming. The module also provides `parse_patch`, a public parser function that converts raw unified-diff text (both `Index:` SVN-style and `diff --git` git-style formats) into `ParsedPatch` structs, with `ParseError` for actionable malformed-input diagnostics. Storage-agnostic JSON load helpers (`load_patch_from_json` for string input, `load_patch_from_json_bytes` for byte input) reconstruct `ParsedPatch` from serialized JSON content with `PatchLoadError` for actionable deserialization diagnostics. Its patch-set operations now include deterministic ordered combination plus target-shaped intersection that prefers exact touched-line matches and falls back to historical `kind`+`content` matching when incremental diffs and canonical post-commit diffs have drifted line numbers; `parse_patch`, `combine_patches`, and `intersect_patches` are consumed by the active post-commit hook runtime. - `cli/src/services/structured_patch.rs` defines the synchronous structured editor-hook derivation seam. It derives Claude `PostToolUse` `Write` create and `Edit` structured-patch payloads into canonical `ParsedPatch` values plus Claude session/tool metadata, returning deterministic skip reasons for unsupported events/tools/payload shapes. The module is pure and side-effect-free. It is wired into `sce hooks diff-trace` for Claude payload classification at intake (T04) and into `AgentTraceDb::recent_diff_trace_patches` for post-commit structured payload parsing dispatch at read time (T05). -- `cli/src/services/` contains module boundaries for command_registry, lifecycle, auth_command, config, setup, doctor, hooks, checkout identity, bash_policy, version, completion, help, patch, SCE web URL helpers, shared database infrastructure, local DB adapters, encrypted auth DB adapters, and Agent Trace DB adapters with explicit trait seams for future implementations. `cli/src/services/checkout/` currently owns checkout ID file infrastructure plus central checkout-registry JSON persistence, but setup/hooks/doctor/Agent Trace DB consumers are not yet integrated with it. `cli/src/services/bash_policy.rs` owns both the CLI-agnostic evaluator logic and the hidden `sce policy bash` command adapter used by OpenCode and Claude hook callers. `cli/src/services/command_registry.rs` defines the static `RuntimeCommand` enum, deterministic `CommandRegistry` name catalog, and `build_default_registry()` function for command dispatch metadata. Service-owned command modules own the runtime command payload structs for help/help-text, version, completion, auth, config, setup, doctor, hooks, and policy. +- `cli/src/services/` contains module boundaries for command_registry, lifecycle, auth_command, config, setup, doctor, hooks, checkout identity, bash_policy, version, completion, help, patch, SCE web URL helpers, shared database infrastructure, local DB adapters, encrypted auth DB adapters, and Agent Trace DB adapters with explicit trait seams for future implementations. `cli/src/services/checkout/` currently owns checkout ID file infrastructure plus central checkout-registry JSON persistence, but setup now uses it through `AgentTraceDbLifecycle::setup()`, while hooks, doctor, and per-checkout Agent Trace DB path consumers remain deferred. `cli/src/services/bash_policy.rs` owns both the CLI-agnostic evaluator logic and the hidden `sce policy bash` command adapter used by OpenCode and Claude hook callers. `cli/src/services/command_registry.rs` defines the static `RuntimeCommand` enum, deterministic `CommandRegistry` name catalog, and `build_default_registry()` function for command dispatch metadata. Service-owned command modules own the runtime command payload structs for help/help-text, version, completion, auth, config, setup, doctor, hooks, and policy. - `cli/README.md` is the crate-local onboarding and usage source of truth for placeholder behavior, safety limitations, and roadmap mapping back to service contracts. - `flake.nix` applies `rust-overlay` (`oxalica/rust-overlay`) to nixpkgs, pins `rust-bin.stable.1.93.1.default` with `rustfmt` + `clippy`, reads the package/check version from repo-root `.version`, builds `packages.sce` through Crane (`buildDepsOnly` -> `buildPackage`) with a filtered repo-root source that preserves the Cargo tree plus `cli/assets/hooks`, then injects generated OpenCode/Claude config payloads and schema inputs into a temporary `cli/assets/generated/` mirror during derivation unpack so `cli/build.rs` can package the crate without requiring committed generated crate assets. The same `cli/build.rs` now scans `cli/migrations/*/*.sql` and writes `cli/src/generated_migrations.rs` with deterministic migration constants sorted by numeric filename prefix. The flake runs `cli-tests`, `cli-clippy`, and `cli-fmt` plus the dedicated `integrations-install-tests`, `integrations-install-clippy`, and `integrations-install-fmt` derivations through Crane-backed paths so both Rust crates have first-class default-flake verification, exposes directory-scoped JS validation derivations for both `npm/` and the shared `config/lib/` plugin package root, and also exposes the non-default `apps.install-channel-integration-tests` flake app for install-channel integration coverage outside the default check set. The shared config-lib source set is rooted at `config/lib/` and includes the shared `package.json`, `bun.lock`, and `tsconfig.json` plus `agent-trace-plugin/` and `bash-policy-plugin/`; `config-lib-bun-tests` runs the bash-policy plugin wrapper tests from that shared root, while `config-lib-biome-check` and `config-lib-biome-format` run Biome over the copied shared package source. `.github/workflows/publish-crates.yml` follows the same asset-preparation rule but runs Cargo packaging from a temporary clean repository copy so crates.io publish no longer needs `--allow-dirty`. - The config-lib check source preserves repo-relative access to shared CLI patch fixtures: Nix copies a filtered repo-shaped source containing `config/lib/**` plus `cli/src/services/structured_patch/fixtures`, then runs Bun/Biome from `config/lib/`. diff --git a/context/cli/checkout-identity.md b/context/cli/checkout-identity.md index b7c27b43..3453246b 100644 --- a/context/cli/checkout-identity.md +++ b/context/cli/checkout-identity.md @@ -2,7 +2,7 @@ The checkout identity service lives in `cli/src/services/checkout/`. -It provides the initial infrastructure for assigning a stable identity to a local Git checkout or linked Git worktree. Integration with setup, hooks, per-checkout Agent Trace database resolution, and doctor reporting is deferred to later tasks in `context/plans/agent-trace-checkout-identity.md`. +It assigns a stable identity to a local Git checkout or linked Git worktree. The setup lifecycle now creates/reuses this identity and registers the checkout; hook integration, per-checkout Agent Trace database resolution, and doctor reporting are deferred to later tasks in `context/plans/agent-trace-checkout-identity.md`. ## Current code surface @@ -19,7 +19,14 @@ It provides the initial infrastructure for assigning a stable identity to a loca ## Current integration state -The module is registered through `cli/src/services/mod.rs` but is not yet called by setup, hooks, doctor, or Agent Trace DB resolution. +The module is registered through `cli/src/services/mod.rs` and is called by `AgentTraceDbLifecycle::setup()` during `sce setup` after the setup command has derived a repository-root-scoped context. + +During setup: + +- `checkout::resolve_git_dir(repo_root)` resolves the checkout metadata directory from Git truth. +- `checkout::get_or_create_checkout_id(git_dir)` creates or reuses `/sce/checkout-id`. +- `checkout::registry::register_checkout(...)` writes or updates the central registry record with `database_path: null`. +- Setup output includes the checkout ID and states that the Agent Trace database will be created on first write. The existing global Agent Trace database path remains the active runtime path until later plan tasks switch consumers to per-checkout database resolution. diff --git a/context/cli/service-lifecycle.md b/context/cli/service-lifecycle.md index 733fd80c..704d716d 100644 --- a/context/cli/service-lifecycle.md +++ b/context/cli/service-lifecycle.md @@ -11,6 +11,7 @@ - `HealthProblem`, `HealthCategory`, `HealthSeverity`, `HealthFixability`, and `HealthProblemKind` are lifecycle-owned types that mirror the current doctor taxonomy without making the trait depend on `doctor` module types. - `FixResultRecord` and `FixOutcome` are lifecycle-owned fix result types. - `SetupOutcome` is a minimal lifecycle-owned carrier for current setup result shapes: + - generic setup `messages` - optional lifecycle-owned `RequiredHooksInstallOutcome` - `LifecycleProvider` is a static enum over the concrete provider implementations, and `lifecycle_providers(include_hooks)` is the shared provider catalog/factory used by command orchestrators. The enum owns inherent generic `id`, `diagnose`, `fix`, and `setup` dispatch methods and does not allocate boxed provider trait objects or erase the lifecycle context to `&dyn HasRepoRoot`; provider dispatch remains compile-time typed over the narrow repo-root accessor. - Provider order is deterministic: `ConfigLifecycle` → `LocalDbLifecycle` → `AuthDbLifecycle` → `AgentTraceDbLifecycle` → `HooksLifecycle` when hooks are included. @@ -33,7 +34,7 @@ - `cli/src/services/agent_trace_db/lifecycle.rs` defines `AgentTraceDbLifecycle`, the Agent Trace DB-owned provider. - `AgentTraceDbLifecycle::diagnose` emits canonical Agent Trace DB path and parent-directory readiness lifecycle health problems. - `AgentTraceDbLifecycle::fix` bootstraps the canonical Agent Trace DB parent directory for auto-fixable DB parent readiness problems. -- `AgentTraceDbLifecycle::setup` initializes the Agent Trace DB through `AgentTraceDb::new()` and returns an empty `SetupOutcome` because DB bootstrap currently has no dedicated outcome carrier. +- `AgentTraceDbLifecycle::setup` resolves the current checkout identity from `ctx.repo_root()` when available, creates or reuses `/sce/checkout-id`, registers the checkout with `database_path: null`, returns setup messaging with the checkout ID, and still initializes the existing global Agent Trace DB through `AgentTraceDb::new()` until per-checkout DB resolution lands. - `doctor` runtime execution now aggregates lifecycle providers for diagnosis and repair: - `cli/src/services/doctor/command.rs` accepts any context implementing `ContextWithRepoRoot`. - `cli/src/services/doctor/mod.rs` resolves the repository root once, creates a repo-root-scoped borrowed context using `with_repo_root()`, and requests the full provider catalog with hooks included. @@ -43,6 +44,7 @@ - `setup` runtime execution now aggregates lifecycle providers for setup: - `cli/src/services/setup/command.rs` accepts any context implementing `ContextWithRepoRoot`, resolves the repository root, derives a repo-root-scoped borrowed context with `with_repo_root()`, and requests the shared provider catalog with hooks included only when `SetupRequest.install_hooks` is true. - Setup lifecycle providers receive only a generic context implementing the narrow repo-root accessor they currently need instead of the full concrete production context type. + - Setup command renders generic lifecycle `SetupOutcome.messages` before adapting any hook install outcomes. - `HooksLifecycle::setup` returns lifecycle-owned `SetupOutcome.required_hooks_install` from the canonical `install_required_git_hooks` flow, and setup command adapts that result into setup-owned hook install outcomes before rendering. - Config asset installation (OpenCode/Claude targets) remains handled by the setup command after lifecycle aggregation. diff --git a/context/context-map.md b/context/context-map.md index f95bc0be..319fe4a9 100644 --- a/context/context-map.md +++ b/context/context-map.md @@ -11,12 +11,12 @@ Feature/domain context: - `context/cli/cli-command-surface.md` (CLI command surface including top-level help with ASCII art banner and gradient rendering, setup install flow, WorkOS device authorization flow + token storage behavior, attribution-only hook routing with validated post-commit `--remote-url` plumbing plus DB-backed `diff-trace` dual persistence and post-commit Agent Trace payload persistence including range `content_hash`, setup-owned local DB + Agent Trace DB bootstrap plus doctor DB health coverage, centralized Rust SCE web URL helpers in `services::agent_trace`, nested flake release package/app installability, Cargo local install + crates.io readiness policy, and hidden `sce policy bash` command adapter for bash-policy hook callers; `sce sync` command wiring is deferred to `0.4.0`; static `RuntimeCommand` enum dispatch lives in `services/command_registry.rs`, command payload structs for help/version/completion/auth/config/setup/doctor/hooks/policy are owned by their respective `services/{name}/command.rs` files, and clap-to-runtime conversion lives in `services/parse/command_runtime.rs`) - `context/cli/default-path-catalog.md` (canonical production CLI path-ownership contract centered on `cli/src/services/default_paths.rs`, including persisted auth/config files, named DB paths for auth/local/Agent Trace databases, repo-relative, embedded-asset, install, hook, and context-path families plus the regression guard that keeps production path ownership centralized) -- `context/cli/checkout-identity.md` (current checkout identity infrastructure in `cli/src/services/checkout/`, including `/sce/checkout-id` UUIDv7 storage, central `/sce/checkout-registry.json` records, atomic registry persistence, and the current no-integration boundary before setup/hooks/doctor/Agent Trace DB consumers are switched) +- `context/cli/checkout-identity.md` (current checkout identity infrastructure in `cli/src/services/checkout/`, including `/sce/checkout-id` UUIDv7 storage, central `/sce/checkout-registry.json` records, atomic registry persistence, setup lifecycle integration that creates/reuses/registers checkout identity and emits checkout-ID messaging, and the remaining boundary before hooks/doctor/per-checkout Agent Trace DB consumers are switched) - `context/cli/patch-service.md` (standalone patch domain model, parser, JSON load helpers, and set operations in `cli/src/services/patch.rs` for in-memory parsed unified-diff representation, capturing only touched lines plus minimal per-file/per-hunk metadata, supporting both `Index:` SVN-style and `diff --git` git-style formats, with `ParseError` for actionable malformed-input diagnostics, `PatchLoadError`/`load_patch_from_json`/`load_patch_from_json_bytes` for storage-agnostic JSON reconstruction, `intersect_patches` for target-shaped overlap with exact-match-first and historical `kind`+`content` fallback semantics plus matched-constructed-line `session_id` and matched-constructed-hunk `model_id` provenance inheritance, and `combine_patches` for ordered patch combination with later-wins conflict resolution plus winning-hunk `model_id` provenance inheritance; `parse_patch`, `intersect_patches`, and `combine_patches` are consumed by the active post-commit hook runtime) - `context/cli/styling-service.md` (CLI text-mode output styling with `owo-colors` and `comfy-table`, TTY/`NO_COLOR` policy, shared helper API for human-facing surfaces, and per-column right-to-left RGB gradient banner rendering) - `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, canonical Pkl-generated `sce/config.json` schema ownership plus CLI embedding/reuse contract, 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, doctor/setup adapter boundaries, the static `LifecycleProvider` enum catalog/dispatcher, hook/config/local_db/auth_db/agent_trace_db lifecycle providers, implemented doctor aggregation over diagnose/fix providers, and implemented setup aggregation over `setup` providers in order config → local_db → auth_db → agent_trace_db → hooks when requested) +- `context/cli/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 checkout identity registration, 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-observability-contract.md` (implemented config-backed runtime observability contract for the flat logging config-file shape with env-over-config fallback, 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` - `context/sce/shared-context-plan-workflow.md` (canonical `/change-to-plan` workflow, clarification/readiness gate contract, and one-task/one-atomic-commit task-slicing policy) diff --git a/context/overview.md b/context/overview.md index b5dc8a60..787314c0 100644 --- a/context/overview.md +++ b/context/overview.md @@ -28,7 +28,7 @@ The config service split now includes `cli/src/services/config/resolver.rs` as t Generated config now includes repo-local plugin assets for both profiles: `sce-bash-policy.ts` plus `sce-agent-trace.ts` are emitted under `config/.opencode/plugins/` and `config/automated/.opencode/plugins/`; the OpenCode agent-trace plugin extracts `{ sessionID, diff, time, model_id }` from user `message.updated` events with diffs, tracks per-session OpenCode client version from `session.created`/`session.updated`, and sends payloads to `sce hooks diff-trace` with `tool_name="opencode"` plus optional `tool_version`. Claude generated config now routes agent-trace events through `.claude/settings.json` command hooks that call `sce hooks` directly: `SessionStart` pipes raw hook event JSON to `sce hooks session-model`, and matched `PostToolUse Write|Edit|MultiEdit|NotebookEdit` pipes raw hook event JSON to `sce hooks diff-trace`; the Rust `session-model` path uses explicit payload version fields when present and otherwise best-effort captures `tool_version` from trimmed `claude --version` stdout when available. Rust handles extraction, validation, and persistence without a TypeScript intermediary; the former `config/.claude/plugins/sce-agent-trace.ts` Bun runtime was removed in T07 of the `claude-rust-diff-trace` plan. The Rust hook validates required fields, resolves missing/nullable diff-trace attribution from `session_models` while preserving direct payload precedence, and persists `model_id`, `tool_name`, and nullable/resolved `tool_version` into `diff_traces` through AgentTraceDb. Bash-policy now delegates OpenCode enforcement to the Rust `sce policy bash` command: the generated OpenCode plugin at `config/.opencode/plugins/sce-bash-policy.ts` (and `config/automated/.opencode/plugins/sce-bash-policy.ts`) is a thin wrapper that calls `sce policy bash --input normalized --output json` via `spawnSync` and throws on deny decisions; it no longer contains independent TypeScript policy logic. The former `bash-policy/runtime.ts` TypeScript runtime has been removed. Preset... The `doctor` command now exposes explicit inspection mode (`sce doctor`) and repair-intent mode (`sce doctor --fix`) at the CLI/help/schema level while keeping diagnosis mode read-only. It now validates both current global operator health and the current repo/hook-integrity slice: state-root resolution, global config path resolution, global and repo-local `sce/config.json` readability/schema validity, local DB and Agent Trace DB path + health, DB parent-directory readiness, git availability, non-repo vs bare-repo targeting failures, effective git hook-path source (default, per-repo `core.hooksPath`, or global `core.hooksPath`), hooks-directory health, required hook presence/executable permissions/content drift against canonical embedded SCE-managed hook assets, and repo-root OpenCode integration presence across the installed `plugins`, `agents`, `commands`, and `skills` inventories with embedded SHA-256 content verification for OpenCode assets. Text mode now renders the approved human-only layout with ordered `Environment` / `Configuration` / `Repository` / `Git Hooks` / `Integrations` sections, `SCE doctor diagnose` / `SCE doctor fix` headers, bracketed `[PASS]`/`[FAIL]`/`[MISS]` status tokens, shared-style green pass plus red fail/miss coloring when color output is enabled, simplified `label (path)` row formatting, top-level-only hook rows, and integration parent/child rows that reflect missing vs content-mismatch states; JSON output now reports Agent Trace DB health under `agent_trace_db` (as a row within the Configuration section in text mode). Repo-scoped database reporting is empty by default because no repo-owned SCE database currently exists. Fix mode reuses the canonical setup hook install flow to repair missing/stale/non-executable required hooks and can also bootstrap missing canonical DB parent directories while preserving manual-only guidance for unsupported issues. Claude bash-policy enforcement is also generated through `.claude/settings.json` as a `PreToolUse` `Bash` command hook running `sce policy bash`, so Claude and OpenCode both delegate to the Rust policy evaluator without a Claude TypeScript runtime. -Local database bootstrap is now owned by `LocalDbLifecycle::setup` and `AgentTraceDbLifecycle::setup` aggregated by the setup command, while doctor validates both DB paths/health and can bootstrap missing parent directories. Wiring a user-invocable `sce sync` command is deferred to `0.4.0`. +Local database bootstrap is now owned by `LocalDbLifecycle::setup` and `AgentTraceDbLifecycle::setup` aggregated by the setup command. Agent Trace setup also creates/reuses the current checkout ID and registers the checkout while the existing global Agent Trace DB setup remains in place until per-checkout DB resolution lands. Doctor validates DB paths/health and can bootstrap missing parent directories. Wiring a user-invocable `sce sync` command is deferred to `0.4.0`. The repository-root flake (`flake.nix`) now applies a Rust overlay-backed stable toolchain pinned to `1.93.1` (with `rustfmt` and `clippy`), reads package/check version from the repo-root `.version` file, builds `packages.sce` through a Crane `buildDepsOnly` + `buildPackage` pipeline with filtered package sources for the Cargo tree plus required embedded config/assets, and runs `cli-tests`, `cli-clippy`, and `cli-fmt` through Crane-backed check derivations (`cargoTest`, `cargoClippy`, `cargoFmt`) that reuse the same filtered source/toolchain setup. The root flake also exposes release install/run outputs directly as `packages.sce` (with `packages.default = packages.sce`) plus `apps.sce` and `apps.default`, so `nix build .#default`, `nix run . -- --help`, `nix run .#sce -- --help`, and `nix profile install github:crocoder-dev/shared-context-engineering` all target the packaged `sce` binary through the same flake-owned entrypoints. The CLI Cargo package metadata now includes crates.io publication-ready fields with crate-local install guidance in `cli/README.md`; supported Cargo install paths are `cargo install shared-context-engineering --locked`, `cargo install --git https://github.com/crocoder-dev/shared-context-engineering shared-context-engineering --locked`, and local `cargo install --path cli --locked`. The published crate installs the `sce` binary. The crate also keeps `cargo clippy --manifest-path cli/Cargo.toml` warnings-denied through `cli/Cargo.toml` lint configuration, so an extra `-- -D warnings` flag is redundant. diff --git a/context/plans/agent-trace-checkout-identity.md b/context/plans/agent-trace-checkout-identity.md index 47418973..0272a3bc 100644 --- a/context/plans/agent-trace-checkout-identity.md +++ b/context/plans/agent-trace-checkout-identity.md @@ -58,12 +58,16 @@ Give each cloned repository (and linked Git worktree) its own `agent-trace` chec - Evidence: `nix flake check` passed. A local `cargo build` reached final `rustc`/link and was terminated by SIGTERM in the environment without Rust diagnostics; the Crane-backed flake validation completed successfully. - Notes: Generated unit tests and their temporary helper/dependency were removed after review feedback; T01 now lands the checkout identity infrastructure surface only. -- [ ] T02: `Integrate checkout detection into setup lifecycle` (status:todo) +- [x] T02: `Integrate checkout detection into setup lifecycle` (status:done) - Task ID: T02 - Goal: `AgentTraceDbLifecycle::setup()` resolves the checkout ID from the repo root, creates or reuses it, and registers the checkout in the central registry. The per-checkout database is **not** eagerly initialized — it will be created lazily on first write (T03). - Boundaries (in/out of scope): In — `AgentTraceDbLifecycle::setup()` calls `checkout::resolve_git_dir()` and `checkout::get_or_create_checkout_id()` using `ctx.repo_root()`, then `checkout::registry::register_checkout()`. Setup outcome messaging reports the resolved checkout ID and notes that the DB will be created on first write. The global `agent_trace_db_path()` DB is still opened for now (backward compat) — the switch to per-checkout paths comes in T03. Lifecycle `diagnose()` and `fix()` continue to target the global DB until T03. Out — hook flow changes, per-checkout DB path changes, registry query from doctor. - Done when: `sce setup` in a cloned repo prints the checkout ID; a second run reuses the same ID; `cat .git/sce/checkout-id` shows the same UUID; `/sce/checkout-registry.json` contains the registered record (with `database_path` still absent/`null` since the DB hasn't been created yet); existing setup tests pass. - Verification notes (commands or checks): Manual end-to-end: run `sce setup` in a test repo, inspect `.git/sce/checkout-id` and registry file, re-run `sce setup` and confirm idempotent ID reuse. Run existing setup tests through `nix flake check`. + - Completed: 2026-06-16 + - Files changed: `cli/src/services/agent_trace_db/lifecycle.rs`, `cli/src/services/lifecycle.rs`, `cli/src/services/setup/command.rs`, `cli/src/services/hooks/lifecycle.rs`, `cli/src/generated_migrations.rs`, `context/cli/checkout-identity.md`, `context/cli/service-lifecycle.md`, `context/sce/agent-trace-db.md`, `context/context-map.md`, `context/overview.md`, `context/architecture.md` + - Evidence: `nix flake check` passed; `nix run .#pkl-check-generated` passed. Manual end-to-end setup in a temporary git repository ran `sce setup --hooks --repo ` twice with isolated `XDG_STATE_HOME`; both runs printed the same checkout ID, `.git/sce/checkout-id` matched the registry record, and registry `database_path` remained `null`. + - Notes: Setup lifecycle now emits a generic lifecycle setup message for Agent Trace checkout identity. The existing global Agent Trace DB setup remains in place until T03, while checkout registry `database_path` stays unset for lazy per-checkout DB creation later. - [ ] T03: `Enable per-checkout database resolution with lazy initialization` (status:todo) - Task ID: T03 diff --git a/context/sce/agent-trace-db.md b/context/sce/agent-trace-db.md index 6e240afc..8a778df3 100644 --- a/context/sce/agent-trace-db.md +++ b/context/sce/agent-trace-db.md @@ -171,7 +171,7 @@ Both triggers compare `OLD.*` vs `NEW.*` for all mutable columns (excluding `upd - `diagnose()` reports canonical Agent Trace DB path and parent-directory readiness problems through the shared DB path-health helper. - `fix()` can bootstrap the canonical Agent Trace DB parent directory for auto-fixable parent-readiness problems. -- `setup()` initializes the database with `AgentTraceDb::new()`, including all ordered Agent Trace migrations and any later migrations not yet recorded in `__sce_migrations`. +- `setup()` creates/reuses the current checkout identity when a repo root is available, registers the checkout in `/sce/checkout-registry.json` with `database_path: null`, emits setup messaging with the checkout ID, and still initializes the existing global database with `AgentTraceDb::new()` until the per-checkout DB path switch lands. - `sce doctor` now surfaces Agent Trace DB health as a row within the `Configuration` section with `[PASS]`/`[FAIL]`/`[MISS]` status tokens (e.g., `Agent Trace DB (/path/to/agent-trace.db)`), and includes it in JSON output under the `agent_trace_db` field. ## Runtime writers From a62213394165fc14c596390466008fd7c1d02efb Mon Sep 17 00:00:00 2001 From: David Abram Date: Tue, 16 Jun 2026 14:40:03 +0200 Subject: [PATCH 4/8] agent-trace: Enable per-checkout database resolution with lazy initialization Switch all Agent Trace DB consumers from the shared global path to per-checkout paths. Each checkout now gets its own agent-trace-{checkout_id}.db, created lazily on first hook write. Co-authored-by: SCE --- cli/src/services/agent_trace_db/lifecycle.rs | 41 +++++++---- cli/src/services/agent_trace_db/mod.rs | 15 ++++ cli/src/services/checkout/mod.rs | 68 +++++++++++++++++++ cli/src/services/db/mod.rs | 27 ++++++++ cli/src/services/default_paths.rs | 19 ++++++ cli/src/services/doctor/inspect.rs | 2 +- cli/src/services/hooks/mod.rs | 67 ++++++++++-------- context/architecture.md | 8 +-- context/cli/checkout-identity.md | 16 ++++- context/cli/default-path-catalog.md | 5 +- context/cli/service-lifecycle.md | 6 +- context/context-map.md | 6 +- context/glossary.md | 10 +-- context/overview.md | 6 +- .../plans/agent-trace-checkout-identity.md | 6 +- context/sce/agent-trace-db.md | 23 +++++-- .../sce/agent-trace-hooks-command-routing.md | 12 ++-- context/sce/shared-turso-db.md | 6 +- 18 files changed, 261 insertions(+), 82 deletions(-) diff --git a/cli/src/services/agent_trace_db/lifecycle.rs b/cli/src/services/agent_trace_db/lifecycle.rs index e2783e6e..61dcdd92 100644 --- a/cli/src/services/agent_trace_db/lifecycle.rs +++ b/cli/src/services/agent_trace_db/lifecycle.rs @@ -1,16 +1,17 @@ use anyhow::{Context, Result}; use chrono::Utc; +use std::path::{Path, PathBuf}; use crate::app::HasRepoRoot; use crate::services::checkout::{self, registry}; use crate::services::db::{bootstrap_db_parent, collect_db_path_health, DbSpec}; -use crate::services::default_paths::agent_trace_db_path; +use crate::services::default_paths::{agent_trace_db_path, agent_trace_db_path_for_checkout}; use crate::services::lifecycle::{ FixOutcome, FixResultRecord, HealthCategory, HealthFixability, HealthProblem, HealthProblemKind, HealthSeverity, LifecycleProviderId, ServiceLifecycle, SetupOutcome, }; -use super::{AgentTraceDb, AgentTraceDbSpec}; +use super::AgentTraceDbSpec; #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] pub struct AgentTraceDbLifecycle; @@ -20,11 +21,11 @@ impl ServiceLifecycle for AgentTraceDbLifecycle { LifecycleProviderId::AgentTraceDb } - fn diagnose(&self, _ctx: &C) -> Vec { - diagnose_agent_trace_db_health() + fn diagnose(&self, ctx: &C) -> Vec { + diagnose_agent_trace_db_health(ctx.repo_root()) } - fn fix(&self, _ctx: &C, problems: &[HealthProblem]) -> Vec { + fn fix(&self, ctx: &C, problems: &[HealthProblem]) -> Vec { let should_bootstrap_parent = problems.iter().any(|problem| { problem.category == HealthCategory::GlobalState && problem.fixability == HealthFixability::AutoFixable @@ -33,7 +34,7 @@ impl ServiceLifecycle for AgentTraceDbLifecycle { return Vec::new(); } - match bootstrap_agent_trace_db_parent() { + match bootstrap_agent_trace_db_parent(ctx.repo_root()) { Ok(parent) => vec![FixResultRecord { category: HealthCategory::GlobalState, outcome: FixOutcome::Fixed, @@ -59,9 +60,6 @@ impl ServiceLifecycle for AgentTraceDbLifecycle { .transpose() .context("Agent trace DB lifecycle setup failed while resolving checkout identity")?; - AgentTraceDb::new() - .context("Agent trace DB lifecycle setup failed while initializing agent trace DB")?; - Ok(SetupOutcome { messages: checkout_setup .iter() @@ -110,10 +108,10 @@ fn format_checkout_identity_setup_message(setup: &CheckoutIdentitySetup) -> Stri ) } -pub fn diagnose_agent_trace_db_health() -> Vec { +pub fn diagnose_agent_trace_db_health(repo_root: Option<&Path>) -> Vec { let mut problems = Vec::new(); - let db_path = match agent_trace_db_path() { + let db_path = match resolve_lifecycle_agent_trace_db_path(repo_root) { Ok(path) => path, Err(error) => { problems.push(HealthProblem { @@ -137,7 +135,24 @@ pub fn diagnose_agent_trace_db_health() -> Vec { problems } -fn bootstrap_agent_trace_db_parent() -> Result { - let db_path = agent_trace_db_path().context("failed to resolve agent trace DB path")?; +fn bootstrap_agent_trace_db_parent(repo_root: Option<&Path>) -> Result { + let db_path = resolve_lifecycle_agent_trace_db_path(repo_root) + .context("failed to resolve agent trace DB path")?; bootstrap_db_parent(::db_name(), &db_path) } + +fn resolve_lifecycle_agent_trace_db_path(repo_root: Option<&Path>) -> Result { + if let Some(repo_root) = repo_root { + let git_dir = checkout::resolve_git_dir(repo_root).with_context(|| { + format!( + "failed to resolve git directory for agent trace DB health from '{}'", + repo_root.display() + ) + })?; + if let Some(checkout_id) = checkout::read_checkout_id(&git_dir)? { + return agent_trace_db_path_for_checkout(&checkout_id); + } + } + + agent_trace_db_path() +} diff --git a/cli/src/services/agent_trace_db/mod.rs b/cli/src/services/agent_trace_db/mod.rs index 9d3cbba5..2387e701 100644 --- a/cli/src/services/agent_trace_db/mod.rs +++ b/cli/src/services/agent_trace_db/mod.rs @@ -118,6 +118,20 @@ impl DbSpec for AgentTraceDbSpec { /// Agent trace Turso database adapter. pub type AgentTraceDb = TursoDb; +impl AgentTraceDb { + /// Open or create an Agent Trace database at an explicit path and run all + /// embedded migrations. + pub fn open_at(path: impl AsRef) -> Result { + TursoDb::::new_at(path) + } + + /// Open or create an Agent Trace database at an explicit path without + /// running migrations. + pub fn open_for_hooks_without_migrations_at(path: impl AsRef) -> Result { + TursoDb::::open_without_migrations_at(path) + } +} + /// Diff trace payload to persist in the agent trace database. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct DiffTraceInsert<'a> { @@ -288,6 +302,7 @@ impl AgentTraceDb { /// Setup/lifecycle initialization must continue to use [`AgentTraceDb::new`] /// so schema migrations remain explicitly owned by setup flows. Hook callers /// must verify schema readiness before reading or writing through this DB. + #[allow(dead_code)] pub fn open_for_hooks_without_migrations() -> Result { TursoDb::::open_without_migrations() } diff --git a/cli/src/services/checkout/mod.rs b/cli/src/services/checkout/mod.rs index ba320d47..384ea1d9 100644 --- a/cli/src/services/checkout/mod.rs +++ b/cli/src/services/checkout/mod.rs @@ -17,8 +17,13 @@ use std::path::{Path, PathBuf}; use std::process::Command; use anyhow::{anyhow, Context, Result}; +use chrono::Utc; use uuid::{NoContext, Timestamp, Uuid}; +use crate::services::{ + agent_trace_db::AgentTraceDb, default_paths::agent_trace_db_path_for_checkout, +}; + /// Subdirectory inside `/` where SCE checkout metadata lives. const SCE_CHECKOUT_DIR: &str = "sce"; @@ -152,3 +157,66 @@ pub fn resolve_checkout_id_for_repo(repo_root: &Path) -> Result { let git_dir = resolve_git_dir(repo_root)?; get_or_create_checkout_id(&git_dir) } + +/// Resolves or creates the current checkout identity and opens its per-checkout +/// Agent Trace DB, lazily initializing schema when needed. +pub fn resolve_or_create_agent_trace_db_for_current_checkout() -> Result<(AgentTraceDb, String)> { + let repo_root = std::env::current_dir() + .context("Failed to determine current directory for Agent Trace checkout DB resolution")?; + resolve_or_create_agent_trace_db_for_checkout(&repo_root) +} + +/// Resolves or creates the checkout identity for `repo_root` and opens its +/// per-checkout Agent Trace DB, lazily initializing schema when needed. +pub fn resolve_or_create_agent_trace_db_for_checkout( + repo_root: &Path, +) -> Result<(AgentTraceDb, String)> { + let git_dir = resolve_git_dir(repo_root).with_context(|| { + format!( + "failed to resolve git directory for Agent Trace checkout DB from '{}'", + repo_root.display() + ) + })?; + let checkout_id = get_or_create_checkout_id(&git_dir).with_context(|| { + format!( + "failed to get or create checkout identity under '{}'", + git_dir.display() + ) + })?; + let db_path = agent_trace_db_path_for_checkout(&checkout_id).with_context(|| { + format!("failed to resolve Agent Trace DB path for checkout ID {checkout_id}") + })?; + + register_checkout_for_db(repo_root, &checkout_id, None)?; + + let fast_open = AgentTraceDb::open_for_hooks_without_migrations_at(&db_path) + .and_then(|db| db.ensure_schema_ready_for_hooks().map(|()| db)); + let db = match fast_open { + Ok(db) => db, + Err(_) => AgentTraceDb::open_at(&db_path).with_context(|| { + format!( + "failed to initialize Agent Trace DB for checkout {checkout_id} at '{}'", + db_path.display() + ) + })?, + }; + + register_checkout_for_db(repo_root, &checkout_id, Some(db_path.display().to_string()))?; + + Ok((db, checkout_id)) +} + +fn register_checkout_for_db( + repo_root: &Path, + checkout_id: &str, + database_path: Option, +) -> Result<()> { + registry::register_checkout(registry::CheckoutRecord { + checkout_id: checkout_id.to_string(), + path: repo_root.display().to_string(), + last_seen: Utc::now().to_rfc3339(), + remote_url: None, + database_path, + }) + .context("failed to register checkout identity") +} diff --git a/cli/src/services/db/mod.rs b/cli/src/services/db/mod.rs index ab41b0eb..59849e0f 100644 --- a/cli/src/services/db/mod.rs +++ b/cli/src/services/db/mod.rs @@ -334,6 +334,20 @@ impl TursoDb { Ok(db) } + /// Open or create the database at an explicit path. + /// + /// Parent directories are created automatically. Migrations are run after + /// the database connection is established. The service-specific retry and + /// migration configuration still comes from `M`. + pub fn new_at(db_path: impl AsRef) -> Result { + let db = Self::open_without_migrations_at(db_path)?; + + db.run_migrations() + .with_context(|| format!("failed to run {} migrations", M::db_name()))?; + + Ok(db) + } + /// Open or create the database at the spec-provided canonical path without /// running embedded migrations. /// @@ -344,6 +358,19 @@ impl TursoDb { let db_name = M::db_name(); let db_path = M::db_path().with_context(|| format!("failed to resolve {db_name} path"))?; + Self::open_without_migrations_at(db_path) + } + + /// Open or create the database at an explicit path without running embedded + /// migrations. + /// + /// Parent directories are created automatically and the connection-open + /// retry policy is preserved. Runtime callers that use this path are + /// responsible for verifying schema readiness before query/write work. + pub fn open_without_migrations_at(db_path: impl AsRef) -> Result { + let db_name = M::db_name(); + let db_path = db_path.as_ref().to_path_buf(); + ensure_db_parent_dir(db_name, &db_path)?; let runtime = build_current_thread_runtime(db_name)?; diff --git a/cli/src/services/default_paths.rs b/cli/src/services/default_paths.rs index d209c724..1a1d0e59 100644 --- a/cli/src/services/default_paths.rs +++ b/cli/src/services/default_paths.rs @@ -293,6 +293,25 @@ pub fn agent_trace_db_path() -> anyhow::Result { .join("agent-trace.db")) } +/// Returns the canonical per-checkout Agent Trace Turso database file path. +/// +/// The path is `/sce/agent-trace-{checkout_id}.db`, where +/// `state_root` comes from the shared default-path catalog (`XDG_STATE_HOME` or +/// platform equivalent). +#[allow(dead_code)] +pub fn agent_trace_db_path_for_checkout(checkout_id: &str) -> anyhow::Result { + let checkout_id = checkout_id.trim(); + if checkout_id.is_empty() { + anyhow::bail!("checkout ID must not be empty when resolving Agent Trace DB path"); + } + + Ok(resolve_sce_default_locations()? + .roots() + .state_root() + .join("sce") + .join(format!("agent-trace-{checkout_id}.db"))) +} + #[allow(dead_code)] #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub(crate) enum PersistedArtifactRootKind { diff --git a/cli/src/services/doctor/inspect.rs b/cli/src/services/doctor/inspect.rs index b9fd495c..eb096ab6 100644 --- a/cli/src/services/doctor/inspect.rs +++ b/cli/src/services/doctor/inspect.rs @@ -287,7 +287,7 @@ fn collect_global_state_locations( } fn collect_agent_trace_db_health(problems: &mut Vec) -> Option { - let agent_trace_problems = diagnose_agent_trace_db_health(); + let agent_trace_problems = diagnose_agent_trace_db_health(None); let mut agent_trace_db = None; for problem in &agent_trace_problems { diff --git a/cli/src/services/hooks/mod.rs b/cli/src/services/hooks/mod.rs index edfadb2a..5eab0156 100644 --- a/cli/src/services/hooks/mod.rs +++ b/cli/src/services/hooks/mod.rs @@ -19,6 +19,7 @@ use crate::services::agent_trace_db::{ MessageRole, PartType, PostCommitPatchIntersectionInsert, RecentDiffTracePatches, SessionModelAttribution, SessionModelUpsert, PAYLOAD_TYPE_PATCH, PAYLOAD_TYPE_STRUCTURED, }; +use crate::services::checkout; use crate::services::observability::traits::Logger; use crate::services::patch::{ combine_patches as combine_patches_fn, intersect_patches as intersect_patches_fn, @@ -204,14 +205,20 @@ fn run_hooks_subcommand_in_repo( run_post_rewrite_subcommand_with_trace(repository_root, subcommand, rewrite_method) } HookSubcommand::DiffTrace => run_diff_trace_subcommand(repository_root, logger), - HookSubcommand::ConversationTrace => run_conversation_trace_subcommand(logger), + HookSubcommand::ConversationTrace => { + run_conversation_trace_subcommand(repository_root, logger) + } HookSubcommand::SessionModel => run_session_model_subcommand(repository_root, logger), } } -fn run_conversation_trace_subcommand(logger: Option<&dyn Logger>) -> Result { +fn run_conversation_trace_subcommand( + repository_root: &Path, + logger: Option<&dyn Logger>, +) -> Result { let stdin_payload = read_hook_stdin()?; - let result = run_conversation_trace_subcommand_from_payload(&stdin_payload, logger); + let result = + run_conversation_trace_subcommand_from_payload(repository_root, &stdin_payload, logger); if let Err(ref error) = result { if let Some(log) = logger { log.error( @@ -225,18 +232,21 @@ fn run_conversation_trace_subcommand(logger: Option<&dyn Logger>) -> Result, ) -> Result { let payload = parse_conversation_trace_payload(stdin_payload)?; - persist_conversation_trace_payload_to_agent_trace_db(payload, logger) + persist_conversation_trace_payload_to_agent_trace_db(repository_root, payload, logger) } fn persist_conversation_trace_payload_to_agent_trace_db( + repository_root: &Path, payload: ConversationTracePayload, logger: Option<&dyn Logger>, ) -> Result { let db = open_agent_trace_db_for_hook_runtime( + repository_root, "Failed to open Agent Trace DB for conversation-trace persistence.", )?; @@ -281,27 +291,13 @@ where } } -fn open_agent_trace_db_for_hook_runtime(context_message: &'static str) -> Result { - prepare_agent_trace_db_for_hook_runtime_with( - AgentTraceDb::open_for_hooks_without_migrations, - AgentTraceDb::ensure_schema_ready_for_hooks, - context_message, - ) -} - -fn prepare_agent_trace_db_for_hook_runtime_with( - open_db: O, - ensure_schema_ready: R, +fn open_agent_trace_db_for_hook_runtime( + repository_root: &Path, context_message: &'static str, -) -> Result -where - O: FnOnce() -> Result, - R: FnOnce(&D) -> Result<()>, -{ - let db = open_db().context(context_message)?; - ensure_schema_ready(&db).context(context_message)?; - - Ok(db) +) -> Result { + checkout::resolve_or_create_agent_trace_db_for_checkout(repository_root) + .map(|(db, _checkout_id)| db) + .context(context_message) } #[derive(Clone, Debug, Eq, PartialEq)] @@ -674,8 +670,11 @@ fn run_diff_trace_subcommand_from_payload( }; let resolve_attribution = |tool_name: &str, session_id: &str| -> Result> { - let db = AgentTraceDb::new() - .context("Failed to open Agent Trace DB for model resolution.")?; + let db = open_agent_trace_db_for_hook_runtime( + repository_root, + "Failed to open Agent Trace DB for model resolution.", + ) + .context("Failed to open Agent Trace DB for model resolution.")?; let attribution = db .session_model_by_tool_and_session(tool_name, session_id) .context("Failed to query session model attribution from Agent Trace DB.")?; @@ -713,6 +712,7 @@ where } persist_diff_trace_payload(repository_root, payload)?; let agent_trace_db_result = persist_diff_trace_payload_to_agent_trace_db( + repository_root, payload, resolved_attribution.model_id.as_deref(), resolved_attribution.tool_version.as_deref(), @@ -784,7 +784,7 @@ fn run_session_model_subcommand( } fn run_session_model_subcommand_from_payload( - _repository_root: &Path, + repository_root: &Path, stdin_payload: &str, logger: Option<&dyn Logger>, ) -> Result { @@ -805,8 +805,11 @@ fn run_session_model_subcommand_from_payload( session_start_time_ms, }; - let db = AgentTraceDb::new() - .context("Failed to open Agent Trace DB for session-model persistence.")?; + let db = open_agent_trace_db_for_hook_runtime( + repository_root, + "Failed to open Agent Trace DB for session-model persistence.", + ) + .context("Failed to open Agent Trace DB for session-model persistence.")?; let result = db .upsert_session_model(upsert_payload) .context("Failed to persist session model attribution to Agent Trace DB."); @@ -1357,12 +1360,14 @@ fn persist_diff_trace_payload( } fn persist_diff_trace_payload_to_agent_trace_db( + repository_root: &Path, payload: &DiffTracePayload, model_id: Option<&str>, tool_version: Option<&str>, ) -> Result<()> { persist_diff_trace_payload_to_agent_trace_db_with(payload, model_id, tool_version, |input| { let db = open_agent_trace_db_for_hook_runtime( + repository_root, "Failed to open Agent Trace DB for diff-trace persistence.", )?; db.insert_diff_trace(input) @@ -1620,12 +1625,13 @@ where } fn run_post_commit_agent_trace_flow( - _repository_root: &Path, + repository_root: &Path, flow_result: &PostCommitIntersectionFlowResult, vcs_type: Option, remote_url: &str, ) -> Result { let db = open_agent_trace_db_for_hook_runtime( + repository_root, "Failed to open Agent Trace DB for post-commit trace.", )?; @@ -1716,6 +1722,7 @@ fn run_post_commit_intersection_flow( repository_root: &Path, ) -> Result { let db = open_agent_trace_db_for_hook_runtime( + repository_root, "Failed to open Agent Trace DB for post-commit intersection.", )?; diff --git a/context/architecture.md b/context/architecture.md index 052129b5..31e73f20 100644 --- a/context/architecture.md +++ b/context/architecture.md @@ -99,18 +99,18 @@ The repository includes a new placeholder Rust binary crate at `cli/`. - `cli/src/services/observability.rs` provides deterministic runtime observability controls and rendering for app lifecycle logs, including shared config-resolved threshold/format and file-sink inputs with precedence `env > config file > defaults` for non-flag observability keys, optional file sink controls (`SCE_LOG_FILE`, `SCE_LOG_FILE_MODE` with deterministic truncate-or-append policy), stable event identifiers, severity filtering, the forced-emission warning path used for invalid discovered config startup diagnostics, stderr-only primary emission with optional mirrored file writes, and redaction-safe emission through the shared security helper. Its `observability::traits` submodule exposes the current `Logger` and object-safe `Telemetry` trait boundaries plus `NoopLogger`; the concrete observability logger and telemetry runtime still own behavior and implement those traits without changing runtime behavior. `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` 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`, `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 files (global config, auth tokens, auth DB, local DB, agent trace DB), and owns canonical repo-relative, embedded-asset, install, hook, and context-path accessors so non-test production path definitions have one shared owner. Current production consumers such as config discovery, doctor reporting, setup/install flows, database adapters, and local hook runtime path resolution consume this shared catalog rather than defining owned path literals in their own modules. +- `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 files (global config, auth tokens, auth DB, local DB, legacy/global agent trace DB fallback, and per-checkout agent trace DB files), and owns canonical repo-relative, embedded-asset, install, hook, and context-path accessors so non-test production path definitions have one shared owner. Current production consumers such as config discovery, doctor reporting, setup/install flows, database adapters, checkout identity, and local hook runtime path resolution consume this shared catalog rather than defining owned path literals in their own modules. - `cli/src/services/agent_trace.rs` is the Rust CLI owner for the SCE web base URL (`SCE_WEB_BASE_URL`) and exposes helpers for SCE-owned URL construction: Agent Trace conversation lookup URLs, persisted Agent Trace trace URLs, Agent Trace session URLs, and setup-created config schema URLs. Production Rust code should consume those helpers instead of repeating `sce.crocoder.dev` literals. - `cli/src/services/config/mod.rs` is the config service facade and `sce config` orchestration surface (`show`, `validate`, `--help`), with bare `sce config` routed by `cli/src/app.rs` to the same help payload as `sce config --help`. Focused submodules own the implementation slices: `types.rs` owns shared config/runtime primitives, `schema.rs` owns generated schema embedding plus typed file parsing, `policy.rs` owns bash-policy semantic validation plus policy-specific formatting and runtime preset-catalog access for the Rust evaluator, `resolver.rs` owns deterministic config-file discovery, file-layer merging, explicit value precedence (`flags > env > config file > defaults` where flag-backed), shared auth-key resolution, observability-runtime resolution, attribution-hooks runtime gate resolution, database-retry config resolution and `DATABASE_RETRY_CONFIG` `OnceLock` initialization, default-discovered invalid-file degradation, and explicit-path fatal errors for `--config` / `SCE_CONFIG_FILE`, and private `render.rs` owns `sce config show` / `sce config validate` text and JSON output construction plus rendering-specific display-value helpers. The facade preserves existing `services::config` imports for startup/auth/hooks callers while delegating command execution to resolution plus rendering submodules. - `cli/src/services/output_format.rs` defines the canonical shared CLI output-format contract (`OutputFormat`) for supporting commands, with deterministic `text|json` parsing and command-scoped actionable invalid-value guidance. - `cli/src/services/config/types.rs` is the canonical owner for the shared runtime/config primitive seam used by the CLI: `LogLevel`, `LogFormat`, `LogFileMode`, the observability env-key constants, and the shared bool parsing helpers used by both config resolution and observability bootstrap; `cli/src/services/config/mod.rs` re-exports those primitives through the facade. - `cli/src/services/capabilities.rs` defines the current broad CLI capability traits consumed by the borrowed, compile-time-typed `AppContext`: `FsOps` with `StdFsOps` for filesystem operations and `GitOps` with `ProcessGitOps` for git command execution plus repository-root/hooks-directory resolution. Existing service internals do not consume these traits directly yet; command execution uses narrow accessors and repo-root-scoped context derivation. -- `cli/src/services/lifecycle.rs` defines the current compile-safe lifecycle seam. `ServiceLifecycle` has default no-op generic `diagnose`, `fix`, and `setup` methods over `C: HasRepoRoot`, with lifecycle-owned health, fix, and setup result types so the trait contract is not publicly anchored to doctor/setup module types or the full `AppContext` shape. The same module owns the static `LifecycleProvider` enum and shared `lifecycle_providers(include_hooks)` catalog/factory, returning providers in deterministic order (config → local_db → auth_db → agent_trace_db → hooks when requested); enum dispatch calls each concrete provider through generic context methods without boxed lifecycle-provider allocation or repo-root trait-object context erasure. Hooks exposes a `HooksLifecycle` provider in `cli/src/services/hooks/lifecycle.rs` for hook rollout diagnosis/fix/setup using lifecycle-owned health records plus the canonical required-hook installer. Config exposes a `ConfigLifecycle` provider in `cli/src/services/config/lifecycle.rs` for global/repo-local config validation and repo-local `.sce/config.json` bootstrap. local_db exposes a `LocalDbLifecycle` provider in `cli/src/services/local_db/lifecycle.rs` for canonical local DB path health, parent-directory readiness/bootstrap, and `LocalDb::new()` setup. auth_db exposes an `AuthDbLifecycle` provider in `cli/src/services/auth_db/lifecycle.rs` for canonical auth DB path health, parent-directory readiness/bootstrap, and `AuthDb::new()` setup. agent_trace_db exposes an `AgentTraceDbLifecycle` provider in `cli/src/services/agent_trace_db/lifecycle.rs` for canonical Agent Trace DB path health, parent-directory readiness/bootstrap, setup-time checkout identity creation/registry registration when a repo root is available, and current global `AgentTraceDb::new()` setup until per-checkout DB resolution lands. Doctor runtime aggregates the full provider catalog for `diagnose` and `fix` and adapts lifecycle records into doctor report/fix records at the orchestration boundary; setup command aggregates the shared catalog for `setup` with hooks included only when requested and adapts hook setup outcomes before rendering setup-owned messages. +- `cli/src/services/lifecycle.rs` defines the current compile-safe lifecycle seam. `ServiceLifecycle` has default no-op generic `diagnose`, `fix`, and `setup` methods over `C: HasRepoRoot`, with lifecycle-owned health, fix, and setup result types so the trait contract is not publicly anchored to doctor/setup module types or the full `AppContext` shape. The same module owns the static `LifecycleProvider` enum and shared `lifecycle_providers(include_hooks)` catalog/factory, returning providers in deterministic order (config → local_db → auth_db → agent_trace_db → hooks when requested); enum dispatch calls each concrete provider through generic context methods without boxed lifecycle-provider allocation or repo-root trait-object context erasure. Hooks exposes a `HooksLifecycle` provider in `cli/src/services/hooks/lifecycle.rs` for hook rollout diagnosis/fix/setup using lifecycle-owned health records plus the canonical required-hook installer. Config exposes a `ConfigLifecycle` provider in `cli/src/services/config/lifecycle.rs` for global/repo-local config validation and repo-local `.sce/config.json` bootstrap. local_db exposes a `LocalDbLifecycle` provider in `cli/src/services/local_db/lifecycle.rs` for canonical local DB path health, parent-directory readiness/bootstrap, and `LocalDb::new()` setup. auth_db exposes an `AuthDbLifecycle` provider in `cli/src/services/auth_db/lifecycle.rs` for canonical auth DB path health, parent-directory readiness/bootstrap, and `AuthDb::new()` setup. agent_trace_db exposes an `AgentTraceDbLifecycle` provider in `cli/src/services/agent_trace_db/lifecycle.rs` for setup-time checkout identity creation/registry registration when a repo root is available, per-checkout Agent Trace DB path health/fix when an ID exists, and legacy global DB fallback outside checkout context. Doctor runtime aggregates the full provider catalog for `diagnose` and `fix` and adapts lifecycle records into doctor report/fix records at the orchestration boundary; setup command aggregates the shared catalog for `setup` with hooks included only when requested and adapts hook setup outcomes before rendering setup-owned messages. - `cli/src/services/auth_command/mod.rs` defines the implemented auth command surface for `sce auth login|renew|logout|status`, including device-flow login, stored-token renewal (`--force` supported for renew), logout, and status rendering in text/JSON formats; `cli/src/services/auth_command/command.rs` owns the `AuthCommand` payload used by the static `RuntimeCommand` enum. - `cli/src/services/db/mod.rs` provides the shared generic Turso infrastructure seam: `DbSpec` supplies a service-specific name, path, ordered embedded migrations, and config-file lookup key (`db_config_key()`), while `TursoDb` owns parent-directory creation, `Builder::new_local(...)` initialization (with `experimental_multiprocess_wal(true)` for safe concurrent access), Turso connection setup, tokio current-thread runtime bridging, retry-backed blocking `execute`/`query`/`query_map` wrappers, and generic migration execution with per-database `__sce_migrations` metadata. `TursoDb::new()` and `EncryptedTursoDb::new()` wrap only their local open/connect block in `run_with_retry_sync` using a config-driven connection-open policy resolved from the `DATABASE_RETRY_CONFIG` `OnceLock` with fallback to hardcoded defaults, while both adapters' `execute()`, `query()`, and `query_map()` methods use a config-driven operation policy from the same source. Both adapters' `query_map()` methods retry the initial query and row-fetch loop, then apply caller row mapping after retry completion. Migration execution is not retried. The same module also provides `EncryptedTursoDb`, a structurally parallel encrypted adapter that resolves the encryption key through `encryption_key::get_or_create_encryption_key()`, enables Turso local encryption with strict `aegis256` cipher selection, and exposes retry-backed synchronous wrappers plus migration execution. `cli/src/services/db/encryption_key.rs` first derives a Turso-compatible 64-character hex key from non-empty `SCE_AUTH_DB_ENCRYPTION_KEY` env-secret text when present, otherwise falls back to keyring-backed credential-store get-or-create behavior; no plaintext auth DB fallback exists. - `cli/src/services/local_db/mod.rs` provides the concrete local DB spec and `LocalDb` type alias over the shared generic `TursoDb` adapter. `LocalDbSpec` resolves the deterministic persistent runtime DB target through the shared default-path seam and declares no local migrations; `TursoDb` supplies retry-backed blocking `execute`/`query`, parent-directory creation, Turso connection setup, tokio current-thread runtime bridging, and generic migration execution. - `cli/src/services/auth_db/mod.rs` provides the encrypted auth DB spec and `AuthDb` type alias over `EncryptedTursoDb`. `AuthDbSpec` resolves `/sce/auth.db` through the shared default-path seam and embeds ordered auth migrations. Auth DB lifecycle setup/doctor integration is wired through `AuthDbLifecycle`; auth command/token-storage reads/writes are directed through `token_storage.rs`. -- `cli/src/services/agent_trace_db/mod.rs` provides the Agent Trace DB spec and `AgentTraceDb` type alias over `TursoDb`. `AgentTraceDbSpec` resolves `/sce/agent-trace.db` through the shared default-path seam and embeds ordered fresh-start migrations `001..016` for `diff_traces`, `post_commit_patch_intersections`, `agent_traces`, `messages`, `parts`, `session_models`, `diff_traces.payload_type`, indexes, and triggers. `agent_traces.agent_trace_id` is `NOT NULL UNIQUE`. The module provides `DiffTraceInsert<'_>`/`insert_diff_trace()` including the `payload_type` discriminator, `PostCommitPatchIntersectionInsert<'_>`/`insert_post_commit_patch_intersection()`, `AgentTraceInsert<'_>`/`insert_agent_trace()`, `InsertMessageInsert`/`insert_message()` plus `insert_messages()` for parent message inserts with duplicate `(session_id, message_id)` writes ignored, `InsertPartInsert`/`insert_part()` plus `insert_parts()` for append-only part text writes, `SessionModelUpsert<'_>`/`upsert_session_model()` plus lookup helpers for durable `session_models` attribution, and `recent_diff_trace_patches(cutoff_time_ms, end_time_ms)` for inclusive chronological `diff_traces` reads that dispatch on `payload_type`, parse valid raw patch or structured payload rows, and return skipped malformed-row reports. `cli/src/services/agent_trace_db/lifecycle.rs` registers Agent Trace DB setup/doctor lifecycle behavior: setup creates/reuses and registers checkout identity when scoped to a repo root, then still runs migration-backed global `AgentTraceDb::new()` until the per-checkout DB switch lands; active hook runtime writes/read paths use `AgentTraceDb::open_for_hooks_without_migrations()` followed by `ensure_schema_ready_for_hooks()` and fail with setup guidance when migration metadata is missing or incomplete. +- `cli/src/services/agent_trace_db/mod.rs` provides the Agent Trace DB spec and `AgentTraceDb` type alias over `TursoDb`. `AgentTraceDbSpec` resolves the legacy `/sce/agent-trace.db` fallback through the shared default-path seam and embeds ordered fresh-start migrations `001..016` for `diff_traces`, `post_commit_patch_intersections`, `agent_traces`, `messages`, `parts`, `session_models`, `diff_traces.payload_type`, indexes, and triggers. `agent_traces.agent_trace_id` is `NOT NULL UNIQUE`. The module provides explicit-path `open_at(path)` and `open_for_hooks_without_migrations_at(path)` helpers for per-checkout DB files plus `DiffTraceInsert<'_>`/`insert_diff_trace()` including the `payload_type` discriminator, `PostCommitPatchIntersectionInsert<'_>`/`insert_post_commit_patch_intersection()`, `AgentTraceInsert<'_>`/`insert_agent_trace()`, `InsertMessageInsert`/`insert_message()` plus `insert_messages()` for parent message inserts with duplicate `(session_id, message_id)` writes ignored, `InsertPartInsert`/`insert_part()` plus `insert_parts()` for append-only part text writes, `SessionModelUpsert<'_>`/`upsert_session_model()` plus lookup helpers for durable `session_models` attribution, and `recent_diff_trace_patches(cutoff_time_ms, end_time_ms)` for inclusive chronological `diff_traces` reads that dispatch on `payload_type`, parse valid raw patch or structured payload rows, and return skipped malformed-row reports. `cli/src/services/agent_trace_db/lifecycle.rs` registers Agent Trace DB setup/doctor lifecycle behavior: setup creates/reuses and registers checkout identity when scoped to a repo root without creating a DB file; active hook runtime resolves checkout identity and lazily creates or upgrades `/sce/agent-trace-{checkout_id}.db` before Agent Trace reads/writes. - `cli/src/test_support.rs` provides a shared test-only temp-directory helper (`TestTempDir`) used by service tests that need filesystem fixtures. - `cli/src/services/setup/mod.rs` defines the setup command contract (`SetupMode`, `SetupTarget`, `SetupRequest`, CLI flag parser/validator), an `inquire`-backed interactive target prompter (`InquireSetupTargetPrompter`), setup dispatch outcomes (proceed/cancelled), compile-time embedded asset access (`EmbeddedAsset`, target-scoped iterators, required-hook asset iterators/lookups) generated by `cli/build.rs` from the ephemeral crate-local `cli/assets/generated/config/{opencode,claude}/**` mirror plus `cli/assets/hooks/**`, and focused internal support seams for install-flow vs prompt-flow logic; `cli/src/services/setup/command.rs` owns the `SetupCommand` payload used by the static `RuntimeCommand` enum and executes against any context implementing repo-root scoping. Its install engine/orchestrator stages embedded files and uses a unified remove-and-replace policy (removing existing targets before swapping staged content, with deterministic recovery guidance on swap failure and no backup artifact creation), and formats deterministic completion messaging; required-hook install orchestration (`install_required_git_hooks`) follows the same remove-and-replace policy (removing existing hooks before swapping staged content, with deterministic recovery guidance on swap failure). The setup command derives a repo-root-scoped context before aggregating static lifecycle provider `setup` dispatch across providers (config → local_db → auth_db → agent_trace_db → hooks when requested), so setup providers consume only repo-root access from the scoped context. - `cli/src/services/setup/mod.rs` keeps those responsibilities inside one file for now, but the current ownership split is explicit: the inline `install` module owns repository-path normalization, staging/swap install behavior, required-hook installation, and filesystem safety guards, while the inline `prompt` module owns interactive target selection and prompt styling. @@ -125,7 +125,7 @@ The repository includes a new placeholder Rust binary crate at `cli/`. - No user-invocable `sce sync` command is wired in the current runtime; local DB and Agent Trace DB bootstrap flows through lifecycle providers aggregated by setup, and DB health/repair flows through the doctor surface. - `cli/src/services/patch.rs` defines the standalone patch domain model (`ParsedPatch`, `PatchFileChange`, `FileChangeKind`, `PatchHunk`, `TouchedLine`, `TouchedLineKind`) for in-memory parsed unified-diff representation, capturing only touched lines (added/removed) plus minimal per-file/per-hunk metadata while excluding non-hunk headers and unchanged context lines. All types are `serde`-serializable/deserializable with `snake_case` JSON field naming. The module also provides `parse_patch`, a public parser function that converts raw unified-diff text (both `Index:` SVN-style and `diff --git` git-style formats) into `ParsedPatch` structs, with `ParseError` for actionable malformed-input diagnostics. Storage-agnostic JSON load helpers (`load_patch_from_json` for string input, `load_patch_from_json_bytes` for byte input) reconstruct `ParsedPatch` from serialized JSON content with `PatchLoadError` for actionable deserialization diagnostics. Its patch-set operations now include deterministic ordered combination plus target-shaped intersection that prefers exact touched-line matches and falls back to historical `kind`+`content` matching when incremental diffs and canonical post-commit diffs have drifted line numbers; `parse_patch`, `combine_patches`, and `intersect_patches` are consumed by the active post-commit hook runtime. - `cli/src/services/structured_patch.rs` defines the synchronous structured editor-hook derivation seam. It derives Claude `PostToolUse` `Write` create and `Edit` structured-patch payloads into canonical `ParsedPatch` values plus Claude session/tool metadata, returning deterministic skip reasons for unsupported events/tools/payload shapes. The module is pure and side-effect-free. It is wired into `sce hooks diff-trace` for Claude payload classification at intake (T04) and into `AgentTraceDb::recent_diff_trace_patches` for post-commit structured payload parsing dispatch at read time (T05). -- `cli/src/services/` contains module boundaries for command_registry, lifecycle, auth_command, config, setup, doctor, hooks, checkout identity, bash_policy, version, completion, help, patch, SCE web URL helpers, shared database infrastructure, local DB adapters, encrypted auth DB adapters, and Agent Trace DB adapters with explicit trait seams for future implementations. `cli/src/services/checkout/` currently owns checkout ID file infrastructure plus central checkout-registry JSON persistence, but setup now uses it through `AgentTraceDbLifecycle::setup()`, while hooks, doctor, and per-checkout Agent Trace DB path consumers remain deferred. `cli/src/services/bash_policy.rs` owns both the CLI-agnostic evaluator logic and the hidden `sce policy bash` command adapter used by OpenCode and Claude hook callers. `cli/src/services/command_registry.rs` defines the static `RuntimeCommand` enum, deterministic `CommandRegistry` name catalog, and `build_default_registry()` function for command dispatch metadata. Service-owned command modules own the runtime command payload structs for help/help-text, version, completion, auth, config, setup, doctor, hooks, and policy. +- `cli/src/services/` contains module boundaries for command_registry, lifecycle, auth_command, config, setup, doctor, hooks, checkout identity, bash_policy, version, completion, help, patch, SCE web URL helpers, shared database infrastructure, local DB adapters, encrypted auth DB adapters, and Agent Trace DB adapters with explicit trait seams for future implementations. `cli/src/services/checkout/` owns checkout ID file infrastructure, central checkout-registry JSON persistence, and per-checkout Agent Trace DB lazy resolution for hook runtime; setup uses it through `AgentTraceDbLifecycle::setup()`, while doctor checkout display and registry listing remain deferred. `cli/src/services/bash_policy.rs` owns both the CLI-agnostic evaluator logic and the hidden `sce policy bash` command adapter used by OpenCode and Claude hook callers. `cli/src/services/command_registry.rs` defines the static `RuntimeCommand` enum, deterministic `CommandRegistry` name catalog, and `build_default_registry()` function for command dispatch metadata. Service-owned command modules own the runtime command payload structs for help/help-text, version, completion, auth, config, setup, doctor, hooks, and policy. - `cli/README.md` is the crate-local onboarding and usage source of truth for placeholder behavior, safety limitations, and roadmap mapping back to service contracts. - `flake.nix` applies `rust-overlay` (`oxalica/rust-overlay`) to nixpkgs, pins `rust-bin.stable.1.93.1.default` with `rustfmt` + `clippy`, reads the package/check version from repo-root `.version`, builds `packages.sce` through Crane (`buildDepsOnly` -> `buildPackage`) with a filtered repo-root source that preserves the Cargo tree plus `cli/assets/hooks`, then injects generated OpenCode/Claude config payloads and schema inputs into a temporary `cli/assets/generated/` mirror during derivation unpack so `cli/build.rs` can package the crate without requiring committed generated crate assets. The same `cli/build.rs` now scans `cli/migrations/*/*.sql` and writes `cli/src/generated_migrations.rs` with deterministic migration constants sorted by numeric filename prefix. The flake runs `cli-tests`, `cli-clippy`, and `cli-fmt` plus the dedicated `integrations-install-tests`, `integrations-install-clippy`, and `integrations-install-fmt` derivations through Crane-backed paths so both Rust crates have first-class default-flake verification, exposes directory-scoped JS validation derivations for both `npm/` and the shared `config/lib/` plugin package root, and also exposes the non-default `apps.install-channel-integration-tests` flake app for install-channel integration coverage outside the default check set. The shared config-lib source set is rooted at `config/lib/` and includes the shared `package.json`, `bun.lock`, and `tsconfig.json` plus `agent-trace-plugin/` and `bash-policy-plugin/`; `config-lib-bun-tests` runs the bash-policy plugin wrapper tests from that shared root, while `config-lib-biome-check` and `config-lib-biome-format` run Biome over the copied shared package source. `.github/workflows/publish-crates.yml` follows the same asset-preparation rule but runs Cargo packaging from a temporary clean repository copy so crates.io publish no longer needs `--allow-dirty`. - The config-lib check source preserves repo-relative access to shared CLI patch fixtures: Nix copies a filtered repo-shaped source containing `config/lib/**` plus `cli/src/services/structured_patch/fixtures`, then runs Bun/Biome from `config/lib/`. diff --git a/context/cli/checkout-identity.md b/context/cli/checkout-identity.md index 3453246b..6b537655 100644 --- a/context/cli/checkout-identity.md +++ b/context/cli/checkout-identity.md @@ -2,7 +2,7 @@ The checkout identity service lives in `cli/src/services/checkout/`. -It assigns a stable identity to a local Git checkout or linked Git worktree. The setup lifecycle now creates/reuses this identity and registers the checkout; hook integration, per-checkout Agent Trace database resolution, and doctor reporting are deferred to later tasks in `context/plans/agent-trace-checkout-identity.md`. +It assigns a stable identity to a local Git checkout or linked Git worktree. The setup lifecycle creates/reuses this identity and registers the checkout. Agent Trace hook runtime now resolves persistence through this identity and lazily initializes a per-checkout database. ## Current code surface @@ -11,6 +11,8 @@ It assigns a stable identity to a local Git checkout or linked Git worktree. The - `read_checkout_id(git_dir)` reads `/sce/checkout-id` and validates non-empty UUID syntax. - `get_or_create_checkout_id(git_dir)` reuses an existing ID or writes a new UUIDv7 checkout ID to `/sce/checkout-id`. - `resolve_checkout_id_for_repo(repo_root)` combines Git directory resolution with get-or-create checkout ID behavior. + - `resolve_or_create_agent_trace_db_for_current_checkout()` resolves from `std::env::current_dir()` and returns `(AgentTraceDb, checkout_id)`. + - `resolve_or_create_agent_trace_db_for_checkout(repo_root)` gets or creates checkout identity, registers it, resolves `/sce/agent-trace-{checkout_id}.db`, fast-opens an existing ready DB, and falls back to migration-running initialization when the DB is absent or schema metadata is incomplete. - `cli/src/services/checkout/registry.rs` - `CheckoutRecord` serializes `checkout_id`, `path`, `last_seen`, optional `remote_url`, and optional `database_path`. - `CheckoutRegistry` serializes the registry as `{ "checkouts": [...] }`. @@ -19,7 +21,7 @@ It assigns a stable identity to a local Git checkout or linked Git worktree. The ## Current integration state -The module is registered through `cli/src/services/mod.rs` and is called by `AgentTraceDbLifecycle::setup()` during `sce setup` after the setup command has derived a repository-root-scoped context. +The module is registered through `cli/src/services/mod.rs` and is called by `AgentTraceDbLifecycle::setup()` during `sce setup` after the setup command has derived a repository-root-scoped context. Hook runtime also calls it before Agent Trace DB reads/writes. During setup: @@ -28,7 +30,15 @@ During setup: - `checkout::registry::register_checkout(...)` writes or updates the central registry record with `database_path: null`. - Setup output includes the checkout ID and states that the Agent Trace database will be created on first write. -The existing global Agent Trace database path remains the active runtime path until later plan tasks switch consumers to per-checkout database resolution. +During hook runtime: + +- `checkout::resolve_git_dir(repo_root)` and `checkout::get_or_create_checkout_id(git_dir)` make hooks self-sufficient when `sce setup` has not run yet. +- `default_paths::agent_trace_db_path_for_checkout(checkout_id)` computes `/sce/agent-trace-{checkout_id}.db`. +- `AgentTraceDb::open_for_hooks_without_migrations_at(path)` is tried first; `ensure_schema_ready_for_hooks()` decides whether the schema is current. +- Missing or incomplete schema falls back to `AgentTraceDb::open_at(path)`, which runs migrations through the shared Turso adapter. +- Successful DB resolution updates the registry record with `database_path`. + +The global `agent-trace.db` path remains only as a lifecycle fallback when no checkout context or checkout ID is available. Doctor checkout identity display and registry listing are deferred to later tasks in `context/plans/agent-trace-checkout-identity.md`. ## Testing boundary diff --git a/context/cli/default-path-catalog.md b/context/cli/default-path-catalog.md index 1ea87232..d5f0290b 100644 --- a/context/cli/default-path-catalog.md +++ b/context/cli/default-path-catalog.md @@ -17,7 +17,9 @@ - global config: `/sce/config.json` - auth DB: `/sce/auth.db` - local DB: `/sce/local.db` -- agent trace DB: `/sce/agent-trace.db` +- legacy/global agent trace DB fallback: `/sce/agent-trace.db` +- per-checkout agent trace DB: `/sce/agent-trace-{checkout_id}.db` +- checkout registry: `/sce/checkout-registry.json` ### Repo-relative paths @@ -43,5 +45,6 @@ - `cli/src/services/setup/mod.rs` now resolves setup target directory names and required hook identifiers through `default_paths.rs` constants/accessors instead of owning those path literals locally. - `cli/src/services/default_paths.rs` includes a regression test that scans non-test Rust source under `cli/src/` and fails when new centralized production path literals appear outside the default-path service. - `cli/src/services/hooks/mod.rs` resolves the collision-safe `context/tmp/` path shape through shared path accessors. +- `cli/src/services/checkout/mod.rs` resolves per-checkout Agent Trace DB files through `agent_trace_db_path_for_checkout(checkout_id)` and stores the resulting path in the checkout registry after lazy DB initialization. See also: [cli-command-surface.md](./cli-command-surface.md), [../architecture.md](../architecture.md), [../context-map.md](../context-map.md) diff --git a/context/cli/service-lifecycle.md b/context/cli/service-lifecycle.md index 704d716d..11c2d32f 100644 --- a/context/cli/service-lifecycle.md +++ b/context/cli/service-lifecycle.md @@ -32,9 +32,9 @@ - `LocalDbLifecycle::fix` bootstraps the canonical local DB parent directory for auto-fixable local DB parent readiness problems. - `LocalDbLifecycle::setup` initializes the canonical local DB through `LocalDb::new()` and returns an empty `SetupOutcome` because DB bootstrap currently has no dedicated outcome carrier. - `cli/src/services/agent_trace_db/lifecycle.rs` defines `AgentTraceDbLifecycle`, the Agent Trace DB-owned provider. -- `AgentTraceDbLifecycle::diagnose` emits canonical Agent Trace DB path and parent-directory readiness lifecycle health problems. -- `AgentTraceDbLifecycle::fix` bootstraps the canonical Agent Trace DB parent directory for auto-fixable DB parent readiness problems. -- `AgentTraceDbLifecycle::setup` resolves the current checkout identity from `ctx.repo_root()` when available, creates or reuses `/sce/checkout-id`, registers the checkout with `database_path: null`, returns setup messaging with the checkout ID, and still initializes the existing global Agent Trace DB through `AgentTraceDb::new()` until per-checkout DB resolution lands. +- `AgentTraceDbLifecycle::diagnose` emits per-checkout Agent Trace DB path and parent-directory readiness lifecycle health problems when `ctx.repo_root()` has an existing checkout ID; otherwise it falls back to the legacy global Agent Trace DB path. +- `AgentTraceDbLifecycle::fix` bootstraps the resolved per-checkout Agent Trace DB parent directory for auto-fixable DB parent readiness problems, with the same global fallback outside checkout context. +- `AgentTraceDbLifecycle::setup` resolves the current checkout identity from `ctx.repo_root()` when available, creates or reuses `/sce/checkout-id`, registers the checkout with `database_path: null`, and returns setup messaging with the checkout ID. It does not initialize an Agent Trace DB file; hook runtime creates or upgrades the per-checkout DB lazily on first use. - `doctor` runtime execution now aggregates lifecycle providers for diagnosis and repair: - `cli/src/services/doctor/command.rs` accepts any context implementing `ContextWithRepoRoot`. - `cli/src/services/doctor/mod.rs` resolves the repository root once, creates a repo-root-scoped borrowed context using `with_repo_root()`, and requests the full provider catalog with hooks included. diff --git a/context/context-map.md b/context/context-map.md index 319fe4a9..fbc645f2 100644 --- a/context/context-map.md +++ b/context/context-map.md @@ -11,7 +11,7 @@ Feature/domain context: - `context/cli/cli-command-surface.md` (CLI command surface including top-level help with ASCII art banner and gradient rendering, setup install flow, WorkOS device authorization flow + token storage behavior, attribution-only hook routing with validated post-commit `--remote-url` plumbing plus DB-backed `diff-trace` dual persistence and post-commit Agent Trace payload persistence including range `content_hash`, setup-owned local DB + Agent Trace DB bootstrap plus doctor DB health coverage, centralized Rust SCE web URL helpers in `services::agent_trace`, nested flake release package/app installability, Cargo local install + crates.io readiness policy, and hidden `sce policy bash` command adapter for bash-policy hook callers; `sce sync` command wiring is deferred to `0.4.0`; static `RuntimeCommand` enum dispatch lives in `services/command_registry.rs`, command payload structs for help/version/completion/auth/config/setup/doctor/hooks/policy are owned by their respective `services/{name}/command.rs` files, and clap-to-runtime conversion lives in `services/parse/command_runtime.rs`) - `context/cli/default-path-catalog.md` (canonical production CLI path-ownership contract centered on `cli/src/services/default_paths.rs`, including persisted auth/config files, named DB paths for auth/local/Agent Trace databases, repo-relative, embedded-asset, install, hook, and context-path families plus the regression guard that keeps production path ownership centralized) -- `context/cli/checkout-identity.md` (current checkout identity infrastructure in `cli/src/services/checkout/`, including `/sce/checkout-id` UUIDv7 storage, central `/sce/checkout-registry.json` records, atomic registry persistence, setup lifecycle integration that creates/reuses/registers checkout identity and emits checkout-ID messaging, and the remaining boundary before hooks/doctor/per-checkout Agent Trace DB consumers are switched) +- `context/cli/checkout-identity.md` (current checkout identity infrastructure in `cli/src/services/checkout/`, including `/sce/checkout-id` UUIDv7 storage, central `/sce/checkout-registry.json` records, atomic registry persistence, setup lifecycle integration that creates/reuses/registers checkout identity and emits checkout-ID messaging, per-checkout Agent Trace DB lazy resolution for hook runtime, and the remaining doctor display/listing boundary) - `context/cli/patch-service.md` (standalone patch domain model, parser, JSON load helpers, and set operations in `cli/src/services/patch.rs` for in-memory parsed unified-diff representation, capturing only touched lines plus minimal per-file/per-hunk metadata, supporting both `Index:` SVN-style and `diff --git` git-style formats, with `ParseError` for actionable malformed-input diagnostics, `PatchLoadError`/`load_patch_from_json`/`load_patch_from_json_bytes` for storage-agnostic JSON reconstruction, `intersect_patches` for target-shaped overlap with exact-match-first and historical `kind`+`content` fallback semantics plus matched-constructed-line `session_id` and matched-constructed-hunk `model_id` provenance inheritance, and `combine_patches` for ordered patch combination with later-wins conflict resolution plus winning-hunk `model_id` provenance inheritance; `parse_patch`, `intersect_patches`, and `combine_patches` are consumed by the active post-commit hook runtime) - `context/cli/styling-service.md` (CLI text-mode output styling with `owo-colors` and `comfy-table`, TTY/`NO_COLOR` policy, shared helper API for human-facing surfaces, and per-column right-to-left RGB gradient banner rendering) - `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, canonical Pkl-generated `sce/config.json` schema ownership plus CLI embedding/reuse contract, config-file selection order, `show` provenance output, and trimmed `validate` output contract) @@ -43,9 +43,9 @@ Feature/domain context: - `context/sce/agent-trace-post-rewrite-local-remap-ingestion.md` (current post-rewrite no-op baseline plus historical remap-ingestion reference) - `context/sce/agent-trace-rewrite-trace-transformation.md` (current post-rewrite no-op baseline plus historical rewrite-transformation reference) - `context/sce/local-db.md` (implemented `cli/src/services/local_db/mod.rs` local database spec with `LocalDb = TursoDb`, canonical local DB path resolution, zero local migrations, and inherited retry-backed blocking `execute`/`query`/`query_map` methods using the shared Turso adapter) -- `context/sce/shared-turso-db.md` (current shared `cli/src/services/db/mod.rs` Turso database infrastructure seam, including `DbSpec`, generic `TursoDb`, encrypted `EncryptedTursoDb`, build-time generated migration constants from `cli/build.rs`/`cli/src/generated_migrations.rs`, config-driven constructor/open-connect retry via `run_with_retry_sync`, no-migration `TursoDb::open_without_migrations()` for hot runtime paths, migration-running `new()`/`run_migrations()` with per-database `__sce_migrations` tracking, config-driven operation retry for `execute`/`query`/`query_map` with a `<= 2_000ms` default query failure budget, row-mapping excluded from retry, generic embedded migration execution, non-mutating `migration_metadata_problems()` and `ensure_schema_ready(setup_guidance)` readiness methods on `TursoDb`, and concrete wrappers for `LocalDb`, `AuthDb`, plus `AgentTraceDb`) +- `context/sce/shared-turso-db.md` (current shared `cli/src/services/db/mod.rs` Turso database infrastructure seam, including `DbSpec`, generic `TursoDb`, encrypted `EncryptedTursoDb`, build-time generated migration constants from `cli/build.rs`/`cli/src/generated_migrations.rs`, config-driven constructor/open-connect retry via `run_with_retry_sync`, no-migration `TursoDb::open_without_migrations()` / explicit-path `open_without_migrations_at(path)` for hot runtime paths, migration-running `new()` / explicit-path `new_at(path)` / `run_migrations()` with per-database `__sce_migrations` tracking, config-driven operation retry for `execute`/`query`/`query_map` with a `<= 2_000ms` default query failure budget, row-mapping excluded from retry, generic embedded migration execution, non-mutating `migration_metadata_problems()` and `ensure_schema_ready(setup_guidance)` readiness methods on `TursoDb`, and concrete wrappers for `LocalDb`, `AuthDb`, plus `AgentTraceDb`) - `context/sce/auth-db.md` (encrypted `AuthDb = EncryptedTursoDb` adapter, canonical `/sce/auth.db` path, build-time generated `AUTH_MIGRATIONS` from `cli/migrations/auth/`, auth credential schema and updated-at trigger baseline, lifecycle setup/doctor integration, encrypted token-storage persistence, and `SCE_AUTH_DB_ENCRYPTION_KEY`/OS credential-store key handling) -- `context/sce/agent-trace-db.md` (implemented `cli/src/services/agent_trace_db/mod.rs` Agent Trace database wrapper with canonical `/sce/agent-trace.db` path, migration-running `AgentTraceDb::new()` setup/lifecycle initialization, no-migration `AgentTraceDb::open_for_hooks_without_migrations()` runtime-open API, non-mutating `ensure_schema_ready_for_hooks()` delegation to `TursoDb::ensure_schema_ready()` with `Run 'sce setup'.` guidance, ordered `diff_traces`, `post_commit_patch_intersections`, `agent_traces`, parent `messages`, append-only `parts`, indexes/triggers, typed parameterized insert helpers, bounded chronological recent `diff_traces` query/parse support with malformed-row skip accounting, registered setup/doctor lifecycle provider, and active hook writers for `diff_traces`, post-commit intersection/agent-trace persistence, `messages`, and `parts`) +- `context/sce/agent-trace-db.md` (implemented `cli/src/services/agent_trace_db/mod.rs` Agent Trace database wrapper with legacy global `/sce/agent-trace.db` fallback plus active per-checkout `/sce/agent-trace-{checkout_id}.db` hook runtime paths, explicit-path migration and no-migration open APIs, non-mutating `ensure_schema_ready_for_hooks()` delegation to `TursoDb::ensure_schema_ready()`, lazy checkout DB initialization/upgrade, ordered `diff_traces`, `post_commit_patch_intersections`, `agent_traces`, parent `messages`, append-only `parts`, indexes/triggers, typed parameterized insert helpers, bounded chronological recent `diff_traces` query/parse support with malformed-row skip accounting, registered setup/doctor lifecycle provider, and active hook writers for `diff_traces`, post-commit intersection/agent-trace persistence, `messages`, and `parts`) - `context/sce/agent-trace-core-schema-migrations.md` (historical reference for removed local DB schema bootstrap behavior; T03 now implements the actual local DB with migrations) - `context/sce/agent-trace-retry-queue-observability.md` (inactive local-hook retry path plus historical retry/metrics reference) - `context/sce/agent-trace-local-hooks-mvp-contract-gap-matrix.md` (T01 Local Hooks MVP production contract freeze and deterministic gap matrix for `agent-trace-local-hooks-production-mvp`) diff --git a/context/glossary.md b/context/glossary.md index 9b11dc4c..240281ad 100644 --- a/context/glossary.md +++ b/context/glossary.md @@ -34,7 +34,7 @@ - `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. - `AuthDbLifecycle`: Lifecycle provider in `cli/src/services/auth_db/lifecycle.rs` that implements `ServiceLifecycle` for encrypted auth DB setup/doctor integration. `diagnose` collects auth DB path health problems, `fix` bootstraps missing auth DB parent directory, and `setup` calls `AuthDb::new()`. Registered as `LifecycleProviderId::AuthDb` in the shared lifecycle catalog. -- `agent trace DB adapter`: Module in `cli/src/services/agent_trace_db/mod.rs` that defines `AgentTraceDbSpec`, exposes `AgentTraceDb` as a `TursoDb` alias, resolves `/sce/agent-trace.db` through `agent_trace_db_path()`, embeds an ordered split fresh-start baseline migration set (`001..016`) covering `diff_traces`, `post_commit_patch_intersections`, `agent_traces`, `messages`, `parts`, `session_models`, and `diff_traces.payload_type` plus indexes and triggers; provides typed parameterized insert/upsert helpers for diff traces with `payload_type` discriminator, post-commit intersection rows, built agent-trace rows, message/part batch inserts, and session model attribution; exposes chronological recent `diff_traces` query/parse support with malformed-row skip accounting; has `AgentTraceDbLifecycle` for setup/doctor integration; and is accessed by active Agent Trace hooks through the no-migration readiness gate. +- `agent trace DB adapter`: Module in `cli/src/services/agent_trace_db/mod.rs` that defines `AgentTraceDbSpec`, exposes `AgentTraceDb` as a `TursoDb` alias, keeps `/sce/agent-trace.db` as the legacy/global fallback path, supports explicit per-checkout DB paths through `open_at(path)` and `open_for_hooks_without_migrations_at(path)`, embeds an ordered split fresh-start baseline migration set (`001..016`) covering `diff_traces`, `post_commit_patch_intersections`, `agent_traces`, `messages`, `parts`, `session_models`, and `diff_traces.payload_type` plus indexes and triggers; provides typed parameterized insert/upsert helpers for diff traces with `payload_type` discriminator, post-commit intersection rows, built agent-trace rows, message/part batch inserts, and session model attribution; exposes chronological recent `diff_traces` query/parse support with malformed-row skip accounting; has `AgentTraceDbLifecycle` for setup/doctor integration; and is accessed by active Agent Trace hooks through checkout-scoped lazy DB resolution. - `structured patch service`: Pure synchronous Rust service in `cli/src/services/structured_patch.rs` that derives supported structured editor hook payloads into canonical `ParsedPatch` values. The current implemented source is Claude `PostToolUse` payloads for `Write` creates and `Edit` structured patches; wired into `sce hooks diff-trace` for Claude payload classification at intake (T04) and into `AgentTraceDb::recent_diff_trace_patches` for post-commit structured payload parsing dispatch at read time (T05). - `Agent Trace SCE metadata`: Implementation-owned top-level metadata emitted by `build_agent_trace(...)` as `metadata.sce.version`; the value is sourced from the compiled `sce` CLI package version via `env!("CARGO_PKG_VERSION")`, is schema-validated with the rest of the payload, and is persisted in AgentTraceDb `agent_traces.trace_json` without changing the top-level Agent Trace payload/schema `version`. - `Agent Trace range content_hash`: Per-range `content_hash` emitted by `build_agent_trace(...)` inside every `ranges[]` entry as `murmur3:`, computed from the touched-line kind/content of the `post_commit_patch` or embedded-patch hunk used to emit that range while excluding positions, paths, metadata, and database IDs. @@ -43,7 +43,7 @@ - `DbSpec`: Service-specific database metadata trait in `cli/src/services/db/mod.rs` that supplies a diagnostic database name, canonical path resolver, ordered embedded migration list, and config-file lookup key (`db_config_key()`) for `TursoDb`. - `TursoDb`: Generic unencrypted Turso database adapter in `cli/src/services/db/mod.rs`; owns parent-directory creation and Turso local open/connect flow wrapped in config-driven connection-open retry, then delegates synchronous `execute()`/`query()`/`query_map()` wrappers with config-driven query retry and migration execution through the shared internal `TursoConnectionCore` for a `DbSpec` implementation. - `TursoConnectionCore`: Internal shared operation core in `cli/src/services/db/mod.rs` used by both `TursoDb` and `EncryptedTursoDb`; owns the Turso connection and tokio current-thread runtime bridging used by the public adapter methods; generic embedded migration execution with per-database `__sce_migrations` metadata is delegated to `run_embedded_migrations` helpers. -- `no-migration DB open path`: `TursoDb::open_without_migrations()` plus the Agent Trace-specific `AgentTraceDb::open_for_hooks_without_migrations()` seam; opens/connects a local Turso database with parent-directory creation and configured connection-open retry but does not create `__sce_migrations` or run embedded schema migrations. Active Agent Trace hook callers route through this path and must pass `AgentTraceDb::ensure_schema_ready_for_hooks()` before persistence or reads. +- `no-migration DB open path`: `TursoDb::open_without_migrations()` / `TursoDb::open_without_migrations_at(path)` plus the Agent Trace-specific `AgentTraceDb::open_for_hooks_without_migrations()` / `open_for_hooks_without_migrations_at(path)` seams; opens/connects a local Turso database with parent-directory creation and configured connection-open retry but does not create `__sce_migrations` or run embedded schema migrations. Active Agent Trace hook callers first try the explicit per-checkout no-migration path and then fall back to migration-running initialization when readiness fails. - `TursoDb migration readiness check`: Public methods on `TursoDb` in `cli/src/services/db/mod.rs` for non-mutating schema-readiness verification: `migration_metadata_problems(&self) -> Result>` queries `__sce_migrations` metadata and compares applied IDs against `M::migrations()`, returning problems (missing table, incomplete migrations, unexpected migrations) or an empty list when ready; `ensure_schema_ready(&self, setup_guidance: &str) -> Result<()>` calls `migration_metadata_problems()` and bails with a formatted error including `M::db_name()` and the caller-provided guidance string when problems are found. `AgentTraceDb::ensure_schema_ready_for_hooks()` delegates to `TursoDb::ensure_schema_ready()` with the Agent Trace–specific `AGENT_TRACE_SCHEMA_SETUP_GUIDANCE` constant. - `database_retry config namespace`: Nested config namespace under `policies.database_retry` in `sce/config.json`, authored in `config/pkl/base/sce-config-schema.pkl` and parsed/resolved in `cli/src/services/config/mod.rs`. Supports per-database overrides (`local_db`, `agent_trace_db`, `auth_db`) each with optional `connection_open` and `query` objects containing `max_attempts`, `timeout_ms`, `initial_backoff_ms`, `max_backoff_ms`. Validated against JSON Schema at config load and surfaced in `sce config show`/`validate`. Wired into DB adapter constructors and operation methods via config-aware retry resolution with fallback to hardcoded defaults. - `DatabaseRetryConfig`: Rust type in `cli/src/services/config/mod.rs` holding parsed and validated per-database retry policy overrides (`local_db`/`agent_trace_db`/`auth_db`, each `Option`) from the `policies.database_retry` config namespace. Initialized at app startup via `DATABASE_RETRY_CONFIG` `OnceLock` and consumed by config-aware retry resolution in DB adapters. @@ -52,7 +52,7 @@ - `DB query retry policy`: Retry policy used by `TursoDb::execute()`, `TursoDb::query()`, `TursoDb::query_map()`, `EncryptedTursoDb::execute()`, `EncryptedTursoDb::query()`, and `EncryptedTursoDb::query_map()` for local Turso operation retry, resolved from `policies.database_retry..query` via the `DATABASE_RETRY_CONFIG` `OnceLock` with fallback to hardcoded defaults (`5` attempts, `200ms` elapsed-attempt timeout, `25ms` initial backoff, `100ms` max backoff; default worst-case failure budget `<= 2_000ms`) through `run_with_retry_sync`. `query_map()` retries the initial query and row-fetch loop, then runs caller row mapping outside retry. - `__sce_migrations`: Per-database migration metadata table created by the shared `TursoConnectionCore` migration path behind public adapter `run_migrations()` methods; records applied migration IDs after successful execution so later setup/lifecycle initialization applies only migrations not yet recorded, while existing metadata-less DBs are brought forward by re-applying the current idempotent migration set and recording each ID. - `CLI generated migration manifest`: Build-time Rust source at `cli/src/generated_migrations.rs` written by `cli/build.rs` from immediate `cli/migrations//*.sql` directories; constants are named from the database directory (for example `AGENT_TRACE_MIGRATIONS`, `AUTH_MIGRATIONS`), sorted by the numeric filename prefix before `_`, and embed SQL via `include_str!`. -- `sync command deferral`: Current plan/state note that a user-invocable `sce sync` command is not wired yet and is deferred to `0.4.0`; local DB and Agent Trace DB bootstrap now flow through lifecycle providers aggregated by the setup command, and DB health/repair flows through the doctor surface. +- `sync command deferral`: Current plan/state note that a user-invocable `sce sync` command is not wired yet and is deferred to `0.4.0`; local DB bootstrap flows through lifecycle providers aggregated by the setup command, Agent Trace setup establishes checkout identity while per-checkout DB files are lazy hook-runtime artifacts, and DB health/repair flows through the doctor surface. - `CLI bounded resilience wrapper`: Shared policy in `cli/src/services/resilience.rs` (`RetryPolicy`, async `run_with_retry`, sync `run_with_retry_sync`) that applies deterministic retries/timeouts/capped backoff to transient operations, emits retry observability events, and returns actionable terminal failure guidance. The sync helper is currently wired into shared database constructors for local open/connect retry and into `TursoDb`/`EncryptedTursoDb` operation retry for `execute()`/`query()`/`query_map()`. - `setup service orchestration`: Setup execution logic in `cli/src/services/setup/command.rs` that resolves the repository root, derives a repo-root-scoped `AppContext` from the runtime command context, dispatches `setup` through the static lifecycle provider catalog (config → local_db → auth_db → agent_trace_db → hooks when requested), handles interactive target selection for config asset installation, and emits deterministic success messaging per target. - `setup target flags`: Mutually-exclusive `sce setup` target selectors (`--opencode`, `--claude`, `--both`) that force non-interactive mode for automation. @@ -64,7 +64,7 @@ - `setup required-hook install orchestration`: Setup-service flow in `cli/src/services/setup/mod.rs` (`install_required_git_hooks`) that resolves repository root + effective hooks directory via git truth, installs canonical required hooks with deterministic per-hook outcomes (`Installed`, `Updated`, `Skipped`), enforces executable permissions, and uses a unified remove-and-replace policy that removes existing hooks before swapping staged content with deterministic recovery guidance on swap failure. - `setup hooks CLI mode`: `sce setup` behavior activated by `--hooks` (with optional `--repo `), supporting both hooks-only runs and composable target+hooks runs in one invocation; implemented through `cli/src/services/setup/command.rs` + `cli/src/services/setup/mod.rs`, enforces deterministic compatibility validation (`--repo` requires `--hooks`; target flags remain mutually exclusive), and emits stable setup/hook status output. - `setup repo gate`: Preflight check in `cli/src/services/setup/command.rs` that calls `cli/src/services/setup/mod.rs` (`ensure_git_repository`) before any setup writes begin; enforces that all `sce setup` modes (config-only, hooks-only, combined, and interactive) require the current directory to be inside a git repository, failing with actionable guidance to run `git init` and rerun `sce setup` when the precondition is not met. -- `setup local bootstrap`: Pre-install setup bootstrap behavior now owned by lifecycle providers: `ConfigLifecycle::setup` creates missing `.sce/config.json` with the canonical schema-only payload (`{"$schema": "https://sce.crocoder.dev/config.json"}`), `LocalDbLifecycle::setup` initializes the canonical local DB via `LocalDb::new()`, and `AgentTraceDbLifecycle::setup` initializes the Agent Trace DB via `AgentTraceDb::new()`; the setup command aggregates these calls before config/hooks dispatch across all setup modes. +- `setup local bootstrap`: Pre-install setup bootstrap behavior now owned by lifecycle providers: `ConfigLifecycle::setup` creates missing `.sce/config.json` with the canonical schema-only payload (`{"$schema": "https://sce.crocoder.dev/config.json"}`), `LocalDbLifecycle::setup` initializes the canonical local DB via `LocalDb::new()`, and `AgentTraceDbLifecycle::setup` creates/reuses checkout identity and registry metadata without eagerly creating an Agent Trace DB; the setup command aggregates these calls before config/hooks dispatch across all setup modes. - `CLI redaction-safe diagnostics contract`: baseline security behavior implemented via `cli/src/services/security.rs` (`redact_sensitive_text`) and applied to app-level errors, setup git-diagnostic surfacing, and observability output sinks so common secret-bearing token forms are masked before emission. - `setup directory write-permission probe`: deterministic pre-write guard implemented in `cli/src/services/security.rs` (`ensure_directory_is_writable`) and used by setup install/hook flows to fail fast with actionable remediation when target directories are not writable. - `setup --repo canonical path guard`: setup-hook runtime behavior in `cli/src/services/setup/mod.rs` that canonicalizes and validates user-supplied `--repo` paths as existing directories before git-root/hooks-path resolution. @@ -84,7 +84,7 @@ - `RunOutcome`: Generic final render payload in `cli/src/services/app_support.rs` (`RunOutcome`) carrying a command result, optional startup diagnostic, and optional logger implementing the logger trait boundary. Production construction in `cli/src/app.rs` uses the concrete observability logger, while rendering is not hardcoded to that production type. - `AppContext`: Generic borrowed dependency view in `cli/src/app.rs` passed through static command dispatch. `AppRuntime` owns the concrete production logger, telemetry, filesystem, and git implementations; `AppContext` stores references to those dependencies plus an optional `repo_root: Option`, not owned `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). The `repo_root` field is `None` at startup and command paths can derive a repo-root-scoped context with `AppContext::with_repo_root(...)` / `ContextWithRepoRoot`, preserving the borrowed runtime dependencies while attaching the resolved root. Narrow accessor traits (`HasLogger`, `HasTelemetry`, `HasFs`, `HasGit`, `HasRepoRoot`) let command and lifecycle call sites express capability requirements without depending on the full production context type; logger/telemetry/fs/git accessors use associated concrete capability types and return `&Self::{Capability}` rather than object-erased `&dyn ...` values. - `CommandRegistry`: Static command-name catalog in `cli/src/services/command_registry.rs`; populated by `build_default_registry()` and carried by `AppRuntime` during command dispatch. It exposes deterministic command-name membership for the current top-level command catalog (`help`, `auth`, `config`, `setup`, `doctor`, `hooks`, `version`, and `completion`) while actual parsed command payloads are represented by the `RuntimeCommand` enum rather than zero-arg boxed constructors. -- `ServiceLifecycle`: Compile-safe lifecycle trait seam in `cli/src/services/lifecycle.rs` with default no-op generic `diagnose`, `fix`, and `setup` methods over `C: HasRepoRoot`; it exposes lifecycle-owned health, fix, and setup result types, while doctor/setup adapt those records at orchestration boundaries before rendering command-owned output. The hooks service has `HooksLifecycle` for hook rollout diagnosis/fix/setup, the config service has `ConfigLifecycle` for global/repo-local config validation plus repo-local config bootstrap, local_db has `LocalDbLifecycle` for canonical local DB path health/bootstrap/setup, auth_db has `AuthDbLifecycle` for canonical auth DB path health/bootstrap/setup, and agent_trace_db has `AgentTraceDbLifecycle` for canonical Agent Trace DB path health/bootstrap/setup. Doctor runtime aggregates the static provider catalog for `diagnose` and `fix`; setup command aggregates providers for `setup` in order (config → local_db → auth_db → agent_trace_db → hooks when requested). +- `ServiceLifecycle`: Compile-safe lifecycle trait seam in `cli/src/services/lifecycle.rs` with default no-op generic `diagnose`, `fix`, and `setup` methods over `C: HasRepoRoot`; it exposes lifecycle-owned health, fix, and setup result types, while doctor/setup adapt those records at orchestration boundaries before rendering command-owned output. The hooks service has `HooksLifecycle` for hook rollout diagnosis/fix/setup, the config service has `ConfigLifecycle` for global/repo-local config validation plus repo-local config bootstrap, local_db has `LocalDbLifecycle` for canonical local DB path health/bootstrap/setup, auth_db has `AuthDbLifecycle` for canonical auth DB path health/bootstrap/setup, and agent_trace_db has `AgentTraceDbLifecycle` for checkout identity setup, per-checkout Agent Trace DB path health/bootstrap when an ID exists, and legacy global fallback outside checkout context. Doctor runtime aggregates the static provider catalog for `diagnose` and `fix`; setup command aggregates providers for `setup` in order (config → local_db → auth_db → agent_trace_db → hooks when requested). - `lifecycle provider catalog`: Shared factory in `cli/src/services/lifecycle.rs` (`lifecycle_providers(include_hooks)`) that returns static `LifecycleProvider` enum values in deterministic config → local_db → auth_db → agent_trace_db → hooks order, used by doctor with hooks included and by setup with hooks included only when requested. The enum owns concrete-provider dispatch for `id`, `diagnose`, `fix`, and `setup` without boxed provider trait objects or `&dyn HasRepoRoot` lifecycle context erasure. - `sce config command surface`: Implemented top-level CLI command routed by `cli/src/app.rs` to `cli/src/services/config/mod.rs` (with schema/file-parsing delegated to `cli/src/services/config/schema.rs`, runtime precedence resolution delegated to `cli/src/services/config/resolver.rs`, and text/JSON output construction delegated to `cli/src/services/config/render.rs`), exposing `show`, `validate`, and `--help` for deterministic runtime config inspection and validation; `show` reports resolved flat logging observability values with provenance, while `validate` reports pass/fail plus validation issues and warnings only. - `sce version command surface`: Implemented top-level CLI command routed by `cli/src/app.rs` to `cli/src/services/version/mod.rs` plus `cli/src/services/version/command.rs`, exposing deterministic runtime identification output in text form by default and JSON form via `--format json`. diff --git a/context/overview.md b/context/overview.md index 787314c0..2f7ae5dc 100644 --- a/context/overview.md +++ b/context/overview.md @@ -12,7 +12,7 @@ The command loop now enforces a stable exit-code contract in `cli/src/app.rs`: ` 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`), optional file sink controls (`SCE_LOG_FILE`, `SCE_LOG_FILE_MODE` with deterministic `truncate` default), stable lifecycle event IDs, stderr-only primary emission so stdout command payloads remain pipe-safe, and `observability::traits` boundaries for logger and telemetry behavior. The app command dispatcher now enforces a centralized stdout/stderr stream contract in `cli/src/app.rs`: command success payloads are emitted on stdout only, while redacted user-facing diagnostics are emitted on stderr. `cli/src/app.rs` also now runs through explicit startup phases — dependency check, observability config resolution, runtime initialization, command parse/execute, and output rendering — with the app runtime carrying logger/telemetry plus static command-catalog state across those phases while preserving the existing exit-code and degraded-startup contracts. Within that lifecycle, `parse_command_phase` delegates clap-to-runtime conversion to `cli/src/services/parse/command_runtime.rs`, which returns a static `RuntimeCommand` enum, `services::app_support::execute_command_phase` logs around enum-owned `execute(...)` dispatch, and generic `RunOutcome` rendering logs classified errors through the logger trait boundary without coupling render support to the production logger type. Command payload structs for `help`, `version`, `completion`, `auth`, `config`, `setup`, `doctor`, and `hooks` live in service-owned `command.rs` files; `cli/src/services/command_registry.rs` owns the deterministic static command-name catalog and enum variants instead of boxed command trait objects. -The CLI now also enforces a shared output-format parser contract in `cli/src/services/output_format.rs`, with canonical `--format ` parsing and command-specific actionable invalid-value guidance reused by `config` and `version` services. A compile-safe service lifecycle seam also exists in `cli/src/services/lifecycle.rs`: `ServiceLifecycle` exposes default no-op `diagnose`, `fix`, and `setup` methods against the narrow `HasRepoRoot` accessor, uses lifecycle-owned health/fix/setup result types, and owns the shared static `LifecycleProvider` enum catalog/factory with deterministic config → local_db → auth_db → agent_trace_db → hooks ordering and no boxed provider aggregation. Hooks has a `services/hooks/lifecycle.rs` provider for hook rollout diagnosis/fix/setup, config has a `services/config/lifecycle.rs` provider for global/repo-local config validation plus repo-local config bootstrap, local_db has a `services/local_db/lifecycle.rs` provider for canonical local DB path health, parent-directory readiness/bootstrap, and `LocalDb::new()` setup, auth_db has a `services/auth_db/lifecycle.rs` provider for canonical auth DB path health, parent-directory readiness/bootstrap, and `AuthDb::new()` setup, and agent_trace_db has a `services/agent_trace_db/lifecycle.rs` provider for canonical Agent Trace DB path health, parent-directory readiness/bootstrap, and `AgentTraceDb::new()` setup. 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. +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 checkout identity setup, per-checkout Agent Trace DB path health/parent readiness when an ID exists, and legacy global fallback outside checkout context. 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. The CLI now also includes a shared text styling service in `cli/src/services/style.rs` that provides deterministic color enablement via `owo-colors`, automatic TTY detection, and `NO_COLOR` compliance for human-facing text output; stdout help/text surfaces, stderr diagnostics, and interactive prompt-adjacent text now reuse that shared styling policy while JSON, completion, and other non-interactive/machine-readable flows remain unstyled. The service exports color-detection, conditional styling, help/diagnostic/label/prompt styling, and `banner_with_gradient()` helpers for use across command surfaces while preserving pipe-safe output for non-interactive environments. The `setup` command includes an `inquire`-backed target-selection flow: default interactive selection for OpenCode/Claude/both with required-hook installation in the same run, explicit non-interactive target flags (`--opencode`, `--claude`, `--both`), deterministic mutually-exclusive validation, and non-destructive cancellation exits. The CLI now compiles an embedded setup asset manifest from `config/.opencode/**`, `config/.claude/**`, and `cli/assets/hooks/**` via `cli/build.rs`; `cli/src/services/setup/mod.rs` exposes deterministic normalized relative paths plus file bytes and target-scoped iteration without runtime reads from `config/`. The same build script also discovers `cli/migrations//*.sql` at compile time and writes `cli/src/generated_migrations.rs` constants sorted by numeric filename prefix for database migration consumers. @@ -28,7 +28,7 @@ The config service split now includes `cli/src/services/config/resolver.rs` as t Generated config now includes repo-local plugin assets for both profiles: `sce-bash-policy.ts` plus `sce-agent-trace.ts` are emitted under `config/.opencode/plugins/` and `config/automated/.opencode/plugins/`; the OpenCode agent-trace plugin extracts `{ sessionID, diff, time, model_id }` from user `message.updated` events with diffs, tracks per-session OpenCode client version from `session.created`/`session.updated`, and sends payloads to `sce hooks diff-trace` with `tool_name="opencode"` plus optional `tool_version`. Claude generated config now routes agent-trace events through `.claude/settings.json` command hooks that call `sce hooks` directly: `SessionStart` pipes raw hook event JSON to `sce hooks session-model`, and matched `PostToolUse Write|Edit|MultiEdit|NotebookEdit` pipes raw hook event JSON to `sce hooks diff-trace`; the Rust `session-model` path uses explicit payload version fields when present and otherwise best-effort captures `tool_version` from trimmed `claude --version` stdout when available. Rust handles extraction, validation, and persistence without a TypeScript intermediary; the former `config/.claude/plugins/sce-agent-trace.ts` Bun runtime was removed in T07 of the `claude-rust-diff-trace` plan. The Rust hook validates required fields, resolves missing/nullable diff-trace attribution from `session_models` while preserving direct payload precedence, and persists `model_id`, `tool_name`, and nullable/resolved `tool_version` into `diff_traces` through AgentTraceDb. Bash-policy now delegates OpenCode enforcement to the Rust `sce policy bash` command: the generated OpenCode plugin at `config/.opencode/plugins/sce-bash-policy.ts` (and `config/automated/.opencode/plugins/sce-bash-policy.ts`) is a thin wrapper that calls `sce policy bash --input normalized --output json` via `spawnSync` and throws on deny decisions; it no longer contains independent TypeScript policy logic. The former `bash-policy/runtime.ts` TypeScript runtime has been removed. Preset... The `doctor` command now exposes explicit inspection mode (`sce doctor`) and repair-intent mode (`sce doctor --fix`) at the CLI/help/schema level while keeping diagnosis mode read-only. It now validates both current global operator health and the current repo/hook-integrity slice: state-root resolution, global config path resolution, global and repo-local `sce/config.json` readability/schema validity, local DB and Agent Trace DB path + health, DB parent-directory readiness, git availability, non-repo vs bare-repo targeting failures, effective git hook-path source (default, per-repo `core.hooksPath`, or global `core.hooksPath`), hooks-directory health, required hook presence/executable permissions/content drift against canonical embedded SCE-managed hook assets, and repo-root OpenCode integration presence across the installed `plugins`, `agents`, `commands`, and `skills` inventories with embedded SHA-256 content verification for OpenCode assets. Text mode now renders the approved human-only layout with ordered `Environment` / `Configuration` / `Repository` / `Git Hooks` / `Integrations` sections, `SCE doctor diagnose` / `SCE doctor fix` headers, bracketed `[PASS]`/`[FAIL]`/`[MISS]` status tokens, shared-style green pass plus red fail/miss coloring when color output is enabled, simplified `label (path)` row formatting, top-level-only hook rows, and integration parent/child rows that reflect missing vs content-mismatch states; JSON output now reports Agent Trace DB health under `agent_trace_db` (as a row within the Configuration section in text mode). Repo-scoped database reporting is empty by default because no repo-owned SCE database currently exists. Fix mode reuses the canonical setup hook install flow to repair missing/stale/non-executable required hooks and can also bootstrap missing canonical DB parent directories while preserving manual-only guidance for unsupported issues. Claude bash-policy enforcement is also generated through `.claude/settings.json` as a `PreToolUse` `Bash` command hook running `sce policy bash`, so Claude and OpenCode both delegate to the Rust policy evaluator without a Claude TypeScript runtime. -Local database bootstrap is now owned by `LocalDbLifecycle::setup` and `AgentTraceDbLifecycle::setup` aggregated by the setup command. Agent Trace setup also creates/reuses the current checkout ID and registers the checkout while the existing global Agent Trace DB setup remains in place until per-checkout DB resolution lands. Doctor validates DB paths/health and can bootstrap missing parent directories. Wiring a user-invocable `sce sync` command is deferred to `0.4.0`. +Local database bootstrap is now owned by `LocalDbLifecycle::setup` and setup-command lifecycle aggregation. Agent Trace setup creates/reuses the current checkout ID and registers the checkout without eagerly creating an Agent Trace DB; hook runtime lazily creates or upgrades the per-checkout `agent-trace-{checkout_id}.db` on first Agent Trace write/read. Doctor validates DB paths/health and can bootstrap missing parent directories. Wiring a user-invocable `sce sync` command is deferred to `0.4.0`. The repository-root flake (`flake.nix`) now applies a Rust overlay-backed stable toolchain pinned to `1.93.1` (with `rustfmt` and `clippy`), reads package/check version from the repo-root `.version` file, builds `packages.sce` through a Crane `buildDepsOnly` + `buildPackage` pipeline with filtered package sources for the Cargo tree plus required embedded config/assets, and runs `cli-tests`, `cli-clippy`, and `cli-fmt` through Crane-backed check derivations (`cargoTest`, `cargoClippy`, `cargoFmt`) that reuse the same filtered source/toolchain setup. The root flake also exposes release install/run outputs directly as `packages.sce` (with `packages.default = packages.sce`) plus `apps.sce` and `apps.default`, so `nix build .#default`, `nix run . -- --help`, `nix run .#sce -- --help`, and `nix profile install github:crocoder-dev/shared-context-engineering` all target the packaged `sce` binary through the same flake-owned entrypoints. The CLI Cargo package metadata now includes crates.io publication-ready fields with crate-local install guidance in `cli/README.md`; supported Cargo install paths are `cargo install shared-context-engineering --locked`, `cargo install --git https://github.com/crocoder-dev/shared-context-engineering shared-context-engineering --locked`, and local `cargo install --path cli --locked`. The published crate installs the `sce` binary. The crate also keeps `cargo clippy --manifest-path cli/Cargo.toml` warnings-denied through `cli/Cargo.toml` lint configuration, so an extra `-- -D warnings` flag is redundant. @@ -50,7 +50,7 @@ The targeted support commands (`handover`, `commit`, `validate`) keep their thin The prior no-git-wrapper Agent Trace design artifacts under `context/sce/agent-trace-*.md` are retained only as historical reference; the current CLI runtime no longer wires the removed Agent Trace schema adaptation, payload building, retry replay, or rewrite handling paths into local hook execution. The hooks service now uses a minimal attribution-only runtime: `commit-msg` is the only hook that mutates behavior, conditionally injecting exactly one canonical SCE trailer when the attribution-hooks gate is enabled and `SCE_DISABLED` is false; `pre-commit` and `post-rewrite` remain deterministic no-op entrypoints; `post-commit` requires validated `--remote-url`, threads that URL through the Agent Trace flow, prints it to stderr, captures current commit patch, queries recent `diff_traces` from past 7 days (dispatching `patch` rows through existing unified-diff parsing and `structured` rows through `structured_patch::derive_claude_structured_patch` at read time), combines/intersects patches, persists intersection metadata to `post_commit_patch_intersections`, and persists the schema-validated built Agent Trace payload, including optional top-level `tool` metadata from recent diff-trace rows, top-level `metadata.sce.version` from the compiled `sce` CLI package version, and range-level `content_hash` values, to AgentTraceDb `agent_traces` (DB-only, no post-commit Agent Trace file artifact); `diff-trace` currently validates/persists required non-empty `sessionID`/`diff`/`tool_name`, optional `model_id` (absent or `null` → `None`, present+non-empty → `Some`, present+empty → error), required nullable/non-empty `tool_version`, plus required `u64` millisecond `time`, resolves missing/nullable attribution from `session_models` by `tool_name` + `session_id` when available while direct payload values keep precedence, and continues with `None` for unresolved attribution, with non-lossy AgentTraceDb `time_ms` conversion and collision-safe timestamp+attempt artifact filenames; and `session-model` performs STDIN intake for normalized model attribution upsert without raw artifact persistence, with Claude `SessionStart` extracting `model_id` from the raw event and best-effort filling missing `tool_version` from `claude --version`. The CLI now also includes an approved operator-environment doctor contract documented in `context/sce/agent-trace-hook-doctor.md`; the runtime now matches the implemented T06 slice for `sce doctor --fix` parsing/help, stable problem/fix-result reporting, canonical hook-repair reuse, and bounded doctor-owned local-DB directory bootstrap for the missing SCE-owned DB parent path. -The local DB service now provides `LocalDb` as a thin `TursoDb` alias in `cli/src/services/local_db/mod.rs`; `LocalDbSpec` resolves the canonical local DB path from the shared default-path catalog and currently declares zero migrations. Shared Turso infrastructure lives in `cli/src/services/db/mod.rs`, where `DbSpec` and generic `TursoDb` support dual-mode operation — local mode via `turso::Builder::new_local()` when `SCE_SYNC_URL`+`SCE_SYNC_TOKEN` are absent, or sync (Turso Cloud) mode via `turso::sync::Builder::new_remote()` when both are set. It owns parent-directory creation, connection setup, tokio current-thread runtime bridging, synchronous `execute`/`query`/`query_map`, generic migration execution, sync operations (`push`/`pull`/`checkpoint`/`stats`) that are no-ops in local mode (sync is never triggered automatically from `execute()`), and shared DB lifecycle helpers for service-specific database wrappers. Auth DB persistence now has a thin encrypted wrapper in `cli/src/services/auth_db/mod.rs`: `AuthDb = EncryptedTursoDb` resolves `/sce/auth.db` and embeds ordered `auth_tokens` table/index migrations, with lifecycle registration wired through `AuthDbLifecycle` in `cli/src/services/auth_db/lifecycle.rs`; auth runtime token-storage is now wired through `token_storage.rs`, which persists tokens via the `auth_credentials` table instead of a JSON file. Agent Trace persistence now has its own `cli/src/services/agent_trace_db/mod.rs` wrapper, canonical `/sce/agent-trace.db` path, a split fresh-start baseline migration set (`001..008`) covering `diff_traces`, `post_commit_patch_intersections`, `agent_traces`, nullable `agent_traces.remote_url`, indexes (`idx_diff_traces_time_ms_id`, `idx_agent_traces_agent_trace_id`, `idx_agent_traces_remote_url`), and `session_models` keyed by `(tool_name, session_id)` without `AUTOINCREMENT`, plus `agent_traces.agent_trace_id` as `NOT NULL UNIQUE`; it also provides type... (line truncated to 2000 chars) +The local DB service now provides `LocalDb` as a thin `TursoDb` alias in `cli/src/services/local_db/mod.rs`; `LocalDbSpec` resolves the canonical local DB path from the shared default-path catalog and currently declares zero migrations. Shared Turso infrastructure lives in `cli/src/services/db/mod.rs`, where `DbSpec` and generic `TursoDb` support dual-mode operation — local mode via `turso::Builder::new_local()` when `SCE_SYNC_URL`+`SCE_SYNC_TOKEN` are absent, or sync (Turso Cloud) mode via `turso::sync::Builder::new_remote()` when both are set. It owns parent-directory creation, connection setup, tokio current-thread runtime bridging, synchronous `execute`/`query`/`query_map`, generic migration execution, sync operations (`push`/`pull`/`checkpoint`/`stats`) that are no-ops in local mode (sync is never triggered automatically from `execute()`), and shared DB lifecycle helpers for service-specific database wrappers. Auth DB persistence now has a thin encrypted wrapper in `cli/src/services/auth_db/mod.rs`: `AuthDb = EncryptedTursoDb` resolves `/sce/auth.db` and embeds ordered `auth_tokens` table/index migrations, with lifecycle registration wired through `AuthDbLifecycle` in `cli/src/services/auth_db/lifecycle.rs`; auth runtime token-storage is now wired through `token_storage.rs`, which persists tokens via the `auth_credentials` table instead of a JSON file. Agent Trace persistence now has its own `cli/src/services/agent_trace_db/mod.rs` wrapper, legacy global `/sce/agent-trace.db` fallback plus active per-checkout `/sce/agent-trace-{checkout_id}.db` hook runtime paths, a split fresh-start baseline migration set (`001..008`) covering `diff_traces`, `post_commit_patch_intersections`, `agent_traces`, nullable `agent_traces.remote_url`, indexes (`idx_diff_traces_time_ms_id`, `idx_agent_traces_agent_trace_id`, `idx_agent_traces_remote_url`), and `session_models` keyed by `(tool_name, session_id)` without `AUTOINCREMENT`, plus `agent_traces.agent_trace_id` as `NOT NULL UNIQUE`; it also provides type... (line truncated to 2000 chars) The hooks command surface now also supports concrete runtime subcommand routing (`pre-commit`, `commit-msg`, `post-commit`, `post-rewrite`, `diff-trace`, and `session-model`) with deterministic argument/STDIN validation. Current runtime behavior keeps attribution disabled by default: the attribution gate enables canonical trailer insertion in `commit-msg`, `pre-commit`/`post-rewrite` remain deterministic no-ops, `post-commit` requires validated `--remote-url`, threads that URL into the Agent Trace flow, prints it to stderr, and remains the active bounded recent-diff-trace intersection path, `diff-trace` is the active intake path for parsed STDIN `{ sessionID, diff, time, model_id?, tool_name, tool_version }` payload persistence with optional `model_id`, required non-empty `tool_name`, required nullable/non-empty `tool_version`, missing/nullable attribution fallback from `session_models` by `tool_name` + `session_id` while direct payload values keep precedence, required `u64` millisecond `time`, non-lossy AgentTraceDb `time_ms` conversion, and collision-safe timestamp+attempt artifact filenames; and `session-model` is the active STDIN intake for normalized model attribution upsert, including Claude `SessionStart` best-effort `claude --version` filling for missing version metadata. This behavior is documented in `context/sce/agent-trace-hooks-command-routing.md`. The removed `sce hooks claude-capture` raw capture route is documented in `context/sce/claude-raw-hook-capture.md` as a removed feature. The setup service now also exposes deterministic required-hook embedded asset accessors (`iter_required_hook_assets`, `get_required_hook_asset`) backed by canonical templates in `cli/assets/hooks/` for `pre-commit`, `commit-msg`, and `post-commit`; this behavior is documented in `context/sce/setup-githooks-hook-asset-packaging.md`. The setup service now also includes required-hook install orchestration (`install_required_git_hooks`) that resolves repository root and effective hooks path from git truth, enforces deterministic per-hook outcomes (`Installed`/`Updated`/`Skipped`), and uses a unified remove-and-replace policy that removes existing hooks before swapping staged content with deterministic recovery guidance on swap failures; this behavior is documented in `context/sce/setup-githooks-install-flow.md`. diff --git a/context/plans/agent-trace-checkout-identity.md b/context/plans/agent-trace-checkout-identity.md index 0272a3bc..73909a82 100644 --- a/context/plans/agent-trace-checkout-identity.md +++ b/context/plans/agent-trace-checkout-identity.md @@ -69,7 +69,7 @@ Give each cloned repository (and linked Git worktree) its own `agent-trace` chec - Evidence: `nix flake check` passed; `nix run .#pkl-check-generated` passed. Manual end-to-end setup in a temporary git repository ran `sce setup --hooks --repo ` twice with isolated `XDG_STATE_HOME`; both runs printed the same checkout ID, `.git/sce/checkout-id` matched the registry record, and registry `database_path` remained `null`. - Notes: Setup lifecycle now emits a generic lifecycle setup message for Agent Trace checkout identity. The existing global Agent Trace DB setup remains in place until T03, while checkout registry `database_path` stays unset for lazy per-checkout DB creation later. -- [ ] T03: `Enable per-checkout database resolution with lazy initialization` (status:todo) +- [x] T03: `Enable per-checkout database resolution with lazy initialization` (status:done) - Task ID: T03 - Goal: Switch all Agent Trace DB consumers from the shared global path to per-checkout paths. Each checkout gets its own `agent-trace-{checkout_id}.db`, created lazily on first write when a hook fires — no explicit setup step needed for the DB itself. Per-checkout DB files provide isolation without any `checkout_id` column in the schema. - Boundaries (in/out of scope): In — @@ -82,6 +82,10 @@ Give each cloned repository (and linked Git worktree) its own `agent-trace` chec Out — schema migrations, `checkout_id` columns, data migration from old global DB, doctor checkout identity display. - Done when: A fresh clone with `sce setup` run (no hooks yet) has a checkout ID and registry entry but no `agent-trace-{checkout_id}.db` file. The first hook invocation (e.g. post-commit) auto-creates the DB, runs migrations, and updates the registry `database_path`. A `git worktree add` followed by a hook invocation auto-creates a new checkout ID and DB for the worktree. Two checkouts have independent databases with no data leakage. `nix flake check` passes. - Verification notes (commands or checks): Manual end-to-end: run `sce setup` in a test repo, verify no per-checkout DB file exists yet, trigger a commit (post-commit hook), verify `agent-trace-{checkout_id}.db` now exists and registry `database_path` is populated. Create a worktree, trigger a hook there, verify a second DB exists independently. Run `nix flake check` for full validation. + - Completed: 2026-06-16 + - Files changed: `cli/src/services/default_paths.rs`, `cli/src/services/db/mod.rs`, `cli/src/services/agent_trace_db/mod.rs`, `cli/src/services/checkout/mod.rs`, `cli/src/services/agent_trace_db/lifecycle.rs`, `cli/src/services/hooks/mod.rs`, `cli/src/services/doctor/inspect.rs` + - Evidence: `nix develop -c sh -c 'cd cli && cargo fmt'` passed; `nix flake check` passed; `nix run .#pkl-check-generated` passed. Manual smoke checks with isolated `XDG_STATE_HOME` verified setup creates a checkout ID without creating `agent-trace-{checkout_id}.db`, explicit `sce hooks post-commit --remote-url ...` lazily creates the per-checkout DB and updates registry `database_path`, and a no-setup repository hook invocation creates checkout ID + per-checkout DB in one pass. + - Notes: Agent Trace hook DB consumers now resolve through checkout identity and per-checkout lazy DB initialization. `AgentTraceDbLifecycle::setup()` is identity-only; lifecycle diagnose/fix use the per-checkout DB path when an ID exists and otherwise retain the global fallback outside checkout context. Doctor checkout display and registry listing remain deferred to T04. - [ ] T04: `Surface checkout identity in doctor and add 'sce doctor dbs'` (status:todo) - Task ID: T04 diff --git a/context/sce/agent-trace-db.md b/context/sce/agent-trace-db.md index 8a778df3..19e1e824 100644 --- a/context/sce/agent-trace-db.md +++ b/context/sce/agent-trace-db.md @@ -10,7 +10,9 @@ pub type AgentTraceDb = TursoDb; - `AgentTraceDbSpec`: `DbSpec` implementation for Agent Trace persistence. - `AgentTraceDb`: type alias for `TursoDb`, inheriting shared constructor and operation retry behavior. +- `open_at(path)`: migration-running explicit-path constructor for per-checkout Agent Trace databases. - `open_for_hooks_without_migrations()`: Agent Trace-specific runtime-open API for high-frequency hook paths; opens/connects via `TursoDb::open_without_migrations()` and does not run embedded migrations. +- `open_for_hooks_without_migrations_at(path)`: explicit-path no-migration runtime-open API used by per-checkout hook resolution. - `ensure_schema_ready_for_hooks()`: non-mutating hook-readiness check that delegates to the shared `TursoDb::ensure_schema_ready()` method with the Agent Trace–specific `AGENT_TRACE_SCHEMA_SETUP_GUIDANCE` constant (`"Run 'sce setup'."`); verifies the Agent Trace DB has the expected applied migration metadata in `__sce_migrations` for every ID in `AGENT_TRACE_MIGRATIONS`; missing/incomplete metadata fails with `Run 'sce setup'.` guidance instead of running migrations. - `DiffTraceInsert<'a>`: insert payload with `time_ms: i64`, `session_id: &'a str`, `patch: &'a str`, `model_id: Option<&'a str>`, `tool_name: &'a str`, nullable `tool_version: Option<&'a str>`, and `payload_type: &'a str` (using `PAYLOAD_TYPE_PATCH` or `PAYLOAD_TYPE_STRUCTURED` constants). - `PAYLOAD_TYPE_PATCH` / `PAYLOAD_TYPE_STRUCTURED`: string constants (`"patch"` / `"structured"`) for the `diff_traces.payload_type` discriminator column; `OpenCode` normalized diff-trace payloads use `patch`, `Claude` structured `PostToolUse` payloads use `structured`. @@ -44,13 +46,20 @@ pub type AgentTraceDb = TursoDb; ## Database path -The Agent Trace DB path is resolved from the shared default-path catalog: +The legacy/global Agent Trace DB path is resolved from the shared default-path catalog and retained as a lifecycle fallback when no checkout context or checkout ID is available: - Function: `agent_trace_db_path()` in `cli/src/services/default_paths.rs` - Path template: `/sce/agent-trace.db` - Linux: `$XDG_STATE_HOME/sce/agent-trace.db` (defaults to `~/.local/state/sce/agent-trace.db`) - Other platforms: platform-equivalent user state root +Active hook runtime resolves per-checkout Agent Trace DB files: + +- Function: `agent_trace_db_path_for_checkout(checkout_id)` in `cli/src/services/default_paths.rs` +- Path template: `/sce/agent-trace-{checkout_id}.db` +- Checkout ID source: `/sce/checkout-id`, where `` comes from `git rev-parse --git-dir` +- Registry metadata: `/sce/checkout-registry.json` stores `database_path` after successful lazy initialization + ## Migrations `AgentTraceDbSpec::migrations()` returns `generated_migrations::AGENT_TRACE_MIGRATIONS`, generated from `cli/migrations/agent-trace/` at build time. Migration IDs are the SQL filename stems, sorted by numeric prefix: @@ -74,7 +83,7 @@ The Agent Trace DB path is resolved from the shared default-path catalog: The shared `TursoDb` runner records applied IDs in the database-local `__sce_migrations` table. Existing Agent Trace DB files without metadata are brought forward by re-applying the idempotent migration set and recording each ID, so rerunning `sce setup` / `AgentTraceDb::new()` applies later Agent Trace migrations to an already-created `~/.local/state/sce/agent-trace.db`. -`AgentTraceDb::open_for_hooks_without_migrations()` is the named no-migration Agent Trace open path for hook runtime code. It preserves Turso open/connect retry behavior from the shared adapter but intentionally skips `run_migrations()`, so it neither creates `__sce_migrations` nor applies Agent Trace schema SQL. Active hook callers (`conversation-trace`, `diff-trace`, and both post-commit Agent Trace DB flows) use this path and call `ensure_schema_ready_for_hooks()` before reads/writes; readiness is based on exact migration metadata parity with `AGENT_TRACE_MIGRATIONS`, not table/index/column introspection. +Per-checkout hook DB resolution first tries `AgentTraceDb::open_for_hooks_without_migrations_at(path)` and `ensure_schema_ready_for_hooks()`. If the DB is missing, metadata is absent, or migrations are incomplete, the checkout resolver falls back to `AgentTraceDb::open_at(path)` so hook invocation lazily creates or upgrades the per-checkout DB before continuing. Readiness is based on exact migration metadata parity with `AGENT_TRACE_MIGRATIONS`, not table/index/column introspection. The `diff_traces` baseline migration creates: @@ -169,9 +178,9 @@ Both triggers compare `OLD.*` vs `NEW.*` for all mutable columns (excluding `upd `AgentTraceDbLifecycle` is registered in `cli/src/services/lifecycle.rs` after `LocalDbLifecycle` and before optional `HooksLifecycle`. -- `diagnose()` reports canonical Agent Trace DB path and parent-directory readiness problems through the shared DB path-health helper. -- `fix()` can bootstrap the canonical Agent Trace DB parent directory for auto-fixable parent-readiness problems. -- `setup()` creates/reuses the current checkout identity when a repo root is available, registers the checkout in `/sce/checkout-registry.json` with `database_path: null`, emits setup messaging with the checkout ID, and still initializes the existing global database with `AgentTraceDb::new()` until the per-checkout DB path switch lands. +- `diagnose()` reports per-checkout Agent Trace DB path and parent-directory readiness when a repo root has a checkout ID; otherwise it falls back to the legacy global Agent Trace DB path. +- `fix()` bootstraps the resolved per-checkout DB parent directory for auto-fixable parent-readiness problems, with the same global fallback outside checkout context. +- `setup()` creates/reuses the current checkout identity when a repo root is available, registers the checkout in `/sce/checkout-registry.json` with `database_path: null`, and emits setup messaging with the checkout ID. It does not eagerly initialize the Agent Trace DB; the first hook write lazily creates the per-checkout DB. - `sce doctor` now surfaces Agent Trace DB health as a row within the `Configuration` section with `[PASS]`/`[FAIL]`/`[MISS]` status tokens (e.g., `Agent Trace DB (/path/to/agent-trace.db)`), and includes it in JSON output under the `agent_trace_db` field. ## Runtime writers @@ -185,7 +194,7 @@ Both triggers compare `OLD.*` vs `NEW.*` for all mutable columns (excluding `upd - Command success requires artifact persistence to succeed; AgentTraceDb open/insert failures are logged and reflected in the success text as failed DB persistence instead of discarding the artifact fallback. - Existing artifact files are not backfilled into the database. -Post-commit intersection rows are written by the active `post-commit` hook flow through readiness-gated AgentTraceDb access, and the same flow now also inserts built Agent Trace payloads into `agent_traces` via `AgentTraceDb::insert_agent_trace()` (see [agent-trace-hooks-command-routing.md](agent-trace-hooks-command-routing.md)). The persisted `trace_json` is the schema-validated `build_agent_trace(...)` output and includes top-level `metadata.sce.version` from the compiled `sce` CLI package version plus `content_hash` on every emitted range. Range `content_hash` values are computed from the touched-line kind/content of the post-commit hunk that produced the persisted range, not from DB IDs, paths, line positions, or runtime metadata. +Post-commit intersection rows are written by the active `post-commit` hook flow through per-checkout lazy AgentTraceDb access, and the same flow now also inserts built Agent Trace payloads into `agent_traces` via `AgentTraceDb::insert_agent_trace()` (see [agent-trace-hooks-command-routing.md](agent-trace-hooks-command-routing.md)). The persisted `trace_json` is the schema-validated `build_agent_trace(...)` output and includes top-level `metadata.sce.version` from the compiled `sce` CLI package version plus `content_hash` on every emitted range. Range `content_hash` values are computed from the touched-line kind/content of the post-commit hunk that produced the persisted range, not from DB IDs, paths, line positions, or runtime metadata. `sce hooks conversation-trace` is the current runtime writer for `messages` and `parts`. @@ -193,7 +202,7 @@ Post-commit intersection rows are written by the active `post-commit` hook flow - `message` items validate and map payloads without message-level `text`, `agent`, or `summary_diffs` to `InsertMessageInsert`; valid rows are inserted through at most one multi-row `AgentTraceDb::insert_messages(...)` call per invocation so repeated `(session_id, message_id)` events are ignored without failing. - `message.part` items validate and map payloads with required part `text` to `InsertPartInsert`; valid rows are inserted through at most one multi-row `AgentTraceDb::insert_parts(...)` call per invocation so parts remain append-only and do not require a pre-existing message row. - Unsupported item types, missing/non-string item types, non-object items, and event-specific parser validation failures are retained as skipped-item diagnostics, logged, and counted as skipped while valid sibling items remain eligible for persistence. -- The hook opens one no-migration `AgentTraceDb` per invocation and checks schema readiness before insertion; DB open or readiness failures remain command-failing because no rows can be attempted. +- The hook opens one per-checkout `AgentTraceDb` per invocation through lazy checkout DB resolution before insertion; DB open/initialization failures remain command-failing because no rows can be attempted. - Multi-row insert failures are logged once and count the whole valid-item batch as skipped without failing the command; the hook does not fall back to row-by-row insertion after a batch failure. Successful inserts contribute to deterministic success output counts (`attempted`, `persisted_messages`, `persisted_parts`, `skipped`). Duplicate parent message inserts preserve the existing `ON CONFLICT DO NOTHING` affected-row semantics. - No `context/tmp` artifact is written for conversation traces. - The generated OpenCode agent-trace plugin sends mixed-batch envelopes for conversation traces: regular `message` and `message.part` events each carry one per-item `type`, while diff-backed `message` events send one envelope containing the synthetic parent message item plus patch part items. diff --git a/context/sce/agent-trace-hooks-command-routing.md b/context/sce/agent-trace-hooks-command-routing.md index c050cf49..b40461cf 100644 --- a/context/sce/agent-trace-hooks-command-routing.md +++ b/context/sce/agent-trace-hooks-command-routing.md @@ -37,8 +37,8 @@ - Writes back only when the attribution gate is enabled, `SCE_DISABLED` is false, and the transformed content differs. - `pre-commit` is a deterministic no-op entrypoint. - **`post-commit` is an active intersection entrypoint** (see [agent-trace-db.md](agent-trace-db.md)): - - Agent Trace DB access uses `AgentTraceDb::open_for_hooks_without_migrations()` followed by `ensure_schema_ready_for_hooks()` before both recent-patch reads/intersection writes and built Agent Trace persistence. - - Missing or incomplete Agent Trace DB schema is a command-failing runtime error with `Run 'sce setup'.` guidance; hook runtime does not run migrations as a fallback. + - Agent Trace DB access resolves the current checkout ID from `/sce/checkout-id`, creating it if missing, then opens `/sce/agent-trace-{checkout_id}.db` through the checkout lazy DB resolver. + - The resolver tries a no-migration open/readiness check first and falls back to migration-running initialization when the per-checkout DB is absent or schema metadata is incomplete. - Captures the current commit's patch from git using `capture_post_commit_patch_from_git()`. - Queries recent `diff_traces` patches from the past 7 days via `AgentTraceDb::recent_diff_trace_patches()`. - Recent-row parsing dispatches on `payload_type`: `patch` rows parse through existing `parse_patch`, while `structured` rows parse stored JSON through `structured_patch::derive_claude_structured_patch` at read time to produce `ParsedPatch`. @@ -66,7 +66,7 @@ - The `DiffTracePayload` struct carries a `payload_type: String` field consumed by `persist_diff_trace_payload_to_agent_trace_db_with` to pass the correct discriminator to `DiffTraceInsert`. - When `model_id` or `tool_version` is missing/nullable in the parsed payload, Rust looks up AgentTraceDb `session_models` by `(tool_name, session_id)` and uses the stored attribution values for missing fields when available. Direct payload `model_id` and `tool_version` values keep precedence over stored values. - If no matching session row exists, missing attribution fields remain `None`; the hook still persists the parsed-payload artifact and attempts the AgentTraceDb insert with nullable attribution. - - Persistence: writes one parsed-payload artifact per invocation to `context/tmp/-000000-diff-trace.json` with atomic create-new retry semantics, and inserts the parsed payload fields into AgentTraceDb via `DiffTraceInsert` + `insert_diff_trace()` using nullable/resolved `model_id` and `tool_version`. + - Persistence: writes one parsed-payload artifact per invocation to `context/tmp/-000000-diff-trace.json` with atomic create-new retry semantics, resolves the current per-checkout AgentTraceDb lazily, and inserts the parsed payload fields via `DiffTraceInsert` + `insert_diff_trace()` using nullable/resolved `model_id` and `tool_version`. - Current TypeScript producers are the OpenCode agent-trace plugin and the generated Claude `sce hooks` command hooks (no TypeScript intermediary). - OpenCode forwards user-message `message` diffs with `tool_name="opencode"`, always including `model_id`, and nullable OpenCode client-version metadata. - Claude forwards supported `PostToolUse` `Write` create and `Edit` structured-patch diffs with `tool_name="claude"` and no direct `model_id`; any explicit payload version metadata is preserved, and missing `model_id` / `tool_version` values are resolved from `session_models` when available. @@ -101,14 +101,14 @@ - `part_type: "patch"` first tries `load_patch_from_json` — if `payload.text` is already a valid JSON-serialized `ParsedPatch`, stores the raw text unchanged (no re-parse, no re-serialize). If JSON loading fails, falls back to the shared unified-diff parser (`parse_patch_from_text`) and stores JSON-serialized `ParsedPatch` in `parts.text`. If both fail, the item is skipped with a validation error mentioning both patch and patch-JSON formats. - Unsupported item `type` values, missing/non-string item `type`, non-object items, and event-specific item validation failures are recorded as skipped-item diagnostics (`index`, `reason`) while valid sibling items remain eligible for persistence; skipped validation items are logged through `sce.hooks.conversation_trace.payload_skipped`. Top-level JSON/object/`payloads` shape failures fail deterministically with `Invalid conversation-trace payload from STDIN: ...` diagnostics. - Shared persistence (both classification paths converge before DB writes): - - Current persistence opens one no-migration `AgentTraceDb` per hook invocation, checks schema readiness, then inserts the non-empty valid `message` batch through at most one multi-row `AgentTraceDb::insert_messages(...)` call and the non-empty valid `message.part` batch through at most one multi-row `AgentTraceDb::insert_parts(...)` call. - - DB open or schema-readiness failures are command-failing runtime errors logged through `sce.hooks.conversation_trace.error`; valid-item multi-row insert failures are logged once through `sce.hooks.conversation_trace.agent_trace_db_batch_failed`, count the whole valid-item batch as skipped, and do not fail the command. The hook does not fall back to row-by-row insertion after a multi-row insert failure. + - Current persistence opens one per-checkout `AgentTraceDb` per hook invocation through lazy checkout DB resolution, then inserts the non-empty valid `message` batch through at most one multi-row `AgentTraceDb::insert_messages(...)` call and the non-empty valid `message.part` batch through at most one multi-row `AgentTraceDb::insert_parts(...)` call. + - DB open/initialization failures are command-failing runtime errors logged through `sce.hooks.conversation_trace.error`; valid-item multi-row insert failures are logged once through `sce.hooks.conversation_trace.agent_trace_db_batch_failed`, count the whole valid-item batch as skipped, and do not fail the command. The hook does not fall back to row-by-row insertion after a multi-row insert failure. - Current success output reports deterministic mixed-batch accounting: `conversation-trace hook persisted mixed payload batch to AgentTraceDb: attempted=, persisted_messages=, persisted_parts=, skipped=.` The hook does not persist `context/tmp` artifacts. - The generated OpenCode agent-trace plugin emits this mixed-batch shape for conversation-trace handoff: ordinary message/part events produce one-item mixed envelopes, and diff-backed message events produce one envelope containing the synthetic parent `message` item plus patch `message.part` items. - `session-model` reads STDIN JSON and classifies the payload: - **Claude `SessionStart` payloads** (detected by presence of top-level `hook_event_name`): extracts `session_id` from `session_id`/`sessionID`, `model_id` from `model`/`model_id` (including nested `model.id`/`model.model`/`model.name` with `claude/` prefix normalization), `time` from `time`/`timestamp` (falls back to current system time), `tool_name="claude"`, and `tool_version` from `tool_version`/`claude_version`/`version`; when no non-empty payload version is present, Rust best-effort runs `claude --version`, trims stdout, and uses that value if non-empty, otherwise leaving `tool_version` nullable without failing intake. - **OpenCode normalized payloads** (no `hook_event_name`): existing `{ sessionID, time, model_id, tool_name, tool_version }` validation applies unchanged. - - Valid payloads are upserted into AgentTraceDb `session_models` via `SessionModelUpsert` using `(tool_name, session_id)` as the unique key. No raw hook artifacts are written. DB open/insert failures are logged through `sce.hooks.session_model.agent_trace_db_write_failed` and reported in the success text as failed persistence. + - Valid payloads are upserted into the per-checkout AgentTraceDb `session_models` via `SessionModelUpsert` using `(tool_name, session_id)` as the unique key. No raw hook artifacts are written. DB open/insert failures are logged through `sce.hooks.session_model.agent_trace_db_write_failed` and reported in the success text as failed persistence. ## Explicit non-goals in the current baseline diff --git a/context/sce/shared-turso-db.md b/context/sce/shared-turso-db.md index 00e8b44a..be14a601 100644 --- a/context/sce/shared-turso-db.md +++ b/context/sce/shared-turso-db.md @@ -17,7 +17,9 @@ - parent-directory creation - retry-backed synchronous `execute()`, `query()`, and row-mapping `query_map()` wrappers via the public adapter methods, with config-driven query retry resolved from `policies.database_retry..query` - migration-running initialization through `new()` and generic embedded migration execution through `run_migrations()` delegated to the shared internal `TursoConnectionCore` with per-database `__sce_migrations` metadata + - explicit-path migration-running initialization through `new_at(path)`, preserving the same service-specific retry/migration behavior while letting callers supply a database path outside `DbSpec::db_path()` - no-migration opening through `open_without_migrations()`, which preserves parent-directory creation and connection-open retry but does not create `__sce_migrations` or apply embedded migrations + - explicit-path no-migration opening through `open_without_migrations_at(path)` for path-resolved hot runtime callers - `migration_metadata_problems(&self) -> Result>`: non-mutating readiness check that queries `__sce_migrations` metadata and compares applied migration IDs against `M::migrations()`; returns a list of problems (missing metadata table, incomplete applied migrations, unexpected extra migrations) or an empty list when the schema is ready - `ensure_schema_ready(&self, setup_guidance: &str) -> Result<()>`: non-mutating hook-readiness gate that calls `migration_metadata_problems()` and bails with a formatted error including `M::db_name()` and the caller-provided guidance string when problems are found; returns `Ok(())` when the schema is ready - `EncryptedTursoDb`: encrypted-adapter seam parallel to `TursoDb` with the same structural shape (connection, runtime bridge, and spec marker). `EncryptedTursoDb::new()` resolves the encryption key via `encryption_key::get_or_create_encryption_key()` (environment variable `SCE_AUTH_DB_ENCRYPTION_KEY` with OS credential-store fallback), enables Turso experimental local encryption, applies strict `aegis256` cipher selection through `turso::EncryptionOpts` during local DB open/connect, wraps that open/connect block in the same connection-open retry policy resolved from `policies.database_retry..connection_open`, and runs embedded migrations after connect. @@ -55,10 +57,10 @@ the secret value. No plaintext fallback exists. The shared module is exported from `cli/src/services/mod.rs` and compile-checked. Current concrete wrappers: - `cli/src/services/local_db/mod.rs`: `LocalDb = TursoDb`, with `LocalDbSpec` resolving `local_db_path()` and declaring zero migrations. -- `cli/src/services/agent_trace_db/mod.rs`: `AgentTraceDb = TursoDb`, with `AgentTraceDbSpec` resolving `agent_trace_db_path()` and loading ordered Agent Trace migrations for `diff_traces`, `post_commit_patch_intersections`, `agent_traces`, `session_models`, `messages`, and `parts` plus supporting indexes and triggers. Setup/lifecycle initialization uses migration-running `AgentTraceDb::new()`; high-frequency hook runtime paths use no-migration `AgentTraceDb::open_for_hooks_without_migrations()` plus `ensure_schema_ready_for_hooks()` so hook invocations never run schema migrations on the hot path. +- `cli/src/services/agent_trace_db/mod.rs`: `AgentTraceDb = TursoDb`, with `AgentTraceDbSpec` resolving the legacy global `agent_trace_db_path()` fallback and loading ordered Agent Trace migrations for `diff_traces`, `post_commit_patch_intersections`, `agent_traces`, `session_models`, `messages`, and `parts` plus supporting indexes and triggers. Active hook runtime paths resolve per-checkout DB files through checkout identity and use `AgentTraceDb::open_for_hooks_without_migrations_at(path)` plus `ensure_schema_ready_for_hooks()` first, falling back to migration-running `AgentTraceDb::open_at(path)` when the per-checkout DB is absent or incomplete. - `cli/src/services/auth_db/mod.rs`: `AuthDb = EncryptedTursoDb`, with `AuthDbSpec` resolving `auth_db_path()` and loading 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. -All three database wrappers (local DB, auth DB, Agent Trace DB) have lifecycle providers. `lifecycle_providers(include_hooks)` registers database providers in order `LocalDbLifecycle` → `AuthDbLifecycle` → `AgentTraceDbLifecycle` before optional hooks, so setup initializes all three databases and doctor diagnoses/fixes all three canonical DB paths. +All three database wrappers (local DB, auth DB, Agent Trace DB) have lifecycle providers. `lifecycle_providers(include_hooks)` registers database providers in order `LocalDbLifecycle` → `AuthDbLifecycle` → `AgentTraceDbLifecycle` before optional hooks. Setup initializes local/auth DBs and establishes Agent Trace checkout identity; Agent Trace per-checkout DB files are initialized lazily by hook runtime. Doctor diagnoses/fixes DB parent/path readiness through lifecycle providers. ## Migration metadata From 6d3c4fe56029e7eef7992acb53aa52dce241be9c Mon Sep 17 00:00:00 2001 From: David Abram Date: Tue, 16 Jun 2026 14:53:52 +0200 Subject: [PATCH 5/8] doctor: Surface checkout identity and add dbs subcommand `sce doctor` now reports the current checkout identity and per-checkout Agent Trace DB health in the Configuration section when a checkout ID exists, falling back to the legacy global Agent Trace DB row outside checkout context. JSON output includes `checkout_identity` plus the resolved `agent_trace_db` record. Add `sce doctor dbs [--format text|json]` to list registered checkouts from `/sce/checkout-registry.json`, sorted by `last_seen` descending. Text mode renders registry rows; JSON outputs stable fields `checkout_id`, `path`, `database_path`, `last_seen`, and `remote_url`. Co-authored-by: SCE --- cli/src/cli_schema.rs | 12 +++ cli/src/services/command_registry.rs | 1 + cli/src/services/doctor/inspect.rs | 72 ++++++++++++++---- cli/src/services/doctor/mod.rs | 74 +++++++++++++++++++ cli/src/services/doctor/render.rs | 60 ++++++++++----- cli/src/services/doctor/types.rs | 8 ++ cli/src/services/parse/command_runtime.rs | 51 ++++++++++--- context/architecture.md | 6 +- context/cli/checkout-identity.md | 2 +- context/cli/cli-command-surface.md | 12 +-- context/context-map.md | 5 +- context/glossary.md | 4 +- context/overview.md | 4 +- .../plans/agent-trace-checkout-identity.md | 52 ++++++++++++- context/sce/agent-trace-db.md | 3 +- context/sce/agent-trace-hook-doctor.md | 12 +-- 16 files changed, 309 insertions(+), 69 deletions(-) diff --git a/cli/src/cli_schema.rs b/cli/src/cli_schema.rs index 794dc722..7fc10173 100644 --- a/cli/src/cli_schema.rs +++ b/cli/src/cli_schema.rs @@ -182,6 +182,9 @@ pub enum Commands { #[arg(long, value_enum, default_value_t = OutputFormat::Text)] format: OutputFormat, + + #[command(subcommand)] + subcommand: Option, }, #[command(about = HOOKS_CLAP_ABOUT, hide = !HOOKS_SHOW_IN_TOP_LEVEL_HELP)] @@ -209,6 +212,15 @@ pub enum Commands { }, } +#[derive(Subcommand, Debug, Clone, PartialEq, Eq)] +pub enum DoctorSubcommand { + #[command(about = "List registered Agent Trace checkouts and databases")] + Dbs { + #[arg(long, value_enum, default_value_t = OutputFormat::Text)] + format: OutputFormat, + }, +} + #[derive(Subcommand, Debug, Clone, PartialEq, Eq)] pub enum AuthSubcommand { #[command(about = "Start login flow and store credentials")] diff --git a/cli/src/services/command_registry.rs b/cli/src/services/command_registry.rs index a9b1d94c..b07d50a8 100644 --- a/cli/src/services/command_registry.rs +++ b/cli/src/services/command_registry.rs @@ -139,6 +139,7 @@ pub fn default_runtime_command(name: &str) -> Option { services::doctor::NAME => Some(RuntimeCommand::Doctor( services::doctor::command::DoctorCommand { request: services::doctor::DoctorRequest { + action: services::doctor::DoctorAction::Report, mode: services::doctor::DoctorMode::Diagnose, format: services::doctor::DoctorFormat::Text, }, diff --git a/cli/src/services/doctor/inspect.rs b/cli/src/services/doctor/inspect.rs index eb096ab6..6eeaa043 100644 --- a/cli/src/services/doctor/inspect.rs +++ b/cli/src/services/doctor/inspect.rs @@ -4,17 +4,20 @@ use std::path::{Path, PathBuf}; use sha2::{Digest, Sha256}; use crate::services::agent_trace_db::lifecycle::diagnose_agent_trace_db_health; -use crate::services::default_paths::{opencode_asset, InstallTargetPaths, RepoPaths}; +use crate::services::checkout; +use crate::services::default_paths::{ + agent_trace_db_path_for_checkout, opencode_asset, InstallTargetPaths, RepoPaths, +}; use crate::services::setup::{ iter_embedded_assets_for_setup_target, iter_required_hook_assets, EmbeddedAsset, SetupTarget, }; use super::types::{ - DoctorProblem, FileLocationHealth, GlobalStateHealth, HookContentState, HookDoctorReport, - HookFileHealth, HookPathSource, IntegrationChildHealth, IntegrationContentState, - IntegrationGroupHealth, ProblemCategory, ProblemFixability, ProblemKind, ProblemSeverity, - Readiness, OPENCODE_AGENTS_LABEL, OPENCODE_COMMANDS_LABEL, OPENCODE_PLUGINS_LABEL, - OPENCODE_SKILLS_LABEL, + CheckoutIdentityHealth, DoctorProblem, FileLocationHealth, GlobalStateHealth, HookContentState, + HookDoctorReport, HookFileHealth, HookPathSource, IntegrationChildHealth, + IntegrationContentState, IntegrationGroupHealth, ProblemCategory, ProblemFixability, + ProblemKind, ProblemSeverity, Readiness, OPENCODE_AGENTS_LABEL, OPENCODE_COMMANDS_LABEL, + OPENCODE_PLUGINS_LABEL, OPENCODE_SKILLS_LABEL, }; use super::{is_executable, DoctorDependencies, DoctorMode, REQUIRED_HOOKS}; @@ -26,7 +29,8 @@ pub(super) fn build_report_with_dependencies( ) -> HookDoctorReport { let mut problems = Vec::new(); let global_state = collect_global_state_health(repository_root, &mut problems, dependencies); - let agent_trace_db = collect_agent_trace_db_health(&mut problems); + let checkout_identity = collect_checkout_identity_health(repository_root); + let agent_trace_db = collect_agent_trace_db_health(repository_root, &mut problems); let git_available = (dependencies.check_git_available)(); let detected_repository_root = if git_available { @@ -110,6 +114,7 @@ pub(super) fn build_report_with_dependencies( mode, readiness, state_root: global_state.state_root, + checkout_identity, agent_trace_db, repository_root: detected_repository_root, hook_path_source, @@ -133,7 +138,8 @@ pub(super) fn build_report_with_lifecycle_problems( dependencies, lifecycle_problems, ); - report.agent_trace_db = collect_agent_trace_db_health(&mut report.problems); + report.checkout_identity = collect_checkout_identity_health(repository_root); + report.agent_trace_db = collect_agent_trace_db_health(repository_root, &mut report.problems); report.readiness = if report .problems .iter() @@ -153,7 +159,8 @@ fn build_report_without_service_owned_problem_checks( mut problems: Vec, ) -> HookDoctorReport { let global_state = collect_global_state_locations(repository_root, dependencies); - let agent_trace_db = collect_agent_trace_db_health(&mut problems); + let checkout_identity = collect_checkout_identity_health(repository_root); + let agent_trace_db = collect_agent_trace_db_health(repository_root, &mut problems); let git_available = (dependencies.check_git_available)(); let detected_repository_root = if git_available { @@ -228,6 +235,7 @@ fn build_report_without_service_owned_problem_checks( mode, readiness: Readiness::Ready, state_root: global_state.state_root, + checkout_identity, agent_trace_db, repository_root: detected_repository_root, hook_path_source, @@ -286,8 +294,28 @@ fn collect_global_state_locations( } } -fn collect_agent_trace_db_health(problems: &mut Vec) -> Option { - let agent_trace_problems = diagnose_agent_trace_db_health(None); +fn collect_checkout_identity_health(repository_root: &Path) -> Option { + let git_dir = checkout::resolve_git_dir(repository_root).ok()?; + let checkout_id = checkout::read_checkout_id(&git_dir).ok()??; + let database_path = agent_trace_db_path_for_checkout(&checkout_id).ok()?; + let database_state = if database_path.exists() { + "present" + } else { + "expected" + }; + + Some(CheckoutIdentityHealth { + checkout_id, + database_path, + database_state, + }) +} + +fn collect_agent_trace_db_health( + repository_root: &Path, + problems: &mut Vec, +) -> Option { + let agent_trace_problems = diagnose_agent_trace_db_health(Some(repository_root)); let mut agent_trace_db = None; for problem in &agent_trace_problems { @@ -307,9 +335,9 @@ fn collect_agent_trace_db_health(problems: &mut Vec) -> Option) -> Option) -> Option Option { + collect_checkout_identity_health(repository_root) + .map(|identity| identity.database_path) + .or_else(|| crate::services::default_paths::agent_trace_db_path().ok()) +} + +fn agent_trace_db_label(repository_root: &Path) -> &'static str { + if collect_checkout_identity_health(repository_root).is_some() { + "Agent Trace checkout DB" + } else { + "Agent Trace DB" + } +} + fn collect_hook_file_health(directory: &Path) -> Vec { REQUIRED_HOOKS .iter() diff --git a/cli/src/services/doctor/mod.rs b/cli/src/services/doctor/mod.rs index 149a5242..8b6e35bc 100644 --- a/cli/src/services/doctor/mod.rs +++ b/cli/src/services/doctor/mod.rs @@ -3,8 +3,10 @@ use std::path::{Path, PathBuf}; use std::process::Command; use anyhow::{Context, Result}; +use serde_json::json; use crate::app::{ContextWithRepoRoot, HasRepoRoot}; +use crate::services::checkout::registry::{self, CheckoutRecord}; use crate::services::default_paths::{resolve_sce_default_locations, resolve_state_data_root}; use crate::services::lifecycle::{ lifecycle_providers, FixOutcome, HealthCategory, HealthFixability, HealthProblem, @@ -40,8 +42,15 @@ pub enum DoctorMode { Fix, } +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum DoctorAction { + Report, + Dbs, +} + #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct DoctorRequest { + pub action: DoctorAction, pub mode: DoctorMode, pub format: DoctorFormat, } @@ -69,6 +78,10 @@ pub fn run_doctor_with_context(request: DoctorRequest, context: &C) -> Result where C: ContextWithRepoRoot, { + if request.action == DoctorAction::Dbs { + return run_doctor_dbs(request.format); + } + let repository_root = if let Some(path) = context.repo_root() { path.to_path_buf() } else { @@ -81,6 +94,67 @@ where render_report(request, &execution) } +fn run_doctor_dbs(format: DoctorFormat) -> Result { + let mut checkouts = registry::list_checkouts().context("failed to read checkout registry")?; + sort_checkouts_by_last_seen_desc(&mut checkouts); + + match format { + DoctorFormat::Text => Ok(render_doctor_dbs_text(&checkouts)), + DoctorFormat::Json => render_doctor_dbs_json(&checkouts), + } +} + +fn sort_checkouts_by_last_seen_desc(checkouts: &mut [CheckoutRecord]) { + checkouts.sort_by(|left, right| { + right + .last_seen + .cmp(&left.last_seen) + .then_with(|| left.checkout_id.cmp(&right.checkout_id)) + }); +} + +fn render_doctor_dbs_text(checkouts: &[CheckoutRecord]) -> String { + let mut lines = vec![String::from("SCE doctor dbs")]; + + if checkouts.is_empty() { + lines.push(String::from("no registered checkouts")); + return lines.join("\n"); + } + + for checkout in checkouts { + lines.push(format!("checkout_id: {}", checkout.checkout_id)); + lines.push(format!(" path: {}", checkout.path)); + lines.push(format!( + " database_path: {}", + checkout.database_path.as_deref().unwrap_or("none") + )); + lines.push(format!(" last_seen: {}", checkout.last_seen)); + lines.push(format!( + " remote_url: {}", + checkout.remote_url.as_deref().unwrap_or("none") + )); + } + + lines.join("\n") +} + +fn render_doctor_dbs_json(checkouts: &[CheckoutRecord]) -> Result { + let payload = json!({ + "status": "ok", + "command": NAME, + "subcommand": "dbs", + "checkouts": checkouts.iter().map(|checkout| json!({ + "checkout_id": checkout.checkout_id, + "path": checkout.path, + "database_path": checkout.database_path, + "last_seen": checkout.last_seen, + "remote_url": checkout.remote_url, + })).collect::>(), + }); + + serde_json::to_string_pretty(&payload).context("failed to serialize doctor dbs report to JSON") +} + fn execute_doctor_with_context( request: DoctorRequest, repository_root: &Path, diff --git a/cli/src/services/doctor/render.rs b/cli/src/services/doctor/render.rs index f71cb223..533f12a5 100644 --- a/cli/src/services/doctor/render.rs +++ b/cli/src/services/doctor/render.rs @@ -82,23 +82,7 @@ fn format_report_with_color_policy(report: &HookDoctorReport, color_enabled: boo )); lines.push(format!("\n{}:", heading("Configuration"))); - for location in &report.config_locations { - lines.push(format_human_text_row( - color_enabled, - config_location_status(report, location), - location.label, - location.path.display().to_string(), - )); - } - - if let Some(agent_trace_db) = &report.agent_trace_db { - lines.push(format_human_text_row( - color_enabled, - agent_trace_db_status(report), - agent_trace_db.label, - agent_trace_db.path.display().to_string(), - )); - } + push_configuration_section_rows(report, color_enabled, &mut lines); lines.push(format!("\n{}:", heading("Repository"))); lines.push(format_human_text_row( @@ -150,6 +134,43 @@ fn format_report_with_color_policy(report: &HookDoctorReport, color_enabled: boo lines.join("\n") } +fn push_configuration_section_rows( + report: &HookDoctorReport, + color_enabled: bool, + lines: &mut Vec, +) { + for location in &report.config_locations { + lines.push(format_human_text_row( + color_enabled, + config_location_status(report, location), + location.label, + location.path.display().to_string(), + )); + } + + if let Some(identity) = &report.checkout_identity { + lines.push(format_human_text_row( + color_enabled, + HumanTextStatus::Pass, + "Checkout identity", + identity.checkout_id.clone(), + )); + lines.push(format_human_text_row( + color_enabled, + agent_trace_db_status(report), + "Agent Trace checkout DB", + identity.database_path.display().to_string(), + )); + } else if let Some(agent_trace_db) = &report.agent_trace_db { + lines.push(format_human_text_row( + color_enabled, + agent_trace_db_status(report), + agent_trace_db.label, + agent_trace_db.path.display().to_string(), + )); + } +} + fn push_git_hooks_section(report: &HookDoctorReport, color_enabled: bool, lines: &mut Vec) { lines.push(format!("\n{}:", heading("Git Hooks"))); if report.hooks.is_empty() { @@ -442,6 +463,11 @@ fn render_report_json(execution: &DoctorExecution) -> Result { "path": location.path.display().to_string(), "state": location.state, })), + "checkout_identity": report.checkout_identity.as_ref().map(|identity| json!({ + "checkout_id": identity.checkout_id, + "database_path": identity.database_path.display().to_string(), + "database_state": identity.database_state, + })), "hook_path_source": match report.hook_path_source { HookPathSource::Default => "default", HookPathSource::LocalConfig => "local_config", diff --git a/cli/src/services/doctor/types.rs b/cli/src/services/doctor/types.rs index 9ba641db..2b0aecf6 100644 --- a/cli/src/services/doctor/types.rs +++ b/cli/src/services/doctor/types.rs @@ -53,6 +53,7 @@ pub(super) struct HookDoctorReport { pub(super) mode: super::DoctorMode, pub(super) readiness: Readiness, pub(super) state_root: Option, + pub(super) checkout_identity: Option, pub(super) agent_trace_db: Option, pub(super) repository_root: Option, pub(super) hook_path_source: HookPathSource, @@ -63,6 +64,13 @@ pub(super) struct HookDoctorReport { pub(super) problems: Vec, } +#[derive(Clone, Debug, Eq, PartialEq)] +pub(super) struct CheckoutIdentityHealth { + pub(super) checkout_id: String, + pub(super) database_path: PathBuf, + pub(super) database_state: &'static str, +} + #[derive(Clone, Debug, Eq, PartialEq)] pub(super) struct IntegrationGroupHealth { pub(super) label: &'static str, diff --git a/cli/src/services/parse/command_runtime.rs b/cli/src/services/parse/command_runtime.rs index 461251be..0d9bfe57 100644 --- a/cli/src/services/parse/command_runtime.rs +++ b/cli/src/services/parse/command_runtime.rs @@ -222,18 +222,11 @@ fn convert_clap_command(command: cli_schema::Commands) -> Result convert_setup_command(opencode, claude, both, non_interactive, hooks, repo), - cli_schema::Commands::Doctor { fix, format } => Ok(RuntimeCommand::Doctor( - services::doctor::command::DoctorCommand { - request: services::doctor::DoctorRequest { - mode: if fix { - services::doctor::DoctorMode::Fix - } else { - services::doctor::DoctorMode::Diagnose - }, - format, - }, - }, - )), + cli_schema::Commands::Doctor { + fix, + format, + subcommand, + } => convert_doctor_command(fix, format, subcommand.as_ref()), cli_schema::Commands::Hooks { subcommand } => convert_hooks_subcommand(subcommand), cli_schema::Commands::Policy { subcommand } => Ok(convert_policy_subcommand(&subcommand)), cli_schema::Commands::Version { format } => Ok(RuntimeCommand::Version( @@ -251,6 +244,40 @@ fn convert_clap_command(command: cli_schema::Commands) -> Result, +) -> Result { + let request = match subcommand { + Some(cli_schema::DoctorSubcommand::Dbs { format }) => { + if fix { + return Err(ClassifiedError::validation( + "'sce doctor dbs' cannot be used with '--fix'. Try: run 'sce doctor dbs --format text' or 'sce doctor --fix'.", + )); + } + services::doctor::DoctorRequest { + action: services::doctor::DoctorAction::Dbs, + mode: services::doctor::DoctorMode::Diagnose, + format: *format, + } + } + None => services::doctor::DoctorRequest { + action: services::doctor::DoctorAction::Report, + mode: if fix { + services::doctor::DoctorMode::Fix + } else { + services::doctor::DoctorMode::Diagnose + }, + format, + }, + }; + + Ok(RuntimeCommand::Doctor( + services::doctor::command::DoctorCommand { request }, + )) +} + fn convert_policy_subcommand(subcommand: &cli_schema::PolicySubcommand) -> RuntimeCommand { let request = match subcommand { cli_schema::PolicySubcommand::Bash { input, output } => { diff --git a/context/architecture.md b/context/architecture.md index 31e73f20..e9d48e67 100644 --- a/context/architecture.md +++ b/context/architecture.md @@ -115,17 +115,17 @@ The repository includes a new placeholder Rust binary crate at `cli/`. - `cli/src/services/setup/mod.rs` defines the setup command contract (`SetupMode`, `SetupTarget`, `SetupRequest`, CLI flag parser/validator), an `inquire`-backed interactive target prompter (`InquireSetupTargetPrompter`), setup dispatch outcomes (proceed/cancelled), compile-time embedded asset access (`EmbeddedAsset`, target-scoped iterators, required-hook asset iterators/lookups) generated by `cli/build.rs` from the ephemeral crate-local `cli/assets/generated/config/{opencode,claude}/**` mirror plus `cli/assets/hooks/**`, and focused internal support seams for install-flow vs prompt-flow logic; `cli/src/services/setup/command.rs` owns the `SetupCommand` payload used by the static `RuntimeCommand` enum and executes against any context implementing repo-root scoping. Its install engine/orchestrator stages embedded files and uses a unified remove-and-replace policy (removing existing targets before swapping staged content, with deterministic recovery guidance on swap failure and no backup artifact creation), and formats deterministic completion messaging; required-hook install orchestration (`install_required_git_hooks`) follows the same remove-and-replace policy (removing existing hooks before swapping staged content, with deterministic recovery guidance on swap failure). The setup command derives a repo-root-scoped context before aggregating static lifecycle provider `setup` dispatch across providers (config → local_db → auth_db → agent_trace_db → hooks when requested), so setup providers consume only repo-root access from the scoped context. - `cli/src/services/setup/mod.rs` keeps those responsibilities inside one file for now, but the current ownership split is explicit: the inline `install` module owns repository-path normalization, staging/swap install behavior, required-hook installation, and filesystem safety guards, while the inline `prompt` module owns interactive target selection and prompt styling. - `cli/src/services/security.rs` provides shared security utilities for deterministic secret redaction (`redact_sensitive_text`) and directory write-permission probes (`ensure_directory_is_writable`) used by app/setup/observability surfaces. -- `cli/src/services/doctor/mod.rs` owns the current doctor request/report surface while focused submodules (`doctor/inspect.rs`, `doctor/render.rs`, `doctor/fixes.rs`, `doctor/types.rs`) split report fact collection, rendering, manual fix reporting, and doctor-owned domain types into smaller seams; `cli/src/services/doctor/command.rs` owns the `DoctorCommand` payload used by the static `RuntimeCommand` enum and executes against any context implementing repo-root scoping. Runtime doctor execution resolves a repository root, derives a scoped context, requests the shared static lifecycle provider catalog with hooks included for service-owned `diagnose` and `fix` behavior, adapts lifecycle-owned health/fix records into doctor-owned problem/fix records, and then renders stable text/JSON problem records with category/severity/fixability/remediation fields plus deterministic fix-result reporting in fix mode. Report fact collection still preserves current environment/repository/hook/integration display data, while service-owned lifecycle providers now own config validation, local DB and Agent Trace DB readiness/bootstrap, and hook rollout diagnosis/repair. +- `cli/src/services/doctor/mod.rs` owns the current doctor request/report surface while focused submodules (`doctor/inspect.rs`, `doctor/render.rs`, `doctor/fixes.rs`, `doctor/types.rs`) split report fact collection, rendering, manual fix reporting, and doctor-owned domain types into smaller seams; `cli/src/services/doctor/command.rs` owns the `DoctorCommand` payload used by the static `RuntimeCommand` enum and executes against any context implementing repo-root scoping. Runtime doctor execution either renders `sce doctor dbs` registry listings or resolves a repository root, derives a scoped context, requests the shared static lifecycle provider catalog with hooks included for service-owned `diagnose` and `fix` behavior, adapts lifecycle-owned health/fix records into doctor-owned problem/fix records, and then renders stable text/JSON problem records with category/severity/fixability/remediation fields plus deterministic fix-result reporting in fix mode. Report fact collection preserves environment/repository/hook/integration display data and adds checkout identity plus per-checkout Agent Trace DB status when a checkout ID exists, while service-owned lifecycle providers own config validation, local DB and Agent Trace DB readiness/bootstrap, and hook rollout diagnosis/repair. - `cli/src/services/version/mod.rs` defines the version command parser/rendering contract (`parse_version_request`, `render_version`) with deterministic text output and stable JSON runtime-identification fields; `cli/src/services/version/command.rs` owns the `VersionCommand` payload used by the static `RuntimeCommand` enum. - `cli/src/services/completion/mod.rs` defines completion parser/rendering contract (`parse_completion_request`, `render_completion`) with deterministic Bash/Zsh/Fish script output aligned to current parser-valid command/flag surfaces; `cli/src/services/completion/command.rs` owns the `CompletionCommand` payload used by the static `RuntimeCommand` enum. - `cli/src/services/hooks/mod.rs` defines the current local hook runtime parsing/dispatch (`HookSubcommand`, `run_hooks_subcommand`) plus a commit-msg co-author policy seam (`apply_commit_msg_coauthor_policy`) that injects one canonical SCE trailer only when the disabled-default attribution-hooks config/env control is enabled and `SCE_DISABLED` is false; `cli/src/services/hooks/command.rs` owns the `HooksCommand` payload used by the static `RuntimeCommand` enum. In the current attribution-only baseline, `pre-commit` and `post-rewrite` are deterministic no-op surfaces; `post-commit` requires validated `--remote-url`, threads that URL through the Agent Trace flow, prints it to stderr, and remains an active intersection + Agent Trace persistence entrypoint (captures current commit patch, queries recent `diff_traces` from the bounded past-7-days window, combines valid patches via `patch::combine_patches`, intersects with post-commit patch via `patch::intersect_patches`, persists result to `post_commit_patch_intersections`, then persists built Agent Trace payloads with range-level `content_hash` values to `agent_traces` in AgentTraceDb without post-commit file artifacts); `diff-trace` performs STDIN JSON intake, validates required non-empty `sessionID`/`diff`/`tool_name`, optional `model_id` (absent/`null` → `None`, resolved from `session_models` by `tool_name` + `session_id` when absent) and required `tool_version` (present and either `null` or non-empty string) plus required `u64` `time` (Unix epoch milliseconds), rejects values that cannot fit AgentTraceDb signed `time_ms` storage, writes one collision-safe parsed-payload `context/tmp/-000000-diff-trace.json` artifact, and inserts the parsed payload fields into AgentTraceDb; `session-model` performs STDIN JSON intake, validates required non-empty `sessionID`/`model_id`/`tool_name`, required `u64` `time` (Unix epoch milliseconds), and required nullable/non-empty `tool_version`, then upserts the parsed payload into AgentTraceDb `se... (line truncated to 2000 chars) - Claude `SessionStart` session-model parsing in `cli/src/services/hooks/mod.rs` uses explicit payload version fields (`tool_version`/`claude_version`/`version`) when present; if no non-empty payload version is available, it best-effort runs `claude --version`, trims stdout, and leaves `tool_version` nullable without failing intake when the command is unavailable, fails, or returns empty output. - Diff-trace attribution resolution in `cli/src/services/hooks/mod.rs` looks up `session_models` when `model_id` or `tool_version` is missing/nullable, fills only missing fields from the stored row when available, preserves direct payload precedence, and continues persistence with `None` for unresolved attribution. - `cli/src/services/resilience.rs` defines bounded retry/timeout/backoff execution policy (`RetryPolicy`, `run_with_retry`) for transient operation hardening with deterministic failure messaging and retry observability. -- No user-invocable `sce sync` command is wired in the current runtime; local DB and Agent Trace DB bootstrap flows through lifecycle providers aggregated by setup, and DB health/repair flows through the doctor surface. +- No user-invocable `sce sync` command is wired in the current runtime; local DB and Agent Trace checkout identity bootstrap flows through lifecycle providers aggregated by setup, while checkout/global DB health/repair and checkout-registry listing flow through the doctor surface. - `cli/src/services/patch.rs` defines the standalone patch domain model (`ParsedPatch`, `PatchFileChange`, `FileChangeKind`, `PatchHunk`, `TouchedLine`, `TouchedLineKind`) for in-memory parsed unified-diff representation, capturing only touched lines (added/removed) plus minimal per-file/per-hunk metadata while excluding non-hunk headers and unchanged context lines. All types are `serde`-serializable/deserializable with `snake_case` JSON field naming. The module also provides `parse_patch`, a public parser function that converts raw unified-diff text (both `Index:` SVN-style and `diff --git` git-style formats) into `ParsedPatch` structs, with `ParseError` for actionable malformed-input diagnostics. Storage-agnostic JSON load helpers (`load_patch_from_json` for string input, `load_patch_from_json_bytes` for byte input) reconstruct `ParsedPatch` from serialized JSON content with `PatchLoadError` for actionable deserialization diagnostics. Its patch-set operations now include deterministic ordered combination plus target-shaped intersection that prefers exact touched-line matches and falls back to historical `kind`+`content` matching when incremental diffs and canonical post-commit diffs have drifted line numbers; `parse_patch`, `combine_patches`, and `intersect_patches` are consumed by the active post-commit hook runtime. - `cli/src/services/structured_patch.rs` defines the synchronous structured editor-hook derivation seam. It derives Claude `PostToolUse` `Write` create and `Edit` structured-patch payloads into canonical `ParsedPatch` values plus Claude session/tool metadata, returning deterministic skip reasons for unsupported events/tools/payload shapes. The module is pure and side-effect-free. It is wired into `sce hooks diff-trace` for Claude payload classification at intake (T04) and into `AgentTraceDb::recent_diff_trace_patches` for post-commit structured payload parsing dispatch at read time (T05). -- `cli/src/services/` contains module boundaries for command_registry, lifecycle, auth_command, config, setup, doctor, hooks, checkout identity, bash_policy, version, completion, help, patch, SCE web URL helpers, shared database infrastructure, local DB adapters, encrypted auth DB adapters, and Agent Trace DB adapters with explicit trait seams for future implementations. `cli/src/services/checkout/` owns checkout ID file infrastructure, central checkout-registry JSON persistence, and per-checkout Agent Trace DB lazy resolution for hook runtime; setup uses it through `AgentTraceDbLifecycle::setup()`, while doctor checkout display and registry listing remain deferred. `cli/src/services/bash_policy.rs` owns both the CLI-agnostic evaluator logic and the hidden `sce policy bash` command adapter used by OpenCode and Claude hook callers. `cli/src/services/command_registry.rs` defines the static `RuntimeCommand` enum, deterministic `CommandRegistry` name catalog, and `build_default_registry()` function for command dispatch metadata. Service-owned command modules own the runtime command payload structs for help/help-text, version, completion, auth, config, setup, doctor, hooks, and policy. +- `cli/src/services/` contains module boundaries for command_registry, lifecycle, auth_command, config, setup, doctor, hooks, checkout identity, bash_policy, version, completion, help, patch, SCE web URL helpers, shared database infrastructure, local DB adapters, encrypted auth DB adapters, and Agent Trace DB adapters with explicit trait seams for future implementations. `cli/src/services/checkout/` owns checkout ID file infrastructure, central checkout-registry JSON persistence, and per-checkout Agent Trace DB lazy resolution for hook runtime; setup uses it through `AgentTraceDbLifecycle::setup()`, while `sce doctor` surfaces checkout identity and per-checkout Agent Trace DB status when a checkout ID exists and `sce doctor dbs` lists registered checkouts from the central registry. `cli/src/services/bash_policy.rs` owns both the CLI-agnostic evaluator logic and the hidden `sce policy bash` command adapter used by OpenCode and Claude hook callers. `cli/src/services/command_registry.rs` defines the static `RuntimeCommand` enum, deterministic `CommandRegistry` name catalog, and `build_default_registry()` function for command dispatch metadata. Service-owned command modules own the runtime command payload structs for help/help-text, version, completion, auth, config, setup, doctor, hooks, and policy. - `cli/README.md` is the crate-local onboarding and usage source of truth for placeholder behavior, safety limitations, and roadmap mapping back to service contracts. - `flake.nix` applies `rust-overlay` (`oxalica/rust-overlay`) to nixpkgs, pins `rust-bin.stable.1.93.1.default` with `rustfmt` + `clippy`, reads the package/check version from repo-root `.version`, builds `packages.sce` through Crane (`buildDepsOnly` -> `buildPackage`) with a filtered repo-root source that preserves the Cargo tree plus `cli/assets/hooks`, then injects generated OpenCode/Claude config payloads and schema inputs into a temporary `cli/assets/generated/` mirror during derivation unpack so `cli/build.rs` can package the crate without requiring committed generated crate assets. The same `cli/build.rs` now scans `cli/migrations/*/*.sql` and writes `cli/src/generated_migrations.rs` with deterministic migration constants sorted by numeric filename prefix. The flake runs `cli-tests`, `cli-clippy`, and `cli-fmt` plus the dedicated `integrations-install-tests`, `integrations-install-clippy`, and `integrations-install-fmt` derivations through Crane-backed paths so both Rust crates have first-class default-flake verification, exposes directory-scoped JS validation derivations for both `npm/` and the shared `config/lib/` plugin package root, and also exposes the non-default `apps.install-channel-integration-tests` flake app for install-channel integration coverage outside the default check set. The shared config-lib source set is rooted at `config/lib/` and includes the shared `package.json`, `bun.lock`, and `tsconfig.json` plus `agent-trace-plugin/` and `bash-policy-plugin/`; `config-lib-bun-tests` runs the bash-policy plugin wrapper tests from that shared root, while `config-lib-biome-check` and `config-lib-biome-format` run Biome over the copied shared package source. `.github/workflows/publish-crates.yml` follows the same asset-preparation rule but runs Cargo packaging from a temporary clean repository copy so crates.io publish no longer needs `--allow-dirty`. - The config-lib check source preserves repo-relative access to shared CLI patch fixtures: Nix copies a filtered repo-shaped source containing `config/lib/**` plus `cli/src/services/structured_patch/fixtures`, then runs Bun/Biome from `config/lib/`. diff --git a/context/cli/checkout-identity.md b/context/cli/checkout-identity.md index 6b537655..fe5aa3ab 100644 --- a/context/cli/checkout-identity.md +++ b/context/cli/checkout-identity.md @@ -38,7 +38,7 @@ During hook runtime: - Missing or incomplete schema falls back to `AgentTraceDb::open_at(path)`, which runs migrations through the shared Turso adapter. - Successful DB resolution updates the registry record with `database_path`. -The global `agent-trace.db` path remains only as a lifecycle fallback when no checkout context or checkout ID is available. Doctor checkout identity display and registry listing are deferred to later tasks in `context/plans/agent-trace-checkout-identity.md`. +The global `agent-trace.db` path remains only as a lifecycle fallback when no checkout context or checkout ID is available. `sce doctor` displays the current checkout ID and per-checkout Agent Trace DB status when a checkout ID exists, and `sce doctor dbs` lists registry records sorted by `last_seen` descending. ## Testing boundary diff --git a/context/cli/cli-command-surface.md b/context/cli/cli-command-surface.md index fba49e6d..168acd81 100644 --- a/context/cli/cli-command-surface.md +++ b/context/cli/cli-command-surface.md @@ -11,7 +11,7 @@ Operator onboarding currently comes from `sce --help`, command-local `--help` ou - Top-level command metadata catalog: `cli/src/cli_schema.rs` - Custom top-level help renderer and known-command classifier: `cli/src/command_surface.rs` - Turso adapters: `cli/src/services/auth_db/mod.rs`, `cli/src/services/local_db/mod.rs`, `cli/src/services/agent_trace_db/mod.rs`, and shared infrastructure in `cli/src/services/db/mod.rs` -- Service domains: `cli/src/services/{agent_trace,agent_trace_db,auth,auth_command,auth_db,completion,config,db,default_paths,hooks,local_db,observability,output_format,resilience,security,setup,style,token_storage,version}` plus the split doctor module at `cli/src/services/doctor/{mod,command,inspect,render,fixes,types}.rs`; service-owned `command.rs` files own command payload structs for help/version/completion/auth/config/setup/doctor/hooks, and `cli/src/services/command_registry.rs` owns the static `RuntimeCommand` enum that dispatches them +- Service domains: `cli/src/services/{agent_trace,agent_trace_db,auth,auth_command,auth_db,checkout,completion,config,db,default_paths,hooks,local_db,observability,output_format,resilience,security,setup,style,token_storage,version}` plus the split doctor module at `cli/src/services/doctor/{mod,command,inspect,render,fixes,types}.rs`; service-owned `command.rs` files own command payload structs for help/version/completion/auth/config/setup/doctor/hooks, and `cli/src/services/command_registry.rs` owns the static `RuntimeCommand` enum that dispatches them - Service lifecycle: `cli/src/services/lifecycle.rs` defines lifecycle-owned health/setup result types, the `ServiceLifecycle` trait with `diagnose`, `fix`, and `setup` methods for concrete providers, and the static `LifecycleProvider` enum used by `doctor`/`setup` to dispatch across `config`, `local_db`, `auth_db`, `agent_trace_db`, and `hooks` without boxed provider aggregation - Shared test temp-path helper: `cli/src/test_support.rs` (`TestTempDir`, test-only module) @@ -63,7 +63,7 @@ Deferred or gated command surfaces currently avoid claiming unimplemented behavi `setup` now also exposes compile-time embedded config assets for OpenCode/Claude targets, sourced from the generated `config/.opencode/**` and `config/.claude/**` trees via `cli/build.rs` with normalized forward-slash relative paths and target-scoped iteration APIs; the embedded asset set includes the OpenCode bash-policy plugin wrapper plus Claude settings `PreToolUse` Bash policy hook, both delegating to the Rust `sce policy bash` path. `setup` additionally includes a repository-root install engine (`install_embedded_setup_assets`) that stages embedded files, intentionally leaves generated `skills/*/tile.json` manifests in `config/` only, skips those tile files during repo-root installs, and uses a unified remove-and-replace policy for `.opencode/`/`.claude/` (removing existing targets before swapping staged content, with deterministic recovery guidance on swap failure) while treating bash-policy enforcement files as first-class SCE-managed assets. `setup` now executes end-to-end and prints deterministic completion details including selected target(s) and per-target install count. -`doctor` now executes end-to-end with explicit diagnosis and repair-intent surfaces: `sce doctor` stays read-only, `sce doctor --fix` selects repair-intent mode, and text/JSON output expose stable mode/problem/fix-result/database-record scaffolding. The current runtime aggregates `ServiceLifecycle::diagnose` and `ServiceLifecycle::fix` calls across all registered service providers (`config`, `local_db`, `auth_db`, `agent_trace_db`, `hooks`) plus integration checks, covering state-root resolution, global and repo-local `sce/config.json` readability/schema validation, local DB and Agent Trace DB path/health, DB-parent readiness barriers, an intentionally empty repo-scoped SCE database section for the active repository, the repo hook rollout slice when a repository target is detected, and repo-root installed OpenCode integration presence for `plugins`, `agents`, `commands`, and `skills`; those integration checks are presence-only and fail a group when any required installed file is missing. Fix mode delegates to each provider's `fix` implementation, which reuses the canonical setup hook install flow to repair missing/stale/non-executable required hooks and missing hooks directories, and it can bootstrap missing canonical database parent directories when the resolved paths match canonical owned locations. +`doctor` now executes end-to-end with explicit diagnosis, repair-intent, and checkout-registry surfaces: `sce doctor` stays read-only, `sce doctor --fix` selects repair-intent mode, and `sce doctor dbs [--format text|json]` lists registered checkouts from `/sce/checkout-registry.json`. The current runtime aggregates `ServiceLifecycle::diagnose` and `ServiceLifecycle::fix` calls across all registered service providers (`config`, `local_db`, `auth_db`, `agent_trace_db`, `hooks`) plus integration checks, covering state-root resolution, global and repo-local `sce/config.json` readability/schema validation, local DB and checkout/global Agent Trace DB path/health, DB-parent readiness barriers, the repo hook rollout slice when a repository target is detected, and repo-root installed OpenCode integration presence for `plugins`, `agents`, `commands`, and `skills`. Fix mode delegates to each provider's `fix` implementation, which reuses the canonical setup hook install flow to repair missing/stale/non-executable required hooks and missing hooks directories, and it can bootstrap missing canonical database parent directories when the resolved paths match canonical owned locations. A user-invocable `sync` command is not wired in the current CLI surface; local DB and Agent Trace DB bootstrap currently happen through `setup`, and DB health/repair currently happens through `doctor`. Command wiring for `sce sync` is deferred to `0.4.0`. ## Command loop and error model @@ -80,7 +80,7 @@ A user-invocable `sync` command is not wired in the current CLI surface; local D - Interactive `sce setup` prompt cancellation/interrupt exits cleanly with: `Setup cancelled. No files were changed.` - Command handlers return deterministic status messaging: - `setup`: `Setup completed successfully.` plus selected targets and per-target install destinations/counts. -- `doctor`: current runtime emits `SCE doctor diagnose` / `SCE doctor fix` human text headers plus ordered `Environment`, `Configuration` (includes Agent Trace DB row), `Repository`, `Git Hooks`, and `Integrations` sections with bracketed `[PASS]`/`[FAIL]`/`[MISS]` row tokens, shared-style green pass plus red fail/miss colorization when enabled, simplified `label (path)` rows, top-level-only hook rows, and a deterministic summary footer; JSON output carries stable problem/fixability records plus deterministic fix-result records in fix mode and reports the Agent Trace DB record under `agent_trace_db`. The runtime validates global and repo-local `sce/config.json` inputs plus local DB and Agent Trace DB health, keeps the repo-scoped database section empty unless a future repo-owned SCE database family is introduced, diagnoses repo hook rollout integrity plus repo-root installed OpenCode integration presence for `OpenCode plugins`, `OpenCode agents`, `OpenCode commands`, and `OpenCode skills`, and in fix mode reuses canonical setup hook installation for supported hook repairs plus bounded bootstrap of canonical missing SCE-owned DB parent directories while preserving manual-only reporting for unsupported issues. +- `doctor`: current runtime emits `SCE doctor diagnose` / `SCE doctor fix` human text headers plus ordered `Environment`, `Configuration` (including checkout identity + Agent Trace checkout DB rows when available), `Repository`, `Git Hooks`, and `Integrations` sections with bracketed `[PASS]`/`[FAIL]`/`[MISS]` row tokens, shared-style green pass plus red fail/miss colorization when enabled, simplified `label (path)` rows, top-level-only hook rows, and a deterministic summary footer; JSON output carries stable problem/fixability records plus deterministic fix-result records in fix mode and reports `checkout_identity` plus the resolved Agent Trace DB record. `sce doctor dbs` emits either `no registered checkouts` or registry rows sorted by `last_seen` descending, with JSON fields `checkout_id`, `path`, `database_path`, `last_seen`, and `remote_url`. - `hooks`: deterministic hook subcommand status messaging for runtime entrypoint invocation and argument/STDIN contract validation. ## Service contracts @@ -88,7 +88,7 @@ A user-invocable `sync` command is not wired in the current CLI surface; local D - `cli/src/services/setup/mod.rs` defines setup parsing/selection contracts plus runtime install orchestration (`run_setup_for_mode`) over the embedded asset install engine; `cli/src/services/setup/command.rs` owns the setup runtime command handler. Setup now aggregates `ServiceLifecycle::setup` calls across registered providers (`config`, `local_db`, `auth_db`, `agent_trace_db`, `hooks`) in order, using a `ContextWithRepoRoot`-scoped context with resolved repository root. - `cli/src/services/setup/mod.rs` now keeps its larger internal responsibilities behind focused inline support modules: `install` owns repository canonicalization, staging/swap install flows, required-hook installation, and repo/writeability guards, while `prompt` owns interactive target selection and styled prompt labels. - `cli/src/services/config/mod.rs` defines config parser/runtime contracts (`show`, `validate`, `--help`), strict config-file key/type validation, deterministic text/JSON rendering, repo-configured bash-policy preset/custom validation and reporting under `policies.bash`, and shared auth-key metadata that declares env key, config-file key, and optional baked-default eligibility for supported auth runtime values starting with `workos_client_id` (`WORKOS_CLIENT_ID` vs `workos_client_id`); auth-key provenance/preference metadata stays on `show`, while `validate` stays trimmed to validation status plus issues/warnings. `cli/src/services/config/lifecycle.rs` implements `ServiceLifecycle` for config health checks and setup (global/local config validation and repo-local config bootstrap). -- `cli/src/services/doctor/mod.rs` defines the implemented doctor request/report contract (`DoctorRequest`, `DoctorMode`, `run_doctor`) while focused submodules under `cli/src/services/doctor/` handle runtime command dispatch (`command.rs`), diagnosis (`inspect.rs`), rendering (`render.rs`), fix execution (`fixes.rs`), and doctor-owned domain types (`types.rs`). Together they preserve explicit fix-mode parsing, stable text/JSON problem and database-record rendering, deterministic fix-result reporting, and aggregation of `ServiceLifecycle::diagnose`/`ServiceLifecycle::fix` across registered providers (`config`, `local_db`, `auth_db`, `agent_trace_db`, `hooks`). The doctor module coordinates state-root/config/database reporting and validation, an empty default repo-scoped database inventory, path-source detection plus required-hook presence/executable/content checks when a repository target is detected, repo-root installed OpenCode integration presence inventory for `plugins`, `agents`, `commands`, and `skills` derived from the embedded OpenCode setup asset catalog, shared-style bracketed human status token rendering (`[PASS]`, `[FAIL]`, `[MISS]`) with simplified `label (path)` text rows, and repair-mode delegation to service-owned fix implementations. +- `cli/src/services/doctor/mod.rs` defines the implemented doctor request/report contract (`DoctorRequest`, `DoctorAction`, `DoctorMode`, `run_doctor`) while focused submodules under `cli/src/services/doctor/` handle runtime command dispatch (`command.rs`), diagnosis (`inspect.rs`), rendering (`render.rs`), fix execution (`fixes.rs`), and doctor-owned domain types (`types.rs`). Together they preserve explicit fix-mode parsing, checkout-registry listing, stable text/JSON problem and database-record rendering, deterministic fix-result reporting, and aggregation of `ServiceLifecycle::diagnose`/`ServiceLifecycle::fix` across registered providers (`config`, `local_db`, `auth_db`, `agent_trace_db`, `hooks`). The doctor module coordinates state-root/config/database reporting and validation, path-source detection plus required-hook presence/executable/content checks when a repository target is detected, repo-root installed OpenCode integration presence inventory for `plugins`, `agents`, `commands`, and `skills` derived from the embedded OpenCode setup asset catalog, shared-style bracketed human status token rendering (`[PASS]`, `[FAIL]`, `[MISS]`) with simplified `label (path)` text rows, and repair-mode delegation to service-owned fix implementations. - `cli/src/services/version/mod.rs` defines the version parser/output contract (`parse_version_request`, `render_version`) with deterministic text/JSON output modes; `cli/src/services/version/command.rs` owns the version runtime command handler. - `cli/src/services/completion/mod.rs` defines the completion output contract (`render_completion`) using clap_complete to generate deterministic shell scripts for Bash, Zsh, and Fish; `cli/src/services/completion/command.rs` owns the completion runtime command handler. - `cli/src/services/hooks/mod.rs` defines production local hook runtime parsing/dispatch (`HookSubcommand`, `run_hooks_subcommand`) for `pre-commit`, `commit-msg`, `post-commit`, `post-rewrite`, `diff-trace`, and `session-model`; `cli/src/services/hooks/command.rs` owns the hook runtime command handler. Current runtime behavior is commit-msg-only attribution behind the disabled-default attribution gate; `pre-commit` and `post-rewrite` are deterministic no-ops; `post-commit` requires validated `--remote-url`, threads that value through Agent Trace flow, prints it to stderr, and remains an active intersection + Agent Trace DB persistence path (captures current commit patch, combines/intersects recent `diff_traces`, persists intersection metadata to `post_commit_patch_intersections`, then persists built Agent Trace payload with range-level `content_hash` values to `agent_traces`); `diff-trace` performs STDIN JSON intake, required non-empty `sessionID`/`diff`/`tool_name`, optional `model_id`, required nullable/non-empty `tool_version`, plus required `u64` `time` (Unix epoch milliseconds) validation, missing/nullable attribution fallback from `session_models` by `tool_name` + `session_id` while preserving direct payload precedence, non-lossy AgentTraceDb `time_ms` conversion, collision-safe parsed-payload `context/tmp/-000000-diff-trace.json` persistence, and best-effort AgentTraceDb insertion whose failure is logged and reflected in success text; and `session-model` performs STDIN JSON intake for normalized model attribution upsert without raw artifact persistence, with Claude `SessionStart` parsing best-effort filling missing `tool_version` from `claude --version`. `cli/src/services/hooks/lifecycle.rs` implements `ServiceLifecycle` for hook health checks, fix, and setup (hook rollout integrity and required-hook installation). @@ -105,9 +105,9 @@ A user-invocable `sync` command is not wired in the current CLI surface; local D - `cli/src/services/local_db/mod.rs` provides `LocalDb = TursoDb` with retry-backed `new()`, `execute()`, `query()`, and `query_map()` inherited from the shared Turso adapter. - `LocalDb::new()` resolves the canonical per-user DB path through `default_paths::local_db_path()`, creates parent directories, opens the local Turso database, and currently runs zero local migrations. - `cli/src/services/agent_trace_db/mod.rs` provides `AgentTraceDb = TursoDb` plus `DiffTraceInsert<'_>`/`insert_diff_trace()` for parameterized writes to `diff_traces`, `AgentTraceInsert<'_>`/`insert_agent_trace()` for built `agent_traces`, and `SessionModelUpsert<'_>`/lookup helpers for durable `session_models` attribution keyed by `(tool_name, session_id)`. -- `AgentTraceDb::new()` resolves `/sce/agent-trace.db` through `default_paths::agent_trace_db_path()`, creates parent directories through `TursoDb`, opens the Turso database, and runs the ordered embedded `cli/migrations/agent-trace/*.sql` migration set. +- `AgentTraceDb::new()` resolves the legacy/global `/sce/agent-trace.db` through `default_paths::agent_trace_db_path()`; active hook runtime resolves per-checkout DB files through checkout identity and `default_paths::agent_trace_db_path_for_checkout(checkout_id)`. - `cli/src/services/local_db/lifecycle.rs` implements `ServiceLifecycle` for local DB health checks and setup (DB path/health validation and DB bootstrap). -- `cli/src/services/agent_trace_db/lifecycle.rs` implements `ServiceLifecycle` for Agent Trace DB health checks and setup (DB path/health validation and DB bootstrap). +- `cli/src/services/agent_trace_db/lifecycle.rs` implements `ServiceLifecycle` for Agent Trace checkout identity setup plus checkout/global DB path-health validation and parent bootstrap. - `sce setup` aggregates `ServiceLifecycle::setup` calls, which includes `LocalDbLifecycle::setup()` and `AgentTraceDbLifecycle::setup()` for DB initialization as part of local prerequisite bootstrap. - `sce doctor` aggregates `ServiceLifecycle::diagnose` and `ServiceLifecycle::fix` calls, which includes both DB lifecycle providers for DB path/health validation and can bootstrap missing canonical parent directories when repair mode is appropriate. diff --git a/context/context-map.md b/context/context-map.md index fbc645f2..01ddc324 100644 --- a/context/context-map.md +++ b/context/context-map.md @@ -11,7 +11,7 @@ Feature/domain context: - `context/cli/cli-command-surface.md` (CLI command surface including top-level help with ASCII art banner and gradient rendering, setup install flow, WorkOS device authorization flow + token storage behavior, attribution-only hook routing with validated post-commit `--remote-url` plumbing plus DB-backed `diff-trace` dual persistence and post-commit Agent Trace payload persistence including range `content_hash`, setup-owned local DB + Agent Trace DB bootstrap plus doctor DB health coverage, centralized Rust SCE web URL helpers in `services::agent_trace`, nested flake release package/app installability, Cargo local install + crates.io readiness policy, and hidden `sce policy bash` command adapter for bash-policy hook callers; `sce sync` command wiring is deferred to `0.4.0`; static `RuntimeCommand` enum dispatch lives in `services/command_registry.rs`, command payload structs for help/version/completion/auth/config/setup/doctor/hooks/policy are owned by their respective `services/{name}/command.rs` files, and clap-to-runtime conversion lives in `services/parse/command_runtime.rs`) - `context/cli/default-path-catalog.md` (canonical production CLI path-ownership contract centered on `cli/src/services/default_paths.rs`, including persisted auth/config files, named DB paths for auth/local/Agent Trace databases, repo-relative, embedded-asset, install, hook, and context-path families plus the regression guard that keeps production path ownership centralized) -- `context/cli/checkout-identity.md` (current checkout identity infrastructure in `cli/src/services/checkout/`, including `/sce/checkout-id` UUIDv7 storage, central `/sce/checkout-registry.json` records, atomic registry persistence, setup lifecycle integration that creates/reuses/registers checkout identity and emits checkout-ID messaging, per-checkout Agent Trace DB lazy resolution for hook runtime, and the remaining doctor display/listing boundary) +- `context/cli/checkout-identity.md` (current checkout identity infrastructure in `cli/src/services/checkout/`, including `/sce/checkout-id` UUIDv7 storage, central `/sce/checkout-registry.json` records, atomic registry persistence, setup lifecycle integration that creates/reuses/registers checkout identity and emits checkout-ID messaging, per-checkout Agent Trace DB lazy resolution for hook runtime, `sce doctor` checkout identity display, and `sce doctor dbs` registry listing) - `context/cli/patch-service.md` (standalone patch domain model, parser, JSON load helpers, and set operations in `cli/src/services/patch.rs` for in-memory parsed unified-diff representation, capturing only touched lines plus minimal per-file/per-hunk metadata, supporting both `Index:` SVN-style and `diff --git` git-style formats, with `ParseError` for actionable malformed-input diagnostics, `PatchLoadError`/`load_patch_from_json`/`load_patch_from_json_bytes` for storage-agnostic JSON reconstruction, `intersect_patches` for target-shaped overlap with exact-match-first and historical `kind`+`content` fallback semantics plus matched-constructed-line `session_id` and matched-constructed-hunk `model_id` provenance inheritance, and `combine_patches` for ordered patch combination with later-wins conflict resolution plus winning-hunk `model_id` provenance inheritance; `parse_patch`, `intersect_patches`, and `combine_patches` are consumed by the active post-commit hook runtime) - `context/cli/styling-service.md` (CLI text-mode output styling with `owo-colors` and `comfy-table`, TTY/`NO_COLOR` policy, shared helper API for human-facing surfaces, and per-column right-to-left RGB gradient banner rendering) - `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, canonical Pkl-generated `sce/config.json` schema ownership plus CLI embedding/reuse contract, config-file selection order, `show` provenance output, and trimmed `validate` output contract) @@ -31,7 +31,7 @@ Feature/domain context: - `context/sce/agent-trace-pre-commit-staged-checkpoint.md` (historical pre-commit staged-checkpoint contract; current runtime baseline has replaced this path with a deterministic no-op) - `context/sce/agent-trace-commit-msg-coauthor-policy.md` (current commit-msg canonical co-author trailer policy with attribution-hooks + co-author gating and idempotent dedupe) - `context/sce/agent-trace-post-commit-dual-write.md` (historical post-commit no-op/dual-write reference; current post-commit behavior is documented in `agent-trace-hooks-command-routing.md`) -- `context/sce/agent-trace-hook-doctor.md` (approved operator-environment contract for broadening `sce doctor` into the canonical health-and-repair entrypoint, including stable problem taxonomy, `--fix` semantics, setup-to-doctor alignment rules, the current neutral local-DB baseline, and the approved downstream human text-mode layout/status/integration contract) +- `context/sce/agent-trace-hook-doctor.md` (approved operator-environment contract for broadening `sce doctor` into the canonical health-and-repair entrypoint, including stable problem taxonomy, `--fix` semantics, checkout-aware Agent Trace DB reporting, setup-to-doctor alignment rules, and the approved downstream human text-mode layout/status/integration contract) - `context/sce/doctor-human-text-contract.md` (implemented `sce doctor` human text layout contract: section order, `[PASS]`/`[FAIL]`/`[MISS]` status vocabulary, simplified hook rows, and OpenCode integration group rendering rules) - `context/sce/setup-githooks-install-contract.md` (T01 canonical `sce setup --hooks` install contract for target-path resolution, idempotent outcomes, remove-and-replace behavior, and doctor-readiness alignment) - `context/sce/setup-no-backup-policy-seam.md` (implemented unified remove-and-replace install policy for both config-install and required-hook install flows, with no backup creation and deterministic recovery guidance on swap failure) @@ -52,7 +52,6 @@ Feature/domain context: - `context/sce/agent-trace-minimal-generator.md` (implemented a library minimal Agent Trace generator seam at `cli/src/services/agent_trace.rs`, used by the active post-commit hook flow to produce strict `0.1.0` JSON payloads with top-level `version`, UUIDv7 `id` derived from commit-time metadata, caller-provided commit-time `timestamp`, optional top-level `vcs` metadata emitted when present (`type` from enum `git|jj|hg|svn`, `revision` from metadata input; current post-commit flow provides `git`), optional top-level `tool` metadata (`name`/`version`) sourced from builder metadata inputs when overlapping AI content exists, and always-emitted `metadata.sce.version` sourced from the compiled `sce` CLI package version, plus per-file trace data from patch inputs via `intersect_patches(constructed_patch, post_commit_patch)` then `post_commit_patch`-anchored hunk classification into `ai`/`mixed`/`unknown` contributor categories, serialized per conversation with a required lookup `url` derived from top-level `AgentTrace.id`, nested `contributor.type` with optional `contributor.model_id` omitted when provenance is missing, one derived `ranges[{start_line,end_line,content_hash}]` entry per post-commit or embedded-patch hunk, and range `content_hash` values that hash touched-line kind/content independent of positions and metadata) - `context/sce/agent-trace-hooks-command-routing.md` (implemented `sce hooks` command routing plus current runtime behavior: disabled-default commit-msg attribution, no-op `pre-commit`/`post-rewrite` entrypoints, active Agent Trace hook DB paths using no-migration readiness-gated AgentTraceDb access, active `post-commit` intersection entrypoint requiring validated `--remote-url`, threading that URL to the Agent Trace flow, printing it to stderr, capturing current commit patch, querying recent `diff_traces` from past 7 days, combining/intersecting patches via `patch::combine_patches` / `patch::intersect_patches`, persisting results to `post_commit_patch_intersections`, building/schema-validating post-commit Agent Trace payloads enriched with optional top-level `tool` metadata, `metadata.sce.version`, and range `content_hash`, and persisting validated payloads to AgentTraceDb `agent_traces` (DB-only), plus `diff-trace` STDIN intake with required non-empty `sessionID`/`diff`/`tool_name`, optional `model_id`, required nullable/non-empty `tool_version`, required `u64` `time` validation, dual persistence to AgentTraceDb, collision-safe `context/tmp/-000000-diff-trace.json` artifacts, `session-model` STDIN intake for normalized model attribution and raw Claude `SessionStart` events, and `conversation-trace` STDIN intake that classifies by `hook_event_name` — raw Claude `UserPromptSubmit` events (`transform_claude_user_prompt_submit`) and `Stop` events (`transform_claude_stop`) are transformed into normalized `message` + `message.part` items (user or assistant role, text part) and forwarded through the existing mixed-batch parser, patch `message.part` text is parsed to JSON-serialized `ParsedPatch` before persistence, unsupported raw Claude hook events fail deterministically with diagnostics listing supported events, and payloads without `hook_event_name` follow the existing `{ payloads: [{ type, ... }] }` mixed-batch validation/persistence path) - `context/sce/automated-profile-contract.md` (deterministic gate policy for automated OpenCode profile, including 10 gate categories, permission mappings, automated `/commit` single-commit execution behavior, and automated profile constraints) -<<<<<<< HEAD - `context/sce/bash-tool-policy-enforcement-contract.md` (approved bash-tool blocking contract plus current Rust evaluator seam and OpenCode/Claude delegation references, including config schema, argv-prefix matching, shell/nix unwrapping, fixed preset catalog/messages, and precedence rules) - `context/sce/generated-opencode-plugin-registration.md` (current generated OpenCode plugin-registration contract, canonical Pkl ownership, generated manifest/plugin paths including `sce-bash-policy` + `sce-agent-trace`, TypeScript source ownership, and Claude generated settings boundary including Agent Trace hooks plus `PreToolUse` Bash policy hook registration) - `context/sce/opencode-agent-trace-plugin-runtime.md` (current OpenCode agent-trace plugin runtime behavior, including captured `message.updated` handoff with `summary.diffs` branching: when diffs exist sends `-patch` variant `message.updated` + per-diff `message.part.updated` with `part_type: "patch"` concurrently via `Promise.all`, when no diffs sends original `message.updated` payload; in-memory dedup `Set` keyed by `"${sessionID}:${messageID}"`; captured `message.part.updated` handoff to `sce hooks conversation-trace` as `{ type: "message.part.updated", payloads: [{ session_id, message_id, part_type, text, generated_at_unix_ms }] }` with `text`/`reasoning` only; existing user-message diff extraction for `{ sessionID, diff, time, model_id }`; session-scoped OpenCode client version capture from `session.created`/`session.updated`; and CLI handoff to `sce hooks diff-trace` over STDIN JSON with required `tool_name="opencode"` plus required nullable `tool_version`; Rust hook parsing and AgentTraceDb insertion persist required payload fields including `model_id`) diff --git a/context/glossary.md b/context/glossary.md index 240281ad..84be8479 100644 --- a/context/glossary.md +++ b/context/glossary.md @@ -8,6 +8,8 @@ - verify-only root context pass: Context-sync mode for localized tasks where root-level behavior, architecture, and terminology are unchanged; root shared files are checked against code truth but are not edited by default. - generated-owned outputs: Files materialized by `config/pkl/generate.pkl` under `config/.opencode/**`, `config/automated/.opencode/**`, and `config/.claude/**`, including OpenCode plugin entrypoints, generated OpenCode `package.json` manifests, generated OpenCode `opencode.json` manifests, generated Claude plugin entrypoints, and Claude settings assets. - `canonical OpenCode plugin registration source`: Shared Pkl-authored plugin-registration definition in `config/pkl/base/opencode.pkl`, re-exported from `config/pkl/renderers/common.pkl` as the canonical plugin list/path JSON consumed by OpenCode renderers before they emit generated `opencode.json` manifests; the current entries are `sce-bash-policy` and `sce-agent-trace`. +- `checkout identity`: Stable UUIDv7 identifier assigned to a cloned repository or linked Git worktree, stored in `/sce/checkout-id` (never committed) and resolved via `git rev-parse --git-dir`. The identity is created or reused by `sce setup` through `AgentTraceDbLifecycle::setup()` and also auto-created by hook runtime when `sce setup` has not been run. Each checkout identity maps to a per-checkout Agent Trace DB file at `/sce/agent-trace-{checkout_id}.db` with lazy initialization. See `context/cli/checkout-identity.md`. +- `checkout registry`: Central JSON registry at `/sce/checkout-registry.json` managed by `cli/src/services/checkout/registry.rs` with atomic write-through-rename persistence. Records carry `checkout_id`, `path`, `last_seen`, optional `remote_url`, and optional `database_path` (populated after lazy per-checkout DB creation). `sce doctor dbs` lists all registered checkouts sorted by `last_seen` descending. See `context/cli/checkout-identity.md`. - `generated OpenCode plugin registration contract`: Current generated-config contract where `config/.opencode/opencode.json` and `config/automated/.opencode/opencode.json` serialize the OpenCode `plugin` field from canonical Pkl sources for SCE-managed plugins only; the current registered paths are `./plugins/sce-bash-policy.ts` and `./plugins/sce-agent-trace.ts`. Claude does not use an OpenCode-style plugin manifest; Claude bash-policy enforcement is registered through generated `.claude/settings.json` as a `PreToolUse` `Bash` command hook running `sce policy bash`. - `root Biome contract`: Repository-root formatting/linting contract owned by `biome.json`, currently scoped only to `npm/**` and the shared `config/lib/**` plugin package root with package-local `node_modules/**` excluded; the canonical execution path is the root Nix dev shell (`nix develop -c biome ...`). - `cli flake checks`: Check derivations in root `flake.nix` (`checks..cli-tests`, `cli-clippy`, `cli-fmt`), dedicated `integrations/install` runner checks (`integrations-install-tests`, `integrations-install-clippy`, `integrations-install-fmt`), plus `pkl-parity`, split `npm/` JS checks (`npm-bun-tests`, `npm-biome-check`, `npm-biome-format`), and split shared `config/lib/` JS checks (`config-lib-bun-tests`, `config-lib-biome-check`, `config-lib-biome-format`); invoked via `nix flake check` at repo root. @@ -135,7 +137,7 @@ - `agent trace commit-msg co-author policy`: Current contract in `cli/src/services/hooks/mod.rs` (`apply_commit_msg_coauthor_policy`) that applies exactly one canonical trailer (`Co-authored-by: SCE `) only when attribution hooks are enabled and SCE is not disabled; duplicate canonical trailers are deduped idempotently. - `local DB migration contract`: `cli/src/services/local_db/mod.rs` delegates migration execution to `TursoDb` through the `DbSpec::migrations()` contract. The current `LocalDbSpec` migration list is empty, so `LocalDb::new()` opens/creates the canonical local DB without creating local tables. - `hook no-op baseline`: Current `cli/src/services/hooks/mod.rs` runtime posture where `pre-commit` and `post-rewrite` return deterministic no-op status text, `commit-msg` is a gated mutating path behind the disabled-default attribution-hooks control, `post-commit` requires validated `--remote-url`, threads that value through the Agent Trace flow, prints it to stderr, captures current commit patch, queries recent `diff_traces` from past 7 days, combines/intersects patches, persists to `post_commit_patch_intersections`, and persists built Agent Trace payloads to `agent_traces` without post-commit file artifacts, `diff-trace` is an active intake path (validates required STDIN payload fields including `sessionID`/`diff`/`tool_name`, optional `model_id`, required nullable/non-empty `tool_version`, fills missing/nullable attribution from `session_models` when available while preserving direct payload precedence, writes collision-safe parsed-payload `context/tmp/-000000-diff-trace.json` artifacts, and inserts parsed payload fields into AgentTraceDb with nullable/resolved attribution), and `session-model` is an active intake path (validates required STDIN payload fields including `sessionID`/`model_id`/`tool_name`, best-effort fills missing Claude `tool_version` from `claude --version`, and upserts into `session_models` without raw artifacts). -- `sce doctor` operator-health contract: `cli/src/services/doctor/mod.rs` is the stable doctor entrypoint, with focused `doctor/{inspect,render,fixes,types}.rs` submodules implementing the current approved operator-health surface in `context/sce/agent-trace-hook-doctor.md`: `sce doctor --fix` selects repair intent, help/output expose deterministic doctor mode, JSON includes stable problem taxonomy/fixability fields plus database records and fix-result records, the runtime validates state-root resolution, global and repo-local `sce/config.json` readability/schema health, local DB and Agent Trace DB path/health, DB-parent readiness barriers, git availability, non-repo vs bare-repo targeting failures, effective hook-path source resolution, required hook presence/executable/content drift against canonical embedded hook assets, and repo-root installed OpenCode integration presence for `OpenCode plugins`, `OpenCode agents`, `OpenCode commands`, and `OpenCode skills`. Human text mode now uses the approved sectioned layout (`Environment`, `Configuration` (includes Agent Trace DB row), `Repository`, `Git Hooks`, `Integrations`), `SCE doctor diagnose` / `SCE doctor fix` headers, bracketed `[PASS]`/`[FAIL]`/`[MISS]` status tokens with shared-style green/red colorization when enabled, simplified `label (path)` row formatting, top-level-only hook rows, and presence-only integration parent/child rows where missing required files surface as `[MISS]` children and `[FAIL]` parent groups. Fix mode still reuses canonical setup hook installation for missing/stale/non-executable required hooks and missing hooks directories and can bootstrap canonical missing SCE-owned DB parent directories. +- `sce doctor` operator-health contract: `cli/src/services/doctor/mod.rs` is the stable doctor entrypoint, with focused `doctor/{inspect,render,fixes,types}.rs` submodules implementing the current approved operator-health surface in `context/sce/agent-trace-hook-doctor.md`: `sce doctor --fix` selects repair intent, `sce doctor dbs` lists registered checkouts, help/output expose deterministic doctor mode/action, JSON includes stable problem taxonomy/fixability fields plus checkout/database records and fix-result records, the runtime validates state-root resolution, global and repo-local `sce/config.json` readability/schema health, local DB and checkout/global Agent Trace DB path/health, DB-parent readiness barriers, git availability, non-repo vs bare-repo targeting failures, effective hook-path source resolution, required hook presence/executable/content drift against canonical embedded hook assets, and repo-root installed OpenCode integration presence. Human text mode uses the approved sectioned layout (`Environment`, `Configuration` with checkout identity + Agent Trace checkout DB rows when available, `Repository`, `Git Hooks`, `Integrations`), `SCE doctor diagnose` / `SCE doctor fix` headers, bracketed `[PASS]`/`[FAIL]`/`[MISS]` status tokens, simplified `label (path)` rows, and top-level-only hook rows. Fix mode reuses canonical setup hook installation and can bootstrap canonical missing SCE-owned DB parent directories. - `cli warnings-denied lint policy`: `cli/Cargo.toml` sets `warnings = "deny"`, so plain `cargo clippy --manifest-path cli/Cargo.toml` already fails on warnings without needing an extra `-- -D warnings` tail. - `agent trace local DB schema migration contract`: Retired `apply_core_schema_migrations` behavior removed from the current runtime during `agent-trace-removal-and-hook-noop-reset` T01; the local DB baseline is now file open/create only. - `agent trace removed local-hook paths`: Current-state shorthand for the removed local-hook runtime behaviors that are no longer active: staged-checkpoint persistence, post-commit dual-write, post-rewrite remap ingestion, rewrite trace transformation, and retry replay. diff --git a/context/overview.md b/context/overview.md index 2f7ae5dc..03ee80c9 100644 --- a/context/overview.md +++ b/context/overview.md @@ -26,9 +26,9 @@ The Rust CLI also centralizes SCE-owned web URI construction in `cli/src/service The same config resolver now also owns the attribution-hooks gate used by local hook runtime: `SCE_ATTRIBUTION_HOOKS_ENABLED` overrides `policies.attribution_hooks.enabled`, and the gate defaults to disabled. The config service split now includes `cli/src/services/config/resolver.rs` as the focused owner for config-file discovery, file-layer merging, env/flag/default precedence, auth-key resolution, observability resolution, attribution-hooks resolution, and default-discovered invalid-file degradation; `cli/src/services/config/mod.rs` remains the facade/rendering orchestration surface while preserving existing `services::config` imports. Generated config now includes repo-local plugin assets for both profiles: `sce-bash-policy.ts` plus `sce-agent-trace.ts` are emitted under `config/.opencode/plugins/` and `config/automated/.opencode/plugins/`; the OpenCode agent-trace plugin extracts `{ sessionID, diff, time, model_id }` from user `message.updated` events with diffs, tracks per-session OpenCode client version from `session.created`/`session.updated`, and sends payloads to `sce hooks diff-trace` with `tool_name="opencode"` plus optional `tool_version`. Claude generated config now routes agent-trace events through `.claude/settings.json` command hooks that call `sce hooks` directly: `SessionStart` pipes raw hook event JSON to `sce hooks session-model`, and matched `PostToolUse Write|Edit|MultiEdit|NotebookEdit` pipes raw hook event JSON to `sce hooks diff-trace`; the Rust `session-model` path uses explicit payload version fields when present and otherwise best-effort captures `tool_version` from trimmed `claude --version` stdout when available. Rust handles extraction, validation, and persistence without a TypeScript intermediary; the former `config/.claude/plugins/sce-agent-trace.ts` Bun runtime was removed in T07 of the `claude-rust-diff-trace` plan. The Rust hook validates required fields, resolves missing/nullable diff-trace attribution from `session_models` while preserving direct payload precedence, and persists `model_id`, `tool_name`, and nullable/resolved `tool_version` into `diff_traces` through AgentTraceDb. Bash-policy now delegates OpenCode enforcement to the Rust `sce policy bash` command: the generated OpenCode plugin at `config/.opencode/plugins/sce-bash-policy.ts` (and `config/automated/.opencode/plugins/sce-bash-policy.ts`) is a thin wrapper that calls `sce policy bash --input normalized --output json` via `spawnSync` and throws on deny decisions; it no longer contains independent TypeScript policy logic. The former `bash-policy/runtime.ts` TypeScript runtime has been removed. Preset... -The `doctor` command now exposes explicit inspection mode (`sce doctor`) and repair-intent mode (`sce doctor --fix`) at the CLI/help/schema level while keeping diagnosis mode read-only. It now validates both current global operator health and the current repo/hook-integrity slice: state-root resolution, global config path resolution, global and repo-local `sce/config.json` readability/schema validity, local DB and Agent Trace DB path + health, DB parent-directory readiness, git availability, non-repo vs bare-repo targeting failures, effective git hook-path source (default, per-repo `core.hooksPath`, or global `core.hooksPath`), hooks-directory health, required hook presence/executable permissions/content drift against canonical embedded SCE-managed hook assets, and repo-root OpenCode integration presence across the installed `plugins`, `agents`, `commands`, and `skills` inventories with embedded SHA-256 content verification for OpenCode assets. Text mode now renders the approved human-only layout with ordered `Environment` / `Configuration` / `Repository` / `Git Hooks` / `Integrations` sections, `SCE doctor diagnose` / `SCE doctor fix` headers, bracketed `[PASS]`/`[FAIL]`/`[MISS]` status tokens, shared-style green pass plus red fail/miss coloring when color output is enabled, simplified `label (path)` row formatting, top-level-only hook rows, and integration parent/child rows that reflect missing vs content-mismatch states; JSON output now reports Agent Trace DB health under `agent_trace_db` (as a row within the Configuration section in text mode). Repo-scoped database reporting is empty by default because no repo-owned SCE database currently exists. Fix mode reuses the canonical setup hook install flow to repair missing/stale/non-executable required hooks and can also bootstrap missing canonical DB parent directories while preserving manual-only guidance for unsupported issues. +The `doctor` command now exposes explicit inspection mode (`sce doctor`), repair-intent mode (`sce doctor --fix`), and checkout registry listing (`sce doctor dbs`) at the CLI/help/schema level while keeping diagnosis mode read-only. It now validates both current global operator health and the current repo/hook-integrity slice: state-root resolution, global config path resolution, global and repo-local `sce/config.json` readability/schema validity, local DB and checkout/global Agent Trace DB path + health, DB parent-directory readiness, git availability, non-repo vs bare-repo targeting failures, effective git hook-path source (default, per-repo `core.hooksPath`, or global `core.hooksPath`), hooks-directory health, required hook presence/executable permissions/content drift against canonical embedded SCE-managed hook assets, and repo-root OpenCode integration presence across the installed `plugins`, `agents`, `commands`, and `skills` inventories with embedded SHA-256 content verification for OpenCode assets. Text mode now renders the approved human-only layout with ordered `Environment` / `Configuration` / `Repository` / `Git Hooks` / `Integrations` sections, `SCE doctor diagnose` / `SCE doctor fix` headers, bracketed `[PASS]`/`[FAIL]`/`[MISS]` status tokens, shared-style green pass plus red fail/miss coloring when color output is enabled, simplified `label (path)` row formatting, top-level-only hook rows, and integration parent/child rows that reflect missing vs content-mismatch states; JSON output reports `checkout_identity` plus resolved Agent Trace DB health under `agent_trace_db` when a checkout ID exists, with global Agent Trace DB reporting retained outside checkout context. Fix mode reuses the canonical setup hook install flow to repair missing/stale/non-executable required hooks and can also bootstrap missing canonical DB parent directories while preserving manual-only guidance for unsupported issues. Claude bash-policy enforcement is also generated through `.claude/settings.json` as a `PreToolUse` `Bash` command hook running `sce policy bash`, so Claude and OpenCode both delegate to the Rust policy evaluator without a Claude TypeScript runtime. -Local database bootstrap is now owned by `LocalDbLifecycle::setup` and setup-command lifecycle aggregation. Agent Trace setup creates/reuses the current checkout ID and registers the checkout without eagerly creating an Agent Trace DB; hook runtime lazily creates or upgrades the per-checkout `agent-trace-{checkout_id}.db` on first Agent Trace write/read. Doctor validates DB paths/health and can bootstrap missing parent directories. Wiring a user-invocable `sce sync` command is deferred to `0.4.0`. +Local database bootstrap is now owned by `LocalDbLifecycle::setup` and setup-command lifecycle aggregation. Agent Trace setup creates/reuses the current checkout ID and registers the checkout without eagerly creating an Agent Trace DB; hook runtime lazily creates or upgrades the per-checkout `agent-trace-{checkout_id}.db` on first Agent Trace write/read. Doctor validates checkout/global DB paths/health, can bootstrap missing parent directories, and lists registered checkouts through `sce doctor dbs`. Wiring a user-invocable `sce sync` command is deferred to `0.4.0`. The repository-root flake (`flake.nix`) now applies a Rust overlay-backed stable toolchain pinned to `1.93.1` (with `rustfmt` and `clippy`), reads package/check version from the repo-root `.version` file, builds `packages.sce` through a Crane `buildDepsOnly` + `buildPackage` pipeline with filtered package sources for the Cargo tree plus required embedded config/assets, and runs `cli-tests`, `cli-clippy`, and `cli-fmt` through Crane-backed check derivations (`cargoTest`, `cargoClippy`, `cargoFmt`) that reuse the same filtered source/toolchain setup. The root flake also exposes release install/run outputs directly as `packages.sce` (with `packages.default = packages.sce`) plus `apps.sce` and `apps.default`, so `nix build .#default`, `nix run . -- --help`, `nix run .#sce -- --help`, and `nix profile install github:crocoder-dev/shared-context-engineering` all target the packaged `sce` binary through the same flake-owned entrypoints. The CLI Cargo package metadata now includes crates.io publication-ready fields with crate-local install guidance in `cli/README.md`; supported Cargo install paths are `cargo install shared-context-engineering --locked`, `cargo install --git https://github.com/crocoder-dev/shared-context-engineering shared-context-engineering --locked`, and local `cargo install --path cli --locked`. The published crate installs the `sce` binary. The crate also keeps `cargo clippy --manifest-path cli/Cargo.toml` warnings-denied through `cli/Cargo.toml` lint configuration, so an extra `-- -D warnings` flag is redundant. diff --git a/context/plans/agent-trace-checkout-identity.md b/context/plans/agent-trace-checkout-identity.md index 73909a82..534e3cc5 100644 --- a/context/plans/agent-trace-checkout-identity.md +++ b/context/plans/agent-trace-checkout-identity.md @@ -87,7 +87,7 @@ Give each cloned repository (and linked Git worktree) its own `agent-trace` chec - Evidence: `nix develop -c sh -c 'cd cli && cargo fmt'` passed; `nix flake check` passed; `nix run .#pkl-check-generated` passed. Manual smoke checks with isolated `XDG_STATE_HOME` verified setup creates a checkout ID without creating `agent-trace-{checkout_id}.db`, explicit `sce hooks post-commit --remote-url ...` lazily creates the per-checkout DB and updates registry `database_path`, and a no-setup repository hook invocation creates checkout ID + per-checkout DB in one pass. - Notes: Agent Trace hook DB consumers now resolve through checkout identity and per-checkout lazy DB initialization. `AgentTraceDbLifecycle::setup()` is identity-only; lifecycle diagnose/fix use the per-checkout DB path when an ID exists and otherwise retain the global fallback outside checkout context. Doctor checkout display and registry listing remain deferred to T04. -- [ ] T04: `Surface checkout identity in doctor and add 'sce doctor dbs'` (status:todo) +- [x] T04: `Surface checkout identity in doctor and add 'sce doctor dbs'` (status:done) - Task ID: T04 - Goal: Extend `sce doctor` to report the current checkout identity, and add a new `sce doctor dbs` subcommand that lists all registered checkouts from the central registry. - Boundaries (in/out of scope): In — @@ -98,14 +98,62 @@ Give each cloned repository (and linked Git worktree) its own `agent-trace` chec Out — modifying checkout rows, purging/cleanup commands, changing the existing Environment/Repository/Git Hooks/Integrations sections. - Done when: `sce doctor` in a repo with a checkout ID shows the checkout ID and DB status; `sce doctor dbs` lists all registered checkouts; `sce doctor dbs --format json` outputs stable machine-readable fields; `nix flake check` passes. - Verification notes (commands or checks): Run `sce doctor` inside a setup repo, verify checkout ID appears in output. Run `sce doctor dbs` and `sce doctor dbs --format json`, verify output shape. Run `nix flake check` for full validation. + - Completed: 2026-06-16 + - Files changed: `cli/src/cli_schema.rs`, `cli/src/services/parse/command_runtime.rs`, `cli/src/services/command_registry.rs`, `cli/src/services/doctor/mod.rs`, `cli/src/services/doctor/types.rs`, `cli/src/services/doctor/inspect.rs`, `cli/src/services/doctor/render.rs` + - Evidence: `nix develop -c sh -c 'cd cli && cargo fmt'` passed; `nix flake check` passed; `nix run .#pkl-check-generated` passed; isolated `XDG_STATE_HOME` smoke check of `nix run .#sce -- doctor dbs --format json` returned an empty `checkouts` array with stable `status`/`command`/`subcommand` fields. + - Notes: `sce doctor` now surfaces checkout identity and per-checkout Agent Trace DB status when a checkout ID exists, while falling back to the legacy/global Agent Trace DB row outside checkout context. `sce doctor dbs` reads the central registry, sorts by `last_seen` descending, and renders text or JSON records with checkout ID, checkout path, database path, last-seen timestamp, and remote URL. -- [ ] T05: `Validation and context sync` (status:todo) +- [x] T05: `Validation and context sync` (status:done) - Task ID: T05 - Goal: Run full repository validation, verify all success criteria, and update durable context files to reflect the new checkout identity and per-checkout database architecture. - Boundaries (in/out of scope): In — `nix run .#pkl-check-generated`, `nix flake check`, context sync for `context/sce/agent-trace-db.md`, `context/sce/agent-trace-hook-doctor.md`, `context/cli/default-path-catalog.md`, `context/cli/cli-command-surface.md`, `context/overview.md`, `context/architecture.md`, `context/glossary.md`, `context/context-map.md`. Add a new `context/cli/checkout-identity.md` context file describing the checkout identity service. Remove any temporary/debug artifacts. Out — new behavior beyond what was implemented in T01-T04. - Done when: All verifications pass; context files accurately describe the current checkout identity architecture including the new `checkout/` service module, `.git/sce/checkout-id` storage, `checkout-registry.json` format, per-checkout DB naming convention (`agent-trace-{checkout_id}.db`), lazy initialization flow, `sce doctor` checkout identity reporting, and `sce doctor dbs` registry listing. - Verification notes (commands or checks): `nix run .#pkl-check-generated`; `nix flake check`; review context files for stale references to a shared global database path or outdated architecture descriptions. + - Completed: 2026-06-16 + - Files changed: `context/architecture.md` + - Evidence: `nix run .#pkl-check-generated` passed ("Generated outputs are up to date."); `nix flake check` passed ("all checks passed!"); `context/architecture.md` stale phrase "doctor checkout display and registry listing remain deferred" updated to reflect T04 implementation. All other context files (`agent-trace-db.md`, `agent-trace-hook-doctor.md`, `default-path-catalog.md`, `cli-command-surface.md`, `overview.md`, `glossary.md`, `context-map.md`, `checkout-identity.md`) were already up to date from T01-T04 context sync. No temporary/debug artifacts found. + - Notes: This is the final task of the `agent-trace-checkout-identity` plan. All success criteria verified: per-checkout DB isolation, lazy initialization, `sce doctor` checkout identity reporting, `sce doctor dbs` registry listing, hook self-sufficiency for checkouts without prior `sce setup`. ## Open questions - None blocking. The lazy initialization model means `git worktree add` followed by a hook invocation just works — the hook creates the checkout ID and DB on first write without requiring `sce setup` in the worktree. `sce setup` remains the canonical way to pre-establish the checkout identity, but hooks are self-sufficient when it hasn't been run. + +--- + +## Validation Report + +### Commands run +| Command | Exit | Output | +|---|---|---| +| `nix run .#pkl-check-generated` | 0 | Generated outputs are up to date. | +| `nix flake check` | 0 | all checks passed! (cli-tests, cli-clippy, cli-fmt, pkl-parity, integrations-install-*, npm-*, config-lib-*) | + +### Success-criteria verification + +| # | Criterion | Evidence | +|---|---|---| +| 1 | `sce setup` creates stable UUIDv7 checkout ID, registers in registry, no eager DB | T02 manual test: `sce setup --hooks --repo ` ran twice in isolated env; both runs produced same checkout ID in `.git/sce/checkout-id`, registry matched, `database_path` was `null` | +| 2 | Second `sce setup` reuses existing ID (idempotent) | T02 evidence: second run preserved same checkout ID | +| 3 | Linked Git worktree gets distinct checkout ID | Inherent from `git rev-parse --git-dir` returning different paths per worktree; `resolve_checkout_id_for_repo()` creates IDs per git-dir | +| 4 | Per-checkout DB created lazily on first write | T03 manual test: `sce hooks post-commit --remote-url ...` lazily created `agent-trace-{checkout_id}.db` and updated registry `database_path` | +| 5 | First hook invocation auto-creates ID + DB when no prior `sce setup` | T03 evidence: no-setup repository hook invocation created checkout ID + per-checkout DB in one pass | +| 6 | Per-checkout DB isolation via `agent-trace-{checkout_id}.db` paths | Code: `agent_trace_db_path_for_checkout(checkout_id)` in `default_paths.rs`; all hook runtime paths use checkout-scoped lazy resolution | +| 7 | No `checkout_id` column needed in any table | Verified: no `checkout_id` column appears in any of the 16 migrations; isolation is file-system-level | +| 8 | `sce doctor` shows checkout identity + per-checkout DB status | T04: doctor renders checkout identity and DB health in Configuration section with `[PASS]`/`[FAIL]`/`[MISS]` tokens when checkout ID exists; global fallback outside checkout context | +| 9 | `sce doctor dbs` lists registered checkouts | T04: `sce doctor dbs --format json` returns `checkouts` array with `checkout_id`, `path`, `database_path`, `last_seen`, `remote_url` sorted by `last_seen` descending; empty registry returns stable `no registered checkouts` | +| 10 | All hook flows continue working with lazy DB resolution | Code: `post-commit`, `diff-trace`, `session-model`, `conversation-trace` all call `resolve_or_create_agent_trace_db_for_current_checkout()` or equivalent | +| 11 | Global lifecycle operations still work | `AgentTraceDbLifecycle` retains global fallback when no checkout context exists; `sce doctor` / `sce setup` on fresh machine work | +| 12 | `nix flake check` and `nix run .#pkl-check-generated` pass | Both passed during T05 validation | + +### Temporary scaffolding +- No debug code, temporary files, or intermediate artifacts found in checkout module or elsewhere + +### Context alignment +- `context/cli/checkout-identity.md` — comprehensive (47 lines), created in T02, covers all checkout identity surface +- `context/sce/agent-trace-db.md` — describes per-checkout paths, lazy init, `open_at()`, `open_for_hooks_without_migrations_at()` +- `context/architecture.md` — fixed stale "deferred" phrase; now describes current doctor checkout display and `dbs` subcommand +- `context/glossary.md` — added `checkout identity` and `checkout registry` entries +- All other root context files verified current + +### Residual risks +- None identified. All success criteria have direct evidence from T01-T04 implementation and T05 verification. diff --git a/context/sce/agent-trace-db.md b/context/sce/agent-trace-db.md index 19e1e824..596cafde 100644 --- a/context/sce/agent-trace-db.md +++ b/context/sce/agent-trace-db.md @@ -181,7 +181,8 @@ Both triggers compare `OLD.*` vs `NEW.*` for all mutable columns (excluding `upd - `diagnose()` reports per-checkout Agent Trace DB path and parent-directory readiness when a repo root has a checkout ID; otherwise it falls back to the legacy global Agent Trace DB path. - `fix()` bootstraps the resolved per-checkout DB parent directory for auto-fixable parent-readiness problems, with the same global fallback outside checkout context. - `setup()` creates/reuses the current checkout identity when a repo root is available, registers the checkout in `/sce/checkout-registry.json` with `database_path: null`, and emits setup messaging with the checkout ID. It does not eagerly initialize the Agent Trace DB; the first hook write lazily creates the per-checkout DB. -- `sce doctor` now surfaces Agent Trace DB health as a row within the `Configuration` section with `[PASS]`/`[FAIL]`/`[MISS]` status tokens (e.g., `Agent Trace DB (/path/to/agent-trace.db)`), and includes it in JSON output under the `agent_trace_db` field. +- `sce doctor` surfaces checkout identity and per-checkout Agent Trace DB health in the `Configuration` section when a checkout ID exists, with `[PASS]`/`[FAIL]`/`[MISS]` status tokens. Outside checkout context it falls back to the legacy/global Agent Trace DB row. JSON output includes `checkout_identity` when available plus the resolved `agent_trace_db` field. +- `sce doctor dbs` lists registered checkout records from `/sce/checkout-registry.json` in text or JSON, sorted by `last_seen` descending. ## Runtime writers diff --git a/context/sce/agent-trace-hook-doctor.md b/context/sce/agent-trace-hook-doctor.md index 4f15e0c3..532d7895 100644 --- a/context/sce/agent-trace-hook-doctor.md +++ b/context/sce/agent-trace-hook-doctor.md @@ -34,7 +34,7 @@ The runtime in `cli/src/services/doctor/mod.rs` exposes the approved doctor comm - default global/local config-file location reporting, plus validation of existing global and repo-local `sce/config.json` readability and schema compliance (delegated to `ConfigLifecycle::diagnose`) - startup config resolution no longer blocks doctor on invalid default-discovered config files; doctor reaches its own config-validation path, reports those files as problems, and keeps invalid-config remediation manual-only - local DB location reporting, DB parent-directory readiness checks, and existing-DB health validation (delegated to `LocalDbLifecycle::diagnose`) -- local DB and Agent Trace DB reporting in default doctor output +- local DB reporting plus checkout-aware Agent Trace DB reporting in default doctor output - explicit git-unavailable, outside-repo, and bare-repo repository-targeting failures - effective hook-path source (`default`, local `core.hooksPath`, global `core.hooksPath`) - repository root and hooks directory resolution when a repository target is detected @@ -123,10 +123,10 @@ The broadened contract for `sce doctor` must cover the following problem invento - expected global config path cannot be resolved - global config file exists but is unreadable, invalid JSON, or fails schema validation - invalid default-discovered config must not prevent `sce doctor` from starting; doctor still reports invalid global or repo-local config as a problem once command dispatch begins -- local DB or Agent Trace DB path cannot be resolved -- local DB and Agent Trace DB parent directories are missing or not writable -- local DB and Agent Trace DB bootstrap or health is broken -- Agent Trace DB path and health are reported in `Configuration` section output +- local DB or checkout/global Agent Trace DB path cannot be resolved +- local DB and checkout/global Agent Trace DB parent directories are missing or not writable +- local DB and checkout/global Agent Trace DB bootstrap or health is broken +- Agent Trace checkout ID plus per-checkout DB path/health are reported in `Configuration` section output when a checkout ID exists, with global Agent Trace DB reporting retained as the no-checkout fallback ### Repository targeting and git readiness @@ -210,7 +210,7 @@ Services implementing `ServiceLifecycle`: - `HooksLifecycle` in `cli/src/services/hooks/lifecycle.rs`: checks hook rollout integrity, required-hook presence/executability/content - `LocalDbLifecycle` in `cli/src/services/local_db/lifecycle.rs`: validates DB path/health, bootstraps DB parent directory - `AuthDbLifecycle` in `cli/src/services/auth_db/lifecycle.rs`: validates encrypted auth DB path/health, bootstraps DB parent directory -- `AgentTraceDbLifecycle` in `cli/src/services/agent_trace_db/lifecycle.rs`: validates Agent Trace DB path/health, bootstraps DB parent directory +- `AgentTraceDbLifecycle` in `cli/src/services/agent_trace_db/lifecycle.rs`: validates checkout-scoped Agent Trace DB path/health when a checkout ID exists, otherwise validates the global fallback path, and bootstraps the resolved DB parent directory The `doctor` command aggregates `diagnose` and `fix` across all registered providers. The `setup` command aggregates `setup` across all registered providers in order (config → local_db → auth_db → agent_trace_db → hooks). From 5bbe6ed70cde446d62fe36c13744e8be46b5c04d Mon Sep 17 00:00:00 2001 From: David Abram Date: Tue, 16 Jun 2026 17:11:06 +0200 Subject: [PATCH 6/8] cli: Preserve error chains, add DB deep health checks, and fix registry resilience Replace `error.to_string()` with `format!("{error:#}")` in hooks, auth, config, doctor, and version command dispatchers so the full anyhow cause chain is visible in diagnostics instead of only the outermost error. In the checkout DB resolver, capture the fast-path failure reason and include it in the fallback error context so both failure causes are visible when the Agent Trace DB lazy-init fails. Add AgentTraceDbConnectionFailed and AgentTraceDbSchemaNotReady health problem kinds with full enum/mapping/render plumbing. When the per-checkout Agent Trace DB file exists, `sce doctor` now opens it non-mutatingly and checks schema readiness, surfacing failures that were previously invisible (doctor only checked file existence). Co-authored-by: SCE --- cli/src/services/agent_trace_db/lifecycle.rs | 42 ++++++- cli/src/services/auth_command/command.rs | 2 +- cli/src/services/checkout/mod.rs | 4 +- cli/src/services/checkout/registry.rs | 32 +++++- cli/src/services/config/command.rs | 2 +- cli/src/services/doctor/command.rs | 2 +- cli/src/services/doctor/mod.rs | 8 ++ cli/src/services/doctor/render.rs | 2 + cli/src/services/doctor/types.rs | 2 + cli/src/services/hooks/command.rs | 2 +- cli/src/services/lifecycle.rs | 2 + cli/src/services/version/command.rs | 2 +- context/cli/checkout-identity.md | 9 ++ context/plans/checkout-registry-resilience.md | 96 ++++++++++++++++ context/plans/hook-db-error-diagnostics.md | 91 +++++++++++++++ .../plans/hook-error-chain-preservation.md | 106 ++++++++++++++++++ context/sce/agent-trace-db.md | 4 +- context/sce/agent-trace-hook-doctor.md | 1 + flake.nix | 10 ++ 19 files changed, 408 insertions(+), 11 deletions(-) create mode 100644 context/plans/checkout-registry-resilience.md create mode 100644 context/plans/hook-db-error-diagnostics.md create mode 100644 context/plans/hook-error-chain-preservation.md diff --git a/cli/src/services/agent_trace_db/lifecycle.rs b/cli/src/services/agent_trace_db/lifecycle.rs index 61dcdd92..9f5f825e 100644 --- a/cli/src/services/agent_trace_db/lifecycle.rs +++ b/cli/src/services/agent_trace_db/lifecycle.rs @@ -11,7 +11,7 @@ use crate::services::lifecycle::{ HealthProblemKind, HealthSeverity, LifecycleProviderId, ServiceLifecycle, SetupOutcome, }; -use super::AgentTraceDbSpec; +use super::{AgentTraceDb, AgentTraceDbSpec}; #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] pub struct AgentTraceDbLifecycle; @@ -132,6 +132,46 @@ pub fn diagnose_agent_trace_db_health(repo_root: Option<&Path>) -> Vec { + if let Err(error) = db.ensure_schema_ready_for_hooks() { + problems.push(HealthProblem { + kind: HealthProblemKind::AgentTraceDbSchemaNotReady, + category: HealthCategory::GlobalState, + severity: HealthSeverity::Error, + fixability: HealthFixability::ManualOnly, + summary: format!( + "Agent Trace database schema at '{}' is not ready: {error}", + db_path.display() + ), + remediation: String::from( + "Re-run 'sce setup' to apply missing migrations, or inspect the database file for corruption.", + ), + next_action: "manual_steps", + }); + } + } + Err(error) => { + problems.push(HealthProblem { + kind: HealthProblemKind::AgentTraceDbConnectionFailed, + category: HealthCategory::GlobalState, + severity: HealthSeverity::Error, + fixability: HealthFixability::ManualOnly, + summary: format!( + "Unable to open checkout Agent Trace database at '{}': {error}", + db_path.display() + ), + remediation: String::from( + "Verify file permissions and ensure the file is a valid SQLite database. Re-run 'sce setup' to recreate it if needed.", + ), + next_action: "manual_steps", + }); + } + } + } + problems } diff --git a/cli/src/services/auth_command/command.rs b/cli/src/services/auth_command/command.rs index 9ae0f3cd..356c379b 100644 --- a/cli/src/services/auth_command/command.rs +++ b/cli/src/services/auth_command/command.rs @@ -8,6 +8,6 @@ pub struct AuthCommand { impl AuthCommand { pub fn execute(&self, _context: &C) -> Result { auth_command::run_auth_subcommand(self.request) - .map_err(|error| ClassifiedError::runtime(error.to_string())) + .map_err(|error| ClassifiedError::runtime(format!("{error:#}"))) } } diff --git a/cli/src/services/checkout/mod.rs b/cli/src/services/checkout/mod.rs index 384ea1d9..ef5bb5b2 100644 --- a/cli/src/services/checkout/mod.rs +++ b/cli/src/services/checkout/mod.rs @@ -193,9 +193,9 @@ pub fn resolve_or_create_agent_trace_db_for_checkout( .and_then(|db| db.ensure_schema_ready_for_hooks().map(|()| db)); let db = match fast_open { Ok(db) => db, - Err(_) => AgentTraceDb::open_at(&db_path).with_context(|| { + Err(fast_error) => AgentTraceDb::open_at(&db_path).with_context(|| { format!( - "failed to initialize Agent Trace DB for checkout {checkout_id} at '{}'", + "failed to initialize Agent Trace DB for checkout {checkout_id} at '{}' (fast-path attempt: {fast_error})", db_path.display() ) })?, diff --git a/cli/src/services/checkout/registry.rs b/cli/src/services/checkout/registry.rs index e410d115..f8267c8b 100644 --- a/cli/src/services/checkout/registry.rs +++ b/cli/src/services/checkout/registry.rs @@ -64,6 +64,16 @@ pub fn read_registry() -> Result { let content = std::fs::read_to_string(&path) .with_context(|| format!("Failed to read checkout registry from '{}'", path.display()))?; + // Empty or whitespace-only files are treated as corrupt — delete and start fresh. + if content.trim().is_empty() { + let _ = std::fs::remove_file(&path); + eprintln!( + "[WARN] Empty checkout registry at '{}' — removing and recreating from scratch", + path.display() + ); + return Ok(CheckoutRegistry::default()); + } + let registry: CheckoutRegistry = serde_json::from_str(&content).with_context(|| { format!( "Failed to parse checkout registry from '{}'", @@ -94,7 +104,9 @@ pub fn write_registry(registry: &CheckoutRegistry) -> Result<()> { .with_context(|| "Failed to serialize checkout registry")?; // Write to a temporary file first, then rename for atomicity. - let temp_path = path.with_extension("json.tmp"); + // Use a PID-unique suffix so concurrent `sce hooks` processes never + // collide on the same temp file. + let temp_path = path.with_extension(format!("json.tmp.{}", std::process::id())); std::fs::write(&temp_path, &content).with_context(|| { format!( @@ -111,6 +123,24 @@ pub fn write_registry(registry: &CheckoutRegistry) -> Result<()> { ) })?; + // Clean up any stale `.tmp.*` files left behind by previous crashed + // processes. The current process's temp file was just renamed, so + // anything remaining matching the pattern is orphaned. + if let Some(parent) = path.parent() { + if let Ok(entries) = std::fs::read_dir(parent) { + let stale_prefix = format!("{CHECKOUT_REGISTRY_FILE}.tmp."); + for entry in entries { + let Ok(entry) = entry else { + continue; + }; + let name = entry.file_name(); + if name.to_string_lossy().starts_with(&stale_prefix) { + let _ = std::fs::remove_file(entry.path()); + } + } + } + } + Ok(()) } diff --git a/cli/src/services/config/command.rs b/cli/src/services/config/command.rs index 53827ddf..66ed0db6 100644 --- a/cli/src/services/config/command.rs +++ b/cli/src/services/config/command.rs @@ -8,6 +8,6 @@ pub struct ConfigCommand { impl ConfigCommand { pub fn execute(&self, _context: &C) -> Result { config::run_config_subcommand(self.subcommand.clone()) - .map_err(|error| ClassifiedError::runtime(error.to_string())) + .map_err(|error| ClassifiedError::runtime(format!("{error:#}"))) } } diff --git a/cli/src/services/doctor/command.rs b/cli/src/services/doctor/command.rs index 918fa3c0..a2bf6f47 100644 --- a/cli/src/services/doctor/command.rs +++ b/cli/src/services/doctor/command.rs @@ -9,6 +9,6 @@ pub struct DoctorCommand { impl DoctorCommand { pub fn execute(&self, context: &C) -> Result { doctor::run_doctor_with_context(self.request, context) - .map_err(|error| ClassifiedError::runtime(error.to_string())) + .map_err(|error| ClassifiedError::runtime(format!("{error:#}"))) } } diff --git a/cli/src/services/doctor/mod.rs b/cli/src/services/doctor/mod.rs index 8b6e35bc..7ab5774a 100644 --- a/cli/src/services/doctor/mod.rs +++ b/cli/src/services/doctor/mod.rs @@ -381,6 +381,10 @@ fn doctor_problem_kind(kind: HealthProblemKind) -> ProblemKind { } HealthProblemKind::HookReadFailed => ProblemKind::HookReadFailed, HealthProblemKind::OpenCodeAssetReadFailed => ProblemKind::OpenCodeAssetReadFailed, + HealthProblemKind::AgentTraceDbConnectionFailed => { + ProblemKind::AgentTraceDbConnectionFailed + } + HealthProblemKind::AgentTraceDbSchemaNotReady => ProblemKind::AgentTraceDbSchemaNotReady, } } @@ -419,6 +423,10 @@ fn health_problem_kind(kind: ProblemKind) -> HealthProblemKind { } ProblemKind::HookReadFailed => HealthProblemKind::HookReadFailed, ProblemKind::OpenCodeAssetReadFailed => HealthProblemKind::OpenCodeAssetReadFailed, + ProblemKind::AgentTraceDbConnectionFailed => { + HealthProblemKind::AgentTraceDbConnectionFailed + } + ProblemKind::AgentTraceDbSchemaNotReady => HealthProblemKind::AgentTraceDbSchemaNotReady, } } diff --git a/cli/src/services/doctor/render.rs b/cli/src/services/doctor/render.rs index 533f12a5..a81c5cd3 100644 --- a/cli/src/services/doctor/render.rs +++ b/cli/src/services/doctor/render.rs @@ -284,6 +284,8 @@ fn agent_trace_db_status(report: &HookDoctorReport) -> HumanTextStatus { } if report.problems.iter().any(|p| { p.kind == ProblemKind::UnableToResolveStateRoot && p.summary.contains("agent trace") + || p.kind == ProblemKind::AgentTraceDbConnectionFailed + || p.kind == ProblemKind::AgentTraceDbSchemaNotReady }) { return HumanTextStatus::Fail; } diff --git a/cli/src/services/doctor/types.rs b/cli/src/services/doctor/types.rs index 2b0aecf6..6dc7cd42 100644 --- a/cli/src/services/doctor/types.rs +++ b/cli/src/services/doctor/types.rs @@ -134,6 +134,8 @@ pub(crate) enum ProblemKind { OpenCodeAssetMissingOrInvalid, HookReadFailed, OpenCodeAssetReadFailed, + AgentTraceDbConnectionFailed, + AgentTraceDbSchemaNotReady, } #[derive(Clone, Copy, Debug, Eq, PartialEq)] diff --git a/cli/src/services/hooks/command.rs b/cli/src/services/hooks/command.rs index 23deab93..4d466c2a 100644 --- a/cli/src/services/hooks/command.rs +++ b/cli/src/services/hooks/command.rs @@ -9,6 +9,6 @@ pub struct HooksCommand { impl HooksCommand { pub fn execute(&self, context: &C) -> Result { hooks::run_hooks_subcommand(&self.subcommand, Some(context.logger())) - .map_err(|error| ClassifiedError::runtime(error.to_string())) + .map_err(|error| ClassifiedError::runtime(format!("{error:#}"))) } } diff --git a/cli/src/services/lifecycle.rs b/cli/src/services/lifecycle.rs index 8eb31e0b..da6afffd 100644 --- a/cli/src/services/lifecycle.rs +++ b/cli/src/services/lifecycle.rs @@ -55,6 +55,8 @@ pub enum HealthProblemKind { OpenCodeAssetMissingOrInvalid, HookReadFailed, OpenCodeAssetReadFailed, + AgentTraceDbConnectionFailed, + AgentTraceDbSchemaNotReady, } #[derive(Clone, Debug, Eq, PartialEq)] diff --git a/cli/src/services/version/command.rs b/cli/src/services/version/command.rs index 56ecfc58..4a073a5a 100644 --- a/cli/src/services/version/command.rs +++ b/cli/src/services/version/command.rs @@ -8,6 +8,6 @@ pub struct VersionCommand { impl VersionCommand { pub fn execute(&self, _context: &C) -> Result { version::render_version(self.request) - .map_err(|error| ClassifiedError::runtime(error.to_string())) + .map_err(|error| ClassifiedError::runtime(format!("{error:#}"))) } } diff --git a/context/cli/checkout-identity.md b/context/cli/checkout-identity.md index fe5aa3ab..8cae166d 100644 --- a/context/cli/checkout-identity.md +++ b/context/cli/checkout-identity.md @@ -4,6 +4,15 @@ The checkout identity service lives in `cli/src/services/checkout/`. It assigns a stable identity to a local Git checkout or linked Git worktree. The setup lifecycle creates/reuses this identity and registers the checkout. Agent Trace hook runtime now resolves persistence through this identity and lazily initializes a per-checkout database. +## Registry resilience + +The registry includes two resilience safeguards against concurrent access and corruption: + +- **Empty-file recovery:** `read_registry()` treats empty or whitespace-only registry files as corrupt. It removes the file (best-effort), prints a `[WARN]` to stderr, and returns `CheckoutRegistry::default()`. This prevents `serde_json` parse errors when a previous write was interrupted, leaving a zero-byte file. +- **PID-unique temp filenames:** `write_registry()` appends the process ID to the temp filename (`checkout-registry.json.tmp.{pid}`) so concurrent `sce hooks` processes never collide on the same temp file. After a successful atomic `rename(2)`, stale `.tmp.*` files left by crashed processes are cleaned up (best-effort, matching `checkout-registry.json.tmp.*` prefix, errors ignored). + +These changes are implemented in `cli/src/services/checkout/registry.rs` in the `read_registry()` and `write_registry()` functions. No caller changes were needed. + ## Current code surface - `cli/src/services/checkout/mod.rs` diff --git a/context/plans/checkout-registry-resilience.md b/context/plans/checkout-registry-resilience.md new file mode 100644 index 00000000..6af601e6 --- /dev/null +++ b/context/plans/checkout-registry-resilience.md @@ -0,0 +1,96 @@ +# checkout-registry-resilience + +## Change summary + +Fix two related bugs in `cli/src/services/checkout/registry.rs` that cause the checkout registry to fail under concurrent access or corruption: + +1. **`read_registry()` crashes on empty files**: When `checkout-registry.json` is empty (0 bytes), `serde_json::from_str("")` fails with "EOF while parsing a value at line 1 column 0". The function should treat empty content the same as a missing file — return a default empty registry. + +2. **`write_registry()` uses a shared temp file**: The temp path `checkout-registry.json.tmp` is a fixed filename shared by all concurrent `sce hooks` processes. When two processes write to the same `.tmp` and one's `rename(2)` consumes it, the other process hits `ENOENT` ("No such file or directory"). This manifests when OpenCode fires multiple hooks concurrently (e.g., `conversation-trace` + `diff-trace` on the same `message.updated` event). + +## Success criteria + +- `read_registry()` returns `Ok(CheckoutRegistry::default())` when the file exists but is empty or whitespace-only (the corrupt file is removed before returning). +- `write_registry()` uses a unique temp filename (appending process ID) so concurrent `sce hooks` processes never collide on the same temp file. +- `nix flake check` passes (all 13 checks). +- Existing registry behavior unchanged for valid files — idempotent read/write/register/update/remove operations continue to work correctly. +- No new dependencies or configuration required. + +## Constraints and non-goals + +- In scope: `read_registry()` empty-file handling, `write_registry()` unique temp filename, `write_registry()` rename failure recovery for stale `.tmp` files. +- Out of scope: adding file locking, changing the registry JSON schema, adding retry logic at the caller level, modifying any callers of `read_registry` or `write_registry`, adding `checkout-registry` doctor diagnostics. +- Out of scope: new external crates or dependencies. +- Out of scope: test-only fixtures for concurrent registry access (the race is environmental and would require multi-process test infrastructure). + +## Assumptions + +- The race condition occurs only when multiple `sce hooks` processes write the registry concurrently; the unique temp filename eliminates this race entirely. +- Empty/corrupt registry files are always safe to delete and recreate from scratch — the registry is a cache that is rebuilt on each `register_checkout` call. +- Process ID (`std::process::id()`) is a sufficient uniqueness suffix for temp filenames; PID + thread ID is unnecessary because each `sce` process only has one registry writer thread. + +## Tasks + +- [x] T01: `Handle empty registry files in read_registry()` (status:done) + - Task ID: T01 + - Goal: In `cli/src/services/checkout/registry.rs::read_registry()`, after reading file content, check if `content.trim().is_empty()`. If so, remove the corrupt file with `std::fs::remove_file` (best-effort, ignore errors), log a warning via `eprintln!`, and return `Ok(CheckoutRegistry::default())`. + - Boundaries (in/out of scope): In — `read_registry()` function (lines 57-75) only. Out — all callers, `write_registry()`, `register_checkout()`, any other function. + - Done when: `read_registry()` returns an empty `CheckoutRegistry` (not an error) when `checkout-registry.json` is 0 bytes or whitespace-only; the corrupt file is deleted; the warning is printed to stderr. + - Verification notes: `nix flake check`; manual test: `touch ~/.local/state/sce/checkout-registry.json && sce hooks diff-trace <<< '{}'` (should not crash with parse error); `cat ~/.local/state/sce/checkout-registry.json` should show the file was recreated with valid JSON. + - **Status:** done + - **Completed:** 2026-06-16 + - **Files changed:** `cli/src/services/checkout/registry.rs` + - **Evidence:** 13/13 flake checks passed; empty-file guard added after `read_to_string`, before `serde_json::from_str` + - **Notes:** 5 lines added; corrupt file deleted best-effort + `eprintln!` warning + fallback to `CheckoutRegistry::default()` + +- [x] T02: `Use unique temp filenames in write_registry()` (status:done) + - Task ID: T02 + - Goal: In `cli/src/services/checkout/registry.rs::write_registry()`, replace the fixed temp path `path.with_extension("json.tmp")` with a unique temp path that appends the process ID: `path.with_extension(format!("json.tmp.{}", std::process::id()))`. After a successful rename, clean up any stale `.tmp.*` files from previous crashed processes (best-effort, ignore errors). + - Boundaries (in/out of scope): In — `write_registry()` function (lines 82-115), temp filename generation, stale-temp cleanup logic. Out — `read_registry()`, `register_checkout()`, any other function. + - Done when: `write_registry()` writes to a PID-unique temp file; rename is atomic per-process; `nix flake check` passes; manual concurrent invocation test no longer produces "No such file or directory" rename errors. + - Verification notes: `nix flake check`; manual test: run two concurrent `sce hooks diff-trace <<< '{}'` in quick succession and confirm neither fails with a rename error; inspect `~/.local/state/sce/` to confirm no orphaned `.tmp.*` files remain after successful writes. + - **Status:** done + - **Completed:** 2026-06-16 + - **Files changed:** `cli/src/services/checkout/registry.rs` + - **Evidence:** 13/13 flake checks passed; temp path now uses `path.with_extension(format!("json.tmp.{}", std::process::id()))`; stale `.tmp.*` files cleaned up post-rename with `read_dir` + prefix match (best-effort, ignore errors) + - **Notes:** 2 clippy pedantic lint fixes applied (uninlined_format_args, manual_let_else); ~16 lines added in stale-cleanup block + +- [x] T03: `Validation and cleanup` (status:done) + - Task ID: T03 + - Goal: Run full validation, confirm both fixes work, and sync context. + - Boundaries (in/out of scope): In — `nix flake check`, `nix run .#pkl-check-generated`, manual smoke test, context sync. Out — additional code changes. + - Done when: All checks pass; manual test with empty registry file no longer crashes; concurrent hook invocations no longer produce rename errors; context files are updated. + - Verification notes: `nix flake check` && `nix run .#pkl-check-generated`; verify context sync completeness. + - **Status:** done + - **Completed:** 2026-06-16 + - **Files changed:** `context/cli/checkout-identity.md` (registry resilience section added) + - **Evidence:** 13/13 flake checks passed; pkl-check-generated passed; manual smoke test confirmed empty registry file triggers warning + returns default without crash; context sync completed (verify-only domain-file update) + - **Notes:** No code changes — T01/T02 already implemented. Domain file updated with registry resilience documentation. + +## Validation Report + +### Commands run + +- `nix flake check` → exit 0 (all 13 checks passed: cli-tests, cli-clippy, cli-fmt, integrations-install-tests, integrations-install-clippy, integrations-install-fmt, pkl-parity, npm-bun-tests, npm-biome-check, npm-biome-format, config-lib-bun-tests, config-lib-biome-check, config-lib-biome-format) +- `nix run .#pkl-check-generated` → exit 0 ("Generated outputs are up to date.") +- Manual smoke test: `: > ~/.local/state/sce/checkout-registry.json && nix run .#sce -- doctor dbs` → `[WARN] Empty checkout registry... removing and recreating from scratch`, no crash, successful completion + +### Temporary scaffolding + +- None introduced by this plan. + +### Success-criteria verification + +- [x] `read_registry()` returns `CheckoutRegistry::default()` on empty/whitespace-only files → confirmed via manual smoke test (warning printed, file deleted, no crash) +- [x] `write_registry()` uses PID-unique temp filename → confirmed via code inspection (`path.with_extension(format!("json.tmp.{}", std::process::id()))`) +- [x] `nix flake check` passes all 13 checks → confirmed +- [x] Existing registry behavior unchanged for valid files → confirmed via all checks passing +- [x] No new dependencies or configuration required → confirmed + +### Residual risks + +- None identified. Both fixes are minimal, localized, and backward-compatible. + +## Open questions + +- None. diff --git a/context/plans/hook-db-error-diagnostics.md b/context/plans/hook-db-error-diagnostics.md new file mode 100644 index 00000000..054afef8 --- /dev/null +++ b/context/plans/hook-db-error-diagnostics.md @@ -0,0 +1,91 @@ +# hook-db-error-diagnostics + +## Change summary + +Improve error visibility when the hook runtime fails to open the per-checkout Agent Trace DB. Two gaps exist: (1) the lazy-init fast-path error is discarded when the migration-running fallback also fails, losing diagnostic context; (2) `sce doctor` only checks file existence, not whether the DB can actually be opened, connected to, or has a valid schema — so doctor can report `[PASS]` while hooks still fail. + +## Success criteria + +- When `resolve_or_create_agent_trace_db_for_checkout` fast-path fails and the fallback `open_at` also fails, the error message includes the fast-path failure reason alongside the fallback failure. +- `sce doctor` reports an error if the per-checkout Agent Trace DB file exists but cannot be opened, or if the schema is missing/incomplete (not just file-level existence). +- `nix flake check` passes. +- Existing hook flows, doctor output, and setup behavior are unchanged except for added error context and deeper health reporting. + +## Constraints and non-goals + +- In scope: preserving fast-path error in `checkout/mod.rs::resolve_or_create_agent_trace_db_for_checkout`; adding `AgentTraceDbConnectionFailed` and `AgentTraceDbSchemaNotReady` health problem kinds with full lifecycle → doctor enum plumbing. +- In scope: `diagnose_agent_trace_db_health` gains a non-mutating DB open + schema check when the file exists. +- Out of scope: changing any hook payload persistence logic, adding retry behavior, or modifying the Turso/LibSQL connection layer. +- Out of scope: new external dependencies. +- Out of scope: changing the `HealthProblemKind` / `ProblemKind` taxonomy beyond the two new variants. +- Out of scope: test-only fixtures or integration tests that require a real DB on disk (the deep check uses the same non-mutating codepath already exercised by hook runtime, and `diagnose_agent_trace_db_health` is currently untested at the unit level). + +## Assumptions + +- `AgentTraceDb::open_for_hooks_without_migrations_at` followed by `ensure_schema_ready_for_hooks` is safe to call during doctor diagnosis — it opens a connection but does not mutate the DB. +- The doctor deep-check failure does not need to be auto-fixable via `sce doctor --fix`; it directs the user to run `sce setup` or fix permissions manually. + +## Tasks + +- [x] T01: `Preserve fast-path error in checkout DB lazy-init fallback` (status:done) + - Task ID: T01 + - Goal: When `resolve_or_create_agent_trace_db_for_checkout` in `cli/src/services/checkout/mod.rs` fails the fast-path open and then fails the fallback `open_at`, include the fast-path error reason in the fallback error context so the operator can see both failure causes. + - Boundaries (in/out of scope): In — `resolve_or_create_agent_trace_db_for_checkout` lines 192-201; the `Err(_)` arm captures the error and chains it into the `.with_context()` closure. Out — all other functions, error classification, hook command routing. + - Done when: The `Err` arm captures the fast-path error value and includes it in the `format!` string inside `.with_context()`, so the resulting anyhow chain reads: `failed to initialize Agent Trace DB for checkout at '' (fast-path attempt: )`. + - Verification notes: `nix flake check`; manual inspection of `cli/src/services/checkout/mod.rs` to confirm `Err(fast_error)` binding. + - **Completed:** 2026-06-16 + - **Files changed:** `cli/src/services/checkout/mod.rs` + - **Evidence:** `nix flake check` passed (cli-tests, cli-clippy, cli-fmt, pkl-parity) + - **Notes:** Changed `Err(_)` to `Err(fast_error)` and added ` (fast-path attempt: {fast_error})` to the format string. + +- [x] T02: `Add Agent Trace DB connectivity and schema health checks to doctor` (status:done) + - T02 consists of two sub-steps (single commit): + - **T02a**: Add `AgentTraceDbConnectionFailed` and `AgentTraceDbSchemaNotReady` variants to `HealthProblemKind` (in `cli/src/services/lifecycle.rs`) and corresponding variants to `ProblemKind` (in `cli/src/services/doctor/types.rs`), plus mapping entries in `doctor_problem_kind` and `health_problem_kind` (in `cli/src/services/doctor/mod.rs`). + - **T02b**: In `diagnose_agent_trace_db_health` (`cli/src/services/agent_trace_db/lifecycle.rs`), after the existing `collect_db_path_health` check, when the DB file exists: open it via `AgentTraceDb::open_for_hooks_without_migrations_at`, check `ensure_schema_ready_for_hooks`, and report `AgentTraceDbConnectionFailed` if open fails or `AgentTraceDbSchemaNotReady` if schema is incomplete. + - Goal: `sce doctor` surfaces DB-level health problems, not just file-level existence. + - Boundaries (in/out of scope): In — `lifecycle.rs` enum, `doctor/types.rs` enum, `doctor/mod.rs` mapping functions, `agent_trace_db/lifecycle.rs` diagnose function. Out — changes to `doctor/inspect.rs`, `doctor/render.rs`, `doctor/fixes.rs`, hook runtime, or any other service. + - Done when: `diagnose_agent_trace_db_health` reports an error when the DB cannot be opened or has incomplete schema; `nix flake check` passes; `sce doctor` text output shows `[FAIL]` for the Agent Trace checkout DB row when the file exists but is unopenable. + - Verification notes: `nix flake check`; `nix develop -c sh -c 'cd cli && cargo build'` to confirm compilation; manual `sce doctor` run in a checkout with a known-good DB to confirm no false positives. + - **Completed:** 2026-06-16 + - **Files changed:** `cli/src/services/lifecycle.rs`, `cli/src/services/doctor/types.rs`, `cli/src/services/doctor/mod.rs`, `cli/src/services/agent_trace_db/lifecycle.rs`, `cli/src/services/doctor/render.rs` + - **Evidence:** `nix flake check` passed (cli-tests, cli-clippy, cli-fmt, pkl-parity) + - **Notes:** Added `AgentTraceDbConnectionFailed` and `AgentTraceDbSchemaNotReady` to both `HealthProblemKind` and `ProblemKind` enums with full mapping. Deep-check logic opens the DB via `open_for_hooks_without_migrations_at` and verifies schema via `ensure_schema_ready_for_hooks` when the DB file exists. `agent_trace_db_status()` in render.rs updated to recognize the new problem kinds for `[FAIL]` rendering. + +- [x] T03: `Validation and cleanup` (status:done) + - Task ID: T03 + - Goal: Run full validation, confirm error chain visibility, and sync context. + - Boundaries (in/out of scope): In — `nix flake check`, `nix run .#pkl-check-generated`, review of changed files, context sync. Out — additional code changes. + - Done when: All checks pass, the fast-path error is visible in the fallback context, doctor deep-check works, and context files are updated. + - Verification notes: `nix flake check` && `nix run .#pkl-check-generated`; review `context/` updates. + - **Completed:** 2026-06-16 + - **Files changed:** `cli/src/services/checkout/mod.rs` (reviewed T01), `cli/src/services/agent_trace_db/lifecycle.rs` (reviewed T02), `cli/src/services/lifecycle.rs` (reviewed T02), `cli/src/services/doctor/types.rs` (reviewed T02), `cli/src/services/doctor/mod.rs` (reviewed T02), `cli/src/services/doctor/render.rs` (reviewed T02) + - **Evidence:** `nix flake check` passed (all 13 checks), `nix run .#pkl-check-generated` passed (generated outputs up to date). Fast-path error chain confirmed visible at `checkout/mod.rs:196-199`. Doctor deep-check confirmed at `agent_trace_db/lifecycle.rs:136-173`. Context sync completed. + - **Notes:** This is the final task in the plan. All three tasks complete. + +## Validation Report + +### Commands run + +- `nix flake check` → exit 0 (all 13 checks passed) + - `cli-tests`, `cli-clippy`, `cli-fmt` + - `integrations-install-tests`, `integrations-install-clippy`, `integrations-install-fmt` + - `pkl-parity` + - `npm-bun-tests`, `npm-biome-check`, `npm-biome-format` + - `config-lib-bun-tests`, `config-lib-biome-check`, `config-lib-biome-format` +- `nix run .#pkl-check-generated` → exit 0 (`Generated outputs are up to date.`) +- Temporary scaffolding: none introduced (no debug code, temp files, or test-only fixtures) + +### Success-criteria verification + +- [x] **Fast-path error preserved in fallback context** — confirmed at `cli/src/services/checkout/mod.rs:196-199`: `Err(fast_error)` binding and `(fast-path attempt: {fast_error})` in `.with_context()` format string. +- [x] **Doctor reports DB connectivity/schema errors** — confirmed at `cli/src/services/agent_trace_db/lifecycle.rs:136-173`: deep-check opens DB via `open_for_hooks_without_migrations_at`, checks `ensure_schema_ready_for_hooks`, reports `AgentTraceDbConnectionFailed` or `AgentTraceDbSchemaNotReady`. +- [x] **`nix flake check` passes** — all 13 derivations evaluated and passed. +- [x] **Existing flows unchanged** — all CLI tests pass, no behavioral changes to hook runtime, doctor output, or setup service beyond added error context and deeper health reporting. + +### Residual risks + +- None identified. The deep-check uses the same non-mutating open + schema-readiness codepath already exercised by hook runtime (`open_for_hooks_without_migrations_at` + `ensure_schema_ready_for_hooks`). No unit-level test coverage for `diagnose_agent_trace_db_health` (explicitly out of scope per plan constraints). + +## Open questions + +- None at this time. diff --git a/context/plans/hook-error-chain-preservation.md b/context/plans/hook-error-chain-preservation.md new file mode 100644 index 00000000..ecea22c9 --- /dev/null +++ b/context/plans/hook-error-chain-preservation.md @@ -0,0 +1,106 @@ +# hook-error-chain-preservation + +## Change summary + +Fix `error.to_string()` truncation of anyhow error chains in command dispatchers. The `hooks/command.rs` (and 4 other command files) use `error.to_string()` which renders only the outermost anyhow error via `{}` Display, silently discarding the full cause chain. The repo convention (established in `setup/command.rs`) is `format!("{error:#}")` which renders the full chain with `: ` separators per `anyhow-1.0.102/src/fmt.rs:8-14`. This prevents operators from seeing the underlying failure reason (e.g., git resolution, DB connection, permissions) behind errors like `"Failed to open Agent Trace DB for conversation-trace persistence."`. + +## Success criteria + +- `hooks/command.rs:12` uses `format!("{error:#}")` instead of `error.to_string()`, exposing the full anyhow chain in hook error diagnostics. +- The same fix is applied to `auth_command/command.rs`, `config/command.rs`, `doctor/command.rs`, and `version/command.rs` for consistency with the established pattern in `setup/command.rs`. +- `nix flake check` passes (all 13 checks: cli-tests, cli-clippy, cli-fmt, integrations-*, pkl-parity, npm-*, config-lib-*). +- No behavioral changes to hook runtime, doctor output, setup, or any other service — only richer diagnostic text when errors occur. + +## Constraints and non-goals + +- In scope: 5 command files, one-line pattern change each (`error.to_string()` → `format!("{error:#}")`). +- Out of scope: hardening the OpenCode plugin's `repoRoot` resolution, investigating the root cause of the Agent Trace DB open failure, fixing error truncation outside of `cli/src/services/*/command.rs`, changing error classification or rendering in `app_support.rs`. +- Out of scope: adding test coverage for error chain rendering. + +## Assumptions + +- The underlying Agent Trace DB open failure in OpenCode is a pre-existing environmental issue; this fix enables diagnosis by surfacing the full error. +- `anyhow::Error` `{:#}` alternate format preserves the chain for all error types produced in these command paths (ContextError chains, Turso/libsql errors, I/O errors). + +## Tasks + +- [x] T01: `Fix hooks/command.rs error chain truncation` (status: done) + - Task ID: T01 + - Goal: Change `error.to_string()` to `format!("{error:#}")` in `cli/src/services/hooks/command.rs:12` so the full anyhow error chain (including the fast-path attempt failure added by the staged fix in `checkout/mod.rs`) is visible in hook error diagnostics. + - Boundaries (in/out of scope): In — `cli/src/services/hooks/command.rs` line 12 only. Out — all other files, hook runtime logic, error classification. + - Done when: Line 12 reads `.map_err(|error| ClassifiedError::runtime(format!("{error:#}")))`; `nix flake check` passes. + - Completed: 2026-06-16 + - Files changed: `cli/src/services/hooks/command.rs` (line 12) + - Evidence: `nix flake check` passed (4/4: cli-tests, cli-clippy, cli-fmt, pkl-parity) + - Verification notes: `nix flake check`; review the changed line to confirm `format!("{error:#}")`. + +- [x] T02: `Fix remaining command file error chain truncations` (status: done) + - Task ID: T02 + - Goal: Apply the same `error.to_string()` → `format!("{error:#}")` fix to `auth_command/command.rs:11`, `config/command.rs:11`, `doctor/command.rs:12`, and `version/command.rs:11` for consistency with `setup/command.rs`. + - Boundaries (in/out of scope): In — the 4 listed command files, one line each. Out — any other files, changes to command logic or error handling patterns. + - Done when: All 4 files use `format!("{error:#}")`; `nix flake check` passes. + - Completed: 2026-06-16 + - Files changed: `cli/src/services/auth_command/command.rs`, `cli/src/services/config/command.rs`, `cli/src/services/doctor/command.rs`, `cli/src/services/version/command.rs` (line 11-12 each) + - Evidence: `nix flake check` passed (4/4: cli-tests, cli-clippy, cli-fmt, pkl-parity); grep confirms zero remaining `error.to_string()` in `cli/src/services/*/command.rs`. + - Verification notes: `nix flake check`; review each file to confirm the pattern change. + +- [x] T03: `Validation and cleanup` (status: done) + - Task ID: T03 + - Goal: Run full validation, confirm all 5 files are correct, the staged fix's fast-path error context is now reachable through `{:#}` format, and context is synced. + - Boundaries (in/out of scope): In — `nix flake check`, `nix run .#pkl-check-generated`, review of changed files, context sync. Out — additional code changes, live hook testing. + - Done when: All checks pass, the full anyhow chain is structurally reachable from hook error diagnostics (confirmed via code review of `hooks/command.rs:12` → `hooks/mod.rs:250` → `checkout/mod.rs:198` chain), and context files are updated. + - Completed: 2026-06-16 + - Files changed: none (validation-only task) + - Evidence: `nix flake check` passed (all 13 checks); `nix run .#pkl-check-generated` passed (generated outputs up to date); zero `error.to_string()` remaining in `cli/src/services/*/command.rs`; error chain review confirms `hooks/command.rs:12` → `hooks/mod.rs:294-300` → `checkout/mod.rs:194-201` structural reachability. + - Verification notes: `nix flake check` && `nix run .#pkl-check-generated`; confirm that `hooks/mod.rs:250` error context string `"Failed to open Agent Trace DB for conversation-trace persistence."` would now be followed by `: failed to initialize Agent Trace DB for checkout {id} at '{path}' (fast-path attempt: {error})` when the DB open fails. + +## Validation Report + +### Commands run + +| Command | Exit code | Key output | +|---|---|---| +| `nix flake check` | 0 | `all checks passed!` (all 13 checks: cli-tests, cli-clippy, cli-fmt, integrations-install-tests, integrations-install-clippy, integrations-install-fmt, pkl-parity, npm-bun-tests, npm-biome-check, npm-biome-format, config-lib-bun-tests, config-lib-biome-check, config-lib-biome-format) | +| `nix run .#pkl-check-generated` | 0 | `Generated outputs are up to date.` | +| grep `error.to_string()` in `cli/src/services/*/command.rs` | — | Zero matches (confirmed none remaining in the 6 command files) | + +### Scaffolding cleanup + +None needed — no temporary scaffolding, debug code, or intermediate artifacts were introduced. + +### Context verification + +- **Root context files**: Verified — `overview.md`, `architecture.md`, `glossary.md`, `patterns.md` all reflect current code truth. No drift. Change classified as verify-only. +- **Domain files**: No new domain concepts introduced; no domain files created. +- **Glossary**: No new SCE-specific terminology; `{:#}` is standard anyhow behavior. + +### Success-criteria verification + +- [x] `hooks/command.rs:12` uses `format!("{error:#}")` — confirmed line reads `.map_err(|error| ClassifiedError::runtime(format!("{error:#}")))` +- [x] Same fix applied to `auth_command/command.rs:11`, `config/command.rs:11`, `doctor/command.rs:12`, `version/command.rs:11` — confirmed all 4 files use `format!("{error:#}")` +- [x] `nix flake check` passes (all 13 checks) — confirmed exit 0 +- [x] No behavioral changes — confirmed: only the format string changed from `error.to_string()` to `format!("{error:#}")`; same error types, same code paths, same `ClassifiedError::runtime(...)` classification + +### Error chain reachability confirmation + +The `{:#}` format ensures the full anyhow chain is visible. Example chain: + +``` +hooks/command.rs:12 → ClassifiedError::runtime(format!("{error:#}")) +hooks/mod.rs:300 → .context("Failed to open Agent Trace DB for conversation-trace persistence.") +hooks/mod.rs:298 → checkout/mod.rs:resolve_or_create_agent_trace_db_for_checkout(...) +checkout/mod.rs:196 → .with_context(|| format!( + "failed to initialize Agent Trace DB for checkout {id} at '{path}' \ + (fast-path attempt: {fast_error})")) +``` + +With `{:#}`, any DB open failure renders as: +`Failed to open Agent Trace DB for conversation-trace persistence.: failed to initialize Agent Trace DB for checkout {id} at '{path}' (fast-path attempt: {original_error})` + +### Residual risks + +- None identified. The fix is a one-line format-string change (no logic, control flow, or error classification changes). The same pattern has been in production in `setup/command.rs` for prior releases. + +## Open questions + +- None at this time. diff --git a/context/sce/agent-trace-db.md b/context/sce/agent-trace-db.md index 596cafde..e020cfa7 100644 --- a/context/sce/agent-trace-db.md +++ b/context/sce/agent-trace-db.md @@ -83,7 +83,7 @@ Active hook runtime resolves per-checkout Agent Trace DB files: The shared `TursoDb` runner records applied IDs in the database-local `__sce_migrations` table. Existing Agent Trace DB files without metadata are brought forward by re-applying the idempotent migration set and recording each ID, so rerunning `sce setup` / `AgentTraceDb::new()` applies later Agent Trace migrations to an already-created `~/.local/state/sce/agent-trace.db`. -Per-checkout hook DB resolution first tries `AgentTraceDb::open_for_hooks_without_migrations_at(path)` and `ensure_schema_ready_for_hooks()`. If the DB is missing, metadata is absent, or migrations are incomplete, the checkout resolver falls back to `AgentTraceDb::open_at(path)` so hook invocation lazily creates or upgrades the per-checkout DB before continuing. Readiness is based on exact migration metadata parity with `AGENT_TRACE_MIGRATIONS`, not table/index/column introspection. +Per-checkout hook DB resolution first tries `AgentTraceDb::open_for_hooks_without_migrations_at(path)` and `ensure_schema_ready_for_hooks()`. If the DB is missing, metadata is absent, or migrations are incomplete, the checkout resolver falls back to `AgentTraceDb::open_at(path)` so hook invocation lazily creates or upgrades the per-checkout DB before continuing. When the fallback also fails, the error context includes the fast-path failure reason (`(fast-path attempt: {fast_error})`) so both failure causes are visible in diagnostics. Readiness is based on exact migration metadata parity with `AGENT_TRACE_MIGRATIONS`, not table/index/column introspection. The `diff_traces` baseline migration creates: @@ -178,7 +178,7 @@ Both triggers compare `OLD.*` vs `NEW.*` for all mutable columns (excluding `upd `AgentTraceDbLifecycle` is registered in `cli/src/services/lifecycle.rs` after `LocalDbLifecycle` and before optional `HooksLifecycle`. -- `diagnose()` reports per-checkout Agent Trace DB path and parent-directory readiness when a repo root has a checkout ID; otherwise it falls back to the legacy global Agent Trace DB path. +- `diagnose()` reports per-checkout Agent Trace DB path and parent-directory readiness when a repo root has a checkout ID; otherwise it falls back to the legacy global Agent Trace DB path. When the DB file exists, it also performs a deep health check: opens the file via `open_for_hooks_without_migrations_at` and verifies schema readiness via `ensure_schema_ready_for_hooks`, reporting `AgentTraceDbConnectionFailed` if open fails or `AgentTraceDbSchemaNotReady` if the schema is incomplete. These deep-check problems are `ManualOnly` (not auto-fixable by `sce doctor --fix`); the remediation directs the operator to re-run `sce setup` or fix file permissions. - `fix()` bootstraps the resolved per-checkout DB parent directory for auto-fixable parent-readiness problems, with the same global fallback outside checkout context. - `setup()` creates/reuses the current checkout identity when a repo root is available, registers the checkout in `/sce/checkout-registry.json` with `database_path: null`, and emits setup messaging with the checkout ID. It does not eagerly initialize the Agent Trace DB; the first hook write lazily creates the per-checkout DB. - `sce doctor` surfaces checkout identity and per-checkout Agent Trace DB health in the `Configuration` section when a checkout ID exists, with `[PASS]`/`[FAIL]`/`[MISS]` status tokens. Outside checkout context it falls back to the legacy/global Agent Trace DB row. JSON output includes `checkout_identity` when available plus the resolved `agent_trace_db` field. diff --git a/context/sce/agent-trace-hook-doctor.md b/context/sce/agent-trace-hook-doctor.md index 532d7895..b85467e1 100644 --- a/context/sce/agent-trace-hook-doctor.md +++ b/context/sce/agent-trace-hook-doctor.md @@ -126,6 +126,7 @@ The broadened contract for `sce doctor` must cover the following problem invento - local DB or checkout/global Agent Trace DB path cannot be resolved - local DB and checkout/global Agent Trace DB parent directories are missing or not writable - local DB and checkout/global Agent Trace DB bootstrap or health is broken +- Agent Trace DB file exists but cannot be opened (connection failure) or has incomplete schema (missing/unapplied migrations) — reported as `AgentTraceDbConnectionFailed` / `AgentTraceDbSchemaNotReady` with manual-only remediation directing to `sce setup` - Agent Trace checkout ID plus per-checkout DB path/health are reported in `Configuration` section output when a checkout ID exists, with global Agent Trace DB reporting retained as the no-checkout fallback ### Repository targeting and git readiness diff --git a/flake.nix b/flake.nix index 5a0d791c..6cc18699 100644 --- a/flake.nix +++ b/flake.nix @@ -33,6 +33,8 @@ pkgs = import nixpkgs { inherit system; overlays = [ rust-overlay.overlays.default ]; + config.allowUnfreePredicate = + pkg: builtins.elem (pkgs.lib.getName pkg) [ "claude-code" ]; }; opencodePkgs = import opencode-nixpkgs { @@ -275,6 +277,8 @@ ''; }); + claudeCodePackage = pkgs.claude-code; + tursoCargoArgs = { pname = "turso"; version = "0.7.0-pre.5"; @@ -964,6 +968,7 @@ sce = scePackage; bun = bunPackage; opencode = opencodePackage; + claude-code = claudeCodePackage; turso = tursoPackage; default = scePackage; }; @@ -1096,6 +1101,7 @@ typescript-language-server vscode-json-languageserver opencodePackage + claudeCodePackage rust-analyzer scePackage tursoPackage @@ -1107,6 +1113,9 @@ "$1" --version 2>/dev/null | awk 'match($0, /[0-9]+(\.[0-9]+)+/) { print substr($0, RSTART, RLENGTH); exit }' } + alias claude="${claudeCodePackage}/bin/claude" + alias cc="${claudeCodePackage}/bin/claude" + echo "- bun: $(version_of bun)" echo "- biome: $(version_of biome)" echo "- pkl: $(version_of pkl)" @@ -1116,6 +1125,7 @@ echo "- rust: $(version_of rustc)" echo "- sce: $(version_of sce)" echo "- opencode: $(version_of opencode)" + echo "- claude-code: $(version_of claude)" echo "- turso: $(version_of turso)" echo "- pkl-generate: nix run .#pkl-generate" echo "- pkl-check-generated: nix run .#pkl-check-generated" From 1771b3c4a976c3f6043a47a10087e6f8c7c52c84 Mon Sep 17 00:00:00 2001 From: stefanskoricdev Date: Wed, 17 Jun 2026 14:33:16 +0200 Subject: [PATCH 7/8] agent-trace-db: Initialize checkout database during setup Make Agent Trace setup create or migrate the per-checkout database immediately and persist its database_path in the checkout registry. Hook runtime keeps lazy initialization as a fallback for checkouts where setup has not prepared the database or schema metadata is incomplete. Co-authored-by: SCE --- cli/src/services/agent_trace_db/lifecycle.rs | 63 +++++++++++++++++--- context/architecture.md | 7 ++- context/cli/checkout-identity.md | 9 ++- context/cli/cli-command-surface.md | 2 +- context/cli/default-path-catalog.md | 2 +- context/cli/service-lifecycle.md | 2 +- context/context-map.md | 6 +- context/glossary.md | 12 ++-- context/overview.md | 3 +- context/sce/agent-trace-db.md | 10 ++-- context/sce/shared-turso-db.md | 2 +- 11 files changed, 84 insertions(+), 34 deletions(-) diff --git a/cli/src/services/agent_trace_db/lifecycle.rs b/cli/src/services/agent_trace_db/lifecycle.rs index 9f5f825e..eec4a1e8 100644 --- a/cli/src/services/agent_trace_db/lifecycle.rs +++ b/cli/src/services/agent_trace_db/lifecycle.rs @@ -54,11 +54,19 @@ impl ServiceLifecycle for AgentTraceDbLifecycle { } fn setup(&self, ctx: &C) -> Result { - let checkout_setup = ctx - .repo_root() - .map(setup_checkout_identity) - .transpose() - .context("Agent trace DB lifecycle setup failed while resolving checkout identity")?; + let checkout_setup = match ctx.repo_root() { + Some(repo_root) => { + let identity_setup = setup_checkout_identity(repo_root).context( + "Agent trace DB lifecycle setup failed while resolving checkout identity", + )?; + Some( + initialize_checkout_agent_trace_db(repo_root, &identity_setup.checkout_id).context( + "Agent trace DB lifecycle setup failed while initializing checkout database", + )?, + ) + } + None => None, + }; Ok(SetupOutcome { messages: checkout_setup @@ -75,6 +83,12 @@ struct CheckoutIdentitySetup { checkout_id: String, } +#[derive(Clone, Debug, Eq, PartialEq)] +struct CheckoutDatabaseSetup { + checkout_id: String, + database_path: PathBuf, +} + fn setup_checkout_identity(repo_root: &std::path::Path) -> Result { let git_dir = checkout::resolve_git_dir(repo_root).with_context(|| { format!( @@ -88,7 +102,6 @@ fn setup_checkout_identity(repo_root: &std::path::Path) -> Result Result String { +fn initialize_checkout_agent_trace_db( + repo_root: &Path, + checkout_id: &str, +) -> Result { + let db_path = agent_trace_db_path_for_checkout(checkout_id).with_context(|| { + format!("failed to resolve Agent Trace DB path for checkout ID {checkout_id}") + })?; + + AgentTraceDb::open_at(&db_path).with_context(|| { + format!( + "failed to initialize Agent Trace DB for checkout {} at '{}'", + checkout_id, + db_path.display() + ) + })?; + + registry::register_checkout(registry::CheckoutRecord { + checkout_id: checkout_id.to_string(), + path: repo_root.display().to_string(), + last_seen: Utc::now().to_rfc3339(), + remote_url: None, + database_path: Some(db_path.display().to_string()), + }) + .context("failed to register checkout Agent Trace database path")?; + + Ok(CheckoutDatabaseSetup { + checkout_id: checkout_id.to_string(), + database_path: db_path, + }) +} + +fn format_checkout_identity_setup_message(setup: &CheckoutDatabaseSetup) -> String { format!( - "Agent Trace checkout identity: {}\nAgent Trace database will be created on first write.", - setup.checkout_id + "Agent Trace checkout identity: {}\nAgent Trace database initialized at '{}'.", + setup.checkout_id, + setup.database_path.display() ) } diff --git a/context/architecture.md b/context/architecture.md index e9d48e67..1f0ab7ea 100644 --- a/context/architecture.md +++ b/context/architecture.md @@ -106,11 +106,12 @@ The repository includes a new placeholder Rust binary crate at `cli/`. - `cli/src/services/config/types.rs` is the canonical owner for the shared runtime/config primitive seam used by the CLI: `LogLevel`, `LogFormat`, `LogFileMode`, the observability env-key constants, and the shared bool parsing helpers used by both config resolution and observability bootstrap; `cli/src/services/config/mod.rs` re-exports those primitives through the facade. - `cli/src/services/capabilities.rs` defines the current broad CLI capability traits consumed by the borrowed, compile-time-typed `AppContext`: `FsOps` with `StdFsOps` for filesystem operations and `GitOps` with `ProcessGitOps` for git command execution plus repository-root/hooks-directory resolution. Existing service internals do not consume these traits directly yet; command execution uses narrow accessors and repo-root-scoped context derivation. - `cli/src/services/lifecycle.rs` defines the current compile-safe lifecycle seam. `ServiceLifecycle` has default no-op generic `diagnose`, `fix`, and `setup` methods over `C: HasRepoRoot`, with lifecycle-owned health, fix, and setup result types so the trait contract is not publicly anchored to doctor/setup module types or the full `AppContext` shape. The same module owns the static `LifecycleProvider` enum and shared `lifecycle_providers(include_hooks)` catalog/factory, returning providers in deterministic order (config → local_db → auth_db → agent_trace_db → hooks when requested); enum dispatch calls each concrete provider through generic context methods without boxed lifecycle-provider allocation or repo-root trait-object context erasure. Hooks exposes a `HooksLifecycle` provider in `cli/src/services/hooks/lifecycle.rs` for hook rollout diagnosis/fix/setup using lifecycle-owned health records plus the canonical required-hook installer. Config exposes a `ConfigLifecycle` provider in `cli/src/services/config/lifecycle.rs` for global/repo-local config validation and repo-local `.sce/config.json` bootstrap. local_db exposes a `LocalDbLifecycle` provider in `cli/src/services/local_db/lifecycle.rs` for canonical local DB path health, parent-directory readiness/bootstrap, and `LocalDb::new()` setup. auth_db exposes an `AuthDbLifecycle` provider in `cli/src/services/auth_db/lifecycle.rs` for canonical auth DB path health, parent-directory readiness/bootstrap, and `AuthDb::new()` setup. agent_trace_db exposes an `AgentTraceDbLifecycle` provider in `cli/src/services/agent_trace_db/lifecycle.rs` for setup-time checkout identity creation/registry registration when a repo root is available, per-checkout Agent Trace DB path health/fix when an ID exists, and legacy global DB fallback outside checkout context. Doctor runtime aggregates the full provider catalog for `diagnose` and `fix` and adapts lifecycle records into doctor report/fix records at the orchestration boundary; setup command aggregates the shared catalog for `setup` with hooks included only when requested and adapts hook setup outcomes before rendering setup-owned messages. +- Agent Trace lifecycle setup uses checkout identity to resolve the per-checkout DB path, initializes it with `AgentTraceDb::open_at(path)`, and records `database_path`; hook runtime keeps lazy per-checkout DB initialization/upgrade only as the fallback for setup-not-run or incomplete-schema cases. - `cli/src/services/auth_command/mod.rs` defines the implemented auth command surface for `sce auth login|renew|logout|status`, including device-flow login, stored-token renewal (`--force` supported for renew), logout, and status rendering in text/JSON formats; `cli/src/services/auth_command/command.rs` owns the `AuthCommand` payload used by the static `RuntimeCommand` enum. - `cli/src/services/db/mod.rs` provides the shared generic Turso infrastructure seam: `DbSpec` supplies a service-specific name, path, ordered embedded migrations, and config-file lookup key (`db_config_key()`), while `TursoDb` owns parent-directory creation, `Builder::new_local(...)` initialization (with `experimental_multiprocess_wal(true)` for safe concurrent access), Turso connection setup, tokio current-thread runtime bridging, retry-backed blocking `execute`/`query`/`query_map` wrappers, and generic migration execution with per-database `__sce_migrations` metadata. `TursoDb::new()` and `EncryptedTursoDb::new()` wrap only their local open/connect block in `run_with_retry_sync` using a config-driven connection-open policy resolved from the `DATABASE_RETRY_CONFIG` `OnceLock` with fallback to hardcoded defaults, while both adapters' `execute()`, `query()`, and `query_map()` methods use a config-driven operation policy from the same source. Both adapters' `query_map()` methods retry the initial query and row-fetch loop, then apply caller row mapping after retry completion. Migration execution is not retried. The same module also provides `EncryptedTursoDb`, a structurally parallel encrypted adapter that resolves the encryption key through `encryption_key::get_or_create_encryption_key()`, enables Turso local encryption with strict `aegis256` cipher selection, and exposes retry-backed synchronous wrappers plus migration execution. `cli/src/services/db/encryption_key.rs` first derives a Turso-compatible 64-character hex key from non-empty `SCE_AUTH_DB_ENCRYPTION_KEY` env-secret text when present, otherwise falls back to keyring-backed credential-store get-or-create behavior; no plaintext auth DB fallback exists. - `cli/src/services/local_db/mod.rs` provides the concrete local DB spec and `LocalDb` type alias over the shared generic `TursoDb` adapter. `LocalDbSpec` resolves the deterministic persistent runtime DB target through the shared default-path seam and declares no local migrations; `TursoDb` supplies retry-backed blocking `execute`/`query`, parent-directory creation, Turso connection setup, tokio current-thread runtime bridging, and generic migration execution. - `cli/src/services/auth_db/mod.rs` provides the encrypted auth DB spec and `AuthDb` type alias over `EncryptedTursoDb`. `AuthDbSpec` resolves `/sce/auth.db` through the shared default-path seam and embeds ordered auth migrations. Auth DB lifecycle setup/doctor integration is wired through `AuthDbLifecycle`; auth command/token-storage reads/writes are directed through `token_storage.rs`. -- `cli/src/services/agent_trace_db/mod.rs` provides the Agent Trace DB spec and `AgentTraceDb` type alias over `TursoDb`. `AgentTraceDbSpec` resolves the legacy `/sce/agent-trace.db` fallback through the shared default-path seam and embeds ordered fresh-start migrations `001..016` for `diff_traces`, `post_commit_patch_intersections`, `agent_traces`, `messages`, `parts`, `session_models`, `diff_traces.payload_type`, indexes, and triggers. `agent_traces.agent_trace_id` is `NOT NULL UNIQUE`. The module provides explicit-path `open_at(path)` and `open_for_hooks_without_migrations_at(path)` helpers for per-checkout DB files plus `DiffTraceInsert<'_>`/`insert_diff_trace()` including the `payload_type` discriminator, `PostCommitPatchIntersectionInsert<'_>`/`insert_post_commit_patch_intersection()`, `AgentTraceInsert<'_>`/`insert_agent_trace()`, `InsertMessageInsert`/`insert_message()` plus `insert_messages()` for parent message inserts with duplicate `(session_id, message_id)` writes ignored, `InsertPartInsert`/`insert_part()` plus `insert_parts()` for append-only part text writes, `SessionModelUpsert<'_>`/`upsert_session_model()` plus lookup helpers for durable `session_models` attribution, and `recent_diff_trace_patches(cutoff_time_ms, end_time_ms)` for inclusive chronological `diff_traces` reads that dispatch on `payload_type`, parse valid raw patch or structured payload rows, and return skipped malformed-row reports. `cli/src/services/agent_trace_db/lifecycle.rs` registers Agent Trace DB setup/doctor lifecycle behavior: setup creates/reuses and registers checkout identity when scoped to a repo root without creating a DB file; active hook runtime resolves checkout identity and lazily creates or upgrades `/sce/agent-trace-{checkout_id}.db` before Agent Trace reads/writes. +- `cli/src/services/agent_trace_db/mod.rs` provides the Agent Trace DB spec and `AgentTraceDb` type alias over `TursoDb`. `AgentTraceDbSpec` resolves the legacy `/sce/agent-trace.db` fallback through the shared default-path seam and embeds ordered fresh-start migrations `001..016` for `diff_traces`, `post_commit_patch_intersections`, `agent_traces`, `messages`, `parts`, `session_models`, `diff_traces.payload_type`, indexes, and triggers. `agent_traces.agent_trace_id` is `NOT NULL UNIQUE`. The module provides explicit-path `open_at(path)` and `open_for_hooks_without_migrations_at(path)` helpers for per-checkout DB files plus `DiffTraceInsert<'_>`/`insert_diff_trace()` including the `payload_type` discriminator, `PostCommitPatchIntersectionInsert<'_>`/`insert_post_commit_patch_intersection()`, `AgentTraceInsert<'_>`/`insert_agent_trace()`, `InsertMessageInsert`/`insert_message()` plus `insert_messages()` for parent message inserts with duplicate `(session_id, message_id)` writes ignored, `InsertPartInsert`/`insert_part()` plus `insert_parts()` for append-only part text writes, `SessionModelUpsert<'_>`/`upsert_session_model()` plus lookup helpers for durable `session_models` attribution, and `recent_diff_trace_patches(cutoff_time_ms, end_time_ms)` for inclusive chronological `diff_traces` reads that dispatch on `payload_type`, parse valid raw patch or structured payload rows, and return skipped malformed-row reports. `cli/src/services/agent_trace_db/lifecycle.rs` registers Agent Trace DB setup/doctor lifecycle behavior: setup creates/reuses and registers checkout identity, resolves `/sce/agent-trace-{checkout_id}.db`, initializes it with `AgentTraceDb::open_at(path)`, and records `database_path`; active hook runtime still resolves checkout identity and lazily creates or upgrades the per-checkout DB when setup has not run or schema metadata is incomplete. - `cli/src/test_support.rs` provides a shared test-only temp-directory helper (`TestTempDir`) used by service tests that need filesystem fixtures. - `cli/src/services/setup/mod.rs` defines the setup command contract (`SetupMode`, `SetupTarget`, `SetupRequest`, CLI flag parser/validator), an `inquire`-backed interactive target prompter (`InquireSetupTargetPrompter`), setup dispatch outcomes (proceed/cancelled), compile-time embedded asset access (`EmbeddedAsset`, target-scoped iterators, required-hook asset iterators/lookups) generated by `cli/build.rs` from the ephemeral crate-local `cli/assets/generated/config/{opencode,claude}/**` mirror plus `cli/assets/hooks/**`, and focused internal support seams for install-flow vs prompt-flow logic; `cli/src/services/setup/command.rs` owns the `SetupCommand` payload used by the static `RuntimeCommand` enum and executes against any context implementing repo-root scoping. Its install engine/orchestrator stages embedded files and uses a unified remove-and-replace policy (removing existing targets before swapping staged content, with deterministic recovery guidance on swap failure and no backup artifact creation), and formats deterministic completion messaging; required-hook install orchestration (`install_required_git_hooks`) follows the same remove-and-replace policy (removing existing hooks before swapping staged content, with deterministic recovery guidance on swap failure). The setup command derives a repo-root-scoped context before aggregating static lifecycle provider `setup` dispatch across providers (config → local_db → auth_db → agent_trace_db → hooks when requested), so setup providers consume only repo-root access from the scoped context. - `cli/src/services/setup/mod.rs` keeps those responsibilities inside one file for now, but the current ownership split is explicit: the inline `install` module owns repository-path normalization, staging/swap install behavior, required-hook installation, and filesystem safety guards, while the inline `prompt` module owns interactive target selection and prompt styling. @@ -122,10 +123,10 @@ The repository includes a new placeholder Rust binary crate at `cli/`. - Claude `SessionStart` session-model parsing in `cli/src/services/hooks/mod.rs` uses explicit payload version fields (`tool_version`/`claude_version`/`version`) when present; if no non-empty payload version is available, it best-effort runs `claude --version`, trims stdout, and leaves `tool_version` nullable without failing intake when the command is unavailable, fails, or returns empty output. - Diff-trace attribution resolution in `cli/src/services/hooks/mod.rs` looks up `session_models` when `model_id` or `tool_version` is missing/nullable, fills only missing fields from the stored row when available, preserves direct payload precedence, and continues persistence with `None` for unresolved attribution. - `cli/src/services/resilience.rs` defines bounded retry/timeout/backoff execution policy (`RetryPolicy`, `run_with_retry`) for transient operation hardening with deterministic failure messaging and retry observability. -- No user-invocable `sce sync` command is wired in the current runtime; local DB and Agent Trace checkout identity bootstrap flows through lifecycle providers aggregated by setup, while checkout/global DB health/repair and checkout-registry listing flow through the doctor surface. +- No user-invocable `sce sync` command is wired in the current runtime; local DB bootstrap and setup-time per-checkout Agent Trace DB initialization flow through lifecycle providers aggregated by setup, while checkout/global DB health/repair and checkout-registry listing flow through the doctor surface. - `cli/src/services/patch.rs` defines the standalone patch domain model (`ParsedPatch`, `PatchFileChange`, `FileChangeKind`, `PatchHunk`, `TouchedLine`, `TouchedLineKind`) for in-memory parsed unified-diff representation, capturing only touched lines (added/removed) plus minimal per-file/per-hunk metadata while excluding non-hunk headers and unchanged context lines. All types are `serde`-serializable/deserializable with `snake_case` JSON field naming. The module also provides `parse_patch`, a public parser function that converts raw unified-diff text (both `Index:` SVN-style and `diff --git` git-style formats) into `ParsedPatch` structs, with `ParseError` for actionable malformed-input diagnostics. Storage-agnostic JSON load helpers (`load_patch_from_json` for string input, `load_patch_from_json_bytes` for byte input) reconstruct `ParsedPatch` from serialized JSON content with `PatchLoadError` for actionable deserialization diagnostics. Its patch-set operations now include deterministic ordered combination plus target-shaped intersection that prefers exact touched-line matches and falls back to historical `kind`+`content` matching when incremental diffs and canonical post-commit diffs have drifted line numbers; `parse_patch`, `combine_patches`, and `intersect_patches` are consumed by the active post-commit hook runtime. - `cli/src/services/structured_patch.rs` defines the synchronous structured editor-hook derivation seam. It derives Claude `PostToolUse` `Write` create and `Edit` structured-patch payloads into canonical `ParsedPatch` values plus Claude session/tool metadata, returning deterministic skip reasons for unsupported events/tools/payload shapes. The module is pure and side-effect-free. It is wired into `sce hooks diff-trace` for Claude payload classification at intake (T04) and into `AgentTraceDb::recent_diff_trace_patches` for post-commit structured payload parsing dispatch at read time (T05). -- `cli/src/services/` contains module boundaries for command_registry, lifecycle, auth_command, config, setup, doctor, hooks, checkout identity, bash_policy, version, completion, help, patch, SCE web URL helpers, shared database infrastructure, local DB adapters, encrypted auth DB adapters, and Agent Trace DB adapters with explicit trait seams for future implementations. `cli/src/services/checkout/` owns checkout ID file infrastructure, central checkout-registry JSON persistence, and per-checkout Agent Trace DB lazy resolution for hook runtime; setup uses it through `AgentTraceDbLifecycle::setup()`, while `sce doctor` surfaces checkout identity and per-checkout Agent Trace DB status when a checkout ID exists and `sce doctor dbs` lists registered checkouts from the central registry. `cli/src/services/bash_policy.rs` owns both the CLI-agnostic evaluator logic and the hidden `sce policy bash` command adapter used by OpenCode and Claude hook callers. `cli/src/services/command_registry.rs` defines the static `RuntimeCommand` enum, deterministic `CommandRegistry` name catalog, and `build_default_registry()` function for command dispatch metadata. Service-owned command modules own the runtime command payload structs for help/help-text, version, completion, auth, config, setup, doctor, hooks, and policy. +- `cli/src/services/` contains module boundaries for command_registry, lifecycle, auth_command, config, setup, doctor, hooks, checkout identity, bash_policy, version, completion, help, patch, SCE web URL helpers, shared database infrastructure, local DB adapters, encrypted auth DB adapters, and Agent Trace DB adapters with explicit trait seams for future implementations. `cli/src/services/checkout/` owns checkout ID file infrastructure, central checkout-registry JSON persistence, and per-checkout Agent Trace DB lazy resolution for hook runtime; setup uses `AgentTraceDbLifecycle::setup()` to establish checkout identity and initialize the per-checkout DB, while `sce doctor` surfaces checkout identity and per-checkout Agent Trace DB status when a checkout ID exists and `sce doctor dbs` lists registered checkouts from the central registry. `cli/src/services/bash_policy.rs` owns both the CLI-agnostic evaluator logic and the hidden `sce policy bash` command adapter used by OpenCode and Claude hook callers. `cli/src/services/command_registry.rs` defines the static `RuntimeCommand` enum, deterministic `CommandRegistry` name catalog, and `build_default_registry()` function for command dispatch metadata. Service-owned command modules own the runtime command payload structs for help/help-text, version, completion, auth, config, setup, doctor, hooks, and policy. - `cli/README.md` is the crate-local onboarding and usage source of truth for placeholder behavior, safety limitations, and roadmap mapping back to service contracts. - `flake.nix` applies `rust-overlay` (`oxalica/rust-overlay`) to nixpkgs, pins `rust-bin.stable.1.93.1.default` with `rustfmt` + `clippy`, reads the package/check version from repo-root `.version`, builds `packages.sce` through Crane (`buildDepsOnly` -> `buildPackage`) with a filtered repo-root source that preserves the Cargo tree plus `cli/assets/hooks`, then injects generated OpenCode/Claude config payloads and schema inputs into a temporary `cli/assets/generated/` mirror during derivation unpack so `cli/build.rs` can package the crate without requiring committed generated crate assets. The same `cli/build.rs` now scans `cli/migrations/*/*.sql` and writes `cli/src/generated_migrations.rs` with deterministic migration constants sorted by numeric filename prefix. The flake runs `cli-tests`, `cli-clippy`, and `cli-fmt` plus the dedicated `integrations-install-tests`, `integrations-install-clippy`, and `integrations-install-fmt` derivations through Crane-backed paths so both Rust crates have first-class default-flake verification, exposes directory-scoped JS validation derivations for both `npm/` and the shared `config/lib/` plugin package root, and also exposes the non-default `apps.install-channel-integration-tests` flake app for install-channel integration coverage outside the default check set. The shared config-lib source set is rooted at `config/lib/` and includes the shared `package.json`, `bun.lock`, and `tsconfig.json` plus `agent-trace-plugin/` and `bash-policy-plugin/`; `config-lib-bun-tests` runs the bash-policy plugin wrapper tests from that shared root, while `config-lib-biome-check` and `config-lib-biome-format` run Biome over the copied shared package source. `.github/workflows/publish-crates.yml` follows the same asset-preparation rule but runs Cargo packaging from a temporary clean repository copy so crates.io publish no longer needs `--allow-dirty`. - The config-lib check source preserves repo-relative access to shared CLI patch fixtures: Nix copies a filtered repo-shaped source containing `config/lib/**` plus `cli/src/services/structured_patch/fixtures`, then runs Bun/Biome from `config/lib/`. diff --git a/context/cli/checkout-identity.md b/context/cli/checkout-identity.md index 8cae166d..1b71501f 100644 --- a/context/cli/checkout-identity.md +++ b/context/cli/checkout-identity.md @@ -2,7 +2,7 @@ The checkout identity service lives in `cli/src/services/checkout/`. -It assigns a stable identity to a local Git checkout or linked Git worktree. The setup lifecycle creates/reuses this identity and registers the checkout. Agent Trace hook runtime now resolves persistence through this identity and lazily initializes a per-checkout database. +It assigns a stable identity to a local Git checkout or linked Git worktree. The setup lifecycle creates/reuses this identity, initializes the per-checkout Agent Trace database, and records the database path. Agent Trace hook runtime resolves persistence through this identity and still lazily initializes or upgrades a per-checkout database when setup has not run or schema metadata is incomplete. ## Registry resilience @@ -36,8 +36,11 @@ During setup: - `checkout::resolve_git_dir(repo_root)` resolves the checkout metadata directory from Git truth. - `checkout::get_or_create_checkout_id(git_dir)` creates or reuses `/sce/checkout-id`. -- `checkout::registry::register_checkout(...)` writes or updates the central registry record with `database_path: null`. -- Setup output includes the checkout ID and states that the Agent Trace database will be created on first write. +- `checkout::registry::register_checkout(...)` first writes or updates the central registry record for the checkout identity. +- `default_paths::agent_trace_db_path_for_checkout(checkout_id)` computes `/sce/agent-trace-{checkout_id}.db`. +- `AgentTraceDb::open_at(path)` opens or creates the per-checkout DB and applies all embedded migrations before setup completes. +- `checkout::registry::register_checkout(...)` updates the central registry record with `database_path` after DB initialization succeeds. +- Setup output includes the checkout ID and initialized Agent Trace database path. During hook runtime: diff --git a/context/cli/cli-command-surface.md b/context/cli/cli-command-surface.md index 168acd81..f1a213d3 100644 --- a/context/cli/cli-command-surface.md +++ b/context/cli/cli-command-surface.md @@ -107,7 +107,7 @@ A user-invocable `sync` command is not wired in the current CLI surface; local D - `cli/src/services/agent_trace_db/mod.rs` provides `AgentTraceDb = TursoDb` plus `DiffTraceInsert<'_>`/`insert_diff_trace()` for parameterized writes to `diff_traces`, `AgentTraceInsert<'_>`/`insert_agent_trace()` for built `agent_traces`, and `SessionModelUpsert<'_>`/lookup helpers for durable `session_models` attribution keyed by `(tool_name, session_id)`. - `AgentTraceDb::new()` resolves the legacy/global `/sce/agent-trace.db` through `default_paths::agent_trace_db_path()`; active hook runtime resolves per-checkout DB files through checkout identity and `default_paths::agent_trace_db_path_for_checkout(checkout_id)`. - `cli/src/services/local_db/lifecycle.rs` implements `ServiceLifecycle` for local DB health checks and setup (DB path/health validation and DB bootstrap). -- `cli/src/services/agent_trace_db/lifecycle.rs` implements `ServiceLifecycle` for Agent Trace checkout identity setup plus checkout/global DB path-health validation and parent bootstrap. +- `cli/src/services/agent_trace_db/lifecycle.rs` implements `ServiceLifecycle` for Agent Trace checkout identity setup, setup-time per-checkout DB initialization, registry `database_path` recording, and checkout/global DB path-health validation plus parent bootstrap. - `sce setup` aggregates `ServiceLifecycle::setup` calls, which includes `LocalDbLifecycle::setup()` and `AgentTraceDbLifecycle::setup()` for DB initialization as part of local prerequisite bootstrap. - `sce doctor` aggregates `ServiceLifecycle::diagnose` and `ServiceLifecycle::fix` calls, which includes both DB lifecycle providers for DB path/health validation and can bootstrap missing canonical parent directories when repair mode is appropriate. diff --git a/context/cli/default-path-catalog.md b/context/cli/default-path-catalog.md index d5f0290b..2b8d5501 100644 --- a/context/cli/default-path-catalog.md +++ b/context/cli/default-path-catalog.md @@ -45,6 +45,6 @@ - `cli/src/services/setup/mod.rs` now resolves setup target directory names and required hook identifiers through `default_paths.rs` constants/accessors instead of owning those path literals locally. - `cli/src/services/default_paths.rs` includes a regression test that scans non-test Rust source under `cli/src/` and fails when new centralized production path literals appear outside the default-path service. - `cli/src/services/hooks/mod.rs` resolves the collision-safe `context/tmp/` path shape through shared path accessors. -- `cli/src/services/checkout/mod.rs` resolves per-checkout Agent Trace DB files through `agent_trace_db_path_for_checkout(checkout_id)` and stores the resulting path in the checkout registry after lazy DB initialization. +- `cli/src/services/agent_trace_db/lifecycle.rs` and `cli/src/services/checkout/mod.rs` resolve per-checkout Agent Trace DB files through `agent_trace_db_path_for_checkout(checkout_id)` and store the resulting path in the checkout registry after setup-time DB initialization or hook-runtime lazy initialization. See also: [cli-command-surface.md](./cli-command-surface.md), [../architecture.md](../architecture.md), [../context-map.md](../context-map.md) diff --git a/context/cli/service-lifecycle.md b/context/cli/service-lifecycle.md index 11c2d32f..4b5a2f51 100644 --- a/context/cli/service-lifecycle.md +++ b/context/cli/service-lifecycle.md @@ -34,7 +34,7 @@ - `cli/src/services/agent_trace_db/lifecycle.rs` defines `AgentTraceDbLifecycle`, the Agent Trace DB-owned provider. - `AgentTraceDbLifecycle::diagnose` emits per-checkout Agent Trace DB path and parent-directory readiness lifecycle health problems when `ctx.repo_root()` has an existing checkout ID; otherwise it falls back to the legacy global Agent Trace DB path. - `AgentTraceDbLifecycle::fix` bootstraps the resolved per-checkout Agent Trace DB parent directory for auto-fixable DB parent readiness problems, with the same global fallback outside checkout context. -- `AgentTraceDbLifecycle::setup` resolves the current checkout identity from `ctx.repo_root()` when available, creates or reuses `/sce/checkout-id`, registers the checkout with `database_path: null`, and returns setup messaging with the checkout ID. It does not initialize an Agent Trace DB file; hook runtime creates or upgrades the per-checkout DB lazily on first use. +- `AgentTraceDbLifecycle::setup` resolves the current checkout identity from `ctx.repo_root()` when available, creates or reuses `/sce/checkout-id`, registers the identity, resolves `/sce/agent-trace-{checkout_id}.db`, opens/creates that database through `AgentTraceDb::open_at(&db_path)` so embedded migrations are applied, updates the checkout registry with `database_path`, and returns setup messaging with the checkout ID plus initialized DB path. Hook runtime still creates or upgrades the per-checkout DB lazily when setup has not run or schema metadata is incomplete. - `doctor` runtime execution now aggregates lifecycle providers for diagnosis and repair: - `cli/src/services/doctor/command.rs` accepts any context implementing `ContextWithRepoRoot`. - `cli/src/services/doctor/mod.rs` resolves the repository root once, creates a repo-root-scoped borrowed context using `with_repo_root()`, and requests the full provider catalog with hooks included. diff --git a/context/context-map.md b/context/context-map.md index 01ddc324..47683939 100644 --- a/context/context-map.md +++ b/context/context-map.md @@ -11,12 +11,12 @@ Feature/domain context: - `context/cli/cli-command-surface.md` (CLI command surface including top-level help with ASCII art banner and gradient rendering, setup install flow, WorkOS device authorization flow + token storage behavior, attribution-only hook routing with validated post-commit `--remote-url` plumbing plus DB-backed `diff-trace` dual persistence and post-commit Agent Trace payload persistence including range `content_hash`, setup-owned local DB + Agent Trace DB bootstrap plus doctor DB health coverage, centralized Rust SCE web URL helpers in `services::agent_trace`, nested flake release package/app installability, Cargo local install + crates.io readiness policy, and hidden `sce policy bash` command adapter for bash-policy hook callers; `sce sync` command wiring is deferred to `0.4.0`; static `RuntimeCommand` enum dispatch lives in `services/command_registry.rs`, command payload structs for help/version/completion/auth/config/setup/doctor/hooks/policy are owned by their respective `services/{name}/command.rs` files, and clap-to-runtime conversion lives in `services/parse/command_runtime.rs`) - `context/cli/default-path-catalog.md` (canonical production CLI path-ownership contract centered on `cli/src/services/default_paths.rs`, including persisted auth/config files, named DB paths for auth/local/Agent Trace databases, repo-relative, embedded-asset, install, hook, and context-path families plus the regression guard that keeps production path ownership centralized) -- `context/cli/checkout-identity.md` (current checkout identity infrastructure in `cli/src/services/checkout/`, including `/sce/checkout-id` UUIDv7 storage, central `/sce/checkout-registry.json` records, atomic registry persistence, setup lifecycle integration that creates/reuses/registers checkout identity and emits checkout-ID messaging, per-checkout Agent Trace DB lazy resolution for hook runtime, `sce doctor` checkout identity display, and `sce doctor dbs` registry listing) +- `context/cli/checkout-identity.md` (current checkout identity infrastructure in `cli/src/services/checkout/`, including `/sce/checkout-id` UUIDv7 storage, central `/sce/checkout-registry.json` records, atomic registry persistence, setup lifecycle integration that creates/reuses/registers checkout identity, initializes the per-checkout Agent Trace DB and records `database_path`, hook-runtime lazy DB fallback, `sce doctor` checkout identity display, and `sce doctor dbs` registry listing) - `context/cli/patch-service.md` (standalone patch domain model, parser, JSON load helpers, and set operations in `cli/src/services/patch.rs` for in-memory parsed unified-diff representation, capturing only touched lines plus minimal per-file/per-hunk metadata, supporting both `Index:` SVN-style and `diff --git` git-style formats, with `ParseError` for actionable malformed-input diagnostics, `PatchLoadError`/`load_patch_from_json`/`load_patch_from_json_bytes` for storage-agnostic JSON reconstruction, `intersect_patches` for target-shaped overlap with exact-match-first and historical `kind`+`content` fallback semantics plus matched-constructed-line `session_id` and matched-constructed-hunk `model_id` provenance inheritance, and `combine_patches` for ordered patch combination with later-wins conflict resolution plus winning-hunk `model_id` provenance inheritance; `parse_patch`, `intersect_patches`, and `combine_patches` are consumed by the active post-commit hook runtime) - `context/cli/styling-service.md` (CLI text-mode output styling with `owo-colors` and `comfy-table`, TTY/`NO_COLOR` policy, shared helper API for human-facing surfaces, and per-column right-to-left RGB gradient banner rendering) - `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, canonical Pkl-generated `sce/config.json` schema ownership plus CLI embedding/reuse contract, 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 checkout identity registration, implemented doctor aggregation over diagnose/fix providers, and implemented setup aggregation over `setup` providers in order config → local_db → auth_db → agent_trace_db → hooks when requested) +- `context/cli/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 checkout identity registration plus per-checkout Agent Trace DB initialization, 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-observability-contract.md` (implemented config-backed runtime observability contract for the flat logging config-file shape with env-over-config fallback, 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` - `context/sce/shared-context-plan-workflow.md` (canonical `/change-to-plan` workflow, clarification/readiness gate contract, and one-task/one-atomic-commit task-slicing policy) @@ -45,7 +45,7 @@ Feature/domain context: - `context/sce/local-db.md` (implemented `cli/src/services/local_db/mod.rs` local database spec with `LocalDb = TursoDb`, canonical local DB path resolution, zero local migrations, and inherited retry-backed blocking `execute`/`query`/`query_map` methods using the shared Turso adapter) - `context/sce/shared-turso-db.md` (current shared `cli/src/services/db/mod.rs` Turso database infrastructure seam, including `DbSpec`, generic `TursoDb`, encrypted `EncryptedTursoDb`, build-time generated migration constants from `cli/build.rs`/`cli/src/generated_migrations.rs`, config-driven constructor/open-connect retry via `run_with_retry_sync`, no-migration `TursoDb::open_without_migrations()` / explicit-path `open_without_migrations_at(path)` for hot runtime paths, migration-running `new()` / explicit-path `new_at(path)` / `run_migrations()` with per-database `__sce_migrations` tracking, config-driven operation retry for `execute`/`query`/`query_map` with a `<= 2_000ms` default query failure budget, row-mapping excluded from retry, generic embedded migration execution, non-mutating `migration_metadata_problems()` and `ensure_schema_ready(setup_guidance)` readiness methods on `TursoDb`, and concrete wrappers for `LocalDb`, `AuthDb`, plus `AgentTraceDb`) - `context/sce/auth-db.md` (encrypted `AuthDb = EncryptedTursoDb` adapter, canonical `/sce/auth.db` path, build-time generated `AUTH_MIGRATIONS` from `cli/migrations/auth/`, auth credential schema and updated-at trigger baseline, lifecycle setup/doctor integration, encrypted token-storage persistence, and `SCE_AUTH_DB_ENCRYPTION_KEY`/OS credential-store key handling) -- `context/sce/agent-trace-db.md` (implemented `cli/src/services/agent_trace_db/mod.rs` Agent Trace database wrapper with legacy global `/sce/agent-trace.db` fallback plus active per-checkout `/sce/agent-trace-{checkout_id}.db` hook runtime paths, explicit-path migration and no-migration open APIs, non-mutating `ensure_schema_ready_for_hooks()` delegation to `TursoDb::ensure_schema_ready()`, lazy checkout DB initialization/upgrade, ordered `diff_traces`, `post_commit_patch_intersections`, `agent_traces`, parent `messages`, append-only `parts`, indexes/triggers, typed parameterized insert helpers, bounded chronological recent `diff_traces` query/parse support with malformed-row skip accounting, registered setup/doctor lifecycle provider, and active hook writers for `diff_traces`, post-commit intersection/agent-trace persistence, `messages`, and `parts`) +- `context/sce/agent-trace-db.md` (implemented `cli/src/services/agent_trace_db/mod.rs` Agent Trace database wrapper with legacy global `/sce/agent-trace.db` fallback plus active per-checkout `/sce/agent-trace-{checkout_id}.db` paths, explicit-path migration and no-migration open APIs, non-mutating `ensure_schema_ready_for_hooks()` delegation to `TursoDb::ensure_schema_ready()`, setup-time per-checkout DB initialization plus hook-runtime lazy initialization/upgrade fallback, ordered `diff_traces`, `post_commit_patch_intersections`, `agent_traces`, parent `messages`, append-only `parts`, indexes/triggers, typed parameterized insert helpers, bounded chronological recent `diff_traces` query/parse support with malformed-row skip accounting, registered setup/doctor lifecycle provider, and active hook writers for `diff_traces`, post-commit intersection/agent-trace persistence, `messages`, and `parts`) - `context/sce/agent-trace-core-schema-migrations.md` (historical reference for removed local DB schema bootstrap behavior; T03 now implements the actual local DB with migrations) - `context/sce/agent-trace-retry-queue-observability.md` (inactive local-hook retry path plus historical retry/metrics reference) - `context/sce/agent-trace-local-hooks-mvp-contract-gap-matrix.md` (T01 Local Hooks MVP production contract freeze and deterministic gap matrix for `agent-trace-local-hooks-production-mvp`) diff --git a/context/glossary.md b/context/glossary.md index 84be8479..276aeaf9 100644 --- a/context/glossary.md +++ b/context/glossary.md @@ -8,8 +8,8 @@ - verify-only root context pass: Context-sync mode for localized tasks where root-level behavior, architecture, and terminology are unchanged; root shared files are checked against code truth but are not edited by default. - generated-owned outputs: Files materialized by `config/pkl/generate.pkl` under `config/.opencode/**`, `config/automated/.opencode/**`, and `config/.claude/**`, including OpenCode plugin entrypoints, generated OpenCode `package.json` manifests, generated OpenCode `opencode.json` manifests, generated Claude plugin entrypoints, and Claude settings assets. - `canonical OpenCode plugin registration source`: Shared Pkl-authored plugin-registration definition in `config/pkl/base/opencode.pkl`, re-exported from `config/pkl/renderers/common.pkl` as the canonical plugin list/path JSON consumed by OpenCode renderers before they emit generated `opencode.json` manifests; the current entries are `sce-bash-policy` and `sce-agent-trace`. -- `checkout identity`: Stable UUIDv7 identifier assigned to a cloned repository or linked Git worktree, stored in `/sce/checkout-id` (never committed) and resolved via `git rev-parse --git-dir`. The identity is created or reused by `sce setup` through `AgentTraceDbLifecycle::setup()` and also auto-created by hook runtime when `sce setup` has not been run. Each checkout identity maps to a per-checkout Agent Trace DB file at `/sce/agent-trace-{checkout_id}.db` with lazy initialization. See `context/cli/checkout-identity.md`. -- `checkout registry`: Central JSON registry at `/sce/checkout-registry.json` managed by `cli/src/services/checkout/registry.rs` with atomic write-through-rename persistence. Records carry `checkout_id`, `path`, `last_seen`, optional `remote_url`, and optional `database_path` (populated after lazy per-checkout DB creation). `sce doctor dbs` lists all registered checkouts sorted by `last_seen` descending. See `context/cli/checkout-identity.md`. +- `checkout identity`: Stable UUIDv7 identifier assigned to a cloned repository or linked Git worktree, stored in `/sce/checkout-id` (never committed) and resolved via `git rev-parse --git-dir`. The identity is created or reused by `sce setup` through `AgentTraceDbLifecycle::setup()` and also auto-created by hook runtime when `sce setup` has not been run. Each checkout identity maps to a per-checkout Agent Trace DB file at `/sce/agent-trace-{checkout_id}.db`; setup initializes that DB with migrations, while hook runtime remains a lazy fallback when setup has not run or schema metadata is incomplete. See `context/cli/checkout-identity.md`. +- `checkout registry`: Central JSON registry at `/sce/checkout-registry.json` managed by `cli/src/services/checkout/registry.rs` with atomic write-through-rename persistence. Records carry `checkout_id`, `path`, `last_seen`, optional `remote_url`, and optional `database_path` (populated by setup-time DB initialization or hook-runtime lazy DB resolution). `sce doctor dbs` lists all registered checkouts sorted by `last_seen` descending. See `context/cli/checkout-identity.md`. - `generated OpenCode plugin registration contract`: Current generated-config contract where `config/.opencode/opencode.json` and `config/automated/.opencode/opencode.json` serialize the OpenCode `plugin` field from canonical Pkl sources for SCE-managed plugins only; the current registered paths are `./plugins/sce-bash-policy.ts` and `./plugins/sce-agent-trace.ts`. Claude does not use an OpenCode-style plugin manifest; Claude bash-policy enforcement is registered through generated `.claude/settings.json` as a `PreToolUse` `Bash` command hook running `sce policy bash`. - `root Biome contract`: Repository-root formatting/linting contract owned by `biome.json`, currently scoped only to `npm/**` and the shared `config/lib/**` plugin package root with package-local `node_modules/**` excluded; the canonical execution path is the root Nix dev shell (`nix develop -c biome ...`). - `cli flake checks`: Check derivations in root `flake.nix` (`checks..cli-tests`, `cli-clippy`, `cli-fmt`), dedicated `integrations/install` runner checks (`integrations-install-tests`, `integrations-install-clippy`, `integrations-install-fmt`), plus `pkl-parity`, split `npm/` JS checks (`npm-bun-tests`, `npm-biome-check`, `npm-biome-format`), and split shared `config/lib/` JS checks (`config-lib-bun-tests`, `config-lib-biome-check`, `config-lib-biome-format`); invoked via `nix flake check` at repo root. @@ -36,7 +36,7 @@ - `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. - `AuthDbLifecycle`: Lifecycle provider in `cli/src/services/auth_db/lifecycle.rs` that implements `ServiceLifecycle` for encrypted auth DB setup/doctor integration. `diagnose` collects auth DB path health problems, `fix` bootstraps missing auth DB parent directory, and `setup` calls `AuthDb::new()`. Registered as `LifecycleProviderId::AuthDb` in the shared lifecycle catalog. -- `agent trace DB adapter`: Module in `cli/src/services/agent_trace_db/mod.rs` that defines `AgentTraceDbSpec`, exposes `AgentTraceDb` as a `TursoDb` alias, keeps `/sce/agent-trace.db` as the legacy/global fallback path, supports explicit per-checkout DB paths through `open_at(path)` and `open_for_hooks_without_migrations_at(path)`, embeds an ordered split fresh-start baseline migration set (`001..016`) covering `diff_traces`, `post_commit_patch_intersections`, `agent_traces`, `messages`, `parts`, `session_models`, and `diff_traces.payload_type` plus indexes and triggers; provides typed parameterized insert/upsert helpers for diff traces with `payload_type` discriminator, post-commit intersection rows, built agent-trace rows, message/part batch inserts, and session model attribution; exposes chronological recent `diff_traces` query/parse support with malformed-row skip accounting; has `AgentTraceDbLifecycle` for setup/doctor integration; and is accessed by active Agent Trace hooks through checkout-scoped lazy DB resolution. +- `agent trace DB adapter`: Module in `cli/src/services/agent_trace_db/mod.rs` that defines `AgentTraceDbSpec`, exposes `AgentTraceDb` as a `TursoDb` alias, keeps `/sce/agent-trace.db` as the legacy/global fallback path, supports explicit per-checkout DB paths through `open_at(path)` and `open_for_hooks_without_migrations_at(path)`, embeds an ordered split fresh-start baseline migration set (`001..016`) covering `diff_traces`, `post_commit_patch_intersections`, `agent_traces`, `messages`, `parts`, `session_models`, and `diff_traces.payload_type` plus indexes and triggers; provides typed parameterized insert/upsert helpers for diff traces with `payload_type` discriminator, post-commit intersection rows, built agent-trace rows, message/part batch inserts, and session model attribution; exposes chronological recent `diff_traces` query/parse support with malformed-row skip accounting; has `AgentTraceDbLifecycle` for setup/doctor integration including setup-time per-checkout DB initialization; and is accessed by active Agent Trace hooks through checkout-scoped lazy DB resolution when setup has not prepared the DB. - `structured patch service`: Pure synchronous Rust service in `cli/src/services/structured_patch.rs` that derives supported structured editor hook payloads into canonical `ParsedPatch` values. The current implemented source is Claude `PostToolUse` payloads for `Write` creates and `Edit` structured patches; wired into `sce hooks diff-trace` for Claude payload classification at intake (T04) and into `AgentTraceDb::recent_diff_trace_patches` for post-commit structured payload parsing dispatch at read time (T05). - `Agent Trace SCE metadata`: Implementation-owned top-level metadata emitted by `build_agent_trace(...)` as `metadata.sce.version`; the value is sourced from the compiled `sce` CLI package version via `env!("CARGO_PKG_VERSION")`, is schema-validated with the rest of the payload, and is persisted in AgentTraceDb `agent_traces.trace_json` without changing the top-level Agent Trace payload/schema `version`. - `Agent Trace range content_hash`: Per-range `content_hash` emitted by `build_agent_trace(...)` inside every `ranges[]` entry as `murmur3:`, computed from the touched-line kind/content of the `post_commit_patch` or embedded-patch hunk used to emit that range while excluding positions, paths, metadata, and database IDs. @@ -54,7 +54,7 @@ - `DB query retry policy`: Retry policy used by `TursoDb::execute()`, `TursoDb::query()`, `TursoDb::query_map()`, `EncryptedTursoDb::execute()`, `EncryptedTursoDb::query()`, and `EncryptedTursoDb::query_map()` for local Turso operation retry, resolved from `policies.database_retry..query` via the `DATABASE_RETRY_CONFIG` `OnceLock` with fallback to hardcoded defaults (`5` attempts, `200ms` elapsed-attempt timeout, `25ms` initial backoff, `100ms` max backoff; default worst-case failure budget `<= 2_000ms`) through `run_with_retry_sync`. `query_map()` retries the initial query and row-fetch loop, then runs caller row mapping outside retry. - `__sce_migrations`: Per-database migration metadata table created by the shared `TursoConnectionCore` migration path behind public adapter `run_migrations()` methods; records applied migration IDs after successful execution so later setup/lifecycle initialization applies only migrations not yet recorded, while existing metadata-less DBs are brought forward by re-applying the current idempotent migration set and recording each ID. - `CLI generated migration manifest`: Build-time Rust source at `cli/src/generated_migrations.rs` written by `cli/build.rs` from immediate `cli/migrations//*.sql` directories; constants are named from the database directory (for example `AGENT_TRACE_MIGRATIONS`, `AUTH_MIGRATIONS`), sorted by the numeric filename prefix before `_`, and embed SQL via `include_str!`. -- `sync command deferral`: Current plan/state note that a user-invocable `sce sync` command is not wired yet and is deferred to `0.4.0`; local DB bootstrap flows through lifecycle providers aggregated by the setup command, Agent Trace setup establishes checkout identity while per-checkout DB files are lazy hook-runtime artifacts, and DB health/repair flows through the doctor surface. +- `sync command deferral`: Current plan/state note that a user-invocable `sce sync` command is not wired yet and is deferred to `0.4.0`; local DB bootstrap and setup-time per-checkout Agent Trace DB initialization flow through lifecycle providers aggregated by the setup command, while hook runtime keeps a lazy Agent Trace DB fallback for checkouts where setup has not run or schema metadata is incomplete, and DB health/repair flows through the doctor surface. - `CLI bounded resilience wrapper`: Shared policy in `cli/src/services/resilience.rs` (`RetryPolicy`, async `run_with_retry`, sync `run_with_retry_sync`) that applies deterministic retries/timeouts/capped backoff to transient operations, emits retry observability events, and returns actionable terminal failure guidance. The sync helper is currently wired into shared database constructors for local open/connect retry and into `TursoDb`/`EncryptedTursoDb` operation retry for `execute()`/`query()`/`query_map()`. - `setup service orchestration`: Setup execution logic in `cli/src/services/setup/command.rs` that resolves the repository root, derives a repo-root-scoped `AppContext` from the runtime command context, dispatches `setup` through the static lifecycle provider catalog (config → local_db → auth_db → agent_trace_db → hooks when requested), handles interactive target selection for config asset installation, and emits deterministic success messaging per target. - `setup target flags`: Mutually-exclusive `sce setup` target selectors (`--opencode`, `--claude`, `--both`) that force non-interactive mode for automation. @@ -66,7 +66,7 @@ - `setup required-hook install orchestration`: Setup-service flow in `cli/src/services/setup/mod.rs` (`install_required_git_hooks`) that resolves repository root + effective hooks directory via git truth, installs canonical required hooks with deterministic per-hook outcomes (`Installed`, `Updated`, `Skipped`), enforces executable permissions, and uses a unified remove-and-replace policy that removes existing hooks before swapping staged content with deterministic recovery guidance on swap failure. - `setup hooks CLI mode`: `sce setup` behavior activated by `--hooks` (with optional `--repo `), supporting both hooks-only runs and composable target+hooks runs in one invocation; implemented through `cli/src/services/setup/command.rs` + `cli/src/services/setup/mod.rs`, enforces deterministic compatibility validation (`--repo` requires `--hooks`; target flags remain mutually exclusive), and emits stable setup/hook status output. - `setup repo gate`: Preflight check in `cli/src/services/setup/command.rs` that calls `cli/src/services/setup/mod.rs` (`ensure_git_repository`) before any setup writes begin; enforces that all `sce setup` modes (config-only, hooks-only, combined, and interactive) require the current directory to be inside a git repository, failing with actionable guidance to run `git init` and rerun `sce setup` when the precondition is not met. -- `setup local bootstrap`: Pre-install setup bootstrap behavior now owned by lifecycle providers: `ConfigLifecycle::setup` creates missing `.sce/config.json` with the canonical schema-only payload (`{"$schema": "https://sce.crocoder.dev/config.json"}`), `LocalDbLifecycle::setup` initializes the canonical local DB via `LocalDb::new()`, and `AgentTraceDbLifecycle::setup` creates/reuses checkout identity and registry metadata without eagerly creating an Agent Trace DB; the setup command aggregates these calls before config/hooks dispatch across all setup modes. +- `setup local bootstrap`: Pre-install setup bootstrap behavior now owned by lifecycle providers: `ConfigLifecycle::setup` creates missing `.sce/config.json` with the canonical schema-only payload (`{"$schema": "https://sce.crocoder.dev/config.json"}`), `LocalDbLifecycle::setup` initializes the canonical local DB via `LocalDb::new()`, and `AgentTraceDbLifecycle::setup` creates/reuses checkout identity, initializes the per-checkout Agent Trace DB via `AgentTraceDb::open_at(path)`, and records `database_path`; the setup command aggregates these calls before config/hooks dispatch across all setup modes. - `CLI redaction-safe diagnostics contract`: baseline security behavior implemented via `cli/src/services/security.rs` (`redact_sensitive_text`) and applied to app-level errors, setup git-diagnostic surfacing, and observability output sinks so common secret-bearing token forms are masked before emission. - `setup directory write-permission probe`: deterministic pre-write guard implemented in `cli/src/services/security.rs` (`ensure_directory_is_writable`) and used by setup install/hook flows to fail fast with actionable remediation when target directories are not writable. - `setup --repo canonical path guard`: setup-hook runtime behavior in `cli/src/services/setup/mod.rs` that canonicalizes and validates user-supplied `--repo` paths as existing directories before git-root/hooks-path resolution. @@ -86,7 +86,7 @@ - `RunOutcome`: Generic final render payload in `cli/src/services/app_support.rs` (`RunOutcome`) carrying a command result, optional startup diagnostic, and optional logger implementing the logger trait boundary. Production construction in `cli/src/app.rs` uses the concrete observability logger, while rendering is not hardcoded to that production type. - `AppContext`: Generic borrowed dependency view in `cli/src/app.rs` passed through static command dispatch. `AppRuntime` owns the concrete production logger, telemetry, filesystem, and git implementations; `AppContext` stores references to those dependencies plus an optional `repo_root: Option`, not owned `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). The `repo_root` field is `None` at startup and command paths can derive a repo-root-scoped context with `AppContext::with_repo_root(...)` / `ContextWithRepoRoot`, preserving the borrowed runtime dependencies while attaching the resolved root. Narrow accessor traits (`HasLogger`, `HasTelemetry`, `HasFs`, `HasGit`, `HasRepoRoot`) let command and lifecycle call sites express capability requirements without depending on the full production context type; logger/telemetry/fs/git accessors use associated concrete capability types and return `&Self::{Capability}` rather than object-erased `&dyn ...` values. - `CommandRegistry`: Static command-name catalog in `cli/src/services/command_registry.rs`; populated by `build_default_registry()` and carried by `AppRuntime` during command dispatch. It exposes deterministic command-name membership for the current top-level command catalog (`help`, `auth`, `config`, `setup`, `doctor`, `hooks`, `version`, and `completion`) while actual parsed command payloads are represented by the `RuntimeCommand` enum rather than zero-arg boxed constructors. -- `ServiceLifecycle`: Compile-safe lifecycle trait seam in `cli/src/services/lifecycle.rs` with default no-op generic `diagnose`, `fix`, and `setup` methods over `C: HasRepoRoot`; it exposes lifecycle-owned health, fix, and setup result types, while doctor/setup adapt those records at orchestration boundaries before rendering command-owned output. The hooks service has `HooksLifecycle` for hook rollout diagnosis/fix/setup, the config service has `ConfigLifecycle` for global/repo-local config validation plus repo-local config bootstrap, local_db has `LocalDbLifecycle` for canonical local DB path health/bootstrap/setup, auth_db has `AuthDbLifecycle` for canonical auth DB path health/bootstrap/setup, and agent_trace_db has `AgentTraceDbLifecycle` for checkout identity setup, per-checkout Agent Trace DB path health/bootstrap when an ID exists, and legacy global fallback outside checkout context. Doctor runtime aggregates the static provider catalog for `diagnose` and `fix`; setup command aggregates providers for `setup` in order (config → local_db → auth_db → agent_trace_db → hooks when requested). +- `ServiceLifecycle`: Compile-safe lifecycle trait seam in `cli/src/services/lifecycle.rs` with default no-op generic `diagnose`, `fix`, and `setup` methods over `C: HasRepoRoot`; it exposes lifecycle-owned health, fix, and setup result types, while doctor/setup adapt those records at orchestration boundaries before rendering command-owned output. The hooks service has `HooksLifecycle` for hook rollout diagnosis/fix/setup, the config service has `ConfigLifecycle` for global/repo-local config validation plus repo-local config bootstrap, local_db has `LocalDbLifecycle` for canonical local DB path health/bootstrap/setup, auth_db has `AuthDbLifecycle` for canonical auth DB path health/bootstrap/setup, and agent_trace_db has `AgentTraceDbLifecycle` for checkout identity setup, setup-time per-checkout Agent Trace DB initialization, per-checkout Agent Trace DB path health/bootstrap when an ID exists, and legacy global fallback outside checkout context. Doctor runtime aggregates the static provider catalog for `diagnose` and `fix`; setup command aggregates providers for `setup` in order (config → local_db → auth_db → agent_trace_db → hooks when requested). - `lifecycle provider catalog`: Shared factory in `cli/src/services/lifecycle.rs` (`lifecycle_providers(include_hooks)`) that returns static `LifecycleProvider` enum values in deterministic config → local_db → auth_db → agent_trace_db → hooks order, used by doctor with hooks included and by setup with hooks included only when requested. The enum owns concrete-provider dispatch for `id`, `diagnose`, `fix`, and `setup` without boxed provider trait objects or `&dyn HasRepoRoot` lifecycle context erasure. - `sce config command surface`: Implemented top-level CLI command routed by `cli/src/app.rs` to `cli/src/services/config/mod.rs` (with schema/file-parsing delegated to `cli/src/services/config/schema.rs`, runtime precedence resolution delegated to `cli/src/services/config/resolver.rs`, and text/JSON output construction delegated to `cli/src/services/config/render.rs`), exposing `show`, `validate`, and `--help` for deterministic runtime config inspection and validation; `show` reports resolved flat logging observability values with provenance, while `validate` reports pass/fail plus validation issues and warnings only. - `sce version command surface`: Implemented top-level CLI command routed by `cli/src/app.rs` to `cli/src/services/version/mod.rs` plus `cli/src/services/version/command.rs`, exposing deterministic runtime identification output in text form by default and JSON form via `--format json`. diff --git a/context/overview.md b/context/overview.md index 03ee80c9..0e88feed 100644 --- a/context/overview.md +++ b/context/overview.md @@ -13,6 +13,7 @@ The same runtime also emits stable user-facing stderr error classes (`SCE-ERR-PA The app runtime now also includes a structured observability baseline in `cli/src/services/observability.rs`: deterministic env-controlled log threshold/format (`SCE_LOG_LEVEL` defaults to `error`; `SCE_LOG_FORMAT` defaults to `text`), optional file sink controls (`SCE_LOG_FILE`, `SCE_LOG_FILE_MODE` with deterministic `truncate` default), stable lifecycle event IDs, stderr-only primary emission so stdout command payloads remain pipe-safe, and `observability::traits` boundaries for logger and telemetry behavior. The app command dispatcher now enforces a centralized stdout/stderr stream contract in `cli/src/app.rs`: command success payloads are emitted on stdout only, while redacted user-facing diagnostics are emitted on stderr. `cli/src/app.rs` also now runs through explicit startup phases — dependency check, observability config resolution, runtime initialization, command parse/execute, and output rendering — with the app runtime carrying logger/telemetry plus static command-catalog state across those phases while preserving the existing exit-code and degraded-startup contracts. Within that lifecycle, `parse_command_phase` delegates clap-to-runtime conversion to `cli/src/services/parse/command_runtime.rs`, which returns a static `RuntimeCommand` enum, `services::app_support::execute_command_phase` logs around enum-owned `execute(...)` dispatch, and generic `RunOutcome` rendering logs classified errors through the logger trait boundary without coupling render support to the production logger type. Command payload structs for `help`, `version`, `completion`, `auth`, `config`, `setup`, `doctor`, and `hooks` live in service-owned `command.rs` files; `cli/src/services/command_registry.rs` owns the deterministic static command-name catalog and enum variants instead of boxed command trait objects. The CLI now also enforces a shared output-format parser contract in `cli/src/services/output_format.rs`, with canonical `--format ` parsing and command-specific actionable invalid-value guidance reused by `config` and `version` services. A compile-safe service lifecycle seam also exists in `cli/src/services/lifecycle.rs`: `ServiceLifecycle` exposes default no-op `diagnose`, `fix`, and `setup` methods against the narrow `HasRepoRoot` accessor, uses lifecycle-owned health/fix/setup result types, and owns the shared static `LifecycleProvider` enum catalog/factory with deterministic config → local_db → auth_db → agent_trace_db → hooks ordering and no boxed provider aggregation. Hooks has a `services/hooks/lifecycle.rs` provider for hook rollout diagnosis/fix/setup, config has a `services/config/lifecycle.rs` provider for global/repo-local config validation plus repo-local config bootstrap, local_db has a `services/local_db/lifecycle.rs` provider for canonical local DB path health, parent-directory readiness/bootstrap, and `LocalDb::new()` setup, auth_db has a `services/auth_db/lifecycle.rs` provider for canonical auth DB path health, parent-directory readiness/bootstrap, and `AuthDb::new()` setup, and agent_trace_db has a `services/agent_trace_db/lifecycle.rs` provider for checkout identity setup, per-checkout Agent Trace DB path health/parent readiness when an ID exists, and legacy global fallback outside checkout context. 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 also initializes the per-checkout Agent Trace DB via `AgentTraceDb::open_at(path)` and records the registry `database_path`; hook runtime lazy initialization remains a fallback when setup has not prepared the DB or schema metadata is incomplete. The CLI now also includes a shared text styling service in `cli/src/services/style.rs` that provides deterministic color enablement via `owo-colors`, automatic TTY detection, and `NO_COLOR` compliance for human-facing text output; stdout help/text surfaces, stderr diagnostics, and interactive prompt-adjacent text now reuse that shared styling policy while JSON, completion, and other non-interactive/machine-readable flows remain unstyled. The service exports color-detection, conditional styling, help/diagnostic/label/prompt styling, and `banner_with_gradient()` helpers for use across command surfaces while preserving pipe-safe output for non-interactive environments. The `setup` command includes an `inquire`-backed target-selection flow: default interactive selection for OpenCode/Claude/both with required-hook installation in the same run, explicit non-interactive target flags (`--opencode`, `--claude`, `--both`), deterministic mutually-exclusive validation, and non-destructive cancellation exits. The CLI now compiles an embedded setup asset manifest from `config/.opencode/**`, `config/.claude/**`, and `cli/assets/hooks/**` via `cli/build.rs`; `cli/src/services/setup/mod.rs` exposes deterministic normalized relative paths plus file bytes and target-scoped iteration without runtime reads from `config/`. The same build script also discovers `cli/migrations//*.sql` at compile time and writes `cli/src/generated_migrations.rs` constants sorted by numeric filename prefix for database migration consumers. @@ -28,7 +29,7 @@ The config service split now includes `cli/src/services/config/resolver.rs` as t Generated config now includes repo-local plugin assets for both profiles: `sce-bash-policy.ts` plus `sce-agent-trace.ts` are emitted under `config/.opencode/plugins/` and `config/automated/.opencode/plugins/`; the OpenCode agent-trace plugin extracts `{ sessionID, diff, time, model_id }` from user `message.updated` events with diffs, tracks per-session OpenCode client version from `session.created`/`session.updated`, and sends payloads to `sce hooks diff-trace` with `tool_name="opencode"` plus optional `tool_version`. Claude generated config now routes agent-trace events through `.claude/settings.json` command hooks that call `sce hooks` directly: `SessionStart` pipes raw hook event JSON to `sce hooks session-model`, and matched `PostToolUse Write|Edit|MultiEdit|NotebookEdit` pipes raw hook event JSON to `sce hooks diff-trace`; the Rust `session-model` path uses explicit payload version fields when present and otherwise best-effort captures `tool_version` from trimmed `claude --version` stdout when available. Rust handles extraction, validation, and persistence without a TypeScript intermediary; the former `config/.claude/plugins/sce-agent-trace.ts` Bun runtime was removed in T07 of the `claude-rust-diff-trace` plan. The Rust hook validates required fields, resolves missing/nullable diff-trace attribution from `session_models` while preserving direct payload precedence, and persists `model_id`, `tool_name`, and nullable/resolved `tool_version` into `diff_traces` through AgentTraceDb. Bash-policy now delegates OpenCode enforcement to the Rust `sce policy bash` command: the generated OpenCode plugin at `config/.opencode/plugins/sce-bash-policy.ts` (and `config/automated/.opencode/plugins/sce-bash-policy.ts`) is a thin wrapper that calls `sce policy bash --input normalized --output json` via `spawnSync` and throws on deny decisions; it no longer contains independent TypeScript policy logic. The former `bash-policy/runtime.ts` TypeScript runtime has been removed. Preset... The `doctor` command now exposes explicit inspection mode (`sce doctor`), repair-intent mode (`sce doctor --fix`), and checkout registry listing (`sce doctor dbs`) at the CLI/help/schema level while keeping diagnosis mode read-only. It now validates both current global operator health and the current repo/hook-integrity slice: state-root resolution, global config path resolution, global and repo-local `sce/config.json` readability/schema validity, local DB and checkout/global Agent Trace DB path + health, DB parent-directory readiness, git availability, non-repo vs bare-repo targeting failures, effective git hook-path source (default, per-repo `core.hooksPath`, or global `core.hooksPath`), hooks-directory health, required hook presence/executable permissions/content drift against canonical embedded SCE-managed hook assets, and repo-root OpenCode integration presence across the installed `plugins`, `agents`, `commands`, and `skills` inventories with embedded SHA-256 content verification for OpenCode assets. Text mode now renders the approved human-only layout with ordered `Environment` / `Configuration` / `Repository` / `Git Hooks` / `Integrations` sections, `SCE doctor diagnose` / `SCE doctor fix` headers, bracketed `[PASS]`/`[FAIL]`/`[MISS]` status tokens, shared-style green pass plus red fail/miss coloring when color output is enabled, simplified `label (path)` row formatting, top-level-only hook rows, and integration parent/child rows that reflect missing vs content-mismatch states; JSON output reports `checkout_identity` plus resolved Agent Trace DB health under `agent_trace_db` when a checkout ID exists, with global Agent Trace DB reporting retained outside checkout context. Fix mode reuses the canonical setup hook install flow to repair missing/stale/non-executable required hooks and can also bootstrap missing canonical DB parent directories while preserving manual-only guidance for unsupported issues. Claude bash-policy enforcement is also generated through `.claude/settings.json` as a `PreToolUse` `Bash` command hook running `sce policy bash`, so Claude and OpenCode both delegate to the Rust policy evaluator without a Claude TypeScript runtime. -Local database bootstrap is now owned by `LocalDbLifecycle::setup` and setup-command lifecycle aggregation. Agent Trace setup creates/reuses the current checkout ID and registers the checkout without eagerly creating an Agent Trace DB; hook runtime lazily creates or upgrades the per-checkout `agent-trace-{checkout_id}.db` on first Agent Trace write/read. Doctor validates checkout/global DB paths/health, can bootstrap missing parent directories, and lists registered checkouts through `sce doctor dbs`. Wiring a user-invocable `sce sync` command is deferred to `0.4.0`. +Local database bootstrap is now owned by `LocalDbLifecycle::setup` and setup-command lifecycle aggregation. Agent Trace setup creates/reuses the current checkout ID, initializes the per-checkout `agent-trace-{checkout_id}.db` with embedded migrations, and records `database_path` in the checkout registry; hook runtime still lazily creates or upgrades the per-checkout DB when setup has not run or schema metadata is incomplete. Doctor validates checkout/global DB paths/health, can bootstrap missing parent directories, and lists registered checkouts through `sce doctor dbs`. Wiring a user-invocable `sce sync` command is deferred to `0.4.0`. The repository-root flake (`flake.nix`) now applies a Rust overlay-backed stable toolchain pinned to `1.93.1` (with `rustfmt` and `clippy`), reads package/check version from the repo-root `.version` file, builds `packages.sce` through a Crane `buildDepsOnly` + `buildPackage` pipeline with filtered package sources for the Cargo tree plus required embedded config/assets, and runs `cli-tests`, `cli-clippy`, and `cli-fmt` through Crane-backed check derivations (`cargoTest`, `cargoClippy`, `cargoFmt`) that reuse the same filtered source/toolchain setup. The root flake also exposes release install/run outputs directly as `packages.sce` (with `packages.default = packages.sce`) plus `apps.sce` and `apps.default`, so `nix build .#default`, `nix run . -- --help`, `nix run .#sce -- --help`, and `nix profile install github:crocoder-dev/shared-context-engineering` all target the packaged `sce` binary through the same flake-owned entrypoints. The CLI Cargo package metadata now includes crates.io publication-ready fields with crate-local install guidance in `cli/README.md`; supported Cargo install paths are `cargo install shared-context-engineering --locked`, `cargo install --git https://github.com/crocoder-dev/shared-context-engineering shared-context-engineering --locked`, and local `cargo install --path cli --locked`. The published crate installs the `sce` binary. The crate also keeps `cargo clippy --manifest-path cli/Cargo.toml` warnings-denied through `cli/Cargo.toml` lint configuration, so an extra `-- -D warnings` flag is redundant. diff --git a/context/sce/agent-trace-db.md b/context/sce/agent-trace-db.md index e020cfa7..4858f4e7 100644 --- a/context/sce/agent-trace-db.md +++ b/context/sce/agent-trace-db.md @@ -58,11 +58,11 @@ Active hook runtime resolves per-checkout Agent Trace DB files: - Function: `agent_trace_db_path_for_checkout(checkout_id)` in `cli/src/services/default_paths.rs` - Path template: `/sce/agent-trace-{checkout_id}.db` - Checkout ID source: `/sce/checkout-id`, where `` comes from `git rev-parse --git-dir` -- Registry metadata: `/sce/checkout-registry.json` stores `database_path` after successful lazy initialization +- Registry metadata: `/sce/checkout-registry.json` stores `database_path` after successful setup-time DB initialization or hook-runtime lazy initialization ## Migrations -`AgentTraceDbSpec::migrations()` returns `generated_migrations::AGENT_TRACE_MIGRATIONS`, generated from `cli/migrations/agent-trace/` at build time. Migration IDs are the SQL filename stems, sorted by numeric prefix: +`AgentTraceDbSpec::migrations()` returns `generated_migrations::AGENT_TRACE_MIGRATIONS`, generated from `cli/migrations/agent-trace/` at build time. Setup-time `AgentTraceDb::open_at(path)` and hook-runtime fallback initialization both apply this migration set. Migration IDs are the SQL filename stems, sorted by numeric prefix: - `001_create_diff_traces.sql` - `002_create_post_commit_patch_intersections.sql` @@ -81,9 +81,9 @@ Active hook runtime resolves per-checkout Agent Trace DB files: - `015_create_session_models.sql` - `016_add_diff_traces_payload_type.sql` (migration ID `016_add_diff_traces_payload_type`; adds `payload_type TEXT NOT NULL DEFAULT 'patch'` to `diff_traces`) -The shared `TursoDb` runner records applied IDs in the database-local `__sce_migrations` table. Existing Agent Trace DB files without metadata are brought forward by re-applying the idempotent migration set and recording each ID, so rerunning `sce setup` / `AgentTraceDb::new()` applies later Agent Trace migrations to an already-created `~/.local/state/sce/agent-trace.db`. +The shared `TursoDb` runner records applied IDs in the database-local `__sce_migrations` table. Existing Agent Trace DB files without metadata are brought forward by re-applying the idempotent migration set and recording each ID, so rerunning `sce setup` / `AgentTraceDb::open_at(path)` applies later Agent Trace migrations to an already-created per-checkout DB. -Per-checkout hook DB resolution first tries `AgentTraceDb::open_for_hooks_without_migrations_at(path)` and `ensure_schema_ready_for_hooks()`. If the DB is missing, metadata is absent, or migrations are incomplete, the checkout resolver falls back to `AgentTraceDb::open_at(path)` so hook invocation lazily creates or upgrades the per-checkout DB before continuing. When the fallback also fails, the error context includes the fast-path failure reason (`(fast-path attempt: {fast_error})`) so both failure causes are visible in diagnostics. Readiness is based on exact migration metadata parity with `AGENT_TRACE_MIGRATIONS`, not table/index/column introspection. +Per-checkout hook DB resolution first tries `AgentTraceDb::open_for_hooks_without_migrations_at(path)` and `ensure_schema_ready_for_hooks()`. If setup has not initialized the DB, metadata is absent, or migrations are incomplete, the checkout resolver falls back to `AgentTraceDb::open_at(path)` so hook invocation lazily creates or upgrades the per-checkout DB before continuing. When the fallback also fails, the error context includes the fast-path failure reason (`(fast-path attempt: {fast_error})`) so both failure causes are visible in diagnostics. Readiness is based on exact migration metadata parity with `AGENT_TRACE_MIGRATIONS`, not table/index/column introspection. The `diff_traces` baseline migration creates: @@ -180,7 +180,7 @@ Both triggers compare `OLD.*` vs `NEW.*` for all mutable columns (excluding `upd - `diagnose()` reports per-checkout Agent Trace DB path and parent-directory readiness when a repo root has a checkout ID; otherwise it falls back to the legacy global Agent Trace DB path. When the DB file exists, it also performs a deep health check: opens the file via `open_for_hooks_without_migrations_at` and verifies schema readiness via `ensure_schema_ready_for_hooks`, reporting `AgentTraceDbConnectionFailed` if open fails or `AgentTraceDbSchemaNotReady` if the schema is incomplete. These deep-check problems are `ManualOnly` (not auto-fixable by `sce doctor --fix`); the remediation directs the operator to re-run `sce setup` or fix file permissions. - `fix()` bootstraps the resolved per-checkout DB parent directory for auto-fixable parent-readiness problems, with the same global fallback outside checkout context. -- `setup()` creates/reuses the current checkout identity when a repo root is available, registers the checkout in `/sce/checkout-registry.json` with `database_path: null`, and emits setup messaging with the checkout ID. It does not eagerly initialize the Agent Trace DB; the first hook write lazily creates the per-checkout DB. +- `setup()` creates/reuses the current checkout identity when a repo root is available, registers the identity, resolves `/sce/agent-trace-{checkout_id}.db` through `agent_trace_db_path_for_checkout(checkout_id)`, opens/creates it with `AgentTraceDb::open_at(&db_path)` to apply embedded migrations, updates `/sce/checkout-registry.json` with `database_path`, and emits setup messaging with the checkout ID plus initialized DB path. Hook runtime lazy initialization remains available for checkouts where setup has not run or schema metadata is incomplete. - `sce doctor` surfaces checkout identity and per-checkout Agent Trace DB health in the `Configuration` section when a checkout ID exists, with `[PASS]`/`[FAIL]`/`[MISS]` status tokens. Outside checkout context it falls back to the legacy/global Agent Trace DB row. JSON output includes `checkout_identity` when available plus the resolved `agent_trace_db` field. - `sce doctor dbs` lists registered checkout records from `/sce/checkout-registry.json` in text or JSON, sorted by `last_seen` descending. diff --git a/context/sce/shared-turso-db.md b/context/sce/shared-turso-db.md index be14a601..65d463fc 100644 --- a/context/sce/shared-turso-db.md +++ b/context/sce/shared-turso-db.md @@ -60,7 +60,7 @@ The shared module is exported from `cli/src/services/mod.rs` and compile-checked - `cli/src/services/agent_trace_db/mod.rs`: `AgentTraceDb = TursoDb`, with `AgentTraceDbSpec` resolving the legacy global `agent_trace_db_path()` fallback and loading ordered Agent Trace migrations for `diff_traces`, `post_commit_patch_intersections`, `agent_traces`, `session_models`, `messages`, and `parts` plus supporting indexes and triggers. Active hook runtime paths resolve per-checkout DB files through checkout identity and use `AgentTraceDb::open_for_hooks_without_migrations_at(path)` plus `ensure_schema_ready_for_hooks()` first, falling back to migration-running `AgentTraceDb::open_at(path)` when the per-checkout DB is absent or incomplete. - `cli/src/services/auth_db/mod.rs`: `AuthDb = EncryptedTursoDb`, with `AuthDbSpec` resolving `auth_db_path()` and loading 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. -All three database wrappers (local DB, auth DB, Agent Trace DB) have lifecycle providers. `lifecycle_providers(include_hooks)` registers database providers in order `LocalDbLifecycle` → `AuthDbLifecycle` → `AgentTraceDbLifecycle` before optional hooks. Setup initializes local/auth DBs and establishes Agent Trace checkout identity; Agent Trace per-checkout DB files are initialized lazily by hook runtime. Doctor diagnoses/fixes DB parent/path readiness through lifecycle providers. +All three database wrappers (local DB, auth DB, Agent Trace DB) have lifecycle providers. `lifecycle_providers(include_hooks)` registers database providers in order `LocalDbLifecycle` → `AuthDbLifecycle` → `AgentTraceDbLifecycle` before optional hooks. Setup initializes local/auth DBs, establishes Agent Trace checkout identity, initializes the per-checkout Agent Trace DB with migrations, and records its registry path; hook runtime keeps lazy initialization/upgrade as a fallback when setup has not run or schema metadata is incomplete. Doctor diagnoses/fixes DB parent/path readiness through lifecycle providers. ## Migration metadata From 42467823eb64aab8b47fad429721e2530677036b Mon Sep 17 00:00:00 2001 From: David Abram Date: Wed, 17 Jun 2026 21:51:27 +0200 Subject: [PATCH 8/8] checkout: Remove central registry, replace with filesystem DB discovery Delete `cli/src/services/checkout/registry.rs` (~220 lines) and strip all five call sites across setup (`agent_trace_db/lifecycle.rs`) and hook runtime (`checkout/mod.rs`). The registry was a single-consumer metadata cache (only `sce doctor dbs` read it) that injected fallible atomic-rename I/O into the hot path of every hook invocation, causing ENOENT failures under concurrent access. Replace `sce doctor dbs` registry listing with filesystem discovery that scans `/sce/agent-trace-*.db` files and derives `checkout_id`, `database_path`, and `last_seen` (from file mtime) from disk. Remove the now-stale `path` and `remote_url` output fields that were always hardcoded to "unknown"/None under filesystem discovery. Co-authored-by: SCE --- cli/src/services/agent_trace_db/lifecycle.rs | 44 +--- cli/src/services/checkout/mod.rs | 56 +---- cli/src/services/checkout/registry.rs | 220 ------------------ cli/src/services/doctor/mod.rs | 98 ++++++-- context/architecture.md | 10 +- context/cli/checkout-identity.md | 25 +- context/cli/cli-command-surface.md | 8 +- context/cli/default-path-catalog.md | 3 +- context/cli/service-lifecycle.md | 2 +- context/context-map.md | 2 +- context/glossary.md | 2 +- context/overview.md | 6 +- .../plans/drop-doctor-dbs-path-remote-url.md | 76 ++++++ context/plans/remove-checkout-registry.md | 92 ++++++++ context/sce/agent-trace-db.md | 6 +- 15 files changed, 284 insertions(+), 366 deletions(-) delete mode 100644 cli/src/services/checkout/registry.rs create mode 100644 context/plans/drop-doctor-dbs-path-remote-url.md create mode 100644 context/plans/remove-checkout-registry.md diff --git a/cli/src/services/agent_trace_db/lifecycle.rs b/cli/src/services/agent_trace_db/lifecycle.rs index eec4a1e8..8103628b 100644 --- a/cli/src/services/agent_trace_db/lifecycle.rs +++ b/cli/src/services/agent_trace_db/lifecycle.rs @@ -1,9 +1,8 @@ use anyhow::{Context, Result}; -use chrono::Utc; use std::path::{Path, PathBuf}; use crate::app::HasRepoRoot; -use crate::services::checkout::{self, registry}; +use crate::services::checkout; use crate::services::db::{bootstrap_db_parent, collect_db_path_health, DbSpec}; use crate::services::default_paths::{agent_trace_db_path, agent_trace_db_path_for_checkout}; use crate::services::lifecycle::{ @@ -56,14 +55,12 @@ impl ServiceLifecycle for AgentTraceDbLifecycle { fn setup(&self, ctx: &C) -> Result { let checkout_setup = match ctx.repo_root() { Some(repo_root) => { - let identity_setup = setup_checkout_identity(repo_root).context( + let checkout_id = setup_checkout_identity(repo_root).context( "Agent trace DB lifecycle setup failed while resolving checkout identity", )?; - Some( - initialize_checkout_agent_trace_db(repo_root, &identity_setup.checkout_id).context( - "Agent trace DB lifecycle setup failed while initializing checkout database", - )?, - ) + Some(initialize_checkout_agent_trace_db(&checkout_id).context( + "Agent trace DB lifecycle setup failed while initializing checkout database", + )?) } None => None, }; @@ -78,18 +75,13 @@ impl ServiceLifecycle for AgentTraceDbLifecycle { } } -#[derive(Clone, Debug, Eq, PartialEq)] -struct CheckoutIdentitySetup { - checkout_id: String, -} - #[derive(Clone, Debug, Eq, PartialEq)] struct CheckoutDatabaseSetup { checkout_id: String, database_path: PathBuf, } -fn setup_checkout_identity(repo_root: &std::path::Path) -> Result { +fn setup_checkout_identity(repo_root: &std::path::Path) -> Result { let git_dir = checkout::resolve_git_dir(repo_root).with_context(|| { format!( "failed to resolve git directory for checkout identity from '{}'", @@ -102,22 +94,11 @@ fn setup_checkout_identity(repo_root: &std::path::Path) -> Result Result { +fn initialize_checkout_agent_trace_db(checkout_id: &str) -> Result { let db_path = agent_trace_db_path_for_checkout(checkout_id).with_context(|| { format!("failed to resolve Agent Trace DB path for checkout ID {checkout_id}") })?; @@ -130,15 +111,6 @@ fn initialize_checkout_agent_trace_db( ) })?; - registry::register_checkout(registry::CheckoutRecord { - checkout_id: checkout_id.to_string(), - path: repo_root.display().to_string(), - last_seen: Utc::now().to_rfc3339(), - remote_url: None, - database_path: Some(db_path.display().to_string()), - }) - .context("failed to register checkout Agent Trace database path")?; - Ok(CheckoutDatabaseSetup { checkout_id: checkout_id.to_string(), database_path: db_path, diff --git a/cli/src/services/checkout/mod.rs b/cli/src/services/checkout/mod.rs index ef5bb5b2..b87f76bb 100644 --- a/cli/src/services/checkout/mod.rs +++ b/cli/src/services/checkout/mod.rs @@ -5,20 +5,14 @@ //! string, consistent with the existing `agent_trace_id` convention in this //! codebase. //! -//! The central JSON registry at `/sce/checkout-registry.json` tracks -//! all known checkouts with metadata like path, last-seen timestamp, remote URL, -//! and per-checkout database path. - -#![allow(dead_code)] - -pub mod registry; +//! Checkout databases are now discovered via filesystem scan in `sce doctor dbs` +//! (see `cli/src/services/doctor/mod.rs`). There is no central registry file. use std::path::{Path, PathBuf}; use std::process::Command; use anyhow::{anyhow, Context, Result}; -use chrono::Utc; -use uuid::{NoContext, Timestamp, Uuid}; +use uuid::Uuid; use crate::services::{ agent_trace_db::AgentTraceDb, default_paths::agent_trace_db_path_for_checkout, @@ -122,12 +116,7 @@ pub fn get_or_create_checkout_id(git_dir: &Path) -> Result { return Ok(existing_id); } - // Generate a new UUIDv7 using the current timestamp. - let now = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default(); - let timestamp = Timestamp::from_unix(NoContext, now.as_secs(), now.subsec_nanos()); - let checkout_id = Uuid::new_v7(timestamp).to_string(); + let checkout_id = Uuid::now_v7().to_string(); let checkout_dir = git_dir.join(SCE_CHECKOUT_DIR); std::fs::create_dir_all(&checkout_dir).with_context(|| { @@ -148,24 +137,6 @@ pub fn get_or_create_checkout_id(git_dir: &Path) -> Result { Ok(checkout_id) } -/// Convenience function that resolves the git directory from a repository root -/// and then gets or creates the checkout ID. -/// -/// This combines `resolve_git_dir` and `get_or_create_checkout_id` into a -/// single call for callers that have a repository root but not a git directory. -pub fn resolve_checkout_id_for_repo(repo_root: &Path) -> Result { - let git_dir = resolve_git_dir(repo_root)?; - get_or_create_checkout_id(&git_dir) -} - -/// Resolves or creates the current checkout identity and opens its per-checkout -/// Agent Trace DB, lazily initializing schema when needed. -pub fn resolve_or_create_agent_trace_db_for_current_checkout() -> Result<(AgentTraceDb, String)> { - let repo_root = std::env::current_dir() - .context("Failed to determine current directory for Agent Trace checkout DB resolution")?; - resolve_or_create_agent_trace_db_for_checkout(&repo_root) -} - /// Resolves or creates the checkout identity for `repo_root` and opens its /// per-checkout Agent Trace DB, lazily initializing schema when needed. pub fn resolve_or_create_agent_trace_db_for_checkout( @@ -187,8 +158,6 @@ pub fn resolve_or_create_agent_trace_db_for_checkout( format!("failed to resolve Agent Trace DB path for checkout ID {checkout_id}") })?; - register_checkout_for_db(repo_root, &checkout_id, None)?; - let fast_open = AgentTraceDb::open_for_hooks_without_migrations_at(&db_path) .and_then(|db| db.ensure_schema_ready_for_hooks().map(|()| db)); let db = match fast_open { @@ -201,22 +170,5 @@ pub fn resolve_or_create_agent_trace_db_for_checkout( })?, }; - register_checkout_for_db(repo_root, &checkout_id, Some(db_path.display().to_string()))?; - Ok((db, checkout_id)) } - -fn register_checkout_for_db( - repo_root: &Path, - checkout_id: &str, - database_path: Option, -) -> Result<()> { - registry::register_checkout(registry::CheckoutRecord { - checkout_id: checkout_id.to_string(), - path: repo_root.display().to_string(), - last_seen: Utc::now().to_rfc3339(), - remote_url: None, - database_path, - }) - .context("failed to register checkout identity") -} diff --git a/cli/src/services/checkout/registry.rs b/cli/src/services/checkout/registry.rs deleted file mode 100644 index f8267c8b..00000000 --- a/cli/src/services/checkout/registry.rs +++ /dev/null @@ -1,220 +0,0 @@ -//! Central checkout registry. -//! -//! The checkout registry is a JSON file at `/sce/checkout-registry.json` -//! that tracks all known checkouts with metadata like path, last-seen timestamp, -//! remote URL, and per-checkout database path. - -#![allow(dead_code)] - -use std::path::PathBuf; - -use anyhow::{Context, Result}; -use serde::{Deserialize, Serialize}; - -use crate::services::default_paths::resolve_state_data_root; - -/// File name for the checkout registry inside `/sce/`. -const CHECKOUT_REGISTRY_FILE: &str = "checkout-registry.json"; - -/// A single checkout record in the central registry. -#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] -pub struct CheckoutRecord { - /// Stable `UUIDv7` checkout identity. - pub checkout_id: String, - /// Absolute path to the repository root (or worktree root). - pub path: String, - /// ISO 8601 / RFC 3339 timestamp of the last time this checkout was seen. - pub last_seen: String, - /// Remote URL if available (e.g. the `origin` URL). - pub remote_url: Option, - /// Absolute path to the per-checkout database file, if it has been created. - pub database_path: Option, -} - -/// The central checkout registry, persisted as a JSON file. -#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] -pub struct CheckoutRegistry { - /// Registered checkouts, keyed by checkout ID for efficient lookup. - /// Serialized as a JSON array for human readability. - #[serde(default)] - pub checkouts: Vec, -} - -/// Returns the canonical path to the checkout registry file. -/// -/// The path is `/sce/checkout-registry.json`, where `state_root` -/// comes from the shared default-path catalog. -pub fn checkout_registry_path() -> Result { - Ok(resolve_state_data_root()? - .join("sce") - .join(CHECKOUT_REGISTRY_FILE)) -} - -/// Reads the checkout registry from disk. -/// -/// Returns an empty registry if the file does not exist. -/// Returns an error if the file exists but cannot be parsed. -pub fn read_registry() -> Result { - let path = checkout_registry_path()?; - - if !path.exists() { - return Ok(CheckoutRegistry::default()); - } - - let content = std::fs::read_to_string(&path) - .with_context(|| format!("Failed to read checkout registry from '{}'", path.display()))?; - - // Empty or whitespace-only files are treated as corrupt — delete and start fresh. - if content.trim().is_empty() { - let _ = std::fs::remove_file(&path); - eprintln!( - "[WARN] Empty checkout registry at '{}' — removing and recreating from scratch", - path.display() - ); - return Ok(CheckoutRegistry::default()); - } - - let registry: CheckoutRegistry = serde_json::from_str(&content).with_context(|| { - format!( - "Failed to parse checkout registry from '{}'", - path.display() - ) - })?; - - Ok(registry) -} - -/// Writes the checkout registry to disk using atomic write-through-rename. -/// -/// This creates the parent directory if it doesn't exist, writes to a temporary -/// file, and then renames the temporary file to the target path. This ensures -/// that concurrent readers never see a partially-written registry. -pub fn write_registry(registry: &CheckoutRegistry) -> Result<()> { - let path = checkout_registry_path()?; - - // Ensure the parent directory exists. - if let Some(parent) = path.parent() { - std::fs::create_dir_all(parent).with_context(|| { - format!("Failed to create registry directory '{}'", parent.display()) - })?; - } - - // Serialize the registry. - let content = serde_json::to_string_pretty(registry) - .with_context(|| "Failed to serialize checkout registry")?; - - // Write to a temporary file first, then rename for atomicity. - // Use a PID-unique suffix so concurrent `sce hooks` processes never - // collide on the same temp file. - let temp_path = path.with_extension(format!("json.tmp.{}", std::process::id())); - - std::fs::write(&temp_path, &content).with_context(|| { - format!( - "Failed to write temporary registry to '{}'", - temp_path.display() - ) - })?; - - std::fs::rename(&temp_path, &path).with_context(|| { - format!( - "Failed to rename temporary registry from '{}' to '{}'", - temp_path.display(), - path.display() - ) - })?; - - // Clean up any stale `.tmp.*` files left behind by previous crashed - // processes. The current process's temp file was just renamed, so - // anything remaining matching the pattern is orphaned. - if let Some(parent) = path.parent() { - if let Ok(entries) = std::fs::read_dir(parent) { - let stale_prefix = format!("{CHECKOUT_REGISTRY_FILE}.tmp."); - for entry in entries { - let Ok(entry) = entry else { - continue; - }; - let name = entry.file_name(); - if name.to_string_lossy().starts_with(&stale_prefix) { - let _ = std::fs::remove_file(entry.path()); - } - } - } - } - - Ok(()) -} - -/// Registers a checkout in the central registry. -/// -/// If a checkout with the same `checkout_id` already exists, its `path`, -/// `last_seen`, and `remote_url` fields are updated. The `database_path` field -/// is preserved from the existing record if present. -/// -/// After registration, the registry is persisted to disk. -pub fn register_checkout(record: CheckoutRecord) -> Result<()> { - let mut registry = read_registry()?; - - if let Some(existing) = registry - .checkouts - .iter_mut() - .find(|r| r.checkout_id == record.checkout_id) - { - // Update existing record, preserving database_path if the new record - // doesn't have one. - existing.path = record.path; - existing.last_seen = record.last_seen; - existing.remote_url = record.remote_url; - if record.database_path.is_some() { - existing.database_path = record.database_path; - } - } else { - registry.checkouts.push(record); - } - - write_registry(®istry) -} - -/// Updates the `last_seen` timestamp for a checkout in the registry. -/// -/// Returns `Ok(())` if the checkout was found and updated, or an error if the -/// checkout ID is not found in the registry. -pub fn update_checkout_last_seen(checkout_id: &str, last_seen: &str) -> Result<()> { - let mut registry = read_registry()?; - - let record = registry - .checkouts - .iter_mut() - .find(|r| r.checkout_id == checkout_id) - .ok_or_else(|| anyhow::anyhow!("Checkout ID '{checkout_id}' not found in registry"))?; - - record.last_seen = last_seen.to_string(); - write_registry(®istry) -} - -/// Lists all registered checkouts from the central registry. -/// -/// Returns an empty list if the registry file does not exist. -pub fn list_checkouts() -> Result> { - let registry = read_registry()?; - Ok(registry.checkouts) -} - -/// Removes a checkout from the central registry by checkout ID. -/// -/// Returns `Ok(true)` if the checkout was found and removed, `Ok(false)` if -/// the checkout was not found, or an error if the registry could not be -/// read or written. -pub fn remove_checkout(checkout_id: &str) -> Result { - let mut registry = read_registry()?; - - let original_len = registry.checkouts.len(); - registry.checkouts.retain(|r| r.checkout_id != checkout_id); - - let removed = registry.checkouts.len() < original_len; - - if removed { - write_registry(®istry)?; - } - - Ok(removed) -} diff --git a/cli/src/services/doctor/mod.rs b/cli/src/services/doctor/mod.rs index 7ab5774a..4c5d8de0 100644 --- a/cli/src/services/doctor/mod.rs +++ b/cli/src/services/doctor/mod.rs @@ -3,10 +3,10 @@ use std::path::{Path, PathBuf}; use std::process::Command; use anyhow::{Context, Result}; +use chrono::{DateTime, Utc}; use serde_json::json; use crate::app::{ContextWithRepoRoot, HasRepoRoot}; -use crate::services::checkout::registry::{self, CheckoutRecord}; use crate::services::default_paths::{resolve_sce_default_locations, resolve_state_data_root}; use crate::services::lifecycle::{ lifecycle_providers, FixOutcome, HealthCategory, HealthFixability, HealthProblem, @@ -68,6 +68,17 @@ struct DoctorExecution { fix_results: Vec, } +/// A checkout discovered from agent-trace-*.db files on disk. +#[derive(Clone, Debug)] +struct DiscoveredCheckout { + /// Stable `UUIDv7` checkout identity extracted from the filename. + checkout_id: String, + /// Absolute path to the per-checkout database file. + database_path: String, + /// ISO 8601 timestamp from file mtime. + last_seen: String, +} + #[derive(Clone, Debug, Eq, PartialEq)] struct ProviderDoctorProblem { provider_id: LifecycleProviderId, @@ -95,7 +106,8 @@ where } fn run_doctor_dbs(format: DoctorFormat) -> Result { - let mut checkouts = registry::list_checkouts().context("failed to read checkout registry")?; + let mut checkouts = discover_checkouts_from_filesystem() + .context("failed to discover checkouts from filesystem")?; sort_checkouts_by_last_seen_desc(&mut checkouts); match format { @@ -104,7 +116,71 @@ fn run_doctor_dbs(format: DoctorFormat) -> Result { } } -fn sort_checkouts_by_last_seen_desc(checkouts: &mut [CheckoutRecord]) { +/// Scans `/sce/` for `agent-trace-*.db` files and derives checkout +/// metadata from each discovered file. +fn discover_checkouts_from_filesystem() -> Result> { + let state_root = resolve_state_data_root().context("failed to resolve state data root")?; + let sce_dir = state_root.join("sce"); + + if !sce_dir.is_dir() { + return Ok(Vec::new()); + } + + let mut checkouts: Vec = Vec::new(); + + for entry in fs::read_dir(&sce_dir) + .with_context(|| format!("failed to read sce directory '{}'", sce_dir.display()))? + { + let entry = entry.with_context(|| { + format!("failed to read directory entry in '{}'", sce_dir.display()) + })?; + + let file_name = entry.file_name(); + let file_name_str = file_name.to_string_lossy(); + + // Match agent-trace-{id}.db + let Some(stripped) = file_name_str.strip_prefix("agent-trace-") else { + continue; + }; + let Some(checkout_id) = stripped.strip_suffix(".db") else { + continue; + }; + if checkout_id.is_empty() { + continue; + } + + let metadata = entry + .metadata() + .with_context(|| format!("failed to read metadata for '{}'", entry.path().display()))?; + + if !metadata.is_file() { + continue; + } + + let last_seen: String = metadata.modified().ok().map_or_else( + || String::from("unknown"), + |mtime| { + let dt: DateTime = mtime.into(); + dt.to_rfc3339() + }, + ); + + let database_path = entry + .path() + .to_str() + .map_or_else(|| String::from("unknown"), String::from); + + checkouts.push(DiscoveredCheckout { + checkout_id: checkout_id.to_string(), + database_path, + last_seen, + }); + } + + Ok(checkouts) +} + +fn sort_checkouts_by_last_seen_desc(checkouts: &mut [DiscoveredCheckout]) { checkouts.sort_by(|left, right| { right .last_seen @@ -113,7 +189,7 @@ fn sort_checkouts_by_last_seen_desc(checkouts: &mut [CheckoutRecord]) { }); } -fn render_doctor_dbs_text(checkouts: &[CheckoutRecord]) -> String { +fn render_doctor_dbs_text(checkouts: &[DiscoveredCheckout]) -> String { let mut lines = vec![String::from("SCE doctor dbs")]; if checkouts.is_empty() { @@ -123,32 +199,22 @@ fn render_doctor_dbs_text(checkouts: &[CheckoutRecord]) -> String { for checkout in checkouts { lines.push(format!("checkout_id: {}", checkout.checkout_id)); - lines.push(format!(" path: {}", checkout.path)); - lines.push(format!( - " database_path: {}", - checkout.database_path.as_deref().unwrap_or("none") - )); + lines.push(format!(" database_path: {}", checkout.database_path)); lines.push(format!(" last_seen: {}", checkout.last_seen)); - lines.push(format!( - " remote_url: {}", - checkout.remote_url.as_deref().unwrap_or("none") - )); } lines.join("\n") } -fn render_doctor_dbs_json(checkouts: &[CheckoutRecord]) -> Result { +fn render_doctor_dbs_json(checkouts: &[DiscoveredCheckout]) -> Result { let payload = json!({ "status": "ok", "command": NAME, "subcommand": "dbs", "checkouts": checkouts.iter().map(|checkout| json!({ "checkout_id": checkout.checkout_id, - "path": checkout.path, "database_path": checkout.database_path, "last_seen": checkout.last_seen, - "remote_url": checkout.remote_url, })).collect::>(), }); diff --git a/context/architecture.md b/context/architecture.md index 1f0ab7ea..3a9807f0 100644 --- a/context/architecture.md +++ b/context/architecture.md @@ -105,8 +105,8 @@ The repository includes a new placeholder Rust binary crate at `cli/`. - `cli/src/services/output_format.rs` defines the canonical shared CLI output-format contract (`OutputFormat`) for supporting commands, with deterministic `text|json` parsing and command-scoped actionable invalid-value guidance. - `cli/src/services/config/types.rs` is the canonical owner for the shared runtime/config primitive seam used by the CLI: `LogLevel`, `LogFormat`, `LogFileMode`, the observability env-key constants, and the shared bool parsing helpers used by both config resolution and observability bootstrap; `cli/src/services/config/mod.rs` re-exports those primitives through the facade. - `cli/src/services/capabilities.rs` defines the current broad CLI capability traits consumed by the borrowed, compile-time-typed `AppContext`: `FsOps` with `StdFsOps` for filesystem operations and `GitOps` with `ProcessGitOps` for git command execution plus repository-root/hooks-directory resolution. Existing service internals do not consume these traits directly yet; command execution uses narrow accessors and repo-root-scoped context derivation. -- `cli/src/services/lifecycle.rs` defines the current compile-safe lifecycle seam. `ServiceLifecycle` has default no-op generic `diagnose`, `fix`, and `setup` methods over `C: HasRepoRoot`, with lifecycle-owned health, fix, and setup result types so the trait contract is not publicly anchored to doctor/setup module types or the full `AppContext` shape. The same module owns the static `LifecycleProvider` enum and shared `lifecycle_providers(include_hooks)` catalog/factory, returning providers in deterministic order (config → local_db → auth_db → agent_trace_db → hooks when requested); enum dispatch calls each concrete provider through generic context methods without boxed lifecycle-provider allocation or repo-root trait-object context erasure. Hooks exposes a `HooksLifecycle` provider in `cli/src/services/hooks/lifecycle.rs` for hook rollout diagnosis/fix/setup using lifecycle-owned health records plus the canonical required-hook installer. Config exposes a `ConfigLifecycle` provider in `cli/src/services/config/lifecycle.rs` for global/repo-local config validation and repo-local `.sce/config.json` bootstrap. local_db exposes a `LocalDbLifecycle` provider in `cli/src/services/local_db/lifecycle.rs` for canonical local DB path health, parent-directory readiness/bootstrap, and `LocalDb::new()` setup. auth_db exposes an `AuthDbLifecycle` provider in `cli/src/services/auth_db/lifecycle.rs` for canonical auth DB path health, parent-directory readiness/bootstrap, and `AuthDb::new()` setup. agent_trace_db exposes an `AgentTraceDbLifecycle` provider in `cli/src/services/agent_trace_db/lifecycle.rs` for setup-time checkout identity creation/registry registration when a repo root is available, per-checkout Agent Trace DB path health/fix when an ID exists, and legacy global DB fallback outside checkout context. Doctor runtime aggregates the full provider catalog for `diagnose` and `fix` and adapts lifecycle records into doctor report/fix records at the orchestration boundary; setup command aggregates the shared catalog for `setup` with hooks included only when requested and adapts hook setup outcomes before rendering setup-owned messages. -- Agent Trace lifecycle setup uses checkout identity to resolve the per-checkout DB path, initializes it with `AgentTraceDb::open_at(path)`, and records `database_path`; hook runtime keeps lazy per-checkout DB initialization/upgrade only as the fallback for setup-not-run or incomplete-schema cases. +- `cli/src/services/lifecycle.rs` defines the current compile-safe lifecycle seam. `ServiceLifecycle` has default no-op generic `diagnose`, `fix`, and `setup` methods over `C: HasRepoRoot`, with lifecycle-owned health, fix, and setup result types so the trait contract is not publicly anchored to doctor/setup module types or the full `AppContext` shape. The same module owns the static `LifecycleProvider` enum and shared `lifecycle_providers(include_hooks)` catalog/factory, returning providers in deterministic order (config → local_db → auth_db → agent_trace_db → hooks when requested); enum dispatch calls each concrete provider through generic context methods without boxed lifecycle-provider allocation or repo-root trait-object context erasure. Hooks exposes a `HooksLifecycle` provider in `cli/src/services/hooks/lifecycle.rs` for hook rollout diagnosis/fix/setup using lifecycle-owned health records plus the canonical required-hook installer. Config exposes a `ConfigLifecycle` provider in `cli/src/services/config/lifecycle.rs` for global/repo-local config validation and repo-local `.sce/config.json` bootstrap. local_db exposes a `LocalDbLifecycle` provider in `cli/src/services/local_db/lifecycle.rs` for canonical local DB path health, parent-directory readiness/bootstrap, and `LocalDb::new()` setup. auth_db exposes an `AuthDbLifecycle` provider in `cli/src/services/auth_db/lifecycle.rs` for canonical auth DB path health, parent-directory readiness/bootstrap, and `AuthDb::new()` setup. agent_trace_db exposes an `AgentTraceDbLifecycle` provider in `cli/src/services/agent_trace_db/lifecycle.rs` for setup-time checkout identity creation when a repo root is available, per-checkout Agent Trace DB path health/fix when an ID exists, and legacy global DB fallback outside checkout context. Doctor runtime aggregates the full provider catalog for `diagnose` and `fix` and adapts lifecycle records into doctor report/fix records at the orchestration boundary; setup command aggregates the shared catalog for `setup` with hooks included only when requested and adapts hook setup outcomes before rendering setup-owned messages. +- Agent Trace lifecycle setup uses checkout identity to resolve the per-checkout DB path and initializes it with `AgentTraceDb::open_at(path)`; hook runtime keeps lazy per-checkout DB initialization/upgrade only as the fallback for setup-not-run or incomplete-schema cases. - `cli/src/services/auth_command/mod.rs` defines the implemented auth command surface for `sce auth login|renew|logout|status`, including device-flow login, stored-token renewal (`--force` supported for renew), logout, and status rendering in text/JSON formats; `cli/src/services/auth_command/command.rs` owns the `AuthCommand` payload used by the static `RuntimeCommand` enum. - `cli/src/services/db/mod.rs` provides the shared generic Turso infrastructure seam: `DbSpec` supplies a service-specific name, path, ordered embedded migrations, and config-file lookup key (`db_config_key()`), while `TursoDb` owns parent-directory creation, `Builder::new_local(...)` initialization (with `experimental_multiprocess_wal(true)` for safe concurrent access), Turso connection setup, tokio current-thread runtime bridging, retry-backed blocking `execute`/`query`/`query_map` wrappers, and generic migration execution with per-database `__sce_migrations` metadata. `TursoDb::new()` and `EncryptedTursoDb::new()` wrap only their local open/connect block in `run_with_retry_sync` using a config-driven connection-open policy resolved from the `DATABASE_RETRY_CONFIG` `OnceLock` with fallback to hardcoded defaults, while both adapters' `execute()`, `query()`, and `query_map()` methods use a config-driven operation policy from the same source. Both adapters' `query_map()` methods retry the initial query and row-fetch loop, then apply caller row mapping after retry completion. Migration execution is not retried. The same module also provides `EncryptedTursoDb`, a structurally parallel encrypted adapter that resolves the encryption key through `encryption_key::get_or_create_encryption_key()`, enables Turso local encryption with strict `aegis256` cipher selection, and exposes retry-backed synchronous wrappers plus migration execution. `cli/src/services/db/encryption_key.rs` first derives a Turso-compatible 64-character hex key from non-empty `SCE_AUTH_DB_ENCRYPTION_KEY` env-secret text when present, otherwise falls back to keyring-backed credential-store get-or-create behavior; no plaintext auth DB fallback exists. - `cli/src/services/local_db/mod.rs` provides the concrete local DB spec and `LocalDb` type alias over the shared generic `TursoDb` adapter. `LocalDbSpec` resolves the deterministic persistent runtime DB target through the shared default-path seam and declares no local migrations; `TursoDb` supplies retry-backed blocking `execute`/`query`, parent-directory creation, Turso connection setup, tokio current-thread runtime bridging, and generic migration execution. @@ -116,17 +116,17 @@ The repository includes a new placeholder Rust binary crate at `cli/`. - `cli/src/services/setup/mod.rs` defines the setup command contract (`SetupMode`, `SetupTarget`, `SetupRequest`, CLI flag parser/validator), an `inquire`-backed interactive target prompter (`InquireSetupTargetPrompter`), setup dispatch outcomes (proceed/cancelled), compile-time embedded asset access (`EmbeddedAsset`, target-scoped iterators, required-hook asset iterators/lookups) generated by `cli/build.rs` from the ephemeral crate-local `cli/assets/generated/config/{opencode,claude}/**` mirror plus `cli/assets/hooks/**`, and focused internal support seams for install-flow vs prompt-flow logic; `cli/src/services/setup/command.rs` owns the `SetupCommand` payload used by the static `RuntimeCommand` enum and executes against any context implementing repo-root scoping. Its install engine/orchestrator stages embedded files and uses a unified remove-and-replace policy (removing existing targets before swapping staged content, with deterministic recovery guidance on swap failure and no backup artifact creation), and formats deterministic completion messaging; required-hook install orchestration (`install_required_git_hooks`) follows the same remove-and-replace policy (removing existing hooks before swapping staged content, with deterministic recovery guidance on swap failure). The setup command derives a repo-root-scoped context before aggregating static lifecycle provider `setup` dispatch across providers (config → local_db → auth_db → agent_trace_db → hooks when requested), so setup providers consume only repo-root access from the scoped context. - `cli/src/services/setup/mod.rs` keeps those responsibilities inside one file for now, but the current ownership split is explicit: the inline `install` module owns repository-path normalization, staging/swap install behavior, required-hook installation, and filesystem safety guards, while the inline `prompt` module owns interactive target selection and prompt styling. - `cli/src/services/security.rs` provides shared security utilities for deterministic secret redaction (`redact_sensitive_text`) and directory write-permission probes (`ensure_directory_is_writable`) used by app/setup/observability surfaces. -- `cli/src/services/doctor/mod.rs` owns the current doctor request/report surface while focused submodules (`doctor/inspect.rs`, `doctor/render.rs`, `doctor/fixes.rs`, `doctor/types.rs`) split report fact collection, rendering, manual fix reporting, and doctor-owned domain types into smaller seams; `cli/src/services/doctor/command.rs` owns the `DoctorCommand` payload used by the static `RuntimeCommand` enum and executes against any context implementing repo-root scoping. Runtime doctor execution either renders `sce doctor dbs` registry listings or resolves a repository root, derives a scoped context, requests the shared static lifecycle provider catalog with hooks included for service-owned `diagnose` and `fix` behavior, adapts lifecycle-owned health/fix records into doctor-owned problem/fix records, and then renders stable text/JSON problem records with category/severity/fixability/remediation fields plus deterministic fix-result reporting in fix mode. Report fact collection preserves environment/repository/hook/integration display data and adds checkout identity plus per-checkout Agent Trace DB status when a checkout ID exists, while service-owned lifecycle providers own config validation, local DB and Agent Trace DB readiness/bootstrap, and hook rollout diagnosis/repair. +- `cli/src/services/doctor/mod.rs` owns the current doctor request/report surface while focused submodules (`doctor/inspect.rs`, `doctor/render.rs`, `doctor/fixes.rs`, `doctor/types.rs`) split report fact collection, rendering, manual fix reporting, and doctor-owned domain types into smaller seams; `cli/src/services/doctor/command.rs` owns the `DoctorCommand` payload used by the static `RuntimeCommand` enum and executes against any context implementing repo-root scoping. Runtime doctor execution either renders `sce doctor dbs` checkout discovery or resolves a repository root, derives a scoped context, requests the shared static lifecycle provider catalog with hooks included for service-owned `diagnose` and `fix` behavior, adapts lifecycle-owned health/fix records into doctor-owned problem/fix records, and then renders stable text/JSON problem records with category/severity/fixability/remediation fields plus deterministic fix-result reporting in fix mode. Report fact collection preserves environment/repository/hook/integration display data and adds checkout identity plus per-checkout Agent Trace DB status when a checkout ID exists, while service-owned lifecycle providers own config validation, local DB and Agent Trace DB readiness/bootstrap, and hook rollout diagnosis/repair. - `cli/src/services/version/mod.rs` defines the version command parser/rendering contract (`parse_version_request`, `render_version`) with deterministic text output and stable JSON runtime-identification fields; `cli/src/services/version/command.rs` owns the `VersionCommand` payload used by the static `RuntimeCommand` enum. - `cli/src/services/completion/mod.rs` defines completion parser/rendering contract (`parse_completion_request`, `render_completion`) with deterministic Bash/Zsh/Fish script output aligned to current parser-valid command/flag surfaces; `cli/src/services/completion/command.rs` owns the `CompletionCommand` payload used by the static `RuntimeCommand` enum. - `cli/src/services/hooks/mod.rs` defines the current local hook runtime parsing/dispatch (`HookSubcommand`, `run_hooks_subcommand`) plus a commit-msg co-author policy seam (`apply_commit_msg_coauthor_policy`) that injects one canonical SCE trailer only when the disabled-default attribution-hooks config/env control is enabled and `SCE_DISABLED` is false; `cli/src/services/hooks/command.rs` owns the `HooksCommand` payload used by the static `RuntimeCommand` enum. In the current attribution-only baseline, `pre-commit` and `post-rewrite` are deterministic no-op surfaces; `post-commit` requires validated `--remote-url`, threads that URL through the Agent Trace flow, prints it to stderr, and remains an active intersection + Agent Trace persistence entrypoint (captures current commit patch, queries recent `diff_traces` from the bounded past-7-days window, combines valid patches via `patch::combine_patches`, intersects with post-commit patch via `patch::intersect_patches`, persists result to `post_commit_patch_intersections`, then persists built Agent Trace payloads with range-level `content_hash` values to `agent_traces` in AgentTraceDb without post-commit file artifacts); `diff-trace` performs STDIN JSON intake, validates required non-empty `sessionID`/`diff`/`tool_name`, optional `model_id` (absent/`null` → `None`, resolved from `session_models` by `tool_name` + `session_id` when absent) and required `tool_version` (present and either `null` or non-empty string) plus required `u64` `time` (Unix epoch milliseconds), rejects values that cannot fit AgentTraceDb signed `time_ms` storage, writes one collision-safe parsed-payload `context/tmp/-000000-diff-trace.json` artifact, and inserts the parsed payload fields into AgentTraceDb; `session-model` performs STDIN JSON intake, validates required non-empty `sessionID`/`model_id`/`tool_name`, required `u64` `time` (Unix epoch milliseconds), and required nullable/non-empty `tool_version`, then upserts the parsed payload into AgentTraceDb `se... (line truncated to 2000 chars) - Claude `SessionStart` session-model parsing in `cli/src/services/hooks/mod.rs` uses explicit payload version fields (`tool_version`/`claude_version`/`version`) when present; if no non-empty payload version is available, it best-effort runs `claude --version`, trims stdout, and leaves `tool_version` nullable without failing intake when the command is unavailable, fails, or returns empty output. - Diff-trace attribution resolution in `cli/src/services/hooks/mod.rs` looks up `session_models` when `model_id` or `tool_version` is missing/nullable, fills only missing fields from the stored row when available, preserves direct payload precedence, and continues persistence with `None` for unresolved attribution. - `cli/src/services/resilience.rs` defines bounded retry/timeout/backoff execution policy (`RetryPolicy`, `run_with_retry`) for transient operation hardening with deterministic failure messaging and retry observability. -- No user-invocable `sce sync` command is wired in the current runtime; local DB bootstrap and setup-time per-checkout Agent Trace DB initialization flow through lifecycle providers aggregated by setup, while checkout/global DB health/repair and checkout-registry listing flow through the doctor surface. +- No user-invocable `sce sync` command is wired in the current runtime; local DB bootstrap and setup-time per-checkout Agent Trace DB initialization flow through lifecycle providers aggregated by setup, while checkout/global DB health/repair and checkout DB discovery flow through the doctor surface. - `cli/src/services/patch.rs` defines the standalone patch domain model (`ParsedPatch`, `PatchFileChange`, `FileChangeKind`, `PatchHunk`, `TouchedLine`, `TouchedLineKind`) for in-memory parsed unified-diff representation, capturing only touched lines (added/removed) plus minimal per-file/per-hunk metadata while excluding non-hunk headers and unchanged context lines. All types are `serde`-serializable/deserializable with `snake_case` JSON field naming. The module also provides `parse_patch`, a public parser function that converts raw unified-diff text (both `Index:` SVN-style and `diff --git` git-style formats) into `ParsedPatch` structs, with `ParseError` for actionable malformed-input diagnostics. Storage-agnostic JSON load helpers (`load_patch_from_json` for string input, `load_patch_from_json_bytes` for byte input) reconstruct `ParsedPatch` from serialized JSON content with `PatchLoadError` for actionable deserialization diagnostics. Its patch-set operations now include deterministic ordered combination plus target-shaped intersection that prefers exact touched-line matches and falls back to historical `kind`+`content` matching when incremental diffs and canonical post-commit diffs have drifted line numbers; `parse_patch`, `combine_patches`, and `intersect_patches` are consumed by the active post-commit hook runtime. - `cli/src/services/structured_patch.rs` defines the synchronous structured editor-hook derivation seam. It derives Claude `PostToolUse` `Write` create and `Edit` structured-patch payloads into canonical `ParsedPatch` values plus Claude session/tool metadata, returning deterministic skip reasons for unsupported events/tools/payload shapes. The module is pure and side-effect-free. It is wired into `sce hooks diff-trace` for Claude payload classification at intake (T04) and into `AgentTraceDb::recent_diff_trace_patches` for post-commit structured payload parsing dispatch at read time (T05). -- `cli/src/services/` contains module boundaries for command_registry, lifecycle, auth_command, config, setup, doctor, hooks, checkout identity, bash_policy, version, completion, help, patch, SCE web URL helpers, shared database infrastructure, local DB adapters, encrypted auth DB adapters, and Agent Trace DB adapters with explicit trait seams for future implementations. `cli/src/services/checkout/` owns checkout ID file infrastructure, central checkout-registry JSON persistence, and per-checkout Agent Trace DB lazy resolution for hook runtime; setup uses `AgentTraceDbLifecycle::setup()` to establish checkout identity and initialize the per-checkout DB, while `sce doctor` surfaces checkout identity and per-checkout Agent Trace DB status when a checkout ID exists and `sce doctor dbs` lists registered checkouts from the central registry. `cli/src/services/bash_policy.rs` owns both the CLI-agnostic evaluator logic and the hidden `sce policy bash` command adapter used by OpenCode and Claude hook callers. `cli/src/services/command_registry.rs` defines the static `RuntimeCommand` enum, deterministic `CommandRegistry` name catalog, and `build_default_registry()` function for command dispatch metadata. Service-owned command modules own the runtime command payload structs for help/help-text, version, completion, auth, config, setup, doctor, hooks, and policy. +- `cli/src/services/` contains module boundaries for command_registry, lifecycle, auth_command, config, setup, doctor, hooks, checkout identity, bash_policy, version, completion, help, patch, SCE web URL helpers, shared database infrastructure, local DB adapters, encrypted auth DB adapters, and Agent Trace DB adapters with explicit trait seams for future implementations. `cli/src/services/checkout/` owns checkout ID file infrastructure and per-checkout Agent Trace DB lazy resolution for hook runtime; setup uses `AgentTraceDbLifecycle::setup()` to establish checkout identity and initialize the per-checkout DB, while `sce doctor` surfaces checkout identity and per-checkout Agent Trace DB status when a checkout ID exists and `sce doctor dbs` discovers checkouts via filesystem scan of `agent-trace-*.db` files on disk. `cli/src/services/bash_policy.rs` owns both the CLI-agnostic evaluator logic and the hidden `sce policy bash` command adapter used by OpenCode and Claude hook callers. `cli/src/services/command_registry.rs` defines the static `RuntimeCommand` enum, deterministic `CommandRegistry` name catalog, and `build_default_registry()` function for command dispatch metadata. Service-owned command modules own the runtime command payload structs for help/help-text, version, completion, auth, config, setup, doctor, hooks, and policy. - `cli/README.md` is the crate-local onboarding and usage source of truth for placeholder behavior, safety limitations, and roadmap mapping back to service contracts. - `flake.nix` applies `rust-overlay` (`oxalica/rust-overlay`) to nixpkgs, pins `rust-bin.stable.1.93.1.default` with `rustfmt` + `clippy`, reads the package/check version from repo-root `.version`, builds `packages.sce` through Crane (`buildDepsOnly` -> `buildPackage`) with a filtered repo-root source that preserves the Cargo tree plus `cli/assets/hooks`, then injects generated OpenCode/Claude config payloads and schema inputs into a temporary `cli/assets/generated/` mirror during derivation unpack so `cli/build.rs` can package the crate without requiring committed generated crate assets. The same `cli/build.rs` now scans `cli/migrations/*/*.sql` and writes `cli/src/generated_migrations.rs` with deterministic migration constants sorted by numeric filename prefix. The flake runs `cli-tests`, `cli-clippy`, and `cli-fmt` plus the dedicated `integrations-install-tests`, `integrations-install-clippy`, and `integrations-install-fmt` derivations through Crane-backed paths so both Rust crates have first-class default-flake verification, exposes directory-scoped JS validation derivations for both `npm/` and the shared `config/lib/` plugin package root, and also exposes the non-default `apps.install-channel-integration-tests` flake app for install-channel integration coverage outside the default check set. The shared config-lib source set is rooted at `config/lib/` and includes the shared `package.json`, `bun.lock`, and `tsconfig.json` plus `agent-trace-plugin/` and `bash-policy-plugin/`; `config-lib-bun-tests` runs the bash-policy plugin wrapper tests from that shared root, while `config-lib-biome-check` and `config-lib-biome-format` run Biome over the copied shared package source. `.github/workflows/publish-crates.yml` follows the same asset-preparation rule but runs Cargo packaging from a temporary clean repository copy so crates.io publish no longer needs `--allow-dirty`. - The config-lib check source preserves repo-relative access to shared CLI patch fixtures: Nix copies a filtered repo-shaped source containing `config/lib/**` plus `cli/src/services/structured_patch/fixtures`, then runs Bun/Biome from `config/lib/`. diff --git a/context/cli/checkout-identity.md b/context/cli/checkout-identity.md index 1b71501f..d1a919aa 100644 --- a/context/cli/checkout-identity.md +++ b/context/cli/checkout-identity.md @@ -2,16 +2,7 @@ The checkout identity service lives in `cli/src/services/checkout/`. -It assigns a stable identity to a local Git checkout or linked Git worktree. The setup lifecycle creates/reuses this identity, initializes the per-checkout Agent Trace database, and records the database path. Agent Trace hook runtime resolves persistence through this identity and still lazily initializes or upgrades a per-checkout database when setup has not run or schema metadata is incomplete. - -## Registry resilience - -The registry includes two resilience safeguards against concurrent access and corruption: - -- **Empty-file recovery:** `read_registry()` treats empty or whitespace-only registry files as corrupt. It removes the file (best-effort), prints a `[WARN]` to stderr, and returns `CheckoutRegistry::default()`. This prevents `serde_json` parse errors when a previous write was interrupted, leaving a zero-byte file. -- **PID-unique temp filenames:** `write_registry()` appends the process ID to the temp filename (`checkout-registry.json.tmp.{pid}`) so concurrent `sce hooks` processes never collide on the same temp file. After a successful atomic `rename(2)`, stale `.tmp.*` files left by crashed processes are cleaned up (best-effort, matching `checkout-registry.json.tmp.*` prefix, errors ignored). - -These changes are implemented in `cli/src/services/checkout/registry.rs` in the `read_registry()` and `write_registry()` functions. No caller changes were needed. +It assigns a stable identity to a local Git checkout or linked Git worktree. The setup lifecycle creates/reuses this identity and initializes the per-checkout Agent Trace database. Agent Trace hook runtime resolves persistence through this identity and still lazily initializes or upgrades a per-checkout database when setup has not run or schema metadata is incomplete. ## Current code surface @@ -19,14 +10,7 @@ These changes are implemented in `cli/src/services/checkout/registry.rs` in the - `resolve_git_dir(repo_root)` runs `git rev-parse --git-dir` from the supplied repository root. - `read_checkout_id(git_dir)` reads `/sce/checkout-id` and validates non-empty UUID syntax. - `get_or_create_checkout_id(git_dir)` reuses an existing ID or writes a new UUIDv7 checkout ID to `/sce/checkout-id`. - - `resolve_checkout_id_for_repo(repo_root)` combines Git directory resolution with get-or-create checkout ID behavior. - - `resolve_or_create_agent_trace_db_for_current_checkout()` resolves from `std::env::current_dir()` and returns `(AgentTraceDb, checkout_id)`. - - `resolve_or_create_agent_trace_db_for_checkout(repo_root)` gets or creates checkout identity, registers it, resolves `/sce/agent-trace-{checkout_id}.db`, fast-opens an existing ready DB, and falls back to migration-running initialization when the DB is absent or schema metadata is incomplete. -- `cli/src/services/checkout/registry.rs` - - `CheckoutRecord` serializes `checkout_id`, `path`, `last_seen`, optional `remote_url`, and optional `database_path`. - - `CheckoutRegistry` serializes the registry as `{ "checkouts": [...] }`. - - Registry persistence uses `/sce/checkout-registry.json` and atomic write-through-rename. - - Registry operations include register, update last seen, list, and remove. + - `resolve_or_create_agent_trace_db_for_checkout(repo_root)` gets or creates checkout identity, resolves `/sce/agent-trace-{checkout_id}.db`, fast-opens an existing ready DB, and falls back to migration-running initialization when the DB is absent or schema metadata is incomplete. ## Current integration state @@ -36,10 +20,8 @@ During setup: - `checkout::resolve_git_dir(repo_root)` resolves the checkout metadata directory from Git truth. - `checkout::get_or_create_checkout_id(git_dir)` creates or reuses `/sce/checkout-id`. -- `checkout::registry::register_checkout(...)` first writes or updates the central registry record for the checkout identity. - `default_paths::agent_trace_db_path_for_checkout(checkout_id)` computes `/sce/agent-trace-{checkout_id}.db`. - `AgentTraceDb::open_at(path)` opens or creates the per-checkout DB and applies all embedded migrations before setup completes. -- `checkout::registry::register_checkout(...)` updates the central registry record with `database_path` after DB initialization succeeds. - Setup output includes the checkout ID and initialized Agent Trace database path. During hook runtime: @@ -48,9 +30,8 @@ During hook runtime: - `default_paths::agent_trace_db_path_for_checkout(checkout_id)` computes `/sce/agent-trace-{checkout_id}.db`. - `AgentTraceDb::open_for_hooks_without_migrations_at(path)` is tried first; `ensure_schema_ready_for_hooks()` decides whether the schema is current. - Missing or incomplete schema falls back to `AgentTraceDb::open_at(path)`, which runs migrations through the shared Turso adapter. -- Successful DB resolution updates the registry record with `database_path`. -The global `agent-trace.db` path remains only as a lifecycle fallback when no checkout context or checkout ID is available. `sce doctor` displays the current checkout ID and per-checkout Agent Trace DB status when a checkout ID exists, and `sce doctor dbs` lists registry records sorted by `last_seen` descending. +The global `agent-trace.db` path remains only as a lifecycle fallback when no checkout context or checkout ID is available. `sce doctor` displays the current checkout ID and per-checkout Agent Trace DB status when a checkout ID exists, and `sce doctor dbs` discovers checkouts by scanning `/sce/agent-trace-*.db` files on disk, sorted by mtime descending. ## Testing boundary diff --git a/context/cli/cli-command-surface.md b/context/cli/cli-command-surface.md index f1a213d3..981389c3 100644 --- a/context/cli/cli-command-surface.md +++ b/context/cli/cli-command-surface.md @@ -63,7 +63,7 @@ Deferred or gated command surfaces currently avoid claiming unimplemented behavi `setup` now also exposes compile-time embedded config assets for OpenCode/Claude targets, sourced from the generated `config/.opencode/**` and `config/.claude/**` trees via `cli/build.rs` with normalized forward-slash relative paths and target-scoped iteration APIs; the embedded asset set includes the OpenCode bash-policy plugin wrapper plus Claude settings `PreToolUse` Bash policy hook, both delegating to the Rust `sce policy bash` path. `setup` additionally includes a repository-root install engine (`install_embedded_setup_assets`) that stages embedded files, intentionally leaves generated `skills/*/tile.json` manifests in `config/` only, skips those tile files during repo-root installs, and uses a unified remove-and-replace policy for `.opencode/`/`.claude/` (removing existing targets before swapping staged content, with deterministic recovery guidance on swap failure) while treating bash-policy enforcement files as first-class SCE-managed assets. `setup` now executes end-to-end and prints deterministic completion details including selected target(s) and per-target install count. -`doctor` now executes end-to-end with explicit diagnosis, repair-intent, and checkout-registry surfaces: `sce doctor` stays read-only, `sce doctor --fix` selects repair-intent mode, and `sce doctor dbs [--format text|json]` lists registered checkouts from `/sce/checkout-registry.json`. The current runtime aggregates `ServiceLifecycle::diagnose` and `ServiceLifecycle::fix` calls across all registered service providers (`config`, `local_db`, `auth_db`, `agent_trace_db`, `hooks`) plus integration checks, covering state-root resolution, global and repo-local `sce/config.json` readability/schema validation, local DB and checkout/global Agent Trace DB path/health, DB-parent readiness barriers, the repo hook rollout slice when a repository target is detected, and repo-root installed OpenCode integration presence for `plugins`, `agents`, `commands`, and `skills`. Fix mode delegates to each provider's `fix` implementation, which reuses the canonical setup hook install flow to repair missing/stale/non-executable required hooks and missing hooks directories, and it can bootstrap missing canonical database parent directories when the resolved paths match canonical owned locations. +`doctor` now executes end-to-end with explicit diagnosis, repair-intent, and checkout-discovery surfaces: `sce doctor` stays read-only, `sce doctor --fix` selects repair-intent mode, and `sce doctor dbs [--format text|json]` discovers checkouts by scanning `/sce/agent-trace-*.db` files on disk. The current runtime aggregates `ServiceLifecycle::diagnose` and `ServiceLifecycle::fix` calls across all registered service providers (`config`, `local_db`, `auth_db`, `agent_trace_db`, `hooks`) plus integration checks, covering state-root resolution, global and repo-local `sce/config.json` readability/schema validation, local DB and checkout/global Agent Trace DB path/health, DB-parent readiness barriers, the repo hook rollout slice when a repository target is detected, and repo-root installed OpenCode integration presence for `plugins`, `agents`, `commands`, and `skills`. Fix mode delegates to each provider's `fix` implementation, which reuses the canonical setup hook install flow to repair missing/stale/non-executable required hooks and missing hooks directories, and it can bootstrap missing canonical database parent directories when the resolved paths match canonical owned locations. A user-invocable `sync` command is not wired in the current CLI surface; local DB and Agent Trace DB bootstrap currently happen through `setup`, and DB health/repair currently happens through `doctor`. Command wiring for `sce sync` is deferred to `0.4.0`. ## Command loop and error model @@ -80,7 +80,7 @@ A user-invocable `sync` command is not wired in the current CLI surface; local D - Interactive `sce setup` prompt cancellation/interrupt exits cleanly with: `Setup cancelled. No files were changed.` - Command handlers return deterministic status messaging: - `setup`: `Setup completed successfully.` plus selected targets and per-target install destinations/counts. -- `doctor`: current runtime emits `SCE doctor diagnose` / `SCE doctor fix` human text headers plus ordered `Environment`, `Configuration` (including checkout identity + Agent Trace checkout DB rows when available), `Repository`, `Git Hooks`, and `Integrations` sections with bracketed `[PASS]`/`[FAIL]`/`[MISS]` row tokens, shared-style green pass plus red fail/miss colorization when enabled, simplified `label (path)` rows, top-level-only hook rows, and a deterministic summary footer; JSON output carries stable problem/fixability records plus deterministic fix-result records in fix mode and reports `checkout_identity` plus the resolved Agent Trace DB record. `sce doctor dbs` emits either `no registered checkouts` or registry rows sorted by `last_seen` descending, with JSON fields `checkout_id`, `path`, `database_path`, `last_seen`, and `remote_url`. +- `doctor`: current runtime emits `SCE doctor diagnose` / `SCE doctor fix` human text headers plus ordered `Environment`, `Configuration` (including checkout identity + Agent Trace checkout DB rows when available), `Repository`, `Git Hooks`, and `Integrations` sections with bracketed `[PASS]`/`[FAIL]`/`[MISS]` row tokens, shared-style green pass plus red fail/miss colorization when enabled, simplified `label (path)` rows, top-level-only hook rows, and a deterministic summary footer; JSON output carries stable problem/fixability records plus deterministic fix-result records in fix mode and reports `checkout_identity` plus the resolved Agent Trace DB record. `sce doctor dbs` emits either `no discovered checkouts` or discovered checkout rows sorted by mtime descending, with JSON fields `checkout_id`, `database_path`, and `last_seen`. - `hooks`: deterministic hook subcommand status messaging for runtime entrypoint invocation and argument/STDIN contract validation. ## Service contracts @@ -88,7 +88,7 @@ A user-invocable `sync` command is not wired in the current CLI surface; local D - `cli/src/services/setup/mod.rs` defines setup parsing/selection contracts plus runtime install orchestration (`run_setup_for_mode`) over the embedded asset install engine; `cli/src/services/setup/command.rs` owns the setup runtime command handler. Setup now aggregates `ServiceLifecycle::setup` calls across registered providers (`config`, `local_db`, `auth_db`, `agent_trace_db`, `hooks`) in order, using a `ContextWithRepoRoot`-scoped context with resolved repository root. - `cli/src/services/setup/mod.rs` now keeps its larger internal responsibilities behind focused inline support modules: `install` owns repository canonicalization, staging/swap install flows, required-hook installation, and repo/writeability guards, while `prompt` owns interactive target selection and styled prompt labels. - `cli/src/services/config/mod.rs` defines config parser/runtime contracts (`show`, `validate`, `--help`), strict config-file key/type validation, deterministic text/JSON rendering, repo-configured bash-policy preset/custom validation and reporting under `policies.bash`, and shared auth-key metadata that declares env key, config-file key, and optional baked-default eligibility for supported auth runtime values starting with `workos_client_id` (`WORKOS_CLIENT_ID` vs `workos_client_id`); auth-key provenance/preference metadata stays on `show`, while `validate` stays trimmed to validation status plus issues/warnings. `cli/src/services/config/lifecycle.rs` implements `ServiceLifecycle` for config health checks and setup (global/local config validation and repo-local config bootstrap). -- `cli/src/services/doctor/mod.rs` defines the implemented doctor request/report contract (`DoctorRequest`, `DoctorAction`, `DoctorMode`, `run_doctor`) while focused submodules under `cli/src/services/doctor/` handle runtime command dispatch (`command.rs`), diagnosis (`inspect.rs`), rendering (`render.rs`), fix execution (`fixes.rs`), and doctor-owned domain types (`types.rs`). Together they preserve explicit fix-mode parsing, checkout-registry listing, stable text/JSON problem and database-record rendering, deterministic fix-result reporting, and aggregation of `ServiceLifecycle::diagnose`/`ServiceLifecycle::fix` across registered providers (`config`, `local_db`, `auth_db`, `agent_trace_db`, `hooks`). The doctor module coordinates state-root/config/database reporting and validation, path-source detection plus required-hook presence/executable/content checks when a repository target is detected, repo-root installed OpenCode integration presence inventory for `plugins`, `agents`, `commands`, and `skills` derived from the embedded OpenCode setup asset catalog, shared-style bracketed human status token rendering (`[PASS]`, `[FAIL]`, `[MISS]`) with simplified `label (path)` text rows, and repair-mode delegation to service-owned fix implementations. +- `cli/src/services/doctor/mod.rs` defines the implemented doctor request/report contract (`DoctorRequest`, `DoctorAction`, `DoctorMode`, `run_doctor`) while focused submodules under `cli/src/services/doctor/` handle runtime command dispatch (`command.rs`), diagnosis (`inspect.rs`), rendering (`render.rs`), fix execution (`fixes.rs`), and doctor-owned domain types (`types.rs`). Together they preserve explicit fix-mode parsing, checkout-database discovery, stable text/JSON problem and database-record rendering, deterministic fix-result reporting, and aggregation of `ServiceLifecycle::diagnose`/`ServiceLifecycle::fix` across registered providers (`config`, `local_db`, `auth_db`, `agent_trace_db`, `hooks`). The doctor module coordinates state-root/config/database reporting and validation, path-source detection plus required-hook presence/executable/content checks when a repository target is detected, repo-root installed OpenCode integration presence inventory for `plugins`, `agents`, `commands`, and `skills` derived from the embedded OpenCode setup asset catalog, shared-style bracketed human status token rendering (`[PASS]`, `[FAIL]`, `[MISS]`) with simplified `label (path)` text rows, and repair-mode delegation to service-owned fix implementations. - `cli/src/services/version/mod.rs` defines the version parser/output contract (`parse_version_request`, `render_version`) with deterministic text/JSON output modes; `cli/src/services/version/command.rs` owns the version runtime command handler. - `cli/src/services/completion/mod.rs` defines the completion output contract (`render_completion`) using clap_complete to generate deterministic shell scripts for Bash, Zsh, and Fish; `cli/src/services/completion/command.rs` owns the completion runtime command handler. - `cli/src/services/hooks/mod.rs` defines production local hook runtime parsing/dispatch (`HookSubcommand`, `run_hooks_subcommand`) for `pre-commit`, `commit-msg`, `post-commit`, `post-rewrite`, `diff-trace`, and `session-model`; `cli/src/services/hooks/command.rs` owns the hook runtime command handler. Current runtime behavior is commit-msg-only attribution behind the disabled-default attribution gate; `pre-commit` and `post-rewrite` are deterministic no-ops; `post-commit` requires validated `--remote-url`, threads that value through Agent Trace flow, prints it to stderr, and remains an active intersection + Agent Trace DB persistence path (captures current commit patch, combines/intersects recent `diff_traces`, persists intersection metadata to `post_commit_patch_intersections`, then persists built Agent Trace payload with range-level `content_hash` values to `agent_traces`); `diff-trace` performs STDIN JSON intake, required non-empty `sessionID`/`diff`/`tool_name`, optional `model_id`, required nullable/non-empty `tool_version`, plus required `u64` `time` (Unix epoch milliseconds) validation, missing/nullable attribution fallback from `session_models` by `tool_name` + `session_id` while preserving direct payload precedence, non-lossy AgentTraceDb `time_ms` conversion, collision-safe parsed-payload `context/tmp/-000000-diff-trace.json` persistence, and best-effort AgentTraceDb insertion whose failure is logged and reflected in success text; and `session-model` performs STDIN JSON intake for normalized model attribution upsert without raw artifact persistence, with Claude `SessionStart` parsing best-effort filling missing `tool_version` from `claude --version`. `cli/src/services/hooks/lifecycle.rs` implements `ServiceLifecycle` for hook health checks, fix, and setup (hook rollout integrity and required-hook installation). @@ -107,7 +107,7 @@ A user-invocable `sync` command is not wired in the current CLI surface; local D - `cli/src/services/agent_trace_db/mod.rs` provides `AgentTraceDb = TursoDb` plus `DiffTraceInsert<'_>`/`insert_diff_trace()` for parameterized writes to `diff_traces`, `AgentTraceInsert<'_>`/`insert_agent_trace()` for built `agent_traces`, and `SessionModelUpsert<'_>`/lookup helpers for durable `session_models` attribution keyed by `(tool_name, session_id)`. - `AgentTraceDb::new()` resolves the legacy/global `/sce/agent-trace.db` through `default_paths::agent_trace_db_path()`; active hook runtime resolves per-checkout DB files through checkout identity and `default_paths::agent_trace_db_path_for_checkout(checkout_id)`. - `cli/src/services/local_db/lifecycle.rs` implements `ServiceLifecycle` for local DB health checks and setup (DB path/health validation and DB bootstrap). -- `cli/src/services/agent_trace_db/lifecycle.rs` implements `ServiceLifecycle` for Agent Trace checkout identity setup, setup-time per-checkout DB initialization, registry `database_path` recording, and checkout/global DB path-health validation plus parent bootstrap. +- `cli/src/services/agent_trace_db/lifecycle.rs` implements `ServiceLifecycle` for Agent Trace checkout identity setup, setup-time per-checkout DB initialization, and checkout/global DB path-health validation plus parent bootstrap. - `sce setup` aggregates `ServiceLifecycle::setup` calls, which includes `LocalDbLifecycle::setup()` and `AgentTraceDbLifecycle::setup()` for DB initialization as part of local prerequisite bootstrap. - `sce doctor` aggregates `ServiceLifecycle::diagnose` and `ServiceLifecycle::fix` calls, which includes both DB lifecycle providers for DB path/health validation and can bootstrap missing canonical parent directories when repair mode is appropriate. diff --git a/context/cli/default-path-catalog.md b/context/cli/default-path-catalog.md index 2b8d5501..7dd15b7a 100644 --- a/context/cli/default-path-catalog.md +++ b/context/cli/default-path-catalog.md @@ -19,7 +19,6 @@ - local DB: `/sce/local.db` - legacy/global agent trace DB fallback: `/sce/agent-trace.db` - per-checkout agent trace DB: `/sce/agent-trace-{checkout_id}.db` -- checkout registry: `/sce/checkout-registry.json` ### Repo-relative paths @@ -45,6 +44,6 @@ - `cli/src/services/setup/mod.rs` now resolves setup target directory names and required hook identifiers through `default_paths.rs` constants/accessors instead of owning those path literals locally. - `cli/src/services/default_paths.rs` includes a regression test that scans non-test Rust source under `cli/src/` and fails when new centralized production path literals appear outside the default-path service. - `cli/src/services/hooks/mod.rs` resolves the collision-safe `context/tmp/` path shape through shared path accessors. -- `cli/src/services/agent_trace_db/lifecycle.rs` and `cli/src/services/checkout/mod.rs` resolve per-checkout Agent Trace DB files through `agent_trace_db_path_for_checkout(checkout_id)` and store the resulting path in the checkout registry after setup-time DB initialization or hook-runtime lazy initialization. +- `cli/src/services/agent_trace_db/lifecycle.rs` and `cli/src/services/checkout/mod.rs` resolve per-checkout Agent Trace DB files through `agent_trace_db_path_for_checkout(checkout_id)` after setup-time DB initialization or hook-runtime lazy initialization. See also: [cli-command-surface.md](./cli-command-surface.md), [../architecture.md](../architecture.md), [../context-map.md](../context-map.md) diff --git a/context/cli/service-lifecycle.md b/context/cli/service-lifecycle.md index 4b5a2f51..f9eea208 100644 --- a/context/cli/service-lifecycle.md +++ b/context/cli/service-lifecycle.md @@ -34,7 +34,7 @@ - `cli/src/services/agent_trace_db/lifecycle.rs` defines `AgentTraceDbLifecycle`, the Agent Trace DB-owned provider. - `AgentTraceDbLifecycle::diagnose` emits per-checkout Agent Trace DB path and parent-directory readiness lifecycle health problems when `ctx.repo_root()` has an existing checkout ID; otherwise it falls back to the legacy global Agent Trace DB path. - `AgentTraceDbLifecycle::fix` bootstraps the resolved per-checkout Agent Trace DB parent directory for auto-fixable DB parent readiness problems, with the same global fallback outside checkout context. -- `AgentTraceDbLifecycle::setup` resolves the current checkout identity from `ctx.repo_root()` when available, creates or reuses `/sce/checkout-id`, registers the identity, resolves `/sce/agent-trace-{checkout_id}.db`, opens/creates that database through `AgentTraceDb::open_at(&db_path)` so embedded migrations are applied, updates the checkout registry with `database_path`, and returns setup messaging with the checkout ID plus initialized DB path. Hook runtime still creates or upgrades the per-checkout DB lazily when setup has not run or schema metadata is incomplete. +- `AgentTraceDbLifecycle::setup` resolves the current checkout identity from `ctx.repo_root()` when available, creates or reuses `/sce/checkout-id`, resolves `/sce/agent-trace-{checkout_id}.db`, opens/creates that database through `AgentTraceDb::open_at(&db_path)` so embedded migrations are applied, and returns setup messaging with the checkout ID plus initialized DB path. Hook runtime still creates or upgrades the per-checkout DB lazily when setup has not run or schema metadata is incomplete. - `doctor` runtime execution now aggregates lifecycle providers for diagnosis and repair: - `cli/src/services/doctor/command.rs` accepts any context implementing `ContextWithRepoRoot`. - `cli/src/services/doctor/mod.rs` resolves the repository root once, creates a repo-root-scoped borrowed context using `with_repo_root()`, and requests the full provider catalog with hooks included. diff --git a/context/context-map.md b/context/context-map.md index 47683939..1402adf3 100644 --- a/context/context-map.md +++ b/context/context-map.md @@ -11,7 +11,7 @@ Feature/domain context: - `context/cli/cli-command-surface.md` (CLI command surface including top-level help with ASCII art banner and gradient rendering, setup install flow, WorkOS device authorization flow + token storage behavior, attribution-only hook routing with validated post-commit `--remote-url` plumbing plus DB-backed `diff-trace` dual persistence and post-commit Agent Trace payload persistence including range `content_hash`, setup-owned local DB + Agent Trace DB bootstrap plus doctor DB health coverage, centralized Rust SCE web URL helpers in `services::agent_trace`, nested flake release package/app installability, Cargo local install + crates.io readiness policy, and hidden `sce policy bash` command adapter for bash-policy hook callers; `sce sync` command wiring is deferred to `0.4.0`; static `RuntimeCommand` enum dispatch lives in `services/command_registry.rs`, command payload structs for help/version/completion/auth/config/setup/doctor/hooks/policy are owned by their respective `services/{name}/command.rs` files, and clap-to-runtime conversion lives in `services/parse/command_runtime.rs`) - `context/cli/default-path-catalog.md` (canonical production CLI path-ownership contract centered on `cli/src/services/default_paths.rs`, including persisted auth/config files, named DB paths for auth/local/Agent Trace databases, repo-relative, embedded-asset, install, hook, and context-path families plus the regression guard that keeps production path ownership centralized) -- `context/cli/checkout-identity.md` (current checkout identity infrastructure in `cli/src/services/checkout/`, including `/sce/checkout-id` UUIDv7 storage, central `/sce/checkout-registry.json` records, atomic registry persistence, setup lifecycle integration that creates/reuses/registers checkout identity, initializes the per-checkout Agent Trace DB and records `database_path`, hook-runtime lazy DB fallback, `sce doctor` checkout identity display, and `sce doctor dbs` registry listing) +- `context/cli/checkout-identity.md` (current checkout identity infrastructure in `cli/src/services/checkout/`, including `/sce/checkout-id` UUIDv7 storage, setup lifecycle integration that creates/reuses checkout identity and initializes the per-checkout Agent Trace DB, hook-runtime lazy DB fallback, `sce doctor` checkout identity display, and `sce doctor dbs` filesystem-based checkout discovery) - `context/cli/patch-service.md` (standalone patch domain model, parser, JSON load helpers, and set operations in `cli/src/services/patch.rs` for in-memory parsed unified-diff representation, capturing only touched lines plus minimal per-file/per-hunk metadata, supporting both `Index:` SVN-style and `diff --git` git-style formats, with `ParseError` for actionable malformed-input diagnostics, `PatchLoadError`/`load_patch_from_json`/`load_patch_from_json_bytes` for storage-agnostic JSON reconstruction, `intersect_patches` for target-shaped overlap with exact-match-first and historical `kind`+`content` fallback semantics plus matched-constructed-line `session_id` and matched-constructed-hunk `model_id` provenance inheritance, and `combine_patches` for ordered patch combination with later-wins conflict resolution plus winning-hunk `model_id` provenance inheritance; `parse_patch`, `intersect_patches`, and `combine_patches` are consumed by the active post-commit hook runtime) - `context/cli/styling-service.md` (CLI text-mode output styling with `owo-colors` and `comfy-table`, TTY/`NO_COLOR` policy, shared helper API for human-facing surfaces, and per-column right-to-left RGB gradient banner rendering) - `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, canonical Pkl-generated `sce/config.json` schema ownership plus CLI embedding/reuse contract, config-file selection order, `show` provenance output, and trimmed `validate` output contract) diff --git a/context/glossary.md b/context/glossary.md index 276aeaf9..84e95253 100644 --- a/context/glossary.md +++ b/context/glossary.md @@ -9,7 +9,7 @@ - generated-owned outputs: Files materialized by `config/pkl/generate.pkl` under `config/.opencode/**`, `config/automated/.opencode/**`, and `config/.claude/**`, including OpenCode plugin entrypoints, generated OpenCode `package.json` manifests, generated OpenCode `opencode.json` manifests, generated Claude plugin entrypoints, and Claude settings assets. - `canonical OpenCode plugin registration source`: Shared Pkl-authored plugin-registration definition in `config/pkl/base/opencode.pkl`, re-exported from `config/pkl/renderers/common.pkl` as the canonical plugin list/path JSON consumed by OpenCode renderers before they emit generated `opencode.json` manifests; the current entries are `sce-bash-policy` and `sce-agent-trace`. - `checkout identity`: Stable UUIDv7 identifier assigned to a cloned repository or linked Git worktree, stored in `/sce/checkout-id` (never committed) and resolved via `git rev-parse --git-dir`. The identity is created or reused by `sce setup` through `AgentTraceDbLifecycle::setup()` and also auto-created by hook runtime when `sce setup` has not been run. Each checkout identity maps to a per-checkout Agent Trace DB file at `/sce/agent-trace-{checkout_id}.db`; setup initializes that DB with migrations, while hook runtime remains a lazy fallback when setup has not run or schema metadata is incomplete. See `context/cli/checkout-identity.md`. -- `checkout registry`: Central JSON registry at `/sce/checkout-registry.json` managed by `cli/src/services/checkout/registry.rs` with atomic write-through-rename persistence. Records carry `checkout_id`, `path`, `last_seen`, optional `remote_url`, and optional `database_path` (populated by setup-time DB initialization or hook-runtime lazy DB resolution). `sce doctor dbs` lists all registered checkouts sorted by `last_seen` descending. See `context/cli/checkout-identity.md`. +- `checkout registry` (removed): The central JSON registry at `/sce/checkout-registry.json` was removed in the `remove-checkout-registry` plan. `sce doctor dbs` now discovers checkouts by scanning `/sce/agent-trace-*.db` files on disk. `checkout_id`, `database_path`, and `last_seen` (from file mtime) are derived from the filesystem; `path` and `remote_url` are no longer rendered. See `context/cli/checkout-identity.md`. - `generated OpenCode plugin registration contract`: Current generated-config contract where `config/.opencode/opencode.json` and `config/automated/.opencode/opencode.json` serialize the OpenCode `plugin` field from canonical Pkl sources for SCE-managed plugins only; the current registered paths are `./plugins/sce-bash-policy.ts` and `./plugins/sce-agent-trace.ts`. Claude does not use an OpenCode-style plugin manifest; Claude bash-policy enforcement is registered through generated `.claude/settings.json` as a `PreToolUse` `Bash` command hook running `sce policy bash`. - `root Biome contract`: Repository-root formatting/linting contract owned by `biome.json`, currently scoped only to `npm/**` and the shared `config/lib/**` plugin package root with package-local `node_modules/**` excluded; the canonical execution path is the root Nix dev shell (`nix develop -c biome ...`). - `cli flake checks`: Check derivations in root `flake.nix` (`checks..cli-tests`, `cli-clippy`, `cli-fmt`), dedicated `integrations/install` runner checks (`integrations-install-tests`, `integrations-install-clippy`, `integrations-install-fmt`), plus `pkl-parity`, split `npm/` JS checks (`npm-bun-tests`, `npm-biome-check`, `npm-biome-format`), and split shared `config/lib/` JS checks (`config-lib-bun-tests`, `config-lib-biome-check`, `config-lib-biome-format`); invoked via `nix flake check` at repo root. diff --git a/context/overview.md b/context/overview.md index 0e88feed..d485de71 100644 --- a/context/overview.md +++ b/context/overview.md @@ -13,7 +13,7 @@ The same runtime also emits stable user-facing stderr error classes (`SCE-ERR-PA The app runtime now also includes a structured observability baseline in `cli/src/services/observability.rs`: deterministic env-controlled log threshold/format (`SCE_LOG_LEVEL` defaults to `error`; `SCE_LOG_FORMAT` defaults to `text`), optional file sink controls (`SCE_LOG_FILE`, `SCE_LOG_FILE_MODE` with deterministic `truncate` default), stable lifecycle event IDs, stderr-only primary emission so stdout command payloads remain pipe-safe, and `observability::traits` boundaries for logger and telemetry behavior. The app command dispatcher now enforces a centralized stdout/stderr stream contract in `cli/src/app.rs`: command success payloads are emitted on stdout only, while redacted user-facing diagnostics are emitted on stderr. `cli/src/app.rs` also now runs through explicit startup phases — dependency check, observability config resolution, runtime initialization, command parse/execute, and output rendering — with the app runtime carrying logger/telemetry plus static command-catalog state across those phases while preserving the existing exit-code and degraded-startup contracts. Within that lifecycle, `parse_command_phase` delegates clap-to-runtime conversion to `cli/src/services/parse/command_runtime.rs`, which returns a static `RuntimeCommand` enum, `services::app_support::execute_command_phase` logs around enum-owned `execute(...)` dispatch, and generic `RunOutcome` rendering logs classified errors through the logger trait boundary without coupling render support to the production logger type. Command payload structs for `help`, `version`, `completion`, `auth`, `config`, `setup`, `doctor`, and `hooks` live in service-owned `command.rs` files; `cli/src/services/command_registry.rs` owns the deterministic static command-name catalog and enum variants instead of boxed command trait objects. The CLI now also enforces a shared output-format parser contract in `cli/src/services/output_format.rs`, with canonical `--format ` parsing and command-specific actionable invalid-value guidance reused by `config` and `version` services. A compile-safe service lifecycle seam also exists in `cli/src/services/lifecycle.rs`: `ServiceLifecycle` exposes default no-op `diagnose`, `fix`, and `setup` methods against the narrow `HasRepoRoot` accessor, uses lifecycle-owned health/fix/setup result types, and owns the shared static `LifecycleProvider` enum catalog/factory with deterministic config → local_db → auth_db → agent_trace_db → hooks ordering and no boxed provider aggregation. Hooks has a `services/hooks/lifecycle.rs` provider for hook rollout diagnosis/fix/setup, config has a `services/config/lifecycle.rs` provider for global/repo-local config validation plus repo-local config bootstrap, local_db has a `services/local_db/lifecycle.rs` provider for canonical local DB path health, parent-directory readiness/bootstrap, and `LocalDb::new()` setup, auth_db has a `services/auth_db/lifecycle.rs` provider for canonical auth DB path health, parent-directory readiness/bootstrap, and `AuthDb::new()` setup, and agent_trace_db has a `services/agent_trace_db/lifecycle.rs` provider for checkout identity setup, per-checkout Agent Trace DB path health/parent readiness when an ID exists, and legacy global fallback outside checkout context. 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 also initializes the per-checkout Agent Trace DB via `AgentTraceDb::open_at(path)` and records the registry `database_path`; hook runtime lazy initialization remains a fallback when setup has not prepared the DB or schema metadata is incomplete. +Agent Trace lifecycle setup now also initializes the per-checkout Agent Trace DB via `AgentTraceDb::open_at(path)`; hook runtime lazy initialization remains a fallback when setup has not prepared the DB or schema metadata is incomplete. The CLI now also includes a shared text styling service in `cli/src/services/style.rs` that provides deterministic color enablement via `owo-colors`, automatic TTY detection, and `NO_COLOR` compliance for human-facing text output; stdout help/text surfaces, stderr diagnostics, and interactive prompt-adjacent text now reuse that shared styling policy while JSON, completion, and other non-interactive/machine-readable flows remain unstyled. The service exports color-detection, conditional styling, help/diagnostic/label/prompt styling, and `banner_with_gradient()` helpers for use across command surfaces while preserving pipe-safe output for non-interactive environments. The `setup` command includes an `inquire`-backed target-selection flow: default interactive selection for OpenCode/Claude/both with required-hook installation in the same run, explicit non-interactive target flags (`--opencode`, `--claude`, `--both`), deterministic mutually-exclusive validation, and non-destructive cancellation exits. The CLI now compiles an embedded setup asset manifest from `config/.opencode/**`, `config/.claude/**`, and `cli/assets/hooks/**` via `cli/build.rs`; `cli/src/services/setup/mod.rs` exposes deterministic normalized relative paths plus file bytes and target-scoped iteration without runtime reads from `config/`. The same build script also discovers `cli/migrations//*.sql` at compile time and writes `cli/src/generated_migrations.rs` constants sorted by numeric filename prefix for database migration consumers. @@ -27,9 +27,9 @@ The Rust CLI also centralizes SCE-owned web URI construction in `cli/src/service The same config resolver now also owns the attribution-hooks gate used by local hook runtime: `SCE_ATTRIBUTION_HOOKS_ENABLED` overrides `policies.attribution_hooks.enabled`, and the gate defaults to disabled. The config service split now includes `cli/src/services/config/resolver.rs` as the focused owner for config-file discovery, file-layer merging, env/flag/default precedence, auth-key resolution, observability resolution, attribution-hooks resolution, and default-discovered invalid-file degradation; `cli/src/services/config/mod.rs` remains the facade/rendering orchestration surface while preserving existing `services::config` imports. Generated config now includes repo-local plugin assets for both profiles: `sce-bash-policy.ts` plus `sce-agent-trace.ts` are emitted under `config/.opencode/plugins/` and `config/automated/.opencode/plugins/`; the OpenCode agent-trace plugin extracts `{ sessionID, diff, time, model_id }` from user `message.updated` events with diffs, tracks per-session OpenCode client version from `session.created`/`session.updated`, and sends payloads to `sce hooks diff-trace` with `tool_name="opencode"` plus optional `tool_version`. Claude generated config now routes agent-trace events through `.claude/settings.json` command hooks that call `sce hooks` directly: `SessionStart` pipes raw hook event JSON to `sce hooks session-model`, and matched `PostToolUse Write|Edit|MultiEdit|NotebookEdit` pipes raw hook event JSON to `sce hooks diff-trace`; the Rust `session-model` path uses explicit payload version fields when present and otherwise best-effort captures `tool_version` from trimmed `claude --version` stdout when available. Rust handles extraction, validation, and persistence without a TypeScript intermediary; the former `config/.claude/plugins/sce-agent-trace.ts` Bun runtime was removed in T07 of the `claude-rust-diff-trace` plan. The Rust hook validates required fields, resolves missing/nullable diff-trace attribution from `session_models` while preserving direct payload precedence, and persists `model_id`, `tool_name`, and nullable/resolved `tool_version` into `diff_traces` through AgentTraceDb. Bash-policy now delegates OpenCode enforcement to the Rust `sce policy bash` command: the generated OpenCode plugin at `config/.opencode/plugins/sce-bash-policy.ts` (and `config/automated/.opencode/plugins/sce-bash-policy.ts`) is a thin wrapper that calls `sce policy bash --input normalized --output json` via `spawnSync` and throws on deny decisions; it no longer contains independent TypeScript policy logic. The former `bash-policy/runtime.ts` TypeScript runtime has been removed. Preset... -The `doctor` command now exposes explicit inspection mode (`sce doctor`), repair-intent mode (`sce doctor --fix`), and checkout registry listing (`sce doctor dbs`) at the CLI/help/schema level while keeping diagnosis mode read-only. It now validates both current global operator health and the current repo/hook-integrity slice: state-root resolution, global config path resolution, global and repo-local `sce/config.json` readability/schema validity, local DB and checkout/global Agent Trace DB path + health, DB parent-directory readiness, git availability, non-repo vs bare-repo targeting failures, effective git hook-path source (default, per-repo `core.hooksPath`, or global `core.hooksPath`), hooks-directory health, required hook presence/executable permissions/content drift against canonical embedded SCE-managed hook assets, and repo-root OpenCode integration presence across the installed `plugins`, `agents`, `commands`, and `skills` inventories with embedded SHA-256 content verification for OpenCode assets. Text mode now renders the approved human-only layout with ordered `Environment` / `Configuration` / `Repository` / `Git Hooks` / `Integrations` sections, `SCE doctor diagnose` / `SCE doctor fix` headers, bracketed `[PASS]`/`[FAIL]`/`[MISS]` status tokens, shared-style green pass plus red fail/miss coloring when color output is enabled, simplified `label (path)` row formatting, top-level-only hook rows, and integration parent/child rows that reflect missing vs content-mismatch states; JSON output reports `checkout_identity` plus resolved Agent Trace DB health under `agent_trace_db` when a checkout ID exists, with global Agent Trace DB reporting retained outside checkout context. Fix mode reuses the canonical setup hook install flow to repair missing/stale/non-executable required hooks and can also bootstrap missing canonical DB parent directories while preserving manual-only guidance for unsupported issues. +The `doctor` command now exposes explicit inspection mode (`sce doctor`), repair-intent mode (`sce doctor --fix`), and checkout discovery (`sce doctor dbs`) at the CLI/help/schema level while keeping diagnosis mode read-only. It now validates both current global operator health and the current repo/hook-integrity slice: state-root resolution, global config path resolution, global and repo-local `sce/config.json` readability/schema validity, local DB and checkout/global Agent Trace DB path + health, DB parent-directory readiness, git availability, non-repo vs bare-repo targeting failures, effective git hook-path source (default, per-repo `core.hooksPath`, or global `core.hooksPath`), hooks-directory health, required hook presence/executable permissions/content drift against canonical embedded SCE-managed hook assets, and repo-root OpenCode integration presence across the installed `plugins`, `agents`, `commands`, and `skills` inventories with embedded SHA-256 content verification for OpenCode assets. Text mode now renders the approved human-only layout with ordered `Environment` / `Configuration` / `Repository` / `Git Hooks` / `Integrations` sections, `SCE doctor diagnose` / `SCE doctor fix` headers, bracketed `[PASS]`/`[FAIL]`/`[MISS]` status tokens, shared-style green pass plus red fail/miss coloring when color output is enabled, simplified `label (path)` row formatting, top-level-only hook rows, and integration parent/child rows that reflect missing vs content-mismatch states; JSON output reports `checkout_identity` plus resolved Agent Trace DB health under `agent_trace_db` when a checkout ID exists, with global Agent Trace DB reporting retained outside checkout context. Fix mode reuses the canonical setup hook install flow to repair missing/stale/non-executable required hooks and can also bootstrap missing canonical DB parent directories while preserving manual-only guidance for unsupported issues. Claude bash-policy enforcement is also generated through `.claude/settings.json` as a `PreToolUse` `Bash` command hook running `sce policy bash`, so Claude and OpenCode both delegate to the Rust policy evaluator without a Claude TypeScript runtime. -Local database bootstrap is now owned by `LocalDbLifecycle::setup` and setup-command lifecycle aggregation. Agent Trace setup creates/reuses the current checkout ID, initializes the per-checkout `agent-trace-{checkout_id}.db` with embedded migrations, and records `database_path` in the checkout registry; hook runtime still lazily creates or upgrades the per-checkout DB when setup has not run or schema metadata is incomplete. Doctor validates checkout/global DB paths/health, can bootstrap missing parent directories, and lists registered checkouts through `sce doctor dbs`. Wiring a user-invocable `sce sync` command is deferred to `0.4.0`. +Local database bootstrap is now owned by `LocalDbLifecycle::setup` and setup-command lifecycle aggregation. Agent Trace setup creates/reuses the current checkout ID and initializes the per-checkout `agent-trace-{checkout_id}.db` with embedded migrations; hook runtime still lazily creates or upgrades the per-checkout DB when setup has not run or schema metadata is incomplete. Doctor validates checkout/global DB paths/health, can bootstrap missing parent directories, and discovers checkouts through `sce doctor dbs`. Wiring a user-invocable `sce sync` command is deferred to `0.4.0`. The repository-root flake (`flake.nix`) now applies a Rust overlay-backed stable toolchain pinned to `1.93.1` (with `rustfmt` and `clippy`), reads package/check version from the repo-root `.version` file, builds `packages.sce` through a Crane `buildDepsOnly` + `buildPackage` pipeline with filtered package sources for the Cargo tree plus required embedded config/assets, and runs `cli-tests`, `cli-clippy`, and `cli-fmt` through Crane-backed check derivations (`cargoTest`, `cargoClippy`, `cargoFmt`) that reuse the same filtered source/toolchain setup. The root flake also exposes release install/run outputs directly as `packages.sce` (with `packages.default = packages.sce`) plus `apps.sce` and `apps.default`, so `nix build .#default`, `nix run . -- --help`, `nix run .#sce -- --help`, and `nix profile install github:crocoder-dev/shared-context-engineering` all target the packaged `sce` binary through the same flake-owned entrypoints. The CLI Cargo package metadata now includes crates.io publication-ready fields with crate-local install guidance in `cli/README.md`; supported Cargo install paths are `cargo install shared-context-engineering --locked`, `cargo install --git https://github.com/crocoder-dev/shared-context-engineering shared-context-engineering --locked`, and local `cargo install --path cli --locked`. The published crate installs the `sce` binary. The crate also keeps `cargo clippy --manifest-path cli/Cargo.toml` warnings-denied through `cli/Cargo.toml` lint configuration, so an extra `-- -D warnings` flag is redundant. diff --git a/context/plans/drop-doctor-dbs-path-remote-url.md b/context/plans/drop-doctor-dbs-path-remote-url.md new file mode 100644 index 00000000..eed051a0 --- /dev/null +++ b/context/plans/drop-doctor-dbs-path-remote-url.md @@ -0,0 +1,76 @@ +# drop-doctor-dbs-path-remote-url + +## Change summary + +Remove the `path` and `remote_url` fields from the `DiscoveredCheckout` struct and the `sce doctor dbs` output (text and JSON). These fields are always hardcoded to `"unknown"` and `None` respectively — they carry zero runtime information for filesystem-discovered checkouts. The prior `remove-checkout-registry` plan already targeted dropping these fields from output; this plan finishes that cleanup by removing them from the struct and both renderers. + +## Success criteria + +- `sce doctor dbs` text output no longer renders `path: unknown` or `remote_url: none` lines. +- `sce doctor dbs` JSON output no longer includes `"path"` or `"remote_url"` keys. +- `nix flake check` passes (compile, clippy, fmt, tests, pkl-parity). + +## Constraints and non-goals + +- **In scope:** Remove `path` and `remote_url` from the `DiscoveredCheckout` struct, their hardcoded construction in `discover_checkouts_from_filesystem()`, and their rendering in `render_doctor_dbs_text()` and `render_doctor_dbs_json()`. +- **Out of scope:** The `remote_url` column in the `agent_traces` database table (unrelated — that's active storage). No changes to the main `sce doctor` (diagnose/fix) command. No new tests (no existing test coverage for this command either). No context-file updates needed (context was already refreshed during `remove-checkout-registry` to describe the output as `checkout_id`, `database_path`, `last_seen` only). + +## Task stack + +- [x] T01: `Remove path and remote_url from DiscoveredCheckout and doctor dbs output` (status:done) + - **Files changed:** `cli/src/services/doctor/mod.rs` + - **Evidence:** `nix flake check` passed (cli-tests, cli-clippy, cli-fmt, pkl-parity all green) + - **Completed:** 2026-06-17 + - **Notes:** Six deletions: struct fields, construction, text renderer lines, JSON keys. `DiscoveredCheckout` now has only `checkout_id`, `database_path`, `last_seen`. + - Task ID: T01 + - Goal: Remove the `path` and `remote_url` fields from the `DiscoveredCheckout` struct and strip them from both the text and JSON renderers for `sce doctor dbs`. + - Boundaries (in/out of scope): In — `cli/src/services/doctor/mod.rs` only: the `DiscoveredCheckout` struct definition (lines 80–83), the hardcoded field assignments in `discover_checkouts_from_filesystem()` (lines 181–182), the `path` and `remote_url` lines in `render_doctor_dbs_text()` (lines 208, 211–214), and the `"path"` and `"remote_url"` entries in `render_doctor_dbs_json()` (lines 227, 230). Out — all other files, the main doctor command, database schema, tests. + - Done when: `DiscoveredCheckout` has only `checkout_id`, `database_path`, `last_seen`; `render_doctor_dbs_text()` no longer emits `path:` or `remote_url:` lines; `render_doctor_dbs_json()` no longer includes `"path"` or `"remote_url"` keys; `nix flake check` passes on compile/clippy/fmt. + - Verification notes (commands or checks): `nix flake check`; visual inspection of `sce doctor dbs` output shows no `path` or `remote_url` fields. + +- [x] T02: `Validation and cleanup` (status:done) + - **Files changed:** none (verification only) + - **Evidence:** `nix flake check` passed all derivations; `sce doctor dbs` text output clean (no `path:`/`remote_url:` lines); `sce doctor dbs --format json` clean (no `"path"`/`"remote_url"` keys); grep for `path.*unknown\|remote_url.*none` in `cli/src/services/doctor/mod.rs` returned zero matches + - **Completed:** 2026-06-17 + - **Notes:** Final validation gate. Plan complete. + - Task ID: T02 + - Goal: Run full `nix flake check`, perform a smoke test of `sce doctor dbs`, and verify no residual references remain. + - Boundaries (in/out of scope): In — `nix flake check`, manual smoke test of `sce doctor dbs` text and JSON output, grep for stale references. Out — new features, test additions, context-file changes. + - Done when: `nix flake check` passes all derivations; `sce doctor dbs` and `sce doctor dbs --format json` produce correct output without `path`/`remote_url`; grep for `path.*unknown|remote_url.*none` in `cli/src/services/doctor/mod.rs` returns zero matches for the `dbs`-specific paths. + - Verification notes (commands or checks): `nix flake check`; `nix run .#sce -- doctor dbs`; `nix run .#sce -- doctor dbs --format json`; `rg 'path.*unknown|remote_url.*none' cli/src/services/doctor/mod.rs`. + +--- + +## Validation Report + +### Commands run + +| Command | Exit | Result | +|---|---|---| +| `nix flake check` | 0 | All derivations passed (cli-tests, cli-clippy, cli-fmt, pkl-parity, integrations, npm, config-lib) | +| `nix run .#pkl-check-generated` | 0 | Generated outputs are up to date | +| `nix run .#sce -- doctor dbs` | 0 | Output: `checkout_id`, `database_path`, `last_seen` only — no `path:` or `remote_url:` lines | +| `nix run .#sce -- doctor dbs --format json` | 0 | Output: `checkout_id`, `database_path`, `last_seen` only — no `"path"` or `"remote_url"` keys | +| `rg 'path.*unknown\|remote_url.*none' cli/src/services/doctor/mod.rs` | 1 (no matches) | Zero residual references | + +### Success-criteria verification + +- [x] `sce doctor dbs` text output no longer renders `path: unknown` or `remote_url: none` lines — confirmed via smoke test output +- [x] `sce doctor dbs` JSON output no longer includes `"path"` or `"remote_url"` keys — confirmed via JSON smoke test output +- [x] `nix flake check` passes (compile, clippy, fmt, tests, pkl-parity) — confirmed, all checks green + +### Failed checks and follow-ups + +None. + +### Temporary scaffolding + +None added or removed. + +### Context sync + +Verify-only — glossary tightened ("no longer stored" → "no longer rendered") for precision. + +### Residual risks + +None identified. The removed fields were hardcoded to `"unknown"`/`None` and had no downstream consumers. diff --git a/context/plans/remove-checkout-registry.md b/context/plans/remove-checkout-registry.md new file mode 100644 index 00000000..2f7eb555 --- /dev/null +++ b/context/plans/remove-checkout-registry.md @@ -0,0 +1,92 @@ +# remove-checkout-registry + +## Change summary + +Remove the central checkout registry (`cli/src/services/checkout/registry.rs`, ~220 lines) and its five call sites across setup and hook runtime. Replace `sce doctor dbs` with a filesystem scan that derives checkout metadata from `agent-trace-*.db` files on disk. The registry is a single-consumer metadata cache (only `sce doctor dbs` reads it) that injects fallible atomic-rename I/O into the hot path of every hook invocation, causing `ENOENT` failures under concurrent access. + +## Success criteria + +- `registry.rs` file is deleted; `pub mod registry;` is removed from `checkout/mod.rs`. +- Zero `register_checkout()` calls remain in the codebase. +- `sce setup` and hook runtime no longer touch `checkout-registry.json` at all. +- `sce doctor dbs` scans `/sce/agent-trace-*.db` files on disk and produces equivalent output (checkout_id, database_path, last_seen from mtime; `path` and `remote_url` omitted or set to `"unknown"`). +- `nix flake check` passes (Rust compile, fmt, clippy, tests). +- All context files referencing the registry are updated to reflect its removal. + +## Constraints and non-goals + +- **In scope:** Delete `registry.rs`, strip all callers, rework `sce doctor dbs` to filesystem scan, update context docs, run full flake check. +- **Out of scope:** Adding a new persistence mechanism (no SQLite metadata table, no new config file). The `path` (repo root) and `remote_url` fields are dropped from `sce doctor dbs` output. No new tests for the doctor dbs command. No changes to `sce doctor` (diagnose/fix) behavior. +- **Assumption:** `sce doctor dbs` is a diagnostic convenience; losing `path`/`remote_url` is acceptable. File mtime is a sufficient proxy for `last_seen`. + +## Task stack + +- [x] T01: `Rework sce doctor dbs to scan filesystem instead of registry` (status:done) + - Task ID: T01 + - Goal: Replace `run_doctor_dbs()` so it scans `/sce/agent-trace-*.db` files instead of calling `registry::list_checkouts()`. Define a local `DiscoveredCheckout` struct to replace `CheckoutRecord` usage. + - Boundaries (in/out of scope): In — `run_doctor_dbs()`, `render_doctor_dbs_text()`, `render_doctor_dbs_json()`, `sort_checkouts_by_last_seen_desc()`, and all `CheckoutRecord` references in `cli/src/services/doctor/mod.rs`. Out — all other doctor functions, registry.rs deletion, lifecycle.rs changes, context files. + - **Status:** done + - **Completed:** 2026-06-17 + - **Files changed:** `cli/src/services/doctor/mod.rs` + - **Evidence:** `nix flake check` passed (cli-tests, cli-clippy, cli-fmt, pkl-parity all green) + - **Notes:** Added `DiscoveredCheckout` struct, `discover_checkouts_from_filesystem()` function, filesystem scan for `agent-trace-*.db` files. Registry import removed. `path` = `"unknown"`, `remote_url` = `None`. + +- [x] T02: `Delete registry.rs and strip all callers` (status:done) + - Task ID: T02 + - Goal: Delete `cli/src/services/checkout/registry.rs`, remove `pub mod registry;` from `checkout/mod.rs`, remove `register_checkout_for_db()` and its two calls from `checkout/mod.rs`, remove two `registry::register_checkout()` calls from `agent_trace_db/lifecycle.rs`. + - Boundaries (in/out of scope): In — `checkout/registry.rs` deletion, `checkout/mod.rs` (module declaration, `register_checkout_for_db` fn, two call sites, `use chrono::Utc` if orphaned), `agent_trace_db/lifecycle.rs` (`setup_checkout_identity` and `initialize_checkout_agent_trace_db` functions, `use` imports for `registry` and `chrono::Utc` if orphaned). Out — doctor/mod.rs, context files, any other service modules. + - Done when: `registry.rs` is deleted; grep for `register_checkout`, `CheckoutRecord`, `checkout_registry_path`, `write_registry`, `read_registry` returns zero matches in `cli/src/`. `nix flake check` passes. `sce setup` runs without touching `checkout-registry.json`. + - Verification notes: `nix flake check`; `rg "register_checkout|CheckoutRecord|checkout_registry" cli/src/` returns nothing; manually run `sce setup` in a test repo, confirm no `checkout-registry.json` is created. + - **Status:** done + - **Completed:** 2026-06-17 + - **Files changed:** Deleted `cli/src/services/checkout/registry.rs`; modified `cli/src/services/checkout/mod.rs` (removed `pub mod registry;`, `register_checkout_for_db()`, both call sites, `use chrono::Utc`); modified `cli/src/services/agent_trace_db/lifecycle.rs` (removed 2x `registry::register_checkout()` calls, removed `chrono::Utc` import, removed `registry` from `use checkout::{self, registry}`, removed unused `repo_root` param from `initialize_checkout_agent_trace_db`) + - **Evidence:** `nix flake check` passed (cli-tests, cli-clippy, cli-fmt, pkl-parity all green); `rg "register_checkout|CheckoutRecord|checkout_registry" cli/src/` returns zero matches + +- [x] T03: `Clean up dead code and unused imports` (status:done) + - Task ID: T03 + - Goal: Remove any `#![allow(dead_code)]` attributes whose sole purpose was registry code. Remove unused imports discovered after T02. Remove `CheckoutIdentitySetup` struct from lifecycle.rs if it becomes a trivial wrapper (just `checkout_id: String`). + - Boundaries (in/out of scope): In — `checkout/mod.rs` `#![allow(dead_code)]`, `agent_trace_db/lifecycle.rs` struct simplification, any orphaned `use` statements. Out — behavior changes, new features, context files. + - **Status:** done + - **Completed:** 2026-06-17 + - **Files changed:** `cli/src/services/checkout/mod.rs` (removed `#![allow(dead_code)]`, `resolve_checkout_id_for_repo()`, `resolve_or_create_agent_trace_db_for_current_checkout()`, stale registry doc comment); `cli/src/services/agent_trace_db/lifecycle.rs` (removed `CheckoutIdentitySetup` struct, `setup_checkout_identity()` now returns `Result`) + - **Evidence:** `nix flake check` passed (cli-tests, cli-clippy, cli-fmt, pkl-parity all green) + +- [x] T04: `Update context files to reflect registry removal` (status:done) + - Task ID: T04 + - Goal: Update all context markdown files that reference the checkout registry, `checkout-registry.json`, `CheckoutRecord`, or registry-based `sce doctor dbs` behavior. Remove stale references; document the new filesystem-scan approach. + - Boundaries (in/out of scope): In — `context/cli/checkout-identity.md`, `context/architecture.md`, `context/cli/cli-command-surface.md`, `context/cli/default-path-catalog.md`, `context/context-map.md`, `context/glossary.md`, `context/overview.md`, `context/sce/agent-trace-db.md`, `context/cli/service-lifecycle.md`. Out — plan files (they are historical), decisions files, code changes. + - **Status:** done + - **Completed:** 2026-06-17 + - **Files changed:** `context/cli/checkout-identity.md` (removed Registry resilience section, removed registry.rs code surface section, rewrote integration state to remove all `register_checkout`/registry references, updated `sce doctor dbs` description to filesystem scan); `context/architecture.md` (updated checkout service description and doctor DB discovery wording); `context/cli/cli-command-surface.md` (updated doctor surfaces, DB listing output, and doctor module description); `context/cli/default-path-catalog.md` (removed checkout registry path entry, removed registry storage wording); `context/context-map.md` (updated checkout-identity description); `context/glossary.md` (updated checkout registry entry to document removal); `context/overview.md` (updated doctor and Agent Trace setup descriptions); `context/sce/agent-trace-db.md` (removed registry metadata path entry, updated setup and doctor dbs descriptions); `context/cli/service-lifecycle.md` (removed registry wording from AgentTraceDbLifecycle::setup description) + - **Evidence:** `nix flake check` passed (pkl-parity green, Rust checks green from earlier build); grep for `checkout-registry\|checkout registry\|CheckoutRecord\|register_checkout` in `context/` outside plans/decisions returns only the intentionally retained glossary entry documenting the registry as removed + +- [x] T05: `Validation and cleanup` (status:done) + - Task ID: T05 + - Goal: Run full flake check, verify end-to-end behavior, remove any stale `checkout-registry.json` guidance, and confirm context accuracy. + - Boundaries (in/out of scope): In — `nix flake check`, manual smoke test of `sce setup` + `sce doctor dbs`, context sync verification. Out — new features, test additions. + - **Status:** done + - **Completed:** 2026-06-17 + - **Files changed:** `context/overview.md` (removed 1 stale `records the registry database_path` reference); `context/architecture.md` (removed 3 stale registry references: `registry registration`, `records database_path`, `registry listings`); `context/cli/cli-command-surface.md` (removed 1 stale `registry database_path recording` reference); temp test repo cleaned up + - **Evidence:** `nix flake check` passed (all 18 derivations green); smoke test confirmed `sce setup --hooks` does NOT create `checkout-registry.json`; `sce doctor dbs` discovers checkouts via filesystem scan with `path=unknown`, `remote_url=none`; grep for `register_checkout|CheckoutRecord|checkout_registry` in `cli/src/` returns zero matches; grep in `context/` outside plans/decisions returns only the intentional glossary `(removed)` entry + +## Validation Report + +### Commands run +- `nix flake check` -> **exit 0** (cli-tests, cli-clippy, cli-fmt, integrations-install-tests, integrations-install-clippy, integrations-install-fmt, pkl-parity, npm-bun-tests, npm-biome-check, npm-biome-format, config-lib-bun-tests, config-lib-biome-check, config-lib-biome-format — all green) +- `nix build .#default` -> **exit 0** (packaged binary produced) +- `./result/bin/sce setup --hooks --repo /tmp/test-repo` -> **exit 0** (checkout identity created, hooks installed, no `checkout-registry.json` created) +- `./result/bin/sce doctor dbs` -> **exit 0** (discovered 2 checkouts via filesystem scan, `path=unknown`, `remote_url=none`) +- `rg "register_checkout|CheckoutRecord|checkout_registry" cli/src/` -> **zero matches** +- `rg "checkout-registry|checkout registry|CheckoutRecord|register_checkout" context/ --glob '!context/plans/*' --glob '!context/decisions/*'` -> **only glossary `(removed)` entry** +- Temporary scaffolding removed: `rm -rf /tmp/test-repo`, `rm -f ~/.local/state/sce/checkout-registry.json*` + +### Success-criteria verification +- [x] `registry.rs` file is deleted; `pub mod registry;` is removed from `checkout/mod.rs` — confirmed in T02 +- [x] Zero `register_checkout()` calls remain in the codebase — grep confirms zero matches in `cli/src/` +- [x] `sce setup` and hook runtime no longer touch `checkout-registry.json` at all — smoke test: `sce setup` deleted old file, reran, no new file created +- [x] `sce doctor dbs` scans `/sce/agent-trace-*.db` files on disk and produces equivalent output — smoke test output shows `checkout_id`, `database_path`, `last_seen` from mtime; `path=unknown`, `remote_url=none` +- [x] `nix flake check` passes — all 13 check derivations green +- [x] All context files referencing the registry are updated — T04 updated 9 files; T05 context sync pass found and fixed 4 additional stale references + +### Residual risks +- None identified. The checkout-registry.json file at `~/.local/state/sce/` is now a stale artifact from a prior code version; it is no longer read or written by any current code path. diff --git a/context/sce/agent-trace-db.md b/context/sce/agent-trace-db.md index 4858f4e7..5758b0c0 100644 --- a/context/sce/agent-trace-db.md +++ b/context/sce/agent-trace-db.md @@ -58,7 +58,7 @@ Active hook runtime resolves per-checkout Agent Trace DB files: - Function: `agent_trace_db_path_for_checkout(checkout_id)` in `cli/src/services/default_paths.rs` - Path template: `/sce/agent-trace-{checkout_id}.db` - Checkout ID source: `/sce/checkout-id`, where `` comes from `git rev-parse --git-dir` -- Registry metadata: `/sce/checkout-registry.json` stores `database_path` after successful setup-time DB initialization or hook-runtime lazy initialization +- Checkouts are discovered by `sce doctor dbs` via filesystem scan of `/sce/agent-trace-*.db` files; there is no central registry file. ## Migrations @@ -180,9 +180,9 @@ Both triggers compare `OLD.*` vs `NEW.*` for all mutable columns (excluding `upd - `diagnose()` reports per-checkout Agent Trace DB path and parent-directory readiness when a repo root has a checkout ID; otherwise it falls back to the legacy global Agent Trace DB path. When the DB file exists, it also performs a deep health check: opens the file via `open_for_hooks_without_migrations_at` and verifies schema readiness via `ensure_schema_ready_for_hooks`, reporting `AgentTraceDbConnectionFailed` if open fails or `AgentTraceDbSchemaNotReady` if the schema is incomplete. These deep-check problems are `ManualOnly` (not auto-fixable by `sce doctor --fix`); the remediation directs the operator to re-run `sce setup` or fix file permissions. - `fix()` bootstraps the resolved per-checkout DB parent directory for auto-fixable parent-readiness problems, with the same global fallback outside checkout context. -- `setup()` creates/reuses the current checkout identity when a repo root is available, registers the identity, resolves `/sce/agent-trace-{checkout_id}.db` through `agent_trace_db_path_for_checkout(checkout_id)`, opens/creates it with `AgentTraceDb::open_at(&db_path)` to apply embedded migrations, updates `/sce/checkout-registry.json` with `database_path`, and emits setup messaging with the checkout ID plus initialized DB path. Hook runtime lazy initialization remains available for checkouts where setup has not run or schema metadata is incomplete. +- `setup()` creates/reuses the current checkout identity when a repo root is available, resolves `/sce/agent-trace-{checkout_id}.db` through `agent_trace_db_path_for_checkout(checkout_id)`, opens/creates it with `AgentTraceDb::open_at(&db_path)` to apply embedded migrations, and emits setup messaging with the checkout ID plus initialized DB path. Hook runtime lazy initialization remains available for checkouts where setup has not run or schema metadata is incomplete. - `sce doctor` surfaces checkout identity and per-checkout Agent Trace DB health in the `Configuration` section when a checkout ID exists, with `[PASS]`/`[FAIL]`/`[MISS]` status tokens. Outside checkout context it falls back to the legacy/global Agent Trace DB row. JSON output includes `checkout_identity` when available plus the resolved `agent_trace_db` field. -- `sce doctor dbs` lists registered checkout records from `/sce/checkout-registry.json` in text or JSON, sorted by `last_seen` descending. +- `sce doctor dbs` discovers checkouts by scanning `/sce/agent-trace-*.db` files on disk, reporting them in text or JSON sorted by mtime descending. ## Runtime writers