From ab7b9e0e2b9f6e8948a217c5716d91a0ddda4ae7 Mon Sep 17 00:00:00 2001 From: David Abram Date: Tue, 14 Jul 2026 17:00:51 +0200 Subject: [PATCH 1/8] config: Add Agent Trace git-notes ref resolution Add policies.agent_trace.git_notes_ref parsing, validation, default resolution, and generated schema support for the Agent Trace post-commit git-notes ref. Co-authored-by: SCE --- cli/src/services/config/resolver.rs | 64 +++++++++++++- cli/src/services/config/schema.rs | 75 ++++++++++++++-- cli/src/services/config/types.rs | 2 + config/pkl/base/sce-config-schema.pkl | 13 +++ config/schema/sce-config.schema.json | 13 +++ context/architecture.md | 2 +- context/cli/config-precedence-contract.md | 7 +- context/context-map.md | 2 +- context/glossary.md | 1 + context/overview.md | 2 +- context/plans/agent-trace-git-notes.md | 100 ++++++++++++++++++++++ 11 files changed, 269 insertions(+), 12 deletions(-) create mode 100644 context/plans/agent-trace-git-notes.md diff --git a/cli/src/services/config/resolver.rs b/cli/src/services/config/resolver.rs index b370ca64..efcc9d27 100644 --- a/cli/src/services/config/resolver.rs +++ b/cli/src/services/config/resolver.rs @@ -16,8 +16,8 @@ use super::types::{ parse_bool_value_from, ConfigPathSource, ConfigRequest, DatabaseRetryConfig, LoadedConfigPath, LogFileMode, LogFormat, LogLevel, ReportFormat, ResolvedAuthRuntimeConfig, ResolvedHookRuntimeConfig, ResolvedObservabilityRuntimeConfig, ResolvedOptionalValue, - ResolvedValue, ValueSource, ENV_ATTRIBUTION_HOOKS_DISABLED, ENV_LOG_FILE, ENV_LOG_FILE_MODE, - ENV_LOG_FORMAT, ENV_LOG_LEVEL, + ResolvedValue, ValueSource, DEFAULT_AGENT_TRACE_GIT_NOTES_REF, ENV_ATTRIBUTION_HOOKS_DISABLED, + ENV_LOG_FILE, ENV_LOG_FILE_MODE, ENV_LOG_FORMAT, ENV_LOG_LEVEL, }; const DEFAULT_TIMEOUT_MS: u64 = 30000; @@ -62,6 +62,7 @@ pub(super) struct RuntimeConfig { pub(super) log_file_mode: ResolvedValue, pub(super) timeout_ms: ResolvedValue, pub(super) attribution_hooks_enabled: ResolvedValue, + pub(super) agent_trace_git_notes_ref: ResolvedValue, pub(super) workos_client_id: ResolvedOptionalValue, pub(super) bash_policies: ResolvedOptionalValue, pub(super) database_retry: ResolvedOptionalValue, @@ -226,6 +227,7 @@ where Ok(ResolvedHookRuntimeConfig { attribution_hooks_enabled: runtime.attribution_hooks_enabled.value, + agent_trace_git_notes_ref: runtime.agent_trace_git_notes_ref.value, }) } @@ -272,6 +274,7 @@ where log_file_mode: None, timeout_ms: None, attribution_hooks_enabled: None, + agent_trace_git_notes_ref: None, workos_client_id: None, bash_policy_presets: None, bash_policy_custom: None, @@ -307,6 +310,9 @@ where if let Some(attribution_hooks_enabled) = layer.attribution_hooks_enabled { file_config.attribution_hooks_enabled = Some(attribution_hooks_enabled); } + if let Some(agent_trace_git_notes_ref) = layer.agent_trace_git_notes_ref { + file_config.agent_trace_git_notes_ref = Some(agent_trace_git_notes_ref); + } if let Some(workos_client_id) = layer.workos_client_id { file_config.workos_client_id = Some(workos_client_id); } @@ -449,6 +455,8 @@ where source: ValueSource::Env, }; } + let resolved_agent_trace_git_notes_ref = + resolve_agent_trace_git_notes_ref(file_config.agent_trace_git_notes_ref.as_ref()); let resolved_workos_client_id = resolve_optional_auth_config_value( WORKOS_CLIENT_ID_KEY, file_config.workos_client_id, @@ -472,6 +480,7 @@ where log_file_mode: resolved_log_file_mode, timeout_ms: resolved_timeout_ms, attribution_hooks_enabled: resolved_attribution_hooks_enabled, + agent_trace_git_notes_ref: resolved_agent_trace_git_notes_ref, workos_client_id: resolved_workos_client_id, bash_policies: resolved_bash_policies, database_retry: resolved_database_retry, @@ -480,6 +489,22 @@ where }) } +fn resolve_agent_trace_git_notes_ref( + file_value: Option<&schema::FileConfigValue>, +) -> ResolvedValue { + if let Some(value) = file_value { + return ResolvedValue { + value: value.value.clone(), + source: ValueSource::ConfigFile(value.source), + }; + } + + ResolvedValue { + value: DEFAULT_AGENT_TRACE_GIT_NOTES_REF.to_string(), + source: ValueSource::Default, + } +} + fn resolve_optional_auth_config_value( key: AuthConfigKeySpec, file_value: Option>, @@ -682,6 +707,7 @@ mod tests { Ok(ResolvedHookRuntimeConfig { attribution_hooks_enabled: runtime.attribution_hooks_enabled.value, + agent_trace_git_notes_ref: runtime.agent_trace_git_notes_ref.value, }) } @@ -692,6 +718,40 @@ mod tests { assert!(resolved.attribution_hooks_enabled); } + #[test] + fn agent_trace_git_notes_ref_uses_default() { + let resolved = resolve_hooks_with_env_and_config(None, None).unwrap(); + + assert_eq!( + resolved.agent_trace_git_notes_ref, + DEFAULT_AGENT_TRACE_GIT_NOTES_REF + ); + } + + #[test] + fn agent_trace_git_notes_ref_uses_explicit_config() { + let resolved = resolve_hooks_with_env_and_config( + None, + Some(r#"{"policies":{"agent_trace":{"git_notes_ref":"refs/notes/custom-sce"}}}"#), + ) + .unwrap(); + + assert_eq!(resolved.agent_trace_git_notes_ref, "refs/notes/custom-sce"); + } + + #[test] + fn blank_agent_trace_git_notes_ref_is_rejected() { + let error = resolve_hooks_with_env_and_config( + None, + Some(r#"{"policies":{"agent_trace":{"git_notes_ref":" "}}}"#), + ) + .unwrap_err(); + + assert!(error + .to_string() + .contains("policies.agent_trace.git_notes_ref")); + } + #[test] fn attribution_hooks_disabled_env_truthy_opts_out() { let resolved = diff --git a/cli/src/services/config/schema.rs b/cli/src/services/config/schema.rs index c5944b8d..0dcb41b2 100644 --- a/cli/src/services/config/schema.rs +++ b/cli/src/services/config/schema.rs @@ -84,9 +84,15 @@ pub(crate) struct ParsedIntegrationsConfigDocument { pub(crate) struct ParsedPoliciesConfigDocument { pub(crate) bash: Option, pub(crate) attribution_hooks: Option, + pub(crate) agent_trace: Option, pub(crate) database_retry: Option, } +#[derive(Clone, Debug, Deserialize, Eq, PartialEq)] +pub(crate) struct ParsedAgentTracePolicyConfigDocument { + pub(crate) git_notes_ref: Option, +} + #[derive(Clone, Debug, Deserialize, Eq, PartialEq)] pub(crate) struct ParsedBashPolicyConfigDocument { pub(crate) presets: Option>, @@ -147,6 +153,7 @@ pub(crate) struct FileConfig { pub(crate) log_file_mode: Option>, pub(crate) timeout_ms: Option>, pub(crate) attribution_hooks_enabled: Option>, + pub(crate) agent_trace_git_notes_ref: Option>, pub(crate) workos_client_id: Option>, pub(crate) bash_policy_presets: Option>>, pub(crate) bash_policy_custom: Option>>, @@ -163,6 +170,7 @@ pub(crate) type ParsedFilePolicies = ( Option>, Option>>, Option>>, + Option>, Option>, ); @@ -294,8 +302,13 @@ pub(crate) fn parse_file_config( let workos_client_id = typed .workos_client_id .map(|value| FileConfigValue { value, source }); - let (attribution_hooks_enabled, bash_policy_presets, bash_policy_custom, database_retry) = - map_policies_config(typed.policies.as_ref(), object, path, source)?; + let ( + attribution_hooks_enabled, + bash_policy_presets, + bash_policy_custom, + agent_trace_git_notes_ref, + database_retry, + ) = map_policies_config(typed.policies.as_ref(), object, path, source)?; let integrations = map_integrations_config(typed.integrations.as_ref(), object, path, source)?; Ok(FileConfig { @@ -305,6 +318,7 @@ pub(crate) fn parse_file_config( log_file_mode, timeout_ms, attribution_hooks_enabled, + agent_trace_git_notes_ref, workos_client_id, bash_policy_presets, bash_policy_custom, @@ -320,7 +334,7 @@ pub(crate) fn map_policies_config( source: ConfigPathSource, ) -> Result { let Some(policies_value) = object.get("policies") else { - return Ok((None, None, None, None)); + return Ok((None, None, None, None, None)); }; let policies_object = policies_value.as_object().with_context(|| { @@ -334,8 +348,8 @@ pub(crate) fn map_policies_config( policies_object, path, Some("policies"), - &["bash", "attribution_hooks", "database_retry"], - "bash, attribution_hooks, database_retry", + &["bash", "attribution_hooks", "agent_trace", "database_retry"], + "bash, attribution_hooks, agent_trace, database_retry", )?; let bash = typed.and_then(|config| config.bash.as_ref()); @@ -347,6 +361,12 @@ pub(crate) fn map_policies_config( )?; let (bash_policy_presets, bash_policy_custom) = map_bash_policy_config(bash, policies_object, path, source)?; + let agent_trace_git_notes_ref = map_agent_trace_policy_config( + typed.and_then(|config| config.agent_trace.as_ref()), + policies_object, + path, + source, + )?; let database_retry = map_database_retry_config( typed.and_then(|config| config.database_retry.as_ref()), policies_object, @@ -358,6 +378,7 @@ pub(crate) fn map_policies_config( attribution_hooks_enabled, bash_policy_presets, bash_policy_custom, + agent_trace_git_notes_ref, database_retry, )) } @@ -392,6 +413,50 @@ pub(crate) fn map_attribution_hooks_config( .map(|value| FileConfigValue { value, source })) } +pub(crate) fn map_agent_trace_policy_config( + typed: Option<&ParsedAgentTracePolicyConfigDocument>, + policies_object: &serde_json::Map, + path: &Path, + source: ConfigPathSource, +) -> Result>> { + let Some(agent_trace_value) = policies_object.get("agent_trace") else { + return Ok(None); + }; + + let agent_trace_object = agent_trace_value.as_object().with_context(|| { + format!( + "Config key 'policies.agent_trace' in '{}' must be an object.", + path.display() + ) + })?; + + validate_object_keys( + agent_trace_object, + path, + Some("policies.agent_trace"), + &["git_notes_ref"], + "git_notes_ref", + )?; + + typed + .and_then(|config| config.git_notes_ref.as_ref()) + .map(|value| { + let trimmed = value.trim(); + if trimmed.is_empty() { + bail!( + "Config key 'policies.agent_trace.git_notes_ref' in '{}' must not be empty.", + path.display() + ); + } + + Ok(FileConfigValue { + value: trimmed.to_string(), + source, + }) + }) + .transpose() +} + pub(crate) fn map_bash_policy_config( typed: Option<&ParsedBashPolicyConfigDocument>, policies_object: &serde_json::Map, diff --git a/cli/src/services/config/types.rs b/cli/src/services/config/types.rs index be3bdc55..bd509efc 100644 --- a/cli/src/services/config/types.rs +++ b/cli/src/services/config/types.rs @@ -18,6 +18,7 @@ pub(crate) const ENV_LOG_FORMAT: &str = "SCE_LOG_FORMAT"; pub(crate) const ENV_LOG_FILE: &str = "SCE_LOG_FILE"; pub(crate) const ENV_LOG_FILE_MODE: &str = "SCE_LOG_FILE_MODE"; pub(crate) const ENV_ATTRIBUTION_HOOKS_DISABLED: &str = "SCE_ATTRIBUTION_HOOKS_DISABLED"; +pub(crate) const DEFAULT_AGENT_TRACE_GIT_NOTES_REF: &str = "refs/notes/sce-agent-trace"; pub type ReportFormat = OutputFormat; @@ -239,6 +240,7 @@ pub(crate) struct ResolvedObservabilityRuntimeConfig { #[derive(Clone, Debug, Eq, PartialEq)] pub(crate) struct ResolvedHookRuntimeConfig { pub(crate) attribution_hooks_enabled: bool, + pub(crate) agent_trace_git_notes_ref: String, } pub(crate) fn parse_bool_value_from(key: &str, raw: &str, source: &str) -> anyhow::Result { diff --git a/config/pkl/base/sce-config-schema.pkl b/config/pkl/base/sce-config-schema.pkl index e8ec8440..a48a49fa 100644 --- a/config/pkl/base/sce-config-schema.pkl +++ b/config/pkl/base/sce-config-schema.pkl @@ -101,6 +101,19 @@ local sceConfigSchema = new JsonSchema { } } } + ["agent_trace"] = new JsonSchema { + type = "object" + description = "Agent Trace hook policy. Controls where post-commit Agent Trace JSON is mirrored as a git note." + additionalProperties = false + properties { + ["git_notes_ref"] = new JsonSchema { + type = "string" + minLength = 1 + description = "Git notes ref used for Agent Trace JSON persistence. Defaults to refs/notes/sce-agent-trace." + default = "refs/notes/sce-agent-trace" + } + } + } ["database_retry"] = new JsonSchema { type = "object" additionalProperties = false diff --git a/config/schema/sce-config.schema.json b/config/schema/sce-config.schema.json index dfd6eff1..11bae84b 100644 --- a/config/schema/sce-config.schema.json +++ b/config/schema/sce-config.schema.json @@ -58,6 +58,19 @@ }, "additionalProperties": false }, + "agent_trace": { + "description": "Agent Trace hook policy. Controls where post-commit Agent Trace JSON is mirrored as a git note.", + "type": "object", + "properties": { + "git_notes_ref": { + "description": "Git notes ref used for Agent Trace JSON persistence. Defaults to refs/notes/sce-agent-trace.", + "default": "refs/notes/sce-agent-trace", + "type": "string", + "minLength": 1 + } + }, + "additionalProperties": false + }, "database_retry": { "type": "object", "properties": { diff --git a/context/architecture.md b/context/architecture.md index 6d7d2089..6ada995a 100644 --- a/context/architecture.md +++ b/context/architecture.md @@ -111,7 +111,7 @@ The repository includes a new placeholder Rust binary crate at `cli/`. - `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, 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/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, Agent Trace hook policy resolution for `policies.agent_trace.git_notes_ref` with default `refs/notes/sce-agent-trace`, 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. diff --git a/context/cli/config-precedence-contract.md b/context/cli/config-precedence-contract.md index 495797d5..8c704f26 100644 --- a/context/cli/config-precedence-contract.md +++ b/context/cli/config-precedence-contract.md @@ -25,6 +25,8 @@ Resolved runtime values follow this deterministic order: Repo-configured bash-tool policy values are config-file only in this task slice: they load from `policies.bash` in the selected config files, merge `global -> local` alongside the rest of the config object, and currently have no flag or environment override layer. +Agent Trace hook policy currently includes `policies.agent_trace.git_notes_ref`, which resolves as config-file value over default `refs/notes/sce-agent-trace`. It has no flag or environment override layer in the current implementation slice. The resolved value is exposed through hook runtime config for later post-commit git-note persistence wiring; current post-commit runtime behavior is not yet changed by this config field. + Resolved observability values that currently have no CLI flag layer follow the same lower-precedence chain without a flag step: 1. environment values (`SCE_LOG_FORMAT`, `SCE_LOG_FILE`, `SCE_LOG_FILE_MODE`) @@ -56,7 +58,7 @@ When a default-discovered global or repo-local config file exists but fails JSON - The canonical JSON Schema artifact for both global and repo-local `sce/config.json` files is authored in `config/pkl/base/sce-config-schema.pkl` and generated to `config/schema/sce-config.schema.json`. - `cli/src/services/config/schema.rs` embeds that generated artifact at compile time as `SCE_CONFIG_SCHEMA_JSON` and uses it for runtime schema validation before mapping parsed files into typed serde DTOs. - `sce config validate` and `sce doctor` both validate config-file structure against that shared generated schema before applying Rust-owned semantic checks such as duplicate custom `argv_prefix` detection and redundancy warnings. -- After schema validation, `cli/src/services/config/schema.rs` deserializes top-level and nested config structure (`policies`, `policies.bash`, `policies.attribution_hooks`) into typed serde DTOs and applies focused Rust-owned mapping helpers for enum conversion and source attribution; policy-specific semantic checks are owned by `cli/src/services/config/policy.rs`. +- After schema validation, `cli/src/services/config/schema.rs` deserializes top-level and nested config structure (`policies`, `policies.bash`, `policies.attribution_hooks`, `policies.agent_trace`) into typed serde DTOs and applies focused Rust-owned mapping helpers for enum conversion and source attribution; policy-specific semantic checks are owned by `cli/src/services/config/policy.rs`. - The canonical top-level schema declaration `"$schema": "https://sce.crocoder.dev/config.json"` is a supported config key for both explicit and discovered `sce/config.json` files, including command-startup paths like `sce version` and other config-loading commands that parse config before normal command dispatch. - Startup/runtime config resolution now degrades gracefully only for default-discovered files: invalid discovered files are skipped and reported via collected `validation_errors`, while explicit `--config` / `SCE_CONFIG_FILE` targets still fail immediately on the same parse or validation errors. @@ -76,8 +78,9 @@ When a default-discovered global or repo-local config file exists but fails JSON - Supported target ID values: `opencode`, `claude`, `pi`. - Unknown target IDs fail schema validation. -- `policies` must be an object when present and currently allows `attribution_hooks`, `database_retry`, and `bash`. +- `policies` must be an object when present and currently allows `attribution_hooks`, `agent_trace`, `database_retry`, and `bash`. - `policies.attribution_hooks` must be an object when present and currently allows `enabled`; the generated schema documents default `true`, and explicit `enabled: false` remains a valid opt-out alongside the runtime `SCE_ATTRIBUTION_HOOKS_DISABLED` environment opt-out. +- `policies.agent_trace` must be an object when present and currently allows `git_notes_ref`; the generated schema documents default `refs/notes/sce-agent-trace`, and Rust mapping rejects blank/whitespace-only refs. - `policies.bash` must be an object when present and currently allows only `presets` and `custom`. - `policies.bash.presets` must be an array of unique built-in preset IDs: `forbid-git-all`, `forbid-git-commit`, `use-pnpm-over-npm`, `use-bun-over-npm`, `use-nix-flake-over-cargo`. - `use-pnpm-over-npm` and `use-bun-over-npm` are mutually exclusive and fail validation when both are present. diff --git a/context/context-map.md b/context/context-map.md index d81a35c5..eddb5150 100644 --- a/context/context-map.md +++ b/context/context-map.md @@ -16,7 +16,7 @@ Feature/domain context: - `context/cli/structured-patch-service.md` (Claude structured editor-hook derivation in `cli/src/services/structured_patch.rs`, including `Write` structured-update hunks, `Write` `tool_input.content` create fallback, `Edit` structured patches, deterministic skip reasons, `ParsedPatch` output semantics, and Rust golden fixture coverage) - `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/trace-command.md` (`sce trace` command group: discovery of per-checkout `agent-trace-*.db` files under `/sce/` with mtime-desc + checkout-id tiebreak alias assignment and six-required-table readiness probing, implemented `sce trace db shell ` wiring that resolves aliases/checkout IDs and opens the embedded in-process SQL shell without external `turso`, including `.tables` table-name listing for visible/internal tables, implemented `sce trace db list` text + JSON rendering using `services::style::heading`, implemented `sce trace status` per-checkout rendering with `StatusError::{NotInGitRepo, NoCheckoutId, DbMissing}` mapped to validation-class exits and skipped-DB pass-through, implemented `sce trace status --all` aggregation across every discovered DB, and the completed removal of `sce doctor dbs` whose discovery scan/rendering moved into `services::trace`) -- `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 including `policies.attribution_hooks.enabled` default-true/explicit-false opt-out metadata, config-file selection order, `show` provenance output, and trimmed `validate` output contract) +- `context/cli/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 including `policies.attribution_hooks.enabled` default-true/explicit-false opt-out metadata and `policies.agent_trace.git_notes_ref` default `refs/notes/sce-agent-trace`, 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 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) diff --git a/context/glossary.md b/context/glossary.md index 18b60e17..efe7ad4e 100644 --- a/context/glossary.md +++ b/context/glossary.md @@ -112,6 +112,7 @@ - `sce config schema artifact`: Canonical JSON Schema for global and repo-local `sce/config.json` files, authored in `config/pkl/base/sce-config-schema.pkl`, generated to `config/schema/sce-config.schema.json`, and embedded by `cli/src/services/config/schema.rs` for shared `sce config validate` and doctor config validation. The current schema accepts the canonical `$schema` declaration, flat logging keys (`log_level`, `log_format`, `log_file`, `log_file_mode`), existing auth/config keys, and enforces the schema-level dependency that `log_file_mode` requires `log_file`. - `bash tool policy config surface`: Nested repo config namespace under `.sce/config.json` at `policies.bash`, currently supporting unique built-in `presets` plus repo-owned `custom` argv-prefix rules with deterministic validation, merged global/local resolution, and first-class `sce config show|validate` reporting. - `attribution hooks gate`: Enabled-by-default local hook runtime gate resolved through shared config precedence in `cli/src/services/config/mod.rs` (with parsing in `schema.rs`): opt-out env `SCE_ATTRIBUTION_HOOKS_DISABLED` overrides repo/global config key `policies.attribution_hooks.enabled` with inverted semantics, and the current enabled path activates commit-msg-only attribution gated by the staged-diff AI-overlap preflight. +- `Agent Trace git-notes ref`: Configurable Agent Trace hook policy value at `policies.agent_trace.git_notes_ref`; defaults to `refs/notes/sce-agent-trace`, rejects blank/whitespace-only refs during Rust config mapping, and is exposed through hook runtime config for post-commit git-note persistence wiring. - `StagedDiffAiOverlapResult`: Three-valued enum in `cli/src/services/hooks/mod.rs` returned by the staged-diff AI-overlap evidence check: `Overlap` (staged diff overlaps with at least one recent AI/editor diff trace), `NoOverlap` (no overlap found; staged diff and recent traces were both available but share no touched lines, or staged patch has no touched lines), `Error` (DB open failure, schema not ready, query error, staged diff read failure, or clock failure). Both `NoOverlap` and `Error` map to `ai_contribution_present = false` at the commit-msg policy seam; `Error` additionally triggers `sce.hooks.commit_msg.ai_overlap_error` logging. - `sce.hooks.commit_msg.ai_overlap_error`: Logger event ID emitted by `staged_diff_has_ai_overlap` when the staged-diff AI-overlap preflight encounters an error (DB open failure, schema not ready, query error, staged diff read failure, or clock failure). - `bash policy preset catalog`: Canonical authored preset source at `config/pkl/base/bash-policy-presets.pkl`, rendered to JSON by `config/pkl/generate.pkl` and embedded by the CLI from `config/.opencode/lib/bash-policy-presets.json` so CLI validation and OpenCode enforcement share the same preset IDs, argv-prefix matchers, fixed messages, and conflict metadata. diff --git a/context/overview.md b/context/overview.md index 2329cce5..38823cec 100644 --- a/context/overview.md +++ b/context/overview.md @@ -27,7 +27,7 @@ The `setup` command includes an `inquire`-backed target-selection flow: default The CLI now compiles an embedded setup asset manifest from `config/.opencode/**`, `config/.claude/**`, `config/.pi/**`, 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. The setup service also provides repository-root install orchestration: it resolves the repository root, derives a repo-root-scoped `AppContext` from the runtime command context, aggregates `ServiceLifecycle::setup` calls across lifecycle providers (config → local_db → auth_db → agent_trace_db → hooks when requested), handles interactive or flag-based target selection for config asset installation, and reports deterministic completion details (selected target(s) and installed file counts). Setup uses a unified remove-and-replace policy for all write flows — it removes existing targets before swapping staged content and returns deterministic recovery guidance (recover from version control) on swap failure, without creating backup artifacts. The setup command gates all modes on an existing git repository before any writes. Internally, `cli/src/services/setup/mod.rs` now separates install-flow logic from interactive prompt logic through focused support seams. The CLI now also applies baseline security hardening for reliability-driven automation: diagnostics/logging paths use deterministic secret redaction, `sce setup --hooks --repo ` canonicalizes and validates repository paths before execution, and setup write flows run explicit directory write-permission probes before staging/swap operations. -The config service now provides deterministic runtime config resolution with explicit precedence (`flags > env > config file > defaults`), strict config-file validation (`$schema`, `log_level`, `log_format`, `log_file`, `log_file_mode`, `timeout_ms`, `workos_client_id`, and nested `policies.bash`, `policies.attribution_hooks.enabled`, plus `policies.database_retry` with per-DB `connection_open`/`query` retry policy specs), deterministic default discovery/merge of global+local config files (`${config_root}/sce/config.json` then `.sce/config.json` with local override, where `config_root` comes from the shared default-path seam with XDG/`dirs::config_dir()` config-root resolution), defaults for the resolved observability value set (`log_level=error`, `log_format=text`, `log_file_mode=truncate`), shared auth-key resolution with optional baked defaults starting at `workos_client_id`, first-class bash-policy preset/custom parsing with deterministic conflict and duplicate-prefix validation, and a canonical Pkl-authored `sce/config.json` JSON Schema generated to `config/schema/sce-config.schema.json` and embedded by `cli/src/services/config/mod.rs` for both `sce config validate` and doctor-time config checks. Runtime startup config loading now keeps parity with that schema by accepting the canonical `"$schema": "https://sce.crocoder.dev/config.json"` declaration in repo-local and global config files, so startup commands such as `sce version` no longer fail before dispatch on that field. App-runtime observability now consumes flat logging keys through the shared resolver, so env values still override config-file values while config files provide deterministic fallback for file logging; `sce config show` reports resolved observability/auth/policy values with provenance, while `sce config validate` is now a trimmed validation surface that reports only pass/fail plus validation errors or warnings in text and JSON modes. The canonical preset catalog and matching contract live in `config/pkl/data/bash-policy-presets.json` and `context/sce/bash-tool-policy-enforcement-contract.md`. +The config service now provides deterministic runtime config resolution with explicit precedence (`flags > env > config file > defaults`), strict config-file validation (`$schema`, `log_level`, `log_format`, `log_file`, `log_file_mode`, `timeout_ms`, `workos_client_id`, and nested `policies.bash`, `policies.attribution_hooks.enabled`, `policies.agent_trace.git_notes_ref` with default `refs/notes/sce-agent-trace`, plus `policies.database_retry` with per-DB `connection_open`/`query` retry policy specs), deterministic default discovery/merge of global+local config files (`${config_root}/sce/config.json` then `.sce/config.json` with local override, where `config_root` comes from the shared default-path seam with XDG/`dirs::config_dir()` config-root resolution), defaults for the resolved observability value set (`log_level=error`, `log_format=text`, `log_file_mode=truncate`), shared auth-key resolution with optional baked defaults starting at `workos_client_id`, first-class bash-policy preset/custom parsing with deterministic conflict and duplicate-prefix validation, and a canonical Pkl-authored `sce/config.json` JSON Schema generated to `config/schema/sce-config.schema.json` and embedded by `cli/src/services/config/mod.rs` for both `sce config validate` and doctor-time config checks. Runtime startup config loading now keeps parity with that schema by accepting the canonical `"$schema": "https://sce.crocoder.dev/config.json"` declaration in repo-local and global config files, so startup commands such as `sce version` no longer fail before dispatch on that field. App-runtime observability now consumes flat logging keys through the shared resolver, so env values still override config-file values while config files provide deterministic fallback for file logging; `sce config show` reports resolved observability/auth/policy values with provenance, while `sce config validate` is now a trimmed validation surface that reports only pass/fail plus validation errors or warnings in text and JSON modes. The canonical preset catalog and matching contract live in `config/pkl/data/bash-policy-presets.json` and `context/sce/bash-tool-policy-enforcement-contract.md`. Invalid default-discovered config files now also degrade gracefully at startup: `sce` keeps running with degraded observability defaults, logs `sce.config.invalid_config` warnings, and reserves hard failures for explicit `--config` / `SCE_CONFIG_FILE` targets or other truly invalid runtime observability inputs. `cli/src/services/config/mod.rs` is now a module facade that declares focused config submodules (`types`, `schema`, `policy`, `resolver`, private `render`, `command`, and `lifecycle`), re-exporting `pub use types::*` and `pub(crate) use schema::validate_config_file`. Shared config primitive ownership is delegated to `cli/src/services/config/types.rs`; schema loading and file parsing to `cli/src/services/config/schema.rs`; bash-policy semantic validation and policy-specific formatting to `cli/src/services/config/policy.rs`; runtime discovery/precedence to `cli/src/services/config/resolver.rs`; and `sce config show` / `sce config validate` text+JSON output construction to `cli/src/services/config/render.rs`. Downstream modules continue importing through `services::config` unchanged. The CLI now has a generic borrowed `AppContext` dependency view in `cli/src/app.rs`; `AppRuntime` owns concrete production logger/telemetry/fs/git dependencies, and command execution receives context views that borrow those dependencies plus an optional `repo_root: Option`. `AppContext::with_repo_root(...)` / `ContextWithRepoRoot` derives repo-root-scoped views while preserving the borrowed runtime dependencies, and command execution is generic over associated-type narrow accessor traits where practical. The broad capability seam lives in `cli/src/services/capabilities.rs`, where `FsOps`/`StdFsOps` wrap filesystem operations and `GitOps`/`ProcessGitOps` wrap git process execution plus repository-root/hooks-directory resolution. The shared default path service in `cli/src/services/default_paths.rs` is now the canonical owner for production CLI path definitions. It resolves per-user config/state/cache roots through a dedicated internal `roots` seam, exposes the current persisted-artifact inventory (global config and auth tokens), and also defines named DB paths (auth DB, local DB, Agent Trace DB) plus the repo-relative, embedded-asset, install, hook, and context-path accessors consumed across current CLI production code. Non-test production modules should consume this shared catalog instead of hardcoding owned path literals. No default cache-backed persisted artifact currently exists, so cache-root resolution remains available without speculative cache-path features and no legacy default-path fallback is supported. diff --git a/context/plans/agent-trace-git-notes.md b/context/plans/agent-trace-git-notes.md new file mode 100644 index 00000000..a90ef833 --- /dev/null +++ b/context/plans/agent-trace-git-notes.md @@ -0,0 +1,100 @@ +# Plan: Persist Agent Trace JSON to git notes on post-commit + +## Change summary + +Extend the existing `sce hooks post-commit` Agent Trace flow so every successfully built and schema-validated Agent Trace payload is also written to a git note on the just-created commit. The note content is the full Agent Trace JSON already persisted in `agent_traces.trace_json`. Git-note persistence is best-effort: failures are logged for diagnostics but must not block the git commit or make the post-commit hook command fail when Agent Trace DB persistence succeeded. + +The default notes ref is dedicated to SCE Agent Trace data and is configurable. + +## Decisions + +- Default git-notes ref: `refs/notes/sce-agent-trace`. +- Config surface: add a repo config field for the notes ref (for example `policies.agent_trace.git_notes_ref`, final naming to follow existing config style during implementation) with default `refs/notes/sce-agent-trace`. +- Note content: full Agent Trace JSON string after schema validation, matching the payload persisted to `agent_traces.trace_json`. +- Write posture: best-effort/non-blocking. Git-note write failures are logged and surfaced only as diagnostics, not as post-commit hook failures. +- Write mode: use replace/upsert semantics for the commit note so rerunning the hook for the same commit updates the SCE Agent Trace note instead of failing on an existing note. + +## Success criteria + +- On a successful `sce hooks post-commit --vcs git --remote-url ` run that builds and validates an Agent Trace payload, the current commit has a git note under `refs/notes/sce-agent-trace` by default. +- The note content is the full Agent Trace JSON and can be read back with `git notes --ref refs/notes/sce-agent-trace show `. +- The git-notes ref is configurable through SCE config and generated schema/docs reflect the default. +- If writing the git note fails, the post-commit hook remains successful when existing Agent Trace DB persistence succeeded; the failure is logged with a stable event name. +- Existing Agent Trace DB insertion remains unchanged and continues to be the source of persisted trace rows. +- Tests cover successful note write orchestration, configured ref use, existing-note replacement/upsert behavior, and non-blocking failure handling. +- Context documents describe the new post-commit git-notes behavior and the no-blocking-error posture. + +## Constraints and non-goals + +- Constraints: + - Keep `post-commit` as the integration point because the commit SHA and Agent Trace JSON are available there. + - Do not write a git note unless Agent Trace JSON validation has passed. + - Preserve stdout/stderr contracts as much as possible; diagnostics belong in logging/stderr, not new stdout payloads. + - Reuse existing hook config resolution and git command helper patterns instead of introducing a new dependency. + - Keep note writes scoped to git; no behavior is required for non-git VCS values. + - Keep failures non-blocking only for the git-note write step. Existing validation/DB insertion failures keep their current behavior. +- Non-goals: + - Backfilling git notes for historical commits. + - Pushing/fetching notes to/from remotes. + - Changing Agent Trace JSON schema or DB schema. + - Replacing Agent Trace DB persistence with git notes. + - Adding a retry queue for failed note writes. + +## Assumptions + +- The dedicated default ref should be exactly `refs/notes/sce-agent-trace`. +- The implementation may write notes by invoking the local `git` binary through existing command helpers. +- Configurability means changing the notes ref, not disabling the feature. Disabling can be added later if a separate product decision requests it. + +## Task stack + +- [x] T01: `Add config surface for Agent Trace git-notes ref` (status:done) + - Task ID: T01 + - Goal: Add a typed SCE config value for the Agent Trace git-notes ref with default `refs/notes/sce-agent-trace`. + - Boundaries (in/out of scope): In - config type/resolver updates, env/config precedence only if this config area already has an established pattern, unit tests for default and explicit configured ref. Out - git-note writing, hook runtime wiring, generated schema/Pkl output. + - Done when: runtime config exposes the resolved notes ref; default resolution returns `refs/notes/sce-agent-trace`; explicit config overrides the default; invalid empty/blank refs are rejected or normalized consistently with existing config validation patterns. + - Verification notes (commands or checks): `nix develop -c sh -c 'cd cli && cargo fmt'`; targeted config tests if permitted by policy; otherwise `nix flake check`. + - Status: done + - Completed: 2026-07-14 + - Files changed: `cli/src/services/config/types.rs`, `cli/src/services/config/schema.rs`, `cli/src/services/config/resolver.rs`, `config/pkl/base/sce-config-schema.pkl`, `config/schema/sce-config.schema.json` + - Evidence: `nix develop -c sh -c 'cd cli && cargo fmt'` passed; targeted `cargo test` was blocked by SCE bash policy preferring `nix flake check`; `nix flake check` passed; `nix run .#pkl-check-generated` passed ("Generated outputs are up to date."). + - Notes: User approved Option A scope expansion to include the minimal Pkl/generated schema update required for explicit config-file override validation. Git-note writing and post-commit hook wiring remain out of scope for T01. + +- [ ] T02: `Sync Pkl schema and generated config docs for git-notes ref` (status:todo) + - Task ID: T02 + - Goal: Update canonical Pkl config schema and regenerate generated JSON/config artifacts so the Agent Trace git-notes ref is documented and parity checks pass. + - Boundaries (in/out of scope): In - Pkl source, generated JSON schema/config outputs, default/description text for the new ref. Out - Rust resolver logic from T01, hook runtime behavior from later tasks. + - Done when: generated outputs include the new config field/default; `nix run .#pkl-check-generated` passes; no unrelated generated drift is present. + - Verification notes (commands or checks): `nix develop -c pkl eval -m . config/pkl/generate.pkl`; `nix run .#pkl-check-generated`. + +- [ ] T03: `Introduce git-notes writer helper for Agent Trace JSON` (status:todo) + - Task ID: T03 + - Goal: Add a small, testable helper that writes full Agent Trace JSON to a git note for a commit/ref using replace/upsert semantics. + - Boundaries (in/out of scope): In - helper surface in the hooks or git utility layer, command construction for `git notes --ref add -f -F `, validation that commit/ref/content inputs are non-empty, unit tests with injected command runner covering success, configured ref, existing-note replacement flag, and command failure. Out - calling the helper from post-commit runtime, changing Agent Trace build/validation, adding a DB migration. + - Done when: helper is deterministic, avoids shell interpolation, handles multiline JSON safely, returns structured success/error for caller-side logging, and has focused tests. + - Verification notes (commands or checks): targeted hooks/git-helper tests if permitted; `nix develop -c sh -c 'cd cli && cargo fmt'`; `nix flake check` as fallback. + +- [ ] T04: `Wire git-note persistence into post-commit Agent Trace flow` (status:todo) + - Task ID: T04 + - Goal: After Agent Trace JSON validation and DB insertion succeed, write the same full JSON to the configured git-notes ref for the committed SHA, while keeping note-write failures non-blocking. + - Boundaries (in/out of scope): In - post-commit flow wiring, resolved config read, stable log event for note-write failure (for example `sce.hooks.post_commit.agent_trace_git_note_write_failed`), tests proving successful write is attempted after DB insert and failures do not change hook success. Out - backfill, notes push/fetch, non-git VCS note behavior, changing existing DB failure semantics. + - Done when: default post-commit writes a note under `refs/notes/sce-agent-trace`; configured ref is honored; note write is skipped or treated as no-op for unsupported/non-git contexts if necessary; note write failure logs diagnostics but does not fail the hook after DB persistence succeeds. + - Verification notes (commands or checks): targeted post-commit hook tests if permitted; manual local check with `git notes --ref refs/notes/sce-agent-trace show HEAD`; `nix flake check`. + +- [ ] T05: `Update Agent Trace context for git-notes persistence` (status:todo) + - Task ID: T05 + - Goal: Document the new git-notes persistence contract in current-state context. + - Boundaries (in/out of scope): In - update `context/sce/agent-trace-hooks-command-routing.md`, `context/sce/agent-trace-db.md`, `context/sce/setup-githooks-hook-asset-packaging.md` if hook behavior text needs adjustment, and `context/context-map.md` entries. Out - implementation code, broad docs rewrites unrelated to post-commit Agent Trace persistence. + - Done when: context states the default notes ref, config override, full-JSON note content, and non-blocking failure behavior; stale `No git-notes persistence` text is removed or qualified. + - Verification notes (commands or checks): `rg "git-notes|git notes|No git-notes" context/`; manual diff review. + +- [ ] T06: `Validate git-notes Agent Trace behavior and cleanup` (status:todo) + - Task ID: T06 + - Goal: Run final validation for the complete plan and clean up any planning or test scaffolding. + - Boundaries (in/out of scope): In - full repo validation, generated-output parity, focused grep for stale docs/config strings, cleanup of temporary test repositories or notes refs created during manual checks. Out - new behavior beyond the completed task stack. + - Done when: `nix flake check` passes or any failure is documented as pre-existing/unrelated; `nix run .#pkl-check-generated` passes; context sync is verified; no temporary scaffolding remains. + - Verification notes (commands or checks): `nix flake check`; `nix run .#pkl-check-generated`; `git diff --check`; `rg "refs/notes/sce-agent-trace|agent_trace.*git.*note|No git-notes" cli/ config/ context/`. + +## Open questions + +None. Plan is ready for T01 execution. From c025b7f53efb7bef7b78a043177c8bb7acad9348 Mon Sep 17 00:00:00 2001 From: David Abram Date: Tue, 14 Jul 2026 20:16:24 +0200 Subject: [PATCH 2/8] hooks: Add Agent Trace git-note writer helper Add a testable git notes helper that validates the ref, commit ID, and Agent Trace JSON before piping content to git notes add -f -F -. Cover configured refs, blank input rejection, command construction, and command failure context. Plan: agent-trace-git-notes Tasks: T02, T03 Co-authored-by: SCE --- cli/src/services/hooks/mod.rs | 242 ++++++++++++++++++++++++- context/plans/agent-trace-git-notes.md | 14 +- 2 files changed, 252 insertions(+), 4 deletions(-) diff --git a/cli/src/services/hooks/mod.rs b/cli/src/services/hooks/mod.rs index 3e9700eb..6fff4d49 100644 --- a/cli/src/services/hooks/mod.rs +++ b/cli/src/services/hooks/mod.rs @@ -1,7 +1,7 @@ use std::fs; -use std::io::{self, Read}; +use std::io::{self, Read, Write}; use std::path::{Path, PathBuf}; -use std::process::Command; +use std::process::{Command, Stdio}; use std::time::{SystemTime, UNIX_EPOCH}; use anyhow::{anyhow, bail, Context, Result}; @@ -1571,6 +1571,128 @@ fn current_unix_time_ms() -> Result { .context("Current time exceeds i64 range for post-commit intersection.") } +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct GitNoteWriteOutcome { + pub commit_id: String, + pub notes_ref: String, +} + +#[allow(dead_code)] +fn write_agent_trace_git_note( + repository_root: &Path, + notes_ref: &str, + commit_id: &str, + trace_json: &str, +) -> Result { + write_agent_trace_git_note_with( + repository_root, + notes_ref, + commit_id, + trace_json, + run_git_notes_add_command, + ) +} + +#[allow(dead_code)] +fn write_agent_trace_git_note_with( + repository_root: &Path, + notes_ref: &str, + commit_id: &str, + trace_json: &str, + run_git_notes: F, +) -> Result +where + F: FnOnce(&Path, &[String], &str) -> Result<()>, +{ + let notes_ref = non_empty_git_note_input("git notes ref", notes_ref)?; + let commit_id = non_empty_git_note_input("commit ID", commit_id)?; + let trace_json = non_empty_git_note_content(trace_json)?; + let args = vec![ + String::from("notes"), + String::from("--ref"), + notes_ref.clone(), + String::from("add"), + String::from("-f"), + String::from("-F"), + String::from("-"), + commit_id.clone(), + ]; + + run_git_notes(repository_root, &args, &trace_json).with_context(|| { + format!( + "Failed to write Agent Trace git note for commit '{commit_id}' under ref '{notes_ref}'." + ) + })?; + + Ok(GitNoteWriteOutcome { + commit_id, + notes_ref, + }) +} + +fn non_empty_git_note_input(label: &str, value: &str) -> Result { + let trimmed = value.trim(); + if trimmed.is_empty() { + bail!("Invalid Agent Trace git-note {label}: value must be non-empty."); + } + + Ok(trimmed.to_string()) +} + +fn non_empty_git_note_content(trace_json: &str) -> Result { + if trace_json.trim().is_empty() { + bail!("Invalid Agent Trace git-note Agent Trace JSON: value must be non-empty."); + } + + Ok(trace_json.to_string()) +} + +fn run_git_notes_add_command( + repository_root: &Path, + args: &[String], + trace_json: &str, +) -> Result<()> { + let mut child = Command::new("git") + .args(args) + .current_dir(repository_root) + .stdin(Stdio::piped()) + .stdout(Stdio::null()) + .stderr(Stdio::piped()) + .spawn() + .with_context(|| { + format!( + "Failed to spawn git notes command in directory '{}'.", + repository_root.display() + ) + })?; + + { + let stdin = child + .stdin + .as_mut() + .context("Failed to open stdin for git notes command.")?; + stdin + .write_all(trace_json.as_bytes()) + .context("Failed to write Agent Trace JSON to git notes command stdin.")?; + } + + let output = child + .wait_with_output() + .context("Failed to wait for git notes command.")?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string(); + let diagnostic = if stderr.is_empty() { + String::from("git notes command exited with a non-zero status") + } else { + stderr + }; + bail!("Failed to write Agent Trace git note: {diagnostic}"); + } + + Ok(()) +} + fn run_post_commit_subcommand_with_trace( repository_root: &Path, vcs_type: Option, @@ -2493,4 +2615,120 @@ mod tests { assert_eq!(output.tool_name, Some(String::from("opencode"))); assert_eq!(output.tool_version, Some(String::from("1.2.3"))); } + + #[test] + fn git_note_writer_builds_replace_command_and_pipes_json() { + let captured_args = RefCell::new(Vec::::new()); + let captured_content = RefCell::new(String::new()); + + let outcome = write_agent_trace_git_note_with( + Path::new("/repo"), + "refs/notes/sce-agent-trace", + "abc123", + "{\n \"version\": \"0.1.0\"\n}\n", + |root, args, content| { + assert_eq!(root, Path::new("/repo")); + *captured_args.borrow_mut() = args.to_vec(); + *captured_content.borrow_mut() = content.to_string(); + Ok(()) + }, + ) + .expect("git-note writer should succeed"); + + assert_eq!(outcome.commit_id, "abc123"); + assert_eq!(outcome.notes_ref, "refs/notes/sce-agent-trace"); + assert_eq!( + captured_args.into_inner(), + vec![ + "notes", + "--ref", + "refs/notes/sce-agent-trace", + "add", + "-f", + "-F", + "-", + "abc123", + ] + ); + assert_eq!( + captured_content.into_inner(), + "{\n \"version\": \"0.1.0\"\n}\n" + ); + } + + #[test] + fn git_note_writer_honors_configured_ref() { + let captured_args = RefCell::new(Vec::::new()); + + write_agent_trace_git_note_with( + Path::new("/repo"), + "refs/notes/custom-agent-trace", + "def456", + "{}", + |_, args, _| { + *captured_args.borrow_mut() = args.to_vec(); + Ok(()) + }, + ) + .expect("git-note writer should succeed"); + + assert_eq!( + captured_args.into_inner()[2], + "refs/notes/custom-agent-trace" + ); + } + + #[test] + fn git_note_writer_rejects_blank_inputs() { + let error = + write_agent_trace_git_note_with(Path::new("/repo"), " ", "abc123", "{}", |_, _, _| { + panic!("runner should not be called for invalid input") + }) + .expect_err("blank ref should fail"); + assert!(error + .to_string() + .contains("Invalid Agent Trace git-note git notes ref")); + + let error = write_agent_trace_git_note_with( + Path::new("/repo"), + "refs/notes/sce-agent-trace", + "\t", + "{}", + |_, _, _| panic!("runner should not be called for invalid input"), + ) + .expect_err("blank commit should fail"); + assert!(error + .to_string() + .contains("Invalid Agent Trace git-note commit ID")); + + let error = write_agent_trace_git_note_with( + Path::new("/repo"), + "refs/notes/sce-agent-trace", + "abc123", + "\n", + |_, _, _| panic!("runner should not be called for invalid input"), + ) + .expect_err("blank content should fail"); + assert!(error + .to_string() + .contains("Invalid Agent Trace git-note Agent Trace JSON")); + } + + #[test] + fn git_note_writer_returns_command_failure_with_context() { + let error = write_agent_trace_git_note_with( + Path::new("/repo"), + "refs/notes/sce-agent-trace", + "abc123", + "{}", + |_, _, _| bail!("simulated git failure"), + ) + .expect_err("git failure should fail helper"); + + let rendered = format!("{error:#}"); + assert!(rendered.contains( + "Failed to write Agent Trace git note for commit 'abc123' under ref 'refs/notes/sce-agent-trace'." + )); + assert!(rendered.contains("simulated git failure")); + } } diff --git a/context/plans/agent-trace-git-notes.md b/context/plans/agent-trace-git-notes.md index a90ef833..79bc116e 100644 --- a/context/plans/agent-trace-git-notes.md +++ b/context/plans/agent-trace-git-notes.md @@ -60,19 +60,29 @@ The default notes ref is dedicated to SCE Agent Trace data and is configurable. - Evidence: `nix develop -c sh -c 'cd cli && cargo fmt'` passed; targeted `cargo test` was blocked by SCE bash policy preferring `nix flake check`; `nix flake check` passed; `nix run .#pkl-check-generated` passed ("Generated outputs are up to date."). - Notes: User approved Option A scope expansion to include the minimal Pkl/generated schema update required for explicit config-file override validation. Git-note writing and post-commit hook wiring remain out of scope for T01. -- [ ] T02: `Sync Pkl schema and generated config docs for git-notes ref` (status:todo) +- [x] T02: `Sync Pkl schema and generated config docs for git-notes ref` (status:done) - Task ID: T02 - Goal: Update canonical Pkl config schema and regenerate generated JSON/config artifacts so the Agent Trace git-notes ref is documented and parity checks pass. - Boundaries (in/out of scope): In - Pkl source, generated JSON schema/config outputs, default/description text for the new ref. Out - Rust resolver logic from T01, hook runtime behavior from later tasks. - Done when: generated outputs include the new config field/default; `nix run .#pkl-check-generated` passes; no unrelated generated drift is present. - Verification notes (commands or checks): `nix develop -c pkl eval -m . config/pkl/generate.pkl`; `nix run .#pkl-check-generated`. + - Status: done + - Completed: 2026-07-14 + - Files changed: `context/plans/agent-trace-git-notes.md` + - Evidence: `nix develop -c pkl eval -m . config/pkl/generate.pkl` completed and rewrote no files; `nix run .#pkl-check-generated` passed ("Generated outputs are up to date."); `git status --short` after regeneration showed no generated drift. + - Notes: Canonical Pkl and generated schema already included `policies.agent_trace.git_notes_ref` with default `refs/notes/sce-agent-trace`; T02 was a verification/regeneration task only. -- [ ] T03: `Introduce git-notes writer helper for Agent Trace JSON` (status:todo) +- [x] T03: `Introduce git-notes writer helper for Agent Trace JSON` (status:done) - Task ID: T03 - Goal: Add a small, testable helper that writes full Agent Trace JSON to a git note for a commit/ref using replace/upsert semantics. - Boundaries (in/out of scope): In - helper surface in the hooks or git utility layer, command construction for `git notes --ref add -f -F `, validation that commit/ref/content inputs are non-empty, unit tests with injected command runner covering success, configured ref, existing-note replacement flag, and command failure. Out - calling the helper from post-commit runtime, changing Agent Trace build/validation, adding a DB migration. - Done when: helper is deterministic, avoids shell interpolation, handles multiline JSON safely, returns structured success/error for caller-side logging, and has focused tests. - Verification notes (commands or checks): targeted hooks/git-helper tests if permitted; `nix develop -c sh -c 'cd cli && cargo fmt'`; `nix flake check` as fallback. + - Status: done + - Completed: 2026-07-14 + - Files changed: `cli/src/services/hooks/mod.rs`, `context/plans/agent-trace-git-notes.md` + - Evidence: `nix develop -c sh -c 'cd cli && cargo fmt'` passed; targeted `cargo test git_note_writer` was blocked by SCE bash policy preferring `nix flake check`; first `nix flake check` caught a clippy issue and then a git-note writer test failure, both fixed; final `nix flake check` passed; `nix run .#pkl-check-generated` passed ("Generated outputs are up to date."). + - Notes: Added an injectable helper that validates non-blank ref/commit/content, invokes `git notes --ref add -f -F - ` without shell interpolation, pipes Agent Trace JSON through stdin preserving content bytes, returns `GitNoteWriteOutcome`, and includes focused unit coverage for command construction, configured refs, blank input rejection, and command failure context. The helper is intentionally not wired into post-commit runtime until T04. - [ ] T04: `Wire git-note persistence into post-commit Agent Trace flow` (status:todo) - Task ID: T04 From 5f44a2335b36d7288691aa56512c265bfb8f5635 Mon Sep 17 00:00:00 2001 From: David Abram Date: Wed, 15 Jul 2026 10:03:21 +0200 Subject: [PATCH 3/8] hooks: Persist post-commit Agent Trace git notes Write validated post-commit Agent Trace JSON to the configured git-notes ref after DB insertion succeeds, skip explicit non-git VCS contexts, and log note-write failures without failing the hook. Co-authored-by: SCE --- cli/src/services/hooks/mod.rs | 272 +++++++++++++++++- context/architecture.md | 2 +- context/cli/cli-command-surface.md | 2 +- context/context-map.md | 2 +- context/overview.md | 2 +- context/plans/agent-trace-git-notes.md | 7 +- context/sce/agent-trace-db.md | 2 +- .../sce/agent-trace-hooks-command-routing.md | 6 +- 8 files changed, 273 insertions(+), 22 deletions(-) diff --git a/cli/src/services/hooks/mod.rs b/cli/src/services/hooks/mod.rs index 6fff4d49..d0b472e7 100644 --- a/cli/src/services/hooks/mod.rs +++ b/cli/src/services/hooks/mod.rs @@ -192,9 +192,12 @@ fn run_hooks_subcommand_in_repo( HookSubcommand::PostCommit { vcs_type, remote_url, - } => { - run_post_commit_subcommand_with_trace(repository_root, *vcs_type, remote_url.as_deref()) - } + } => run_post_commit_subcommand_with_trace( + repository_root, + *vcs_type, + remote_url.as_deref(), + logger, + ), HookSubcommand::PostRewrite { rewrite_method } => { run_post_rewrite_subcommand_with_trace(repository_root, subcommand, rewrite_method) } @@ -1219,11 +1222,13 @@ fn run_post_commit_subcommand( repository_root: &Path, vcs_type: Option, remote_url: &str, + logger: Option<&dyn Logger>, ) -> Result { run_post_commit_subcommand_with( repository_root, vcs_type, remote_url, + logger, run_post_commit_intersection_flow, run_post_commit_agent_trace_flow, ) @@ -1233,6 +1238,7 @@ fn run_post_commit_subcommand_with( repository_root: &Path, vcs_type: Option, remote_url: &str, + logger: Option<&dyn Logger>, run_intersection_flow: F, run_agent_trace_flow: B, ) -> Result @@ -1243,10 +1249,12 @@ where &PostCommitIntersectionFlowResult, Option, &str, - ) -> Result, + Option<&dyn Logger>, + ) -> Result, { let result = run_intersection_flow(repository_root)?; - let _agent_trace = run_agent_trace_flow(repository_root, &result, vcs_type, remote_url)?; + let _agent_trace = + run_agent_trace_flow(repository_root, &result, vcs_type, remote_url, logger)?; Ok(format!( "post-commit hook processed intersection: commit={}, intersection_files={}", @@ -1260,15 +1268,25 @@ fn run_post_commit_agent_trace_flow( flow_result: &PostCommitIntersectionFlowResult, vcs_type: Option, remote_url: &str, -) -> Result { + logger: Option<&dyn Logger>, +) -> Result { let db = open_agent_trace_db_for_hook_runtime( repository_root, "Failed to open Agent Trace DB for post-commit trace.", )?; + let hook_config = config::resolve_hook_runtime_config(repository_root) + .context("Failed to resolve hook runtime config for post-commit Agent Trace git note.")?; + + let git_note_persistence = PostCommitGitNotePersistence { + repository_root, + vcs_type, + git_notes_ref: &hook_config.agent_trace_git_notes_ref, + logger, + }; run_post_commit_agent_trace_flow_with( + &git_note_persistence, flow_result, - vcs_type, remote_url, |trace_value| { validate_agent_trace_value(trace_value) @@ -1283,19 +1301,36 @@ fn run_post_commit_agent_trace_flow( Ok(()) }, + write_agent_trace_git_note, ) } -fn run_post_commit_agent_trace_flow_with( - flow_result: &PostCommitIntersectionFlowResult, +#[derive(Clone, Debug, Eq, PartialEq)] +struct PostCommitAgentTraceFlowResult { + agent_trace: AgentTrace, + trace_json: String, +} + +#[derive(Clone, Copy)] +struct PostCommitGitNotePersistence<'a> { + repository_root: &'a Path, vcs_type: Option, + git_notes_ref: &'a str, + logger: Option<&'a dyn Logger>, +} + +fn run_post_commit_agent_trace_flow_with( + git_note_persistence: &PostCommitGitNotePersistence<'_>, + flow_result: &PostCommitIntersectionFlowResult, remote_url: &str, validate_agent_trace: V, persist_agent_trace: I, -) -> Result + write_git_note: G, +) -> Result where V: FnOnce(&Value) -> Result<()>, I: for<'a> FnOnce(AgentTraceInsert<'a>) -> Result<()>, + G: FnOnce(&Path, &str, &str, &str) -> Result, { let commit_timestamp = DateTime::::from_timestamp_millis(flow_result.post_commit_data.commit_time_ms) @@ -1313,7 +1348,7 @@ where AgentTraceMetadataInput { commit_timestamp: &commit_timestamp, commit_revision: &flow_result.post_commit_data.commit_oid, - vcs_type, + vcs_type: git_note_persistence.vcs_type, tool_name: flow_result.tool_name.as_deref(), tool_version: flow_result.tool_version.as_deref(), }, @@ -1343,7 +1378,47 @@ where }; persist_agent_trace(insert_input)?; - Ok(agent_trace) + if should_write_agent_trace_git_note(git_note_persistence.vcs_type) { + if let Err(error) = write_git_note( + git_note_persistence.repository_root, + git_note_persistence.git_notes_ref, + &flow_result.post_commit_data.commit_oid, + &serialized, + ) { + log_agent_trace_git_note_write_failure( + git_note_persistence.logger, + &flow_result.post_commit_data.commit_oid, + git_note_persistence.git_notes_ref, + &error, + ); + } + } + + Ok(PostCommitAgentTraceFlowResult { + agent_trace, + trace_json: serialized, + }) +} + +fn should_write_agent_trace_git_note(vcs_type: Option) -> bool { + matches!(vcs_type, None | Some(AgentTraceVcsType::Git)) +} + +fn log_agent_trace_git_note_write_failure( + logger: Option<&dyn Logger>, + commit_id: &str, + git_notes_ref: &str, + error: &anyhow::Error, +) { + if let Some(log) = logger { + log.error( + "sce.hooks.post_commit.agent_trace_git_note_write_failed", + &format!( + "Failed to write Agent Trace git note for commit '{commit_id}' under ref '{git_notes_ref}': {error}." + ), + &[("commit_id", commit_id), ("git_notes_ref", git_notes_ref)], + ); + } } /// Duration for looking up recent diff traces: 7 days in milliseconds. @@ -1697,8 +1772,14 @@ fn run_post_commit_subcommand_with_trace( repository_root: &Path, vcs_type: Option, remote_url: Option<&str>, + logger: Option<&dyn Logger>, ) -> Result { - run_post_commit_subcommand(repository_root, vcs_type, remote_url.unwrap_or_default()) + run_post_commit_subcommand( + repository_root, + vcs_type, + remote_url.unwrap_or_default(), + logger, + ) } fn run_post_rewrite_subcommand(repository_root: &Path, rewrite_method: &str) -> Result { @@ -2170,7 +2251,7 @@ where #[cfg(test)] mod tests { - use std::{cell::RefCell, path::Path}; + use std::{cell::RefCell, path::Path, sync::Mutex}; use super::*; use crate::services::agent_trace_db::{ParsedDiffTracePatch, SkippedDiffTracePatch}; @@ -2186,6 +2267,28 @@ mod tests { intersection_patch: String, } + #[derive(Default)] + struct CapturingLogger { + errors: Mutex>, + } + + impl Logger for CapturingLogger { + fn info(&self, _event_id: &str, _message: &str, _fields: &[(&str, &str)]) {} + + fn debug(&self, _event_id: &str, _message: &str, _fields: &[(&str, &str)]) {} + + fn warn(&self, _event_id: &str, _message: &str, _fields: &[(&str, &str)]) {} + + fn error(&self, event_id: &str, message: &str, _fields: &[(&str, &str)]) { + self.errors + .lock() + .expect("test logger mutex should not be poisoned") + .push((event_id.to_string(), message.to_string())); + } + + fn log_classified_error(&self, _error: &crate::services::error::ClassifiedError) {} + } + fn valid_patch_text(path: &str, content: &str) -> String { format!( "Index: {path}\n===================================================================\n--- {path}\n+++ {path}\n@@ -0,0 +1,1 @@\n+{content}\n" @@ -2198,6 +2301,147 @@ mod tests { parse_patch_from_text(&patch_text, None).expect("test patch should parse") } + fn post_commit_flow_result() -> PostCommitIntersectionFlowResult { + PostCommitIntersectionFlowResult { + combined_recent_patch: valid_patch("src/lib.rs", "shared line"), + post_commit_data: PostCommitPatchData { + commit_oid: String::from("abc123"), + commit_time_ms: 1_800_000_000_000_i64, + parsed_patch: valid_patch("src/lib.rs", "shared line"), + }, + tool_name: Some(String::from("opencode")), + tool_version: Some(String::from("1.2.3")), + } + } + + #[test] + fn post_commit_agent_trace_flow_writes_git_note_after_db_insert() { + let order = RefCell::new(Vec::::new()); + let captured_note = RefCell::new(None::<(String, String, String)>); + + let git_note_persistence = PostCommitGitNotePersistence { + repository_root: Path::new("/repo"), + vcs_type: Some(AgentTraceVcsType::Git), + git_notes_ref: "refs/notes/sce-agent-trace", + logger: None, + }; + let result = run_post_commit_agent_trace_flow_with( + &git_note_persistence, + &post_commit_flow_result(), + "https://example.test/repo.git", + |_| Ok(()), + |insert_input| { + order.borrow_mut().push(String::from("db")); + assert_eq!(insert_input.commit_id, "abc123"); + assert!(insert_input.trace_json.contains("\"version\": \"0.1.0\"")); + Ok(()) + }, + |root, notes_ref, commit_id, trace_json| { + order.borrow_mut().push(String::from("note")); + assert_eq!(root, Path::new("/repo")); + *captured_note.borrow_mut() = Some(( + notes_ref.to_string(), + commit_id.to_string(), + trace_json.to_string(), + )); + Ok(GitNoteWriteOutcome { + commit_id: commit_id.to_string(), + notes_ref: notes_ref.to_string(), + }) + }, + ) + .expect("Agent Trace flow should persist DB row and git note"); + + assert_eq!(order.into_inner(), vec!["db", "note"]); + let (notes_ref, commit_id, trace_json) = captured_note + .into_inner() + .expect("git note write should be attempted"); + assert_eq!(notes_ref, "refs/notes/sce-agent-trace"); + assert_eq!(commit_id, "abc123"); + assert_eq!(trace_json, result.trace_json); + assert_eq!(trace_json, captured_note_trace_json(&result)); + } + + #[test] + fn post_commit_agent_trace_flow_honors_configured_git_notes_ref() { + let captured_ref = RefCell::new(String::new()); + + let git_note_persistence = PostCommitGitNotePersistence { + repository_root: Path::new("/repo"), + vcs_type: Some(AgentTraceVcsType::Git), + git_notes_ref: "refs/notes/custom-sce", + logger: None, + }; + run_post_commit_agent_trace_flow_with( + &git_note_persistence, + &post_commit_flow_result(), + "https://example.test/repo.git", + |_| Ok(()), + |_| Ok(()), + |_, notes_ref, commit_id, trace_json| { + *captured_ref.borrow_mut() = notes_ref.to_string(); + Ok(GitNoteWriteOutcome { + commit_id: commit_id.to_string(), + notes_ref: trace_json + .contains("\"version\": \"0.1.0\"") + .then(|| notes_ref.to_string()) + .expect("git-note content should be full Agent Trace JSON"), + }) + }, + ) + .expect("Agent Trace flow should succeed with configured git-notes ref"); + + assert_eq!(captured_ref.into_inner(), "refs/notes/custom-sce"); + } + + #[test] + fn post_commit_agent_trace_flow_logs_git_note_failure_without_failing() { + let logger = CapturingLogger::default(); + let order = RefCell::new(Vec::::new()); + + let git_note_persistence = PostCommitGitNotePersistence { + repository_root: Path::new("/repo"), + vcs_type: Some(AgentTraceVcsType::Git), + git_notes_ref: "refs/notes/sce-agent-trace", + logger: Some(&logger), + }; + let result = run_post_commit_agent_trace_flow_with( + &git_note_persistence, + &post_commit_flow_result(), + "https://example.test/repo.git", + |_| Ok(()), + |_| { + order.borrow_mut().push(String::from("db")); + Ok(()) + }, + |_, _, _, _| { + order.borrow_mut().push(String::from("note")); + bail!("simulated git notes failure") + }, + ) + .expect("git-note failure should not fail post-commit Agent Trace flow"); + + assert_eq!(order.into_inner(), vec!["db", "note"]); + assert!(result.trace_json.contains("\"version\": \"0.1.0\"")); + + let errors = logger + .errors + .lock() + .expect("test logger mutex should not be poisoned"); + assert_eq!(errors.len(), 1); + assert_eq!( + errors[0].0, + "sce.hooks.post_commit.agent_trace_git_note_write_failed" + ); + assert!(errors[0].1.contains("simulated git notes failure")); + } + + fn captured_note_trace_json(result: &PostCommitAgentTraceFlowResult) -> String { + serde_json::to_string_pretty(&result.agent_trace) + .map(|value| format!("{value}\n")) + .expect("test Agent Trace should serialize") + } + #[test] fn conversation_trace_mixed_payload_maps_to_message_and_part_insert_inputs() { let patch_text = valid_patch_text("src/lib.rs", "let answer = 42;"); diff --git a/context/architecture.md b/context/architecture.md index 6ada995a..37d76aba 100644 --- a/context/architecture.md +++ b/context/architecture.md @@ -129,7 +129,7 @@ The repository includes a new placeholder Rust binary crate at `cli/`. - `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. Checkout DB discovery no longer lives in `doctor`; it moved to the `trace` group (`sce trace db list`) in `cli/src/services/trace/`. 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 enabled-by-default attribution-hooks config/env control is not opted out, `SCE_DISABLED` is false, and the staged-diff AI-overlap preflight confirms AI/editor evidence (`StagedDiffAiOverlapResult::Overlap`); the preflight is wired into `run_commit_msg_subcommand_in_repo` and logs `sce.hooks.commit_msg.ai_overlap_error` on error paths; `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`), required nullable/non-empty `tool_version` plus required `u64` `time` (Unix epoch milliseconds), rejects values that cannot fit AgentTraceDb signed `time_ms` storage, prefixes the stored `diff_traces.session_id` before insert construction (`oc_` for OpenCode, `cc_` for Claude, `pi_` for Pi, same-tool idempotent), and inserts the parsed payload fields into AgentTraceDb without creating a parsed-payload `context/tmp` artifact; Claude structured `PostToolUse` diff-trace intake extracts direct model metadata from top-level or nested `model` fields and normalizes it with the `claude/` prefix when present. `session-model` is no longer a supported hook route. +- `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 enabled-by-default attribution-hooks config/env control is not opted out, `SCE_DISABLED` is false, and the staged-diff AI-overlap preflight confirms AI/editor evidence (`StagedDiffAiOverlapResult::Overlap`); the preflight is wired into `run_commit_msg_subcommand_in_repo` and logs `sce.hooks.commit_msg.ai_overlap_error` on error paths; `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 and best-effort git notes 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`), required nullable/non-empty `tool_version` plus required `u64` `time` (Unix epoch milliseconds), rejects values that cannot fit AgentTraceDb signed `time_ms` storage, prefixes the stored `diff_traces.session_id` before insert construction (`oc_` for OpenCode, `cc_` for Claude, `pi_` for Pi, same-tool idempotent), and inserts the parsed payload fields into AgentTraceDb without creating a parsed-payload `context/tmp` artifact; Claude structured `PostToolUse` diff-trace intake extracts direct model metadata from top-level or nested `model` fields and normalizes it with the `claude/` prefix when present. `session-model` is no longer a supported hook route. - Generated Claude settings no longer register `SessionStart` for Agent Trace model attribution, and `sce hooks session-model` is no longer a supported hook command. The `session_models` table/API and diff-trace fallback lookup were removed in T02 of the `remove-session-models-direct-claude-model-id` plan; `diff-trace` now uses only direct payload `model_id` and `tool_version` values. - `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 flow through the doctor surface and checkout DB discovery flows through the `trace` group (`sce trace db list`). diff --git a/context/cli/cli-command-surface.md b/context/cli/cli-command-surface.md index c4aa2e9b..fd34bb2d 100644 --- a/context/cli/cli-command-surface.md +++ b/context/cli/cli-command-surface.md @@ -91,7 +91,7 @@ A user-invocable `sync` command is not wired in the current CLI surface; local D - `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, Claude, and Pi integration inventory derived from embedded setup asset catalogs, 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. Claude grouping is path-based: `settings.json`/`hooks/**` as `ClaudeCode plugins` (including `.claude/hooks/run-sce-or-show-install-guidance.sh`), plus `ClaudeCode agents`, `ClaudeCode commands`, and `ClaudeCode skills`; Pi grouping is path-based: `prompts/**` as `Pi prompts` and `skills/**` as `Pi skills`. - `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 `conversation-trace`; `cli/src/services/hooks/command.rs` owns the hook runtime command handler. Current runtime behavior is commit-msg-only attribution behind the enabled-by-default attribution gate with explicit opt-out controls; `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; `diff-trace` performs STDIN JSON intake, required-field validation, and best-effort AgentTraceDb insertion with tool-prefixed stored `session_id` values plus direct nullable `model_id` / `tool_version` attribution. Claude structured `PostToolUse` payloads may derive `model_id` from top-level or nested `model` metadata with `claude/` prefix normalization. `session-model` is no longer a supported hooks route. `cli/src/services/hooks/lifecycle.rs` implements `ServiceLifecycle` for hook health checks, fix, and setup (hook rollout integrity and required-hook installation). +- `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 `conversation-trace`; `cli/src/services/hooks/command.rs` owns the hook runtime command handler. Current runtime behavior is commit-msg-only attribution behind the enabled-by-default attribution gate with explicit opt-out controls; `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 with best-effort git-note JSON persistence after DB insert; `diff-trace` performs STDIN JSON intake, required-field validation, and best-effort AgentTraceDb insertion with tool-prefixed stored `session_id` values plus direct nullable `model_id` / `tool_version` attribution. Claude structured `PostToolUse` payloads may derive `model_id` from top-level or nested `model` metadata with `claude/` prefix normalization. `session-model` is no longer a supported hooks route. `cli/src/services/hooks/lifecycle.rs` implements `ServiceLifecycle` for hook health checks, fix, and setup (hook rollout integrity and required-hook installation). - `cli/src/services/resilience.rs` defines shared bounded retry/timeout/backoff execution policy (`RetryPolicy`, `run_with_retry`) with deterministic failure messaging and retry observability hooks. - No `cli/src/services/sync.rs` module exists in the current codebase; `sce sync` command wiring is deferred, while local DB initialization and health ownership are split between setup and doctor. - `cli/src/services/default_paths.rs` defines the canonical per-user persisted-location seam for config/state/cache roots plus named default file paths for current persisted artifacts (`global config`, `auth tokens`, `local DB`, `agent trace DB`) used by config discovery, token storage, database adapters, and doctor diagnostics; its internal `roots` seam now owns the platform-aware root-directory resolution so non-test production modules consume shared path accessors instead of resolving owned roots directly. diff --git a/context/context-map.md b/context/context-map.md index eddb5150..584119f9 100644 --- a/context/context-map.md +++ b/context/context-map.md @@ -52,7 +52,7 @@ Feature/domain context: - `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`) - `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: enabled-by-default commit-msg attribution with explicit opt-out controls, no-op `pre-commit`/`post-rewrite` entrypoints, active `post-commit` intersection and Agent Trace DB persistence, DB-only `diff-trace` STDIN intake with OpenCode normalized payloads and Claude structured `PostToolUse` payload classification, tool-prefixed stored `diff_traces.session_id` values (`oc_` for OpenCode, `cc_` for Claude, `pi_` for Pi), direct nullable `model_id`/`tool_version` persistence without session fallback, Claude direct model metadata extraction from top-level or nested `model` fields with `claude/` prefix normalization, no parsed-payload artifact persistence under `context/tmp`, `session-model` removed from the supported hook surface, and `conversation-trace` STDIN intake for normalized batches plus supported raw Claude events; this document also owns the current `diff-trace` and `conversation-trace` fail-open intake contracts.) +- `context/sce/agent-trace-hooks-command-routing.md` (implemented `sce hooks` command routing plus current runtime behavior: enabled-by-default commit-msg attribution with explicit opt-out controls, no-op `pre-commit`/`post-rewrite` entrypoints, active `post-commit` intersection plus Agent Trace DB persistence and best-effort git-note JSON persistence, DB-only `diff-trace` STDIN intake with OpenCode normalized payloads and Claude structured `PostToolUse` payload classification, tool-prefixed stored `diff_traces.session_id` values (`oc_` for OpenCode, `cc_` for Claude, `pi_` for Pi), direct nullable `model_id`/`tool_version` persistence without session fallback, Claude direct model metadata extraction from top-level or nested `model` fields with `claude/` prefix normalization, no parsed-payload artifact persistence under `context/tmp`, `session-model` removed from the supported hook surface, and `conversation-trace` STDIN intake for normalized batches plus supported raw Claude events; this document also owns the current `diff-trace` and `conversation-trace` fail-open intake contracts.) - `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) - `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 through the missing-CLI install-guidance helper) diff --git a/context/overview.md b/context/overview.md index 38823cec..de8d7668 100644 --- a/context/overview.md +++ b/context/overview.md @@ -59,7 +59,7 @@ Context sync now uses an important-change gate: cross-cutting/policy/architectur The `/change-to-plan` command body is also intentionally thin orchestration: it delegates clarification and plan-shape contracts to `sce-plan-authoring` (including one-task/one-atomic-commit task slicing) while keeping wrapper-level plan output and handoff obligations explicit. The generated OpenCode command doc now also emits `entry-skill: sce-plan-authoring` plus an ordered `skills` list. The targeted support commands (`handover`, `commit`, `validate`) keep their thin-wrapper behavior and now also emit machine-readable OpenCode command frontmatter describing their entry skill and ordered skill chain. `/commit` is now split by profile: manual generated commands remain proposal-only and allow split guidance when staged changes mix unrelated goals, while the automated OpenCode `/commit` command generates exactly one commit message and runs `git commit` against the staged diff. The shared `sce-atomic-commit` contract also requires commit bodies to cite affected plan slug(s) and updated task ID(s) when staged changes include `context/plans/*.md`, and to stop for clarification instead of inventing those references when the staged plan diff is ambiguous. 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, `SCE_DISABLED` is false, and the staged-diff AI-overlap preflight confirms AI/editor evidence (`StagedDiffAiOverlapResult::Overlap`); when the preflight returns `NoOverlap` or `Error` (including DB open failure, schema not ready, query error, staged diff read failure, or zero overlap), the trailer is not appended and errors are logged via `sce.hooks.commit_msg.ai_overlap_error`; `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`, uses only direct payload `model_id` and `tool_version` (no longer resolves from `session_models`), and continues with `None` for missing attribution, with same-tool-idempotent stored `session_id` prefixing (`oc_` for OpenCode, `cc_` for Claude, `pi_` for Pi), non-lossy AgentTraceDb `time_ms` conversion, and no parsed-payload artifact fallback. Claude structured `PostToolUse` diff-trace intake extracts direct model metadata from top-level or nested `model` fields and normalizes it with the `claude/` prefix when present. +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, `SCE_DISABLED` is false, and the staged-diff AI-overlap preflight confirms AI/editor evidence (`StagedDiffAiOverlapResult::Overlap`); when the preflight returns `NoOverlap` or `Error` (including DB open failure, schema not ready, query error, staged diff read failure, or zero overlap), the trailer is not appended and errors are logged via `sce.hooks.commit_msg.ai_overlap_error`; `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`, 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`, and then writes the same full JSON best-effort to git notes under the configured Agent Trace notes ref (default `refs/notes/sce-agent-trace`) without creating a 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`, uses only direct payload `model_id` and `tool_version` (no longer resolves from `session_models`), and continues with `None` for missing attribution, with same-tool-idempotent stored `session_id` prefixing (`oc_` for OpenCode, `cc_` for Claude, `pi_` for Pi), non-lossy AgentTraceDb `time_ms` conversion, and no parsed-payload artifact fallback. Claude structured `PostToolUse` diff-trace intake extracts direct model metadata from top-level or nested `model` fields and normalizes it with the `claude/` prefix when present. 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 local or remote sync-mode opens, parent-directory creation, connection setup, synchronous query helpers, embedded migration execution, and shared DB lifecycle helpers. Auth DB persistence uses encrypted `AuthDb = EncryptedTursoDb` and token storage persists credentials through the `auth_credentials` table. Agent Trace persistence uses `AgentTraceDb = TursoDb` with legacy global plus active per-checkout DB paths, fresh-start migrations for `diff_traces`, `post_commit_patch_intersections`, `agent_traces`, `messages`, `parts`, indexes/triggers, and retired `015_create_session_models` metadata handling; active hook runtime writes direct nullable diff-trace attribution without a `session_models` API/table dependency. The hooks command surface now also supports concrete runtime subcommand routing (`pre-commit`, `commit-msg`, `post-commit`, `post-rewrite`, `diff-trace`, and `conversation-trace`) with deterministic argument/STDIN validation; `session-model` is no longer supported. Current runtime behavior keeps commit-msg attribution enabled by default unless explicitly opted out: the attribution gate enables canonical trailer insertion in `commit-msg` only when the staged-diff AI-overlap preflight confirms AI/editor evidence (no trailer is appended when the preflight finds no overlap or encounters any error); `pre-commit`/`post-rewrite` remain deterministic no-ops, `post-commit` requires validated `--remote-url`, threads that URL into the Agent Trace flow, prints it to stderr, and remains the active bounded recent-diff-trace intersection path, and `diff-trace` is the active intake path for parsed STDIN `{ sessionID, diff, time, model_id?, tool_name, tool_version }` payload persistence with optional `model_id`, required non-empty `tool_name`, required nullable/non-empty `tool_version`, direct `model_id` and `tool_version` values (no session-model fallback), required `u64` millisecond `time`, same-tool-idempotent stored `session_id` prefixing (`oc_` for OpenCode, `cc_` for Claude, `pi_` for Pi), non-lossy AgentTraceDb `time_ms` conversion, and no parsed-payload artifact fallback. This behavior is documented in `context/sce/agent-trace-hooks-command-routing.md`. The removed `sce hooks claude-capture` raw capture route is documented in `context/sce/claude-raw-hook-capture.md` as a removed feature. diff --git a/context/plans/agent-trace-git-notes.md b/context/plans/agent-trace-git-notes.md index 79bc116e..4911138d 100644 --- a/context/plans/agent-trace-git-notes.md +++ b/context/plans/agent-trace-git-notes.md @@ -84,12 +84,17 @@ The default notes ref is dedicated to SCE Agent Trace data and is configurable. - Evidence: `nix develop -c sh -c 'cd cli && cargo fmt'` passed; targeted `cargo test git_note_writer` was blocked by SCE bash policy preferring `nix flake check`; first `nix flake check` caught a clippy issue and then a git-note writer test failure, both fixed; final `nix flake check` passed; `nix run .#pkl-check-generated` passed ("Generated outputs are up to date."). - Notes: Added an injectable helper that validates non-blank ref/commit/content, invokes `git notes --ref add -f -F - ` without shell interpolation, pipes Agent Trace JSON through stdin preserving content bytes, returns `GitNoteWriteOutcome`, and includes focused unit coverage for command construction, configured refs, blank input rejection, and command failure context. The helper is intentionally not wired into post-commit runtime until T04. -- [ ] T04: `Wire git-note persistence into post-commit Agent Trace flow` (status:todo) +- [x] T04: `Wire git-note persistence into post-commit Agent Trace flow` (status:done) - Task ID: T04 - Goal: After Agent Trace JSON validation and DB insertion succeed, write the same full JSON to the configured git-notes ref for the committed SHA, while keeping note-write failures non-blocking. - Boundaries (in/out of scope): In - post-commit flow wiring, resolved config read, stable log event for note-write failure (for example `sce.hooks.post_commit.agent_trace_git_note_write_failed`), tests proving successful write is attempted after DB insert and failures do not change hook success. Out - backfill, notes push/fetch, non-git VCS note behavior, changing existing DB failure semantics. - Done when: default post-commit writes a note under `refs/notes/sce-agent-trace`; configured ref is honored; note write is skipped or treated as no-op for unsupported/non-git contexts if necessary; note write failure logs diagnostics but does not fail the hook after DB persistence succeeds. - Verification notes (commands or checks): targeted post-commit hook tests if permitted; manual local check with `git notes --ref refs/notes/sce-agent-trace show HEAD`; `nix flake check`. + - Status: done + - Completed: 2026-07-15 + - Files changed: `cli/src/services/hooks/mod.rs`, `context/plans/agent-trace-git-notes.md` + - Evidence: `nix develop -c sh -c 'cd cli && cargo fmt'` passed; targeted `cargo test post_commit_agent_trace_flow -- --nocapture` was blocked by SCE bash policy preferring `nix flake check`; first `nix flake check` caught a clippy `too_many_arguments` issue, fixed by grouping git-note persistence inputs; final `nix flake check` passed; `nix run .#pkl-check-generated` passed ("Generated outputs are up to date."). + - Notes: Post-commit now resolves `policies.agent_trace.git_notes_ref`, writes the validated serialized Agent Trace JSON to git notes after Agent Trace DB insertion succeeds, skips note writes for explicit non-git VCS values, and logs non-blocking note-write failures with `sce.hooks.post_commit.agent_trace_git_note_write_failed`. - [ ] T05: `Update Agent Trace context for git-notes persistence` (status:todo) - Task ID: T05 diff --git a/context/sce/agent-trace-db.md b/context/sce/agent-trace-db.md index 157ab772..2f703d81 100644 --- a/context/sce/agent-trace-db.md +++ b/context/sce/agent-trace-db.md @@ -180,7 +180,7 @@ Both triggers compare `OLD.*` vs `NEW.*` for all mutable columns (excluding `upd - AgentTraceDb open/insert failures are logged and reflected in deterministic success text as failed DB persistence; no artifact fallback is created. - Existing artifact files are not backfilled into the database. -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. +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. After DB insertion succeeds in git contexts, the same full serialized JSON is also written best-effort to a git note on the committed SHA under the configured Agent Trace notes ref (default `refs/notes/sce-agent-trace`); git-note write failures are logged but do not roll back or fail DB persistence. Range `content_hash` values are computed from the touched-line kind/content of the post-commit hunk that produced the persisted range, not from DB IDs, paths, line positions, or runtime metadata. `sce hooks conversation-trace` is the current runtime writer for `messages` and `parts`. diff --git a/context/sce/agent-trace-hooks-command-routing.md b/context/sce/agent-trace-hooks-command-routing.md index 9d4a6a27..23d97ff5 100644 --- a/context/sce/agent-trace-hooks-command-routing.md +++ b/context/sce/agent-trace-hooks-command-routing.md @@ -59,7 +59,9 @@ - The built Agent Trace payload is converted to JSON `Value` and validated via `agent_trace::validate_agent_trace_value(...)` before persistence. - Validation failures are returned through the same post-commit runtime failure path/class used for Agent Trace DB insertion failures (no silent fallback). - When validation passes, the payload is serialized and inserted into Agent Trace DB `agent_traces` using `commit_id` from flow-result commit metadata, `commit_time_ms` from flow-result post-commit timestamp metadata, a derived non-null `url` value formatted as `sce.crocoder.dev/trace/`, and the validated runtime `--remote-url` value persisted to nullable `agent_traces.remote_url`. - - Post-commit Agent Trace success requires both schema validation and Agent Trace DB `agent_traces` persistence to succeed. + - After Agent Trace DB insertion succeeds, git post-commit contexts also write the same full serialized Agent Trace JSON to a git note on the committed SHA. The default ref is `refs/notes/sce-agent-trace`, resolved through `policies.agent_trace.git_notes_ref`; explicit non-git `--vcs` values skip the note write. + - Git-note writes use replace/upsert semantics (`git notes --ref add -f -F - `) and preserve multiline JSON by piping content through stdin. + - Post-commit Agent Trace success requires both schema validation and Agent Trace DB `agent_traces` persistence to succeed. Git-note write failures are best-effort: they are logged with `sce.hooks.post_commit.agent_trace_git_note_write_failed` and do not fail the hook after DB persistence succeeded. - Current command-surface success output is: `post-commit hook processed intersection: commit=, intersection_files=`. - `post-rewrite` is a deterministic no-op entrypoint. - `diff-trace` reads STDIN JSON and classifies the payload: @@ -115,7 +117,7 @@ ## Explicit non-goals in the current baseline - No checkpoint handoff file -- No git-notes persistence +- No git-notes push/fetch/backfill behavior - No backfill/import of existing `context/tmp/*-diff-trace.json` artifacts into AgentTraceDb - No retry queue replay - No rewrite remap ingestion From 63221b8df45cd1ff9133b896aa58203b7cb4764e Mon Sep 17 00:00:00 2001 From: David Abram Date: Wed, 15 Jul 2026 10:28:22 +0200 Subject: [PATCH 4/8] context: Document Agent Trace git-notes validation Record the completed Agent Trace git-notes context update and final validation, including current post-commit behavior for config precedence and setup-installed hooks. Plan: agent-trace-git-notes Tasks: T05, T06 Co-authored-by: SCE --- context/cli/config-precedence-contract.md | 2 +- context/plans/agent-trace-git-notes.md | 37 ++++++++++++++++++- .../setup-githooks-hook-asset-packaging.md | 1 + 3 files changed, 37 insertions(+), 3 deletions(-) diff --git a/context/cli/config-precedence-contract.md b/context/cli/config-precedence-contract.md index 8c704f26..b1f642ca 100644 --- a/context/cli/config-precedence-contract.md +++ b/context/cli/config-precedence-contract.md @@ -25,7 +25,7 @@ Resolved runtime values follow this deterministic order: Repo-configured bash-tool policy values are config-file only in this task slice: they load from `policies.bash` in the selected config files, merge `global -> local` alongside the rest of the config object, and currently have no flag or environment override layer. -Agent Trace hook policy currently includes `policies.agent_trace.git_notes_ref`, which resolves as config-file value over default `refs/notes/sce-agent-trace`. It has no flag or environment override layer in the current implementation slice. The resolved value is exposed through hook runtime config for later post-commit git-note persistence wiring; current post-commit runtime behavior is not yet changed by this config field. +Agent Trace hook policy currently includes `policies.agent_trace.git_notes_ref`, which resolves as config-file value over default `refs/notes/sce-agent-trace`. It has no flag or environment override layer in the current implementation slice. The resolved value is consumed by the post-commit Agent Trace flow when writing the validated full Agent Trace JSON to git notes after Agent Trace DB persistence succeeds. Resolved observability values that currently have no CLI flag layer follow the same lower-precedence chain without a flag step: diff --git a/context/plans/agent-trace-git-notes.md b/context/plans/agent-trace-git-notes.md index 4911138d..7d90de0a 100644 --- a/context/plans/agent-trace-git-notes.md +++ b/context/plans/agent-trace-git-notes.md @@ -96,19 +96,52 @@ The default notes ref is dedicated to SCE Agent Trace data and is configurable. - Evidence: `nix develop -c sh -c 'cd cli && cargo fmt'` passed; targeted `cargo test post_commit_agent_trace_flow -- --nocapture` was blocked by SCE bash policy preferring `nix flake check`; first `nix flake check` caught a clippy `too_many_arguments` issue, fixed by grouping git-note persistence inputs; final `nix flake check` passed; `nix run .#pkl-check-generated` passed ("Generated outputs are up to date."). - Notes: Post-commit now resolves `policies.agent_trace.git_notes_ref`, writes the validated serialized Agent Trace JSON to git notes after Agent Trace DB insertion succeeds, skips note writes for explicit non-git VCS values, and logs non-blocking note-write failures with `sce.hooks.post_commit.agent_trace_git_note_write_failed`. -- [ ] T05: `Update Agent Trace context for git-notes persistence` (status:todo) +- [x] T05: `Update Agent Trace context for git-notes persistence` (status:done) - Task ID: T05 - Goal: Document the new git-notes persistence contract in current-state context. - Boundaries (in/out of scope): In - update `context/sce/agent-trace-hooks-command-routing.md`, `context/sce/agent-trace-db.md`, `context/sce/setup-githooks-hook-asset-packaging.md` if hook behavior text needs adjustment, and `context/context-map.md` entries. Out - implementation code, broad docs rewrites unrelated to post-commit Agent Trace persistence. - Done when: context states the default notes ref, config override, full-JSON note content, and non-blocking failure behavior; stale `No git-notes persistence` text is removed or qualified. - Verification notes (commands or checks): `rg "git-notes|git notes|No git-notes" context/`; manual diff review. + - Status: done + - Completed: 2026-07-15 + - Files changed: `context/cli/config-precedence-contract.md`, `context/sce/setup-githooks-hook-asset-packaging.md`, `context/plans/agent-trace-git-notes.md` + - Evidence: `rg "git-notes|git notes|No git-notes" context/` reviewed remaining current/historical matches; `git diff --check` passed. + - Notes: Removed stale config-contract wording that described git-note runtime wiring as future-only and clarified that setup-installed post-commit hooks hand off to Rust, where Agent Trace DB persistence remains required and git-note writes are best-effort under the configured notes ref. -- [ ] T06: `Validate git-notes Agent Trace behavior and cleanup` (status:todo) +- [x] T06: `Validate git-notes Agent Trace behavior and cleanup` (status:done) - Task ID: T06 - Goal: Run final validation for the complete plan and clean up any planning or test scaffolding. - Boundaries (in/out of scope): In - full repo validation, generated-output parity, focused grep for stale docs/config strings, cleanup of temporary test repositories or notes refs created during manual checks. Out - new behavior beyond the completed task stack. - Done when: `nix flake check` passes or any failure is documented as pre-existing/unrelated; `nix run .#pkl-check-generated` passes; context sync is verified; no temporary scaffolding remains. - Verification notes (commands or checks): `nix flake check`; `nix run .#pkl-check-generated`; `git diff --check`; `rg "refs/notes/sce-agent-trace|agent_trace.*git.*note|No git-notes" cli/ config/ context/`. + - Status: done + - Completed: 2026-07-15 + - Files changed: `context/plans/agent-trace-git-notes.md` + - Evidence: `git diff --check` passed; `rg "refs/notes/sce-agent-trace|agent_trace.*git.*note|No git-notes" cli/ config/ context/` reviewed current and historical matches; `nix run .#pkl-check-generated` passed ("Generated outputs are up to date."); `nix flake check` passed ("all checks passed"). + - Notes: No temporary test repositories or notes refs were created during T06, so no cleanup was required. + +## Validation Report + +### Commands run +- `git diff --check` -> exit 0 (no whitespace errors). +- `rg "refs/notes/sce-agent-trace|agent_trace.*git.*note|No git-notes" cli/ config/ context/` -> exit 0; reviewed current Agent Trace git-notes references plus historical `No git-notes` references. +- `nix run .#pkl-check-generated` -> exit 0 (`Generated outputs are up to date.`). +- `nix flake check` -> exit 0 (`all checks passed!`). +- `git status --short --untracked-files=all` -> reviewed staged/unstaged plan/context changes; no T06-created temporary scaffolding found. + +### Success-criteria verification +- [x] Successful post-commit Agent Trace writes a default git note under `refs/notes/sce-agent-trace` after validation/DB persistence -> covered by completed T04 implementation evidence and final `nix flake check`. +- [x] Note content is the same full Agent Trace JSON persisted to `agent_traces.trace_json` -> covered by completed T04 implementation/context evidence and final grep review. +- [x] Configurable notes ref documented in generated schema/context -> confirmed by final grep review and `nix run .#pkl-check-generated`. +- [x] Git-note write failures remain non-blocking and use stable logging -> confirmed by final grep review and `nix flake check`. +- [x] Existing Agent Trace DB persistence remains the required source of trace rows -> confirmed by context review and final validation. +- [x] Context describes current git-notes behavior and non-blocking posture -> confirmed by context sync review. + +### Failed checks and follow-ups +- None. + +### Residual risks +- No residual risks identified for the completed plan. ## Open questions diff --git a/context/sce/setup-githooks-hook-asset-packaging.md b/context/sce/setup-githooks-hook-asset-packaging.md index ad592370..02cfe35f 100644 --- a/context/sce/setup-githooks-hook-asset-packaging.md +++ b/context/sce/setup-githooks-hook-asset-packaging.md @@ -19,6 +19,7 @@ Current `post-commit` template behavior is: - resolve `origin` with `git remote get-url origin`; if `sce` is not on `PATH`, print `sce CLI not found. Install it from https://sce.crocoder.dev/docs/getting-started#install-cli` to stderr and exit successfully so missing local CLI installation does not block the commit - if the remote lookup returns a non-empty URL, invoke `sce hooks post-commit --vcs git --remote-url "$remote_url" "$@"` - otherwise still invoke `sce hooks post-commit --vcs git "$@"`; Rust-side validation fails this missing-URL path without blocking git commit completion under the hook script policy. +- the Rust `post-commit` runtime handles Agent Trace persistence after this handoff: DB insertion remains the required persistence path, and successful git contexts also write the validated full Agent Trace JSON best-effort to git notes under the configured Agent Trace notes ref (default `refs/notes/sce-agent-trace`). ## Setup-service accessor surface From 7bfeed4ccea5c54f5c1bc6b491463c3f54b7f400 Mon Sep 17 00:00:00 2001 From: David Abram Date: Wed, 15 Jul 2026 11:35:12 +0200 Subject: [PATCH 5/8] config: Add Agent Trace notes auto-push policy Expose policies.agent_trace.push_notes.enabled with default true across schema generation, config parsing, runtime resolution, and config show output. Update current-state context for the new config surface.\n\nPlan: agent-trace-git-notes-auto-push\nTask: T01 Co-authored-by: SCE --- .../config/schema/sce-config.schema.json | 25 ++++++ cli/src/services/config/render.rs | 44 ++++++++++ cli/src/services/config/resolver.rs | 53 ++++++++++++ cli/src/services/config/schema.rs | 50 ++++++++--- cli/src/services/config/types.rs | 1 + config/pkl/base/sce-config-schema.pkl | 14 +++- config/schema/sce-config.schema.json | 14 +++- context/architecture.md | 2 +- context/cli/config-precedence-contract.md | 6 +- context/context-map.md | 2 +- context/glossary.md | 1 + context/overview.md | 2 +- .../plans/agent-trace-git-notes-auto-push.md | 84 +++++++++++++++++++ 13 files changed, 278 insertions(+), 20 deletions(-) create mode 100644 context/plans/agent-trace-git-notes-auto-push.md diff --git a/cli/assets/generated/config/schema/sce-config.schema.json b/cli/assets/generated/config/schema/sce-config.schema.json index dfd6eff1..36268146 100644 --- a/cli/assets/generated/config/schema/sce-config.schema.json +++ b/cli/assets/generated/config/schema/sce-config.schema.json @@ -58,6 +58,31 @@ }, "additionalProperties": false }, + "agent_trace": { + "description": "Agent Trace hook policy. Controls where post-commit Agent Trace JSON is mirrored as a git note and whether that notes ref is auto-pushed.", + "type": "object", + "properties": { + "git_notes_ref": { + "description": "Git notes ref used for Agent Trace JSON persistence. Defaults to refs/notes/sce-agent-trace.", + "default": "refs/notes/sce-agent-trace", + "type": "string", + "minLength": 1 + }, + "push_notes": { + "description": "Agent Trace git-notes push policy. Notes auto-push is enabled by default.", + "type": "object", + "properties": { + "enabled": { + "description": "Enable best-effort Agent Trace git-notes auto-push after local note persistence. Defaults to true when omitted; set false to disable.", + "default": true, + "type": "boolean" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, "database_retry": { "type": "object", "properties": { diff --git a/cli/src/services/config/render.rs b/cli/src/services/config/render.rs index 720907dd..165b28e2 100644 --- a/cli/src/services/config/render.rs +++ b/cli/src/services/config/render.rs @@ -35,6 +35,7 @@ pub(super) fn format_show_output(runtime: &RuntimeConfig, report_format: ReportF &runtime.workos_client_id, ), format_bash_policies_text(&runtime.bash_policies), + format_agent_trace_policy_text(runtime), format_database_retry_text(&runtime.database_retry), format_validation_warnings_text(&warnings), ]; @@ -70,6 +71,7 @@ pub(super) fn format_show_output(runtime: &RuntimeConfig, report_format: ReportF "workos_client_id": format_optional_auth_resolved_value_json(WORKOS_CLIENT_ID_KEY, &runtime.workos_client_id), "policies": { "bash": format_bash_policies_json(&runtime.bash_policies), + "agent_trace": format_agent_trace_policy_json(runtime), "database_retry": format_database_retry_json(&runtime.database_retry), } }, @@ -369,6 +371,48 @@ fn abbreviate_text_value(value: &str) -> String { format!("{prefix}...{suffix}") } +fn format_agent_trace_policy_text(runtime: &RuntimeConfig) -> String { + [ + format!(" {}:", style::label("policies.agent_trace")), + format!( + " {}", + format_resolved_value_text( + "git_notes_ref", + &runtime.agent_trace_git_notes_ref.value, + runtime.agent_trace_git_notes_ref.source, + ) + ), + format!( + " {}", + format_resolved_value_text( + "push_notes.enabled", + if runtime.agent_trace_push_notes_enabled.value { + "true" + } else { + "false" + }, + runtime.agent_trace_push_notes_enabled.source, + ) + ), + ] + .join("\n") +} + +fn format_agent_trace_policy_json(runtime: &RuntimeConfig) -> Value { + json!({ + "git_notes_ref": format_resolved_value_json( + &runtime.agent_trace_git_notes_ref.value, + runtime.agent_trace_git_notes_ref.source, + ), + "push_notes": { + "enabled": format_resolved_value_json( + runtime.agent_trace_push_notes_enabled.value, + runtime.agent_trace_push_notes_enabled.source, + ), + }, + }) +} + fn retry_policy_display(policy: &crate::services::resilience::RetryPolicy) -> String { format!( "{} attempts, {}ms timeout, {}..{}ms backoff", diff --git a/cli/src/services/config/resolver.rs b/cli/src/services/config/resolver.rs index efcc9d27..383567c7 100644 --- a/cli/src/services/config/resolver.rs +++ b/cli/src/services/config/resolver.rs @@ -63,6 +63,7 @@ pub(super) struct RuntimeConfig { pub(super) timeout_ms: ResolvedValue, pub(super) attribution_hooks_enabled: ResolvedValue, pub(super) agent_trace_git_notes_ref: ResolvedValue, + pub(super) agent_trace_push_notes_enabled: ResolvedValue, pub(super) workos_client_id: ResolvedOptionalValue, pub(super) bash_policies: ResolvedOptionalValue, pub(super) database_retry: ResolvedOptionalValue, @@ -228,6 +229,7 @@ where Ok(ResolvedHookRuntimeConfig { attribution_hooks_enabled: runtime.attribution_hooks_enabled.value, agent_trace_git_notes_ref: runtime.agent_trace_git_notes_ref.value, + agent_trace_push_notes_enabled: runtime.agent_trace_push_notes_enabled.value, }) } @@ -275,6 +277,7 @@ where timeout_ms: None, attribution_hooks_enabled: None, agent_trace_git_notes_ref: None, + agent_trace_push_notes_enabled: None, workos_client_id: None, bash_policy_presets: None, bash_policy_custom: None, @@ -313,6 +316,9 @@ where if let Some(agent_trace_git_notes_ref) = layer.agent_trace_git_notes_ref { file_config.agent_trace_git_notes_ref = Some(agent_trace_git_notes_ref); } + if let Some(agent_trace_push_notes_enabled) = layer.agent_trace_push_notes_enabled { + file_config.agent_trace_push_notes_enabled = Some(agent_trace_push_notes_enabled); + } if let Some(workos_client_id) = layer.workos_client_id { file_config.workos_client_id = Some(workos_client_id); } @@ -457,6 +463,8 @@ where } let resolved_agent_trace_git_notes_ref = resolve_agent_trace_git_notes_ref(file_config.agent_trace_git_notes_ref.as_ref()); + let resolved_agent_trace_push_notes_enabled = + resolve_agent_trace_push_notes_enabled(file_config.agent_trace_push_notes_enabled.as_ref()); let resolved_workos_client_id = resolve_optional_auth_config_value( WORKOS_CLIENT_ID_KEY, file_config.workos_client_id, @@ -481,6 +489,7 @@ where timeout_ms: resolved_timeout_ms, attribution_hooks_enabled: resolved_attribution_hooks_enabled, agent_trace_git_notes_ref: resolved_agent_trace_git_notes_ref, + agent_trace_push_notes_enabled: resolved_agent_trace_push_notes_enabled, workos_client_id: resolved_workos_client_id, bash_policies: resolved_bash_policies, database_retry: resolved_database_retry, @@ -505,6 +514,22 @@ fn resolve_agent_trace_git_notes_ref( } } +fn resolve_agent_trace_push_notes_enabled( + file_value: Option<&schema::FileConfigValue>, +) -> ResolvedValue { + if let Some(value) = file_value { + return ResolvedValue { + value: value.value, + source: ValueSource::ConfigFile(value.source), + }; + } + + ResolvedValue { + value: true, + source: ValueSource::Default, + } +} + fn resolve_optional_auth_config_value( key: AuthConfigKeySpec, file_value: Option>, @@ -708,6 +733,7 @@ mod tests { Ok(ResolvedHookRuntimeConfig { attribution_hooks_enabled: runtime.attribution_hooks_enabled.value, agent_trace_git_notes_ref: runtime.agent_trace_git_notes_ref.value, + agent_trace_push_notes_enabled: runtime.agent_trace_push_notes_enabled.value, }) } @@ -739,6 +765,33 @@ mod tests { assert_eq!(resolved.agent_trace_git_notes_ref, "refs/notes/custom-sce"); } + #[test] + fn agent_trace_push_notes_enabled_uses_default() { + let resolved = resolve_hooks_with_env_and_config(None, None).unwrap(); + + assert!(resolved.agent_trace_push_notes_enabled); + } + + #[test] + fn agent_trace_push_notes_enabled_uses_explicit_config_false() { + let resolved = resolve_hooks_with_env_and_config( + None, + Some(r#"{"policies":{"agent_trace":{"push_notes":{"enabled":false}}}}"#), + ) + .unwrap(); + + assert!(!resolved.agent_trace_push_notes_enabled); + } + + #[test] + fn invalid_agent_trace_push_notes_shape_is_rejected() { + resolve_hooks_with_env_and_config( + None, + Some(r#"{"policies":{"agent_trace":{"push_notes":{"enabled":"no"}}}}"#), + ) + .unwrap_err(); + } + #[test] fn blank_agent_trace_git_notes_ref_is_rejected() { let error = resolve_hooks_with_env_and_config( diff --git a/cli/src/services/config/schema.rs b/cli/src/services/config/schema.rs index 0dcb41b2..0646ab4c 100644 --- a/cli/src/services/config/schema.rs +++ b/cli/src/services/config/schema.rs @@ -91,6 +91,12 @@ pub(crate) struct ParsedPoliciesConfigDocument { #[derive(Clone, Debug, Deserialize, Eq, PartialEq)] pub(crate) struct ParsedAgentTracePolicyConfigDocument { pub(crate) git_notes_ref: Option, + pub(crate) push_notes: Option, +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq)] +pub(crate) struct ParsedAgentTracePushNotesConfigDocument { + pub(crate) enabled: Option, } #[derive(Clone, Debug, Deserialize, Eq, PartialEq)] @@ -154,6 +160,7 @@ pub(crate) struct FileConfig { pub(crate) timeout_ms: Option>, pub(crate) attribution_hooks_enabled: Option>, pub(crate) agent_trace_git_notes_ref: Option>, + pub(crate) agent_trace_push_notes_enabled: Option>, pub(crate) workos_client_id: Option>, pub(crate) bash_policy_presets: Option>>, pub(crate) bash_policy_custom: Option>>, @@ -166,11 +173,17 @@ pub(crate) type ParsedBashPolicyConfig = ( Option>>, ); +pub(crate) type ParsedAgentTracePolicy = ( + Option>, + Option>, +); + pub(crate) type ParsedFilePolicies = ( Option>, Option>>, Option>>, Option>, + Option>, Option>, ); @@ -307,6 +320,7 @@ pub(crate) fn parse_file_config( bash_policy_presets, bash_policy_custom, agent_trace_git_notes_ref, + agent_trace_push_notes_enabled, database_retry, ) = map_policies_config(typed.policies.as_ref(), object, path, source)?; let integrations = map_integrations_config(typed.integrations.as_ref(), object, path, source)?; @@ -319,6 +333,7 @@ pub(crate) fn parse_file_config( timeout_ms, attribution_hooks_enabled, agent_trace_git_notes_ref, + agent_trace_push_notes_enabled, workos_client_id, bash_policy_presets, bash_policy_custom, @@ -334,7 +349,7 @@ pub(crate) fn map_policies_config( source: ConfigPathSource, ) -> Result { let Some(policies_value) = object.get("policies") else { - return Ok((None, None, None, None, None)); + return Ok((None, None, None, None, None, None)); }; let policies_object = policies_value.as_object().with_context(|| { @@ -361,12 +376,13 @@ pub(crate) fn map_policies_config( )?; let (bash_policy_presets, bash_policy_custom) = map_bash_policy_config(bash, policies_object, path, source)?; - let agent_trace_git_notes_ref = map_agent_trace_policy_config( - typed.and_then(|config| config.agent_trace.as_ref()), - policies_object, - path, - source, - )?; + let (agent_trace_git_notes_ref, agent_trace_push_notes_enabled) = + map_agent_trace_policy_config( + typed.and_then(|config| config.agent_trace.as_ref()), + policies_object, + path, + source, + )?; let database_retry = map_database_retry_config( typed.and_then(|config| config.database_retry.as_ref()), policies_object, @@ -379,6 +395,7 @@ pub(crate) fn map_policies_config( bash_policy_presets, bash_policy_custom, agent_trace_git_notes_ref, + agent_trace_push_notes_enabled, database_retry, )) } @@ -418,9 +435,9 @@ pub(crate) fn map_agent_trace_policy_config( policies_object: &serde_json::Map, path: &Path, source: ConfigPathSource, -) -> Result>> { +) -> Result { let Some(agent_trace_value) = policies_object.get("agent_trace") else { - return Ok(None); + return Ok((None, None)); }; let agent_trace_object = agent_trace_value.as_object().with_context(|| { @@ -434,11 +451,11 @@ pub(crate) fn map_agent_trace_policy_config( agent_trace_object, path, Some("policies.agent_trace"), - &["git_notes_ref"], - "git_notes_ref", + &["git_notes_ref", "push_notes"], + "git_notes_ref, push_notes", )?; - typed + let git_notes_ref = typed .and_then(|config| config.git_notes_ref.as_ref()) .map(|value| { let trimmed = value.trim(); @@ -454,7 +471,14 @@ pub(crate) fn map_agent_trace_policy_config( source, }) }) - .transpose() + .transpose()?; + + let push_notes_enabled = typed + .and_then(|config| config.push_notes.as_ref()) + .and_then(|config| config.enabled) + .map(|value| FileConfigValue { value, source }); + + Ok((git_notes_ref, push_notes_enabled)) } pub(crate) fn map_bash_policy_config( diff --git a/cli/src/services/config/types.rs b/cli/src/services/config/types.rs index bd509efc..a48dc7b3 100644 --- a/cli/src/services/config/types.rs +++ b/cli/src/services/config/types.rs @@ -241,6 +241,7 @@ pub(crate) struct ResolvedObservabilityRuntimeConfig { pub(crate) struct ResolvedHookRuntimeConfig { pub(crate) attribution_hooks_enabled: bool, pub(crate) agent_trace_git_notes_ref: String, + pub(crate) agent_trace_push_notes_enabled: bool, } pub(crate) fn parse_bool_value_from(key: &str, raw: &str, source: &str) -> anyhow::Result { diff --git a/config/pkl/base/sce-config-schema.pkl b/config/pkl/base/sce-config-schema.pkl index a48a49fa..a206a4f0 100644 --- a/config/pkl/base/sce-config-schema.pkl +++ b/config/pkl/base/sce-config-schema.pkl @@ -103,7 +103,7 @@ local sceConfigSchema = new JsonSchema { } ["agent_trace"] = new JsonSchema { type = "object" - description = "Agent Trace hook policy. Controls where post-commit Agent Trace JSON is mirrored as a git note." + description = "Agent Trace hook policy. Controls where post-commit Agent Trace JSON is mirrored as a git note and whether that notes ref is auto-pushed." additionalProperties = false properties { ["git_notes_ref"] = new JsonSchema { @@ -112,6 +112,18 @@ local sceConfigSchema = new JsonSchema { description = "Git notes ref used for Agent Trace JSON persistence. Defaults to refs/notes/sce-agent-trace." default = "refs/notes/sce-agent-trace" } + ["push_notes"] = new JsonSchema { + type = "object" + description = "Agent Trace git-notes push policy. Notes auto-push is enabled by default." + additionalProperties = false + properties { + ["enabled"] = new JsonSchema { + type = "boolean" + description = "Enable best-effort Agent Trace git-notes auto-push after local note persistence. Defaults to true when omitted; set false to disable." + default = true + } + } + } } } ["database_retry"] = new JsonSchema { diff --git a/config/schema/sce-config.schema.json b/config/schema/sce-config.schema.json index 11bae84b..36268146 100644 --- a/config/schema/sce-config.schema.json +++ b/config/schema/sce-config.schema.json @@ -59,7 +59,7 @@ "additionalProperties": false }, "agent_trace": { - "description": "Agent Trace hook policy. Controls where post-commit Agent Trace JSON is mirrored as a git note.", + "description": "Agent Trace hook policy. Controls where post-commit Agent Trace JSON is mirrored as a git note and whether that notes ref is auto-pushed.", "type": "object", "properties": { "git_notes_ref": { @@ -67,6 +67,18 @@ "default": "refs/notes/sce-agent-trace", "type": "string", "minLength": 1 + }, + "push_notes": { + "description": "Agent Trace git-notes push policy. Notes auto-push is enabled by default.", + "type": "object", + "properties": { + "enabled": { + "description": "Enable best-effort Agent Trace git-notes auto-push after local note persistence. Defaults to true when omitted; set false to disable.", + "default": true, + "type": "boolean" + } + }, + "additionalProperties": false } }, "additionalProperties": false diff --git a/context/architecture.md b/context/architecture.md index 37d76aba..423601b1 100644 --- a/context/architecture.md +++ b/context/architecture.md @@ -111,7 +111,7 @@ The repository includes a new placeholder Rust binary crate at `cli/`. - `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, 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, Agent Trace hook policy resolution for `policies.agent_trace.git_notes_ref` with default `refs/notes/sce-agent-trace`, 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/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, Agent Trace hook policy resolution for `policies.agent_trace.git_notes_ref` with default `refs/notes/sce-agent-trace` plus `policies.agent_trace.push_notes.enabled` with default `true`, 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. diff --git a/context/cli/config-precedence-contract.md b/context/cli/config-precedence-contract.md index b1f642ca..5216ef25 100644 --- a/context/cli/config-precedence-contract.md +++ b/context/cli/config-precedence-contract.md @@ -25,7 +25,7 @@ Resolved runtime values follow this deterministic order: Repo-configured bash-tool policy values are config-file only in this task slice: they load from `policies.bash` in the selected config files, merge `global -> local` alongside the rest of the config object, and currently have no flag or environment override layer. -Agent Trace hook policy currently includes `policies.agent_trace.git_notes_ref`, which resolves as config-file value over default `refs/notes/sce-agent-trace`. It has no flag or environment override layer in the current implementation slice. The resolved value is consumed by the post-commit Agent Trace flow when writing the validated full Agent Trace JSON to git notes after Agent Trace DB persistence succeeds. +Agent Trace hook policy currently includes `policies.agent_trace.git_notes_ref`, which resolves as config-file value over default `refs/notes/sce-agent-trace`, and `policies.agent_trace.push_notes.enabled`, which resolves as config-file value over default `true`. These keys have no flag or environment override layer in the current implementation slice. The resolved notes ref is consumed by the post-commit Agent Trace flow when writing the validated full Agent Trace JSON to git notes after Agent Trace DB persistence succeeds. The resolved push-notes boolean is exposed through hook runtime config for the follow-on auto-push wiring task; current post-commit hook behavior does not push git notes yet. Resolved observability values that currently have no CLI flag layer follow the same lower-precedence chain without a flag step: @@ -80,7 +80,8 @@ When a default-discovered global or repo-local config file exists but fails JSON - `policies` must be an object when present and currently allows `attribution_hooks`, `agent_trace`, `database_retry`, and `bash`. - `policies.attribution_hooks` must be an object when present and currently allows `enabled`; the generated schema documents default `true`, and explicit `enabled: false` remains a valid opt-out alongside the runtime `SCE_ATTRIBUTION_HOOKS_DISABLED` environment opt-out. -- `policies.agent_trace` must be an object when present and currently allows `git_notes_ref`; the generated schema documents default `refs/notes/sce-agent-trace`, and Rust mapping rejects blank/whitespace-only refs. +- `policies.agent_trace` must be an object when present and currently allows `git_notes_ref` and `push_notes`; the generated schema documents default `refs/notes/sce-agent-trace`, and Rust mapping rejects blank/whitespace-only refs. +- `policies.agent_trace.push_notes` must be an object when present and currently allows `enabled`; the generated schema documents default `true`, and explicit `enabled: false` is a valid opt-out value for the follow-on Agent Trace notes auto-push wiring. - `policies.bash` must be an object when present and currently allows only `presets` and `custom`. - `policies.bash.presets` must be an array of unique built-in preset IDs: `forbid-git-all`, `forbid-git-commit`, `use-pnpm-over-npm`, `use-bun-over-npm`, `use-nix-flake-over-cargo`. - `use-pnpm-over-npm` and `use-bun-over-npm` are mutually exclusive and fail validation when both are present. @@ -101,6 +102,7 @@ When a default-discovered global or repo-local config file exists but fails JSON - `validate` text output is limited to `SCE config validation`, `Validation issues`, and `Validation warnings` lines. - `validate` JSON output is limited to `result.command`, `result.valid`, `result.issues`, and `result.warnings`. - `show` includes resolved bash-tool policies under `result.resolved.policies.bash`. +- `show` includes resolved Agent Trace hook policy under `result.resolved.policies.agent_trace`, including `git_notes_ref` and `push_notes.enabled` with source metadata. - Bash-policy output includes resolved preset IDs, expanded custom entries (`id`, `match.argv_prefix`, `message`), and config-file source metadata when present. - `show` text output renders `policies.bash` as a single deterministic line and reports `(unset)` when no policy config resolves. - `show` text output renders observability values as deterministic per-key lines, reporting `(unset)` for `log_file` when no value resolves. diff --git a/context/context-map.md b/context/context-map.md index 584119f9..0d41db10 100644 --- a/context/context-map.md +++ b/context/context-map.md @@ -16,7 +16,7 @@ Feature/domain context: - `context/cli/structured-patch-service.md` (Claude structured editor-hook derivation in `cli/src/services/structured_patch.rs`, including `Write` structured-update hunks, `Write` `tool_input.content` create fallback, `Edit` structured patches, deterministic skip reasons, `ParsedPatch` output semantics, and Rust golden fixture coverage) - `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/trace-command.md` (`sce trace` command group: discovery of per-checkout `agent-trace-*.db` files under `/sce/` with mtime-desc + checkout-id tiebreak alias assignment and six-required-table readiness probing, implemented `sce trace db shell ` wiring that resolves aliases/checkout IDs and opens the embedded in-process SQL shell without external `turso`, including `.tables` table-name listing for visible/internal tables, implemented `sce trace db list` text + JSON rendering using `services::style::heading`, implemented `sce trace status` per-checkout rendering with `StatusError::{NotInGitRepo, NoCheckoutId, DbMissing}` mapped to validation-class exits and skipped-DB pass-through, implemented `sce trace status --all` aggregation across every discovered DB, and the completed removal of `sce doctor dbs` whose discovery scan/rendering moved into `services::trace`) -- `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 including `policies.attribution_hooks.enabled` default-true/explicit-false opt-out metadata and `policies.agent_trace.git_notes_ref` default `refs/notes/sce-agent-trace`, config-file selection order, `show` provenance output, and trimmed `validate` output contract) +- `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 including `policies.attribution_hooks.enabled` default-true/explicit-false opt-out metadata, `policies.agent_trace.git_notes_ref` default `refs/notes/sce-agent-trace`, and `policies.agent_trace.push_notes.enabled` default-true/explicit-false metadata, config-file selection order, `show` provenance output, and trimmed `validate` output contract) - `context/cli/capability-traits.md` (current broad CLI capability seam in `cli/src/services/capabilities.rs`, including `FsOps`/`StdFsOps`, `GitOps`/`ProcessGitOps`, git root/hooks resolution behavior, compile-time-typed borrowed AppContext wiring with associated-type narrow capability accessors plus `ContextWithRepoRoot` repo-root-scoped context derivation, generic command execution bounds, and test-only unimplemented stubs; current service internals do not consume fs/git traits until later lifecycle migration tasks) - `context/cli/service-lifecycle.md` (current compile-safe lifecycle seam in `cli/src/services/lifecycle.rs`, including default no-op `ServiceLifecycle` diagnose/fix/setup methods against narrow `HasRepoRoot`, lifecycle-owned health/fix/setup result types with generic setup messages, doctor/setup adapter boundaries, the static `LifecycleProvider` enum catalog/dispatcher, hook/config/local_db/auth_db/agent_trace_db lifecycle providers including setup-time 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) diff --git a/context/glossary.md b/context/glossary.md index efe7ad4e..38468ed6 100644 --- a/context/glossary.md +++ b/context/glossary.md @@ -113,6 +113,7 @@ - `bash tool policy config surface`: Nested repo config namespace under `.sce/config.json` at `policies.bash`, currently supporting unique built-in `presets` plus repo-owned `custom` argv-prefix rules with deterministic validation, merged global/local resolution, and first-class `sce config show|validate` reporting. - `attribution hooks gate`: Enabled-by-default local hook runtime gate resolved through shared config precedence in `cli/src/services/config/mod.rs` (with parsing in `schema.rs`): opt-out env `SCE_ATTRIBUTION_HOOKS_DISABLED` overrides repo/global config key `policies.attribution_hooks.enabled` with inverted semantics, and the current enabled path activates commit-msg-only attribution gated by the staged-diff AI-overlap preflight. - `Agent Trace git-notes ref`: Configurable Agent Trace hook policy value at `policies.agent_trace.git_notes_ref`; defaults to `refs/notes/sce-agent-trace`, rejects blank/whitespace-only refs during Rust config mapping, and is exposed through hook runtime config for post-commit git-note persistence wiring. +- `Agent Trace notes auto-push gate`: Configurable Agent Trace hook policy value at `policies.agent_trace.push_notes.enabled`; defaults to `true`, accepts explicit `false` as an opt-out, and is exposed through hook runtime config for the post-commit git-notes auto-push wiring slice. - `StagedDiffAiOverlapResult`: Three-valued enum in `cli/src/services/hooks/mod.rs` returned by the staged-diff AI-overlap evidence check: `Overlap` (staged diff overlaps with at least one recent AI/editor diff trace), `NoOverlap` (no overlap found; staged diff and recent traces were both available but share no touched lines, or staged patch has no touched lines), `Error` (DB open failure, schema not ready, query error, staged diff read failure, or clock failure). Both `NoOverlap` and `Error` map to `ai_contribution_present = false` at the commit-msg policy seam; `Error` additionally triggers `sce.hooks.commit_msg.ai_overlap_error` logging. - `sce.hooks.commit_msg.ai_overlap_error`: Logger event ID emitted by `staged_diff_has_ai_overlap` when the staged-diff AI-overlap preflight encounters an error (DB open failure, schema not ready, query error, staged diff read failure, or clock failure). - `bash policy preset catalog`: Canonical authored preset source at `config/pkl/base/bash-policy-presets.pkl`, rendered to JSON by `config/pkl/generate.pkl` and embedded by the CLI from `config/.opencode/lib/bash-policy-presets.json` so CLI validation and OpenCode enforcement share the same preset IDs, argv-prefix matchers, fixed messages, and conflict metadata. diff --git a/context/overview.md b/context/overview.md index de8d7668..34d66993 100644 --- a/context/overview.md +++ b/context/overview.md @@ -27,7 +27,7 @@ The `setup` command includes an `inquire`-backed target-selection flow: default The CLI now compiles an embedded setup asset manifest from `config/.opencode/**`, `config/.claude/**`, `config/.pi/**`, 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. The setup service also provides repository-root install orchestration: it resolves the repository root, derives a repo-root-scoped `AppContext` from the runtime command context, aggregates `ServiceLifecycle::setup` calls across lifecycle providers (config → local_db → auth_db → agent_trace_db → hooks when requested), handles interactive or flag-based target selection for config asset installation, and reports deterministic completion details (selected target(s) and installed file counts). Setup uses a unified remove-and-replace policy for all write flows — it removes existing targets before swapping staged content and returns deterministic recovery guidance (recover from version control) on swap failure, without creating backup artifacts. The setup command gates all modes on an existing git repository before any writes. Internally, `cli/src/services/setup/mod.rs` now separates install-flow logic from interactive prompt logic through focused support seams. The CLI now also applies baseline security hardening for reliability-driven automation: diagnostics/logging paths use deterministic secret redaction, `sce setup --hooks --repo ` canonicalizes and validates repository paths before execution, and setup write flows run explicit directory write-permission probes before staging/swap operations. -The config service now provides deterministic runtime config resolution with explicit precedence (`flags > env > config file > defaults`), strict config-file validation (`$schema`, `log_level`, `log_format`, `log_file`, `log_file_mode`, `timeout_ms`, `workos_client_id`, and nested `policies.bash`, `policies.attribution_hooks.enabled`, `policies.agent_trace.git_notes_ref` with default `refs/notes/sce-agent-trace`, plus `policies.database_retry` with per-DB `connection_open`/`query` retry policy specs), deterministic default discovery/merge of global+local config files (`${config_root}/sce/config.json` then `.sce/config.json` with local override, where `config_root` comes from the shared default-path seam with XDG/`dirs::config_dir()` config-root resolution), defaults for the resolved observability value set (`log_level=error`, `log_format=text`, `log_file_mode=truncate`), shared auth-key resolution with optional baked defaults starting at `workos_client_id`, first-class bash-policy preset/custom parsing with deterministic conflict and duplicate-prefix validation, and a canonical Pkl-authored `sce/config.json` JSON Schema generated to `config/schema/sce-config.schema.json` and embedded by `cli/src/services/config/mod.rs` for both `sce config validate` and doctor-time config checks. Runtime startup config loading now keeps parity with that schema by accepting the canonical `"$schema": "https://sce.crocoder.dev/config.json"` declaration in repo-local and global config files, so startup commands such as `sce version` no longer fail before dispatch on that field. App-runtime observability now consumes flat logging keys through the shared resolver, so env values still override config-file values while config files provide deterministic fallback for file logging; `sce config show` reports resolved observability/auth/policy values with provenance, while `sce config validate` is now a trimmed validation surface that reports only pass/fail plus validation errors or warnings in text and JSON modes. The canonical preset catalog and matching contract live in `config/pkl/data/bash-policy-presets.json` and `context/sce/bash-tool-policy-enforcement-contract.md`. +The config service now provides deterministic runtime config resolution with explicit precedence (`flags > env > config file > defaults`), strict config-file validation (`$schema`, `log_level`, `log_format`, `log_file`, `log_file_mode`, `timeout_ms`, `workos_client_id`, and nested `policies.bash`, `policies.attribution_hooks.enabled`, `policies.agent_trace.git_notes_ref` with default `refs/notes/sce-agent-trace`, `policies.agent_trace.push_notes.enabled` with default `true`, plus `policies.database_retry` with per-DB `connection_open`/`query` retry policy specs), deterministic default discovery/merge of global+local config files (`${config_root}/sce/config.json` then `.sce/config.json` with local override, where `config_root` comes from the shared default-path seam with XDG/`dirs::config_dir()` config-root resolution), defaults for the resolved observability value set (`log_level=error`, `log_format=text`, `log_file_mode=truncate`), shared auth-key resolution with optional baked defaults starting at `workos_client_id`, first-class bash-policy preset/custom parsing with deterministic conflict and duplicate-prefix validation, and a canonical Pkl-authored `sce/config.json` JSON Schema generated to `config/schema/sce-config.schema.json` and embedded by `cli/src/services/config/mod.rs` for both `sce config validate` and doctor-time config checks. Runtime startup config loading now keeps parity with that schema by accepting the canonical `"$schema": "https://sce.crocoder.dev/config.json"` declaration in repo-local and global config files, so startup commands such as `sce version` no longer fail before dispatch on that field. App-runtime observability now consumes flat logging keys through the shared resolver, so env values still override config-file values while config files provide deterministic fallback for file logging; `sce config show` reports resolved observability/auth/policy values with provenance, while `sce config validate` is now a trimmed validation surface that reports only pass/fail plus validation errors or warnings in text and JSON modes. The canonical preset catalog and matching contract live in `config/pkl/data/bash-policy-presets.json` and `context/sce/bash-tool-policy-enforcement-contract.md`. Invalid default-discovered config files now also degrade gracefully at startup: `sce` keeps running with degraded observability defaults, logs `sce.config.invalid_config` warnings, and reserves hard failures for explicit `--config` / `SCE_CONFIG_FILE` targets or other truly invalid runtime observability inputs. `cli/src/services/config/mod.rs` is now a module facade that declares focused config submodules (`types`, `schema`, `policy`, `resolver`, private `render`, `command`, and `lifecycle`), re-exporting `pub use types::*` and `pub(crate) use schema::validate_config_file`. Shared config primitive ownership is delegated to `cli/src/services/config/types.rs`; schema loading and file parsing to `cli/src/services/config/schema.rs`; bash-policy semantic validation and policy-specific formatting to `cli/src/services/config/policy.rs`; runtime discovery/precedence to `cli/src/services/config/resolver.rs`; and `sce config show` / `sce config validate` text+JSON output construction to `cli/src/services/config/render.rs`. Downstream modules continue importing through `services::config` unchanged. The CLI now has a generic borrowed `AppContext` dependency view in `cli/src/app.rs`; `AppRuntime` owns concrete production logger/telemetry/fs/git dependencies, and command execution receives context views that borrow those dependencies plus an optional `repo_root: Option`. `AppContext::with_repo_root(...)` / `ContextWithRepoRoot` derives repo-root-scoped views while preserving the borrowed runtime dependencies, and command execution is generic over associated-type narrow accessor traits where practical. The broad capability seam lives in `cli/src/services/capabilities.rs`, where `FsOps`/`StdFsOps` wrap filesystem operations and `GitOps`/`ProcessGitOps` wrap git process execution plus repository-root/hooks-directory resolution. The shared default path service in `cli/src/services/default_paths.rs` is now the canonical owner for production CLI path definitions. It resolves per-user config/state/cache roots through a dedicated internal `roots` seam, exposes the current persisted-artifact inventory (global config and auth tokens), and also defines named DB paths (auth DB, local DB, Agent Trace DB) plus the repo-relative, embedded-asset, install, hook, and context-path accessors consumed across current CLI production code. Non-test production modules should consume this shared catalog instead of hardcoding owned path literals. No default cache-backed persisted artifact currently exists, so cache-root resolution remains available without speculative cache-path features and no legacy default-path fallback is supported. diff --git a/context/plans/agent-trace-git-notes-auto-push.md b/context/plans/agent-trace-git-notes-auto-push.md new file mode 100644 index 00000000..bee29cd4 --- /dev/null +++ b/context/plans/agent-trace-git-notes-auto-push.md @@ -0,0 +1,84 @@ +# Plan: Auto-push Agent Trace git notes after SCE post-commit + +## Change summary + +Extend the existing `sce hooks post-commit` Agent Trace git-notes flow so SCE also attempts to push the configured Agent Trace notes ref to the commit remote after successfully writing the local git note. The default behavior is automatic push to the existing post-commit remote (`origin` as passed by the setup-installed hook template), with an optional config switch to disable the push. + +The push is best-effort and silent on failure: if the push cannot complete, the hook must not fail and SCE should simply try again on a later post-commit hook invocation. + +## Success criteria + +- After a successful git `sce hooks post-commit --vcs git --remote-url ` Agent Trace run, SCE writes the local git note as today and then attempts to push the configured notes ref to the target remote. +- The default notes ref remains `refs/notes/sce-agent-trace` unless overridden by `policies.agent_trace.git_notes_ref`. +- Notes auto-push is enabled by default and can be disabled through SCE config. +- Push failures are fail-open and silent from the user's perspective: they do not fail the hook, do not block commits, and do not require immediate action. +- A later successful post-commit hook invocation can attempt the push again without requiring a retry queue or persisted failed-push state. +- Tests cover default enabled behavior, config-disabled behavior, configured notes ref behavior, command construction, and silent fail-open push errors. +- Current-state context documents the new default auto-push behavior, disable switch, and fail-open/no-retry-queue posture. + +## Constraints and non-goals + +- Constraints: + - Preserve the existing post-commit local git-note write behavior and its best-effort posture. + - Push only after Agent Trace JSON validation, Agent Trace DB persistence, and local git-note write have completed successfully enough to have a local note to publish. + - Reuse existing hook config resolution and git command execution patterns; avoid shell interpolation. + - Keep stdout/stderr output stable unless existing logging conventions require a debug-level internal event. + - Honor the configured Agent Trace notes ref consistently for local note write and remote push. + - Use the post-commit remote context already available to the hook flow rather than inventing a separate remote-discovery path in this plan. +- Non-goals: + - Fetching notes from remotes. + - Backfilling or pushing historical notes outside normal post-commit hook flow. + - Adding a retry queue, background daemon, scheduled sync, or persisted failed-push state. + - Introducing a user-facing `sce sync` command. + - Changing Agent Trace JSON schema or Agent Trace DB schema. + - Blocking commits or surfacing push failures as hook failures. + +## Assumptions + +- The disable switch should live under the existing config namespace, e.g. `policies.agent_trace.push_notes.enabled`, with default `true`; exact naming may follow existing config style during implementation. +- The remote push target should be the remote used by the setup-installed hook template (`origin` in current behavior) or an equivalent validated remote derived from the existing post-commit handoff; this plan should not add arbitrary remote selection UX. +- Silent fail means no user-facing failure and no new stdout/stderr warning. Internal debug/error logging may still be acceptable if it follows existing observability conventions and does not disturb normal hook UX. + +## Task stack + +- [x] T01: `Add config switch for Agent Trace notes auto-push` (status:done) + - Task ID: T01 + - Completed: 2026-07-15 + - Files changed: `config/pkl/base/sce-config-schema.pkl`, `config/schema/sce-config.schema.json`, `cli/assets/generated/config/schema/sce-config.schema.json`, `cli/src/services/config/{types,schema,resolver,render}.rs`, `context/architecture.md`, `context/overview.md`, `context/glossary.md`, `context/context-map.md`, `context/cli/config-precedence-contract.md` + - Evidence: `nix develop -c pkl eval -m . config/pkl/generate.pkl`; `nix run .#pkl-check-generated`; `nix flake check --print-build-logs` passed (144 Rust tests, clippy/fmt/parity checks clean). + - Goal: Add a typed config value that controls whether post-commit Agent Trace git notes are auto-pushed, defaulting to enabled. + - Boundaries (in/out of scope): In - Pkl schema/config schema updates, Rust config DTO/resolver mapping, default `true`, explicit `false` override, `sce config show|validate` visibility if required by existing policy rendering. Out - git push execution, hook runtime wiring, remote selection behavior. + - Done when: runtime config exposes a resolved auto-push boolean; default resolution is enabled; explicit config disable is honored; generated schema/parity covers the new field; invalid config shapes fail validation consistently with existing config policy fields. + - Verification notes (commands or checks): `nix develop -c pkl eval -m . config/pkl/generate.pkl`; targeted config tests if appropriate; `nix run .#pkl-check-generated`; `nix flake check`. + +- [ ] T02: `Introduce git-notes push helper` (status:todo) + - Task ID: T02 + - Goal: Add a small, injectable helper that attempts to push the configured Agent Trace notes ref to the chosen git remote without shell interpolation. + - Boundaries (in/out of scope): In - helper function/type near existing git-note writer logic, command construction for pushing one notes ref, validation of non-blank remote/ref inputs, tests for command args and failure outcome. Out - deciding when to call the helper, config resolution, backfill/fetch/retry behavior. + - Done when: helper constructs a deterministic `git push `-equivalent invocation for the configured notes ref, returns a structured success/failure outcome, does not emit user-facing output directly, and focused tests cover success and git-command failure. + - Verification notes (commands or checks): targeted hook/helper tests if appropriate; `nix develop -c sh -c 'cd cli && cargo fmt'`; `nix flake check`. + +- [ ] T03: `Wire silent auto-push into post-commit Agent Trace flow` (status:todo) + - Task ID: T03 + - Goal: After successful local Agent Trace git-note persistence, conditionally attempt a best-effort notes push when auto-push config is enabled. + - Boundaries (in/out of scope): In - post-commit flow ordering, config gate, git-only behavior, existing remote context reuse, fail-open/silent handling, tests proving enabled/default attempt, disabled skip, configured ref use, and push failure does not change hook success. Out - retry queue, user-facing command output changes, fetch/backfill, non-git VCS note pushing. + - Done when: default git post-commit flow attempts the push after local note write; explicit config disable skips the push; configured notes ref is used; push failure is swallowed from hook success/output and can be retried by a later hook invocation. + - Verification notes (commands or checks): targeted post-commit hook tests if appropriate; manual local dry-run/review of command construction; `nix flake check`. + +- [ ] T04: `Document Agent Trace notes auto-push behavior` (status:todo) + - Task ID: T04 + - Goal: Sync current-state context to describe default auto-push, disable config, and silent fail-open retry-on-next-commit behavior. + - Boundaries (in/out of scope): In - focused updates to `context/sce/agent-trace-hooks-command-routing.md`, `context/cli/config-precedence-contract.md`, `context/sce/setup-githooks-hook-asset-packaging.md` if hook behavior text needs adjustment, `context/context-map.md`, and glossary entry if a new term is introduced. Out - broad narrative docs rewrites, completed-work summaries in durable context, implementation code. + - Done when: context no longer states “No git-notes push/fetch/backfill behavior” as current behavior without qualification; documents that push is default-enabled, config-disableable, silent fail-open, and retried only by future post-commit invocations. + - Verification notes (commands or checks): `rg "git-notes|git notes|push_notes|push notes|No git-notes" context/`; manual diff review; `git diff --check`. + +- [ ] T05: `Validate notes auto-push and cleanup` (status:todo) + - Task ID: T05 + - Goal: Run final validation for the complete plan and clean up temporary scaffolding. + - Boundaries (in/out of scope): In - full repo validation, generated-output parity, formatting/lint/test checks, stale-string review, cleanup of temporary repos/remotes/notes refs used during testing, plan status/evidence updates. Out - new behavior beyond completed task stack. + - Done when: `nix flake check` passes or any failure is documented as pre-existing/unrelated; `nix run .#pkl-check-generated` passes; context sync is verified; no temporary scaffolding remains. + - Verification notes (commands or checks): `nix flake check`; `nix run .#pkl-check-generated`; `git diff --check`; `rg "refs/notes/sce-agent-trace|push_notes|git notes.*push|No git-notes" cli/ config/ context/`. + +## Open questions + +None. Plan is ready for T01 execution. From 85f36ebae9adb5f630d17f5cfee269c96a2e4857 Mon Sep 17 00:00:00 2001 From: David Abram Date: Wed, 15 Jul 2026 11:48:27 +0200 Subject: [PATCH 6/8] hooks: Add Agent Trace git notes push helper Adds an injectable helper for pushing the configured Agent Trace notes ref with validation, structured outcome data, and focused tests around command construction and failure context. Plan: agent-trace-git-notes-auto-push Task: T02 Co-authored-by: SCE --- cli/src/services/hooks/mod.rs | 154 ++++++++++++++++++ .../plans/agent-trace-git-notes-auto-push.md | 5 +- 2 files changed, 158 insertions(+), 1 deletion(-) diff --git a/cli/src/services/hooks/mod.rs b/cli/src/services/hooks/mod.rs index d0b472e7..b9fc0827 100644 --- a/cli/src/services/hooks/mod.rs +++ b/cli/src/services/hooks/mod.rs @@ -1652,6 +1652,12 @@ pub struct GitNoteWriteOutcome { pub notes_ref: String, } +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct GitNotesPushOutcome { + pub remote: String, + pub notes_ref: String, +} + #[allow(dead_code)] fn write_agent_trace_git_note( repository_root: &Path, @@ -1705,6 +1711,41 @@ where }) } +#[allow(dead_code)] +fn push_agent_trace_git_notes_ref( + repository_root: &Path, + remote: &str, + notes_ref: &str, +) -> Result { + push_agent_trace_git_notes_ref_with( + repository_root, + remote, + notes_ref, + run_git_notes_push_command, + ) +} + +#[allow(dead_code)] +fn push_agent_trace_git_notes_ref_with( + repository_root: &Path, + remote: &str, + notes_ref: &str, + run_git_push: F, +) -> Result +where + F: FnOnce(&Path, &[String]) -> Result<()>, +{ + let remote = non_empty_git_note_input("git remote", remote)?; + let notes_ref = non_empty_git_note_input("git notes ref", notes_ref)?; + let args = vec![String::from("push"), remote.clone(), notes_ref.clone()]; + + run_git_push(repository_root, &args).with_context(|| { + format!("Failed to push Agent Trace git notes ref '{notes_ref}' to remote '{remote}'.") + })?; + + Ok(GitNotesPushOutcome { remote, notes_ref }) +} + fn non_empty_git_note_input(label: &str, value: &str) -> Result { let trimmed = value.trim(); if trimmed.is_empty() { @@ -1722,6 +1763,34 @@ fn non_empty_git_note_content(trace_json: &str) -> Result { Ok(trace_json.to_string()) } +fn run_git_notes_push_command(repository_root: &Path, args: &[String]) -> Result<()> { + let output = Command::new("git") + .args(args) + .current_dir(repository_root) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::piped()) + .output() + .with_context(|| { + format!( + "Failed to spawn git notes push command in directory '{}'.", + repository_root.display() + ) + })?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string(); + let diagnostic = if stderr.is_empty() { + String::from("git notes push command exited with a non-zero status") + } else { + stderr + }; + bail!("Failed to push Agent Trace git notes: {diagnostic}"); + } + + Ok(()) +} + fn run_git_notes_add_command( repository_root: &Path, args: &[String], @@ -2900,6 +2969,91 @@ mod tests { ); } + #[test] + fn git_notes_push_helper_builds_push_command() { + let captured_args = RefCell::new(Vec::::new()); + + let outcome = push_agent_trace_git_notes_ref_with( + Path::new("/repo"), + "origin", + "refs/notes/sce-agent-trace", + |root, args| { + assert_eq!(root, Path::new("/repo")); + *captured_args.borrow_mut() = args.to_vec(); + Ok(()) + }, + ) + .expect("git-notes push helper should succeed"); + + assert_eq!(outcome.remote, "origin"); + assert_eq!(outcome.notes_ref, "refs/notes/sce-agent-trace"); + assert_eq!( + captured_args.into_inner(), + vec!["push", "origin", "refs/notes/sce-agent-trace"] + ); + } + + #[test] + fn git_notes_push_helper_honors_configured_ref() { + let captured_args = RefCell::new(Vec::::new()); + + push_agent_trace_git_notes_ref_with( + Path::new("/repo"), + "origin", + "refs/notes/custom-agent-trace", + |_, args| { + *captured_args.borrow_mut() = args.to_vec(); + Ok(()) + }, + ) + .expect("git-notes push helper should succeed"); + + assert_eq!( + captured_args.into_inner(), + vec!["push", "origin", "refs/notes/custom-agent-trace"] + ); + } + + #[test] + fn git_notes_push_helper_rejects_blank_inputs() { + let error = push_agent_trace_git_notes_ref_with( + Path::new("/repo"), + " ", + "refs/notes/sce-agent-trace", + |_, _| panic!("runner should not be called for invalid input"), + ) + .expect_err("blank remote should fail"); + assert!(error + .to_string() + .contains("Invalid Agent Trace git-note git remote")); + + let error = + push_agent_trace_git_notes_ref_with(Path::new("/repo"), "origin", "\t", |_, _| { + panic!("runner should not be called for invalid input") + }) + .expect_err("blank ref should fail"); + assert!(error + .to_string() + .contains("Invalid Agent Trace git-note git notes ref")); + } + + #[test] + fn git_notes_push_helper_returns_command_failure_with_context() { + let error = push_agent_trace_git_notes_ref_with( + Path::new("/repo"), + "origin", + "refs/notes/sce-agent-trace", + |_, _| bail!("simulated git push failure"), + ) + .expect_err("git push failure should fail helper"); + + let rendered = format!("{error:#}"); + assert!(rendered.contains( + "Failed to push Agent Trace git notes ref 'refs/notes/sce-agent-trace' to remote 'origin'." + )); + assert!(rendered.contains("simulated git push failure")); + } + #[test] fn git_note_writer_honors_configured_ref() { let captured_args = RefCell::new(Vec::::new()); diff --git a/context/plans/agent-trace-git-notes-auto-push.md b/context/plans/agent-trace-git-notes-auto-push.md index bee29cd4..e90ecfc1 100644 --- a/context/plans/agent-trace-git-notes-auto-push.md +++ b/context/plans/agent-trace-git-notes-auto-push.md @@ -51,8 +51,11 @@ The push is best-effort and silent on failure: if the push cannot complete, the - Done when: runtime config exposes a resolved auto-push boolean; default resolution is enabled; explicit config disable is honored; generated schema/parity covers the new field; invalid config shapes fail validation consistently with existing config policy fields. - Verification notes (commands or checks): `nix develop -c pkl eval -m . config/pkl/generate.pkl`; targeted config tests if appropriate; `nix run .#pkl-check-generated`; `nix flake check`. -- [ ] T02: `Introduce git-notes push helper` (status:todo) +- [x] T02: `Introduce git-notes push helper` (status:done) - Task ID: T02 + - Completed: 2026-07-15 + - Files changed: `cli/src/services/hooks/mod.rs` + - Evidence: Direct targeted `cargo test` was blocked by repository bash policy; `nix develop -c sh -c 'cd cli && cargo fmt'`; `nix flake check --print-build-logs` passed (148 Rust tests, clippy/fmt/parity checks clean). - Goal: Add a small, injectable helper that attempts to push the configured Agent Trace notes ref to the chosen git remote without shell interpolation. - Boundaries (in/out of scope): In - helper function/type near existing git-note writer logic, command construction for pushing one notes ref, validation of non-blank remote/ref inputs, tests for command args and failure outcome. Out - deciding when to call the helper, config resolution, backfill/fetch/retry behavior. - Done when: helper constructs a deterministic `git push `-equivalent invocation for the configured notes ref, returns a structured success/failure outcome, does not emit user-facing output directly, and focused tests cover success and git-command failure. From 7e744ed6103dae2dfb8bd993df3f1d7ed9c41e70 Mon Sep 17 00:00:00 2001 From: David Abram Date: Wed, 15 Jul 2026 11:59:05 +0200 Subject: [PATCH 7/8] hooks: Push Agent Trace git notes after local write Wire post-commit Agent Trace persistence to attempt a silent best-effort push of the configured notes ref after local git-note writes succeed, while honoring the push-notes opt-out and preserving fail-open behavior on push errors. Plan: agent-trace-git-notes-auto-push Task: T03 Co-authored-by: SCE --- cli/src/services/hooks/mod.rs | 145 ++++++++++++++++-- context/cli/config-precedence-contract.md | 4 +- context/context-map.md | 2 +- context/glossary.md | 2 +- context/overview.md | 2 +- .../plans/agent-trace-git-notes-auto-push.md | 5 +- .../sce/agent-trace-hooks-command-routing.md | 5 +- .../setup-githooks-hook-asset-packaging.md | 2 +- 8 files changed, 149 insertions(+), 18 deletions(-) diff --git a/cli/src/services/hooks/mod.rs b/cli/src/services/hooks/mod.rs index b9fc0827..9fd92e62 100644 --- a/cli/src/services/hooks/mod.rs +++ b/cli/src/services/hooks/mod.rs @@ -1281,6 +1281,7 @@ fn run_post_commit_agent_trace_flow( repository_root, vcs_type, git_notes_ref: &hook_config.agent_trace_git_notes_ref, + push_notes_enabled: hook_config.agent_trace_push_notes_enabled, logger, }; @@ -1302,6 +1303,7 @@ fn run_post_commit_agent_trace_flow( Ok(()) }, write_agent_trace_git_note, + push_agent_trace_git_notes_ref, ) } @@ -1316,21 +1318,24 @@ struct PostCommitGitNotePersistence<'a> { repository_root: &'a Path, vcs_type: Option, git_notes_ref: &'a str, + push_notes_enabled: bool, logger: Option<&'a dyn Logger>, } -fn run_post_commit_agent_trace_flow_with( +fn run_post_commit_agent_trace_flow_with( git_note_persistence: &PostCommitGitNotePersistence<'_>, flow_result: &PostCommitIntersectionFlowResult, remote_url: &str, validate_agent_trace: V, persist_agent_trace: I, write_git_note: G, + push_git_notes: P, ) -> Result where V: FnOnce(&Value) -> Result<()>, I: for<'a> FnOnce(AgentTraceInsert<'a>) -> Result<()>, G: FnOnce(&Path, &str, &str, &str) -> Result, + P: FnOnce(&Path, &str, &str) -> Result, { let commit_timestamp = DateTime::::from_timestamp_millis(flow_result.post_commit_data.commit_time_ms) @@ -1379,18 +1384,28 @@ where persist_agent_trace(insert_input)?; if should_write_agent_trace_git_note(git_note_persistence.vcs_type) { - if let Err(error) = write_git_note( + match write_git_note( git_note_persistence.repository_root, git_note_persistence.git_notes_ref, &flow_result.post_commit_data.commit_oid, &serialized, ) { - log_agent_trace_git_note_write_failure( - git_note_persistence.logger, - &flow_result.post_commit_data.commit_oid, - git_note_persistence.git_notes_ref, - &error, - ); + Ok(_) if git_note_persistence.push_notes_enabled => { + let _ = push_git_notes( + git_note_persistence.repository_root, + remote_url, + git_note_persistence.git_notes_ref, + ); + } + Ok(_) => {} + Err(error) => { + log_agent_trace_git_note_write_failure( + git_note_persistence.logger, + &flow_result.post_commit_data.commit_oid, + git_note_persistence.git_notes_ref, + &error, + ); + } } } @@ -2392,6 +2407,7 @@ mod tests { repository_root: Path::new("/repo"), vcs_type: Some(AgentTraceVcsType::Git), git_notes_ref: "refs/notes/sce-agent-trace", + push_notes_enabled: true, logger: None, }; let result = run_post_commit_agent_trace_flow_with( @@ -2418,10 +2434,18 @@ mod tests { notes_ref: notes_ref.to_string(), }) }, + |root, remote, notes_ref| { + order.borrow_mut().push(String::from("push")); + assert_eq!(root, Path::new("/repo")); + Ok(GitNotesPushOutcome { + remote: remote.to_string(), + notes_ref: notes_ref.to_string(), + }) + }, ) .expect("Agent Trace flow should persist DB row and git note"); - assert_eq!(order.into_inner(), vec!["db", "note"]); + assert_eq!(order.into_inner(), vec!["db", "note", "push"]); let (notes_ref, commit_id, trace_json) = captured_note .into_inner() .expect("git note write should be attempted"); @@ -2439,6 +2463,7 @@ mod tests { repository_root: Path::new("/repo"), vcs_type: Some(AgentTraceVcsType::Git), git_notes_ref: "refs/notes/custom-sce", + push_notes_enabled: true, logger: None, }; run_post_commit_agent_trace_flow_with( @@ -2457,12 +2482,106 @@ mod tests { .expect("git-note content should be full Agent Trace JSON"), }) }, + |_, remote, notes_ref| { + assert_eq!(remote, "https://example.test/repo.git"); + assert_eq!(notes_ref, "refs/notes/custom-sce"); + Ok(GitNotesPushOutcome { + remote: remote.to_string(), + notes_ref: notes_ref.to_string(), + }) + }, ) .expect("Agent Trace flow should succeed with configured git-notes ref"); assert_eq!(captured_ref.into_inner(), "refs/notes/custom-sce"); } + #[test] + fn post_commit_agent_trace_flow_skips_push_when_config_disabled() { + let order = RefCell::new(Vec::::new()); + + let git_note_persistence = PostCommitGitNotePersistence { + repository_root: Path::new("/repo"), + vcs_type: Some(AgentTraceVcsType::Git), + git_notes_ref: "refs/notes/sce-agent-trace", + push_notes_enabled: false, + logger: None, + }; + run_post_commit_agent_trace_flow_with( + &git_note_persistence, + &post_commit_flow_result(), + "https://example.test/repo.git", + |_| Ok(()), + |_| { + order.borrow_mut().push(String::from("db")); + Ok(()) + }, + |_, notes_ref, commit_id, _| { + order.borrow_mut().push(String::from("note")); + Ok(GitNoteWriteOutcome { + commit_id: commit_id.to_string(), + notes_ref: notes_ref.to_string(), + }) + }, + |_, _, _| { + order.borrow_mut().push(String::from("push")); + Ok(GitNotesPushOutcome { + remote: String::from("https://example.test/repo.git"), + notes_ref: String::from("refs/notes/sce-agent-trace"), + }) + }, + ) + .expect("Agent Trace flow should succeed with notes push disabled"); + + assert_eq!(order.into_inner(), vec!["db", "note"]); + } + + #[test] + fn post_commit_agent_trace_flow_swallows_git_notes_push_failure() { + let logger = CapturingLogger::default(); + let order = RefCell::new(Vec::::new()); + + let git_note_persistence = PostCommitGitNotePersistence { + repository_root: Path::new("/repo"), + vcs_type: Some(AgentTraceVcsType::Git), + git_notes_ref: "refs/notes/sce-agent-trace", + push_notes_enabled: true, + logger: Some(&logger), + }; + let result = run_post_commit_agent_trace_flow_with( + &git_note_persistence, + &post_commit_flow_result(), + "https://example.test/repo.git", + |_| Ok(()), + |_| { + order.borrow_mut().push(String::from("db")); + Ok(()) + }, + |_, notes_ref, commit_id, _| { + order.borrow_mut().push(String::from("note")); + Ok(GitNoteWriteOutcome { + commit_id: commit_id.to_string(), + notes_ref: notes_ref.to_string(), + }) + }, + |_, remote, notes_ref| { + order.borrow_mut().push(String::from("push")); + assert_eq!(remote, "https://example.test/repo.git"); + assert_eq!(notes_ref, "refs/notes/sce-agent-trace"); + bail!("simulated git push failure") + }, + ) + .expect("git-notes push failure should not fail post-commit Agent Trace flow"); + + assert_eq!(order.into_inner(), vec!["db", "note", "push"]); + assert!(result.trace_json.contains("\"version\": \"0.1.0\"")); + assert!(logger + .errors + .lock() + .expect("test logger mutex should not be poisoned") + .is_empty()); + } + #[test] fn post_commit_agent_trace_flow_logs_git_note_failure_without_failing() { let logger = CapturingLogger::default(); @@ -2472,6 +2591,7 @@ mod tests { repository_root: Path::new("/repo"), vcs_type: Some(AgentTraceVcsType::Git), git_notes_ref: "refs/notes/sce-agent-trace", + push_notes_enabled: true, logger: Some(&logger), }; let result = run_post_commit_agent_trace_flow_with( @@ -2487,6 +2607,13 @@ mod tests { order.borrow_mut().push(String::from("note")); bail!("simulated git notes failure") }, + |_, _, _| { + order.borrow_mut().push(String::from("push")); + Ok(GitNotesPushOutcome { + remote: String::from("https://example.test/repo.git"), + notes_ref: String::from("refs/notes/sce-agent-trace"), + }) + }, ) .expect("git-note failure should not fail post-commit Agent Trace flow"); diff --git a/context/cli/config-precedence-contract.md b/context/cli/config-precedence-contract.md index 5216ef25..bcb57dfb 100644 --- a/context/cli/config-precedence-contract.md +++ b/context/cli/config-precedence-contract.md @@ -25,7 +25,7 @@ Resolved runtime values follow this deterministic order: Repo-configured bash-tool policy values are config-file only in this task slice: they load from `policies.bash` in the selected config files, merge `global -> local` alongside the rest of the config object, and currently have no flag or environment override layer. -Agent Trace hook policy currently includes `policies.agent_trace.git_notes_ref`, which resolves as config-file value over default `refs/notes/sce-agent-trace`, and `policies.agent_trace.push_notes.enabled`, which resolves as config-file value over default `true`. These keys have no flag or environment override layer in the current implementation slice. The resolved notes ref is consumed by the post-commit Agent Trace flow when writing the validated full Agent Trace JSON to git notes after Agent Trace DB persistence succeeds. The resolved push-notes boolean is exposed through hook runtime config for the follow-on auto-push wiring task; current post-commit hook behavior does not push git notes yet. +Agent Trace hook policy currently includes `policies.agent_trace.git_notes_ref`, which resolves as config-file value over default `refs/notes/sce-agent-trace`, and `policies.agent_trace.push_notes.enabled`, which resolves as config-file value over default `true`. These keys have no flag or environment override layer in the current implementation slice. The resolved notes ref is consumed by the post-commit Agent Trace flow when writing the validated full Agent Trace JSON to git notes after Agent Trace DB persistence succeeds and when pushing that same notes ref. The resolved push-notes boolean gates the post-commit best-effort `git push ` attempt; explicit `false` skips the push. Resolved observability values that currently have no CLI flag layer follow the same lower-precedence chain without a flag step: @@ -81,7 +81,7 @@ When a default-discovered global or repo-local config file exists but fails JSON - `policies` must be an object when present and currently allows `attribution_hooks`, `agent_trace`, `database_retry`, and `bash`. - `policies.attribution_hooks` must be an object when present and currently allows `enabled`; the generated schema documents default `true`, and explicit `enabled: false` remains a valid opt-out alongside the runtime `SCE_ATTRIBUTION_HOOKS_DISABLED` environment opt-out. - `policies.agent_trace` must be an object when present and currently allows `git_notes_ref` and `push_notes`; the generated schema documents default `refs/notes/sce-agent-trace`, and Rust mapping rejects blank/whitespace-only refs. -- `policies.agent_trace.push_notes` must be an object when present and currently allows `enabled`; the generated schema documents default `true`, and explicit `enabled: false` is a valid opt-out value for the follow-on Agent Trace notes auto-push wiring. +- `policies.agent_trace.push_notes` must be an object when present and currently allows `enabled`; the generated schema documents default `true`, and explicit `enabled: false` opts out of post-commit Agent Trace notes auto-push. - `policies.bash` must be an object when present and currently allows only `presets` and `custom`. - `policies.bash.presets` must be an array of unique built-in preset IDs: `forbid-git-all`, `forbid-git-commit`, `use-pnpm-over-npm`, `use-bun-over-npm`, `use-nix-flake-over-cargo`. - `use-pnpm-over-npm` and `use-bun-over-npm` are mutually exclusive and fail validation when both are present. diff --git a/context/context-map.md b/context/context-map.md index 0d41db10..163a9586 100644 --- a/context/context-map.md +++ b/context/context-map.md @@ -52,7 +52,7 @@ Feature/domain context: - `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`) - `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: enabled-by-default commit-msg attribution with explicit opt-out controls, no-op `pre-commit`/`post-rewrite` entrypoints, active `post-commit` intersection plus Agent Trace DB persistence and best-effort git-note JSON persistence, DB-only `diff-trace` STDIN intake with OpenCode normalized payloads and Claude structured `PostToolUse` payload classification, tool-prefixed stored `diff_traces.session_id` values (`oc_` for OpenCode, `cc_` for Claude, `pi_` for Pi), direct nullable `model_id`/`tool_version` persistence without session fallback, Claude direct model metadata extraction from top-level or nested `model` fields with `claude/` prefix normalization, no parsed-payload artifact persistence under `context/tmp`, `session-model` removed from the supported hook surface, and `conversation-trace` STDIN intake for normalized batches plus supported raw Claude events; this document also owns the current `diff-trace` and `conversation-trace` fail-open intake contracts.) +- `context/sce/agent-trace-hooks-command-routing.md` (implemented `sce hooks` command routing plus current runtime behavior: enabled-by-default commit-msg attribution with explicit opt-out controls, no-op `pre-commit`/`post-rewrite` entrypoints, active `post-commit` intersection plus Agent Trace DB persistence, best-effort git-note JSON persistence, and default-enabled silent fail-open git-notes push controlled by `policies.agent_trace.push_notes.enabled`, DB-only `diff-trace` STDIN intake with OpenCode normalized payloads and Claude structured `PostToolUse` payload classification, tool-prefixed stored `diff_traces.session_id` values (`oc_` for OpenCode, `cc_` for Claude, `pi_` for Pi), direct nullable `model_id`/`tool_version` persistence without session fallback, Claude direct model metadata extraction from top-level or nested `model` fields with `claude/` prefix normalization, no parsed-payload artifact persistence under `context/tmp`, `session-model` removed from the supported hook surface, and `conversation-trace` STDIN intake for normalized batches plus supported raw Claude events; this document also owns the current `diff-trace` and `conversation-trace` fail-open intake contracts.) - `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) - `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 through the missing-CLI install-guidance helper) diff --git a/context/glossary.md b/context/glossary.md index 38468ed6..4471389b 100644 --- a/context/glossary.md +++ b/context/glossary.md @@ -113,7 +113,7 @@ - `bash tool policy config surface`: Nested repo config namespace under `.sce/config.json` at `policies.bash`, currently supporting unique built-in `presets` plus repo-owned `custom` argv-prefix rules with deterministic validation, merged global/local resolution, and first-class `sce config show|validate` reporting. - `attribution hooks gate`: Enabled-by-default local hook runtime gate resolved through shared config precedence in `cli/src/services/config/mod.rs` (with parsing in `schema.rs`): opt-out env `SCE_ATTRIBUTION_HOOKS_DISABLED` overrides repo/global config key `policies.attribution_hooks.enabled` with inverted semantics, and the current enabled path activates commit-msg-only attribution gated by the staged-diff AI-overlap preflight. - `Agent Trace git-notes ref`: Configurable Agent Trace hook policy value at `policies.agent_trace.git_notes_ref`; defaults to `refs/notes/sce-agent-trace`, rejects blank/whitespace-only refs during Rust config mapping, and is exposed through hook runtime config for post-commit git-note persistence wiring. -- `Agent Trace notes auto-push gate`: Configurable Agent Trace hook policy value at `policies.agent_trace.push_notes.enabled`; defaults to `true`, accepts explicit `false` as an opt-out, and is exposed through hook runtime config for the post-commit git-notes auto-push wiring slice. +- `Agent Trace notes auto-push gate`: Configurable Agent Trace hook policy value at `policies.agent_trace.push_notes.enabled`; defaults to `true`, accepts explicit `false` as an opt-out, and gates the post-commit best-effort `git push ` attempt after local Agent Trace git-note persistence succeeds. - `StagedDiffAiOverlapResult`: Three-valued enum in `cli/src/services/hooks/mod.rs` returned by the staged-diff AI-overlap evidence check: `Overlap` (staged diff overlaps with at least one recent AI/editor diff trace), `NoOverlap` (no overlap found; staged diff and recent traces were both available but share no touched lines, or staged patch has no touched lines), `Error` (DB open failure, schema not ready, query error, staged diff read failure, or clock failure). Both `NoOverlap` and `Error` map to `ai_contribution_present = false` at the commit-msg policy seam; `Error` additionally triggers `sce.hooks.commit_msg.ai_overlap_error` logging. - `sce.hooks.commit_msg.ai_overlap_error`: Logger event ID emitted by `staged_diff_has_ai_overlap` when the staged-diff AI-overlap preflight encounters an error (DB open failure, schema not ready, query error, staged diff read failure, or clock failure). - `bash policy preset catalog`: Canonical authored preset source at `config/pkl/base/bash-policy-presets.pkl`, rendered to JSON by `config/pkl/generate.pkl` and embedded by the CLI from `config/.opencode/lib/bash-policy-presets.json` so CLI validation and OpenCode enforcement share the same preset IDs, argv-prefix matchers, fixed messages, and conflict metadata. diff --git a/context/overview.md b/context/overview.md index 34d66993..30205066 100644 --- a/context/overview.md +++ b/context/overview.md @@ -59,7 +59,7 @@ Context sync now uses an important-change gate: cross-cutting/policy/architectur The `/change-to-plan` command body is also intentionally thin orchestration: it delegates clarification and plan-shape contracts to `sce-plan-authoring` (including one-task/one-atomic-commit task slicing) while keeping wrapper-level plan output and handoff obligations explicit. The generated OpenCode command doc now also emits `entry-skill: sce-plan-authoring` plus an ordered `skills` list. The targeted support commands (`handover`, `commit`, `validate`) keep their thin-wrapper behavior and now also emit machine-readable OpenCode command frontmatter describing their entry skill and ordered skill chain. `/commit` is now split by profile: manual generated commands remain proposal-only and allow split guidance when staged changes mix unrelated goals, while the automated OpenCode `/commit` command generates exactly one commit message and runs `git commit` against the staged diff. The shared `sce-atomic-commit` contract also requires commit bodies to cite affected plan slug(s) and updated task ID(s) when staged changes include `context/plans/*.md`, and to stop for clarification instead of inventing those references when the staged plan diff is ambiguous. 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, `SCE_DISABLED` is false, and the staged-diff AI-overlap preflight confirms AI/editor evidence (`StagedDiffAiOverlapResult::Overlap`); when the preflight returns `NoOverlap` or `Error` (including DB open failure, schema not ready, query error, staged diff read failure, or zero overlap), the trailer is not appended and errors are logged via `sce.hooks.commit_msg.ai_overlap_error`; `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`, 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`, and then writes the same full JSON best-effort to git notes under the configured Agent Trace notes ref (default `refs/notes/sce-agent-trace`) without creating a 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`, uses only direct payload `model_id` and `tool_version` (no longer resolves from `session_models`), and continues with `None` for missing attribution, with same-tool-idempotent stored `session_id` prefixing (`oc_` for OpenCode, `cc_` for Claude, `pi_` for Pi), non-lossy AgentTraceDb `time_ms` conversion, and no parsed-payload artifact fallback. Claude structured `PostToolUse` diff-trace intake extracts direct model metadata from top-level or nested `model` fields and normalizes it with the `claude/` prefix when present. +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, `SCE_DISABLED` is false, and the staged-diff AI-overlap preflight confirms AI/editor evidence (`StagedDiffAiOverlapResult::Overlap`); when the preflight returns `NoOverlap` or `Error` (including DB open failure, schema not ready, query error, staged diff read failure, or zero overlap), the trailer is not appended and errors are logged via `sce.hooks.commit_msg.ai_overlap_error`; `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`, 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`, then writes the same full JSON best-effort to git notes under the configured Agent Trace notes ref (default `refs/notes/sce-agent-trace`), and after a successful local note write attempts a silent fail-open `git push ` unless `policies.agent_trace.push_notes.enabled` is `false`, without creating a 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`, uses only direct payload `model_id` and `tool_version` (no longer resolves from `session_models`), and continues with `None` for missing attribution, with same-tool-idempotent stored `session_id` prefixing (`oc_` for OpenCode, `cc_` for Claude, `pi_` for Pi), non-lossy AgentTraceDb `time_ms` conversion, and no parsed-payload artifact fallback. Claude structured `PostToolUse` diff-trace intake extracts direct model metadata from top-level or nested `model` fields and normalizes it with the `claude/` prefix when present. 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 local or remote sync-mode opens, parent-directory creation, connection setup, synchronous query helpers, embedded migration execution, and shared DB lifecycle helpers. Auth DB persistence uses encrypted `AuthDb = EncryptedTursoDb` and token storage persists credentials through the `auth_credentials` table. Agent Trace persistence uses `AgentTraceDb = TursoDb` with legacy global plus active per-checkout DB paths, fresh-start migrations for `diff_traces`, `post_commit_patch_intersections`, `agent_traces`, `messages`, `parts`, indexes/triggers, and retired `015_create_session_models` metadata handling; active hook runtime writes direct nullable diff-trace attribution without a `session_models` API/table dependency. The hooks command surface now also supports concrete runtime subcommand routing (`pre-commit`, `commit-msg`, `post-commit`, `post-rewrite`, `diff-trace`, and `conversation-trace`) with deterministic argument/STDIN validation; `session-model` is no longer supported. Current runtime behavior keeps commit-msg attribution enabled by default unless explicitly opted out: the attribution gate enables canonical trailer insertion in `commit-msg` only when the staged-diff AI-overlap preflight confirms AI/editor evidence (no trailer is appended when the preflight finds no overlap or encounters any error); `pre-commit`/`post-rewrite` remain deterministic no-ops, `post-commit` requires validated `--remote-url`, threads that URL into the Agent Trace flow, prints it to stderr, and remains the active bounded recent-diff-trace intersection path, and `diff-trace` is the active intake path for parsed STDIN `{ sessionID, diff, time, model_id?, tool_name, tool_version }` payload persistence with optional `model_id`, required non-empty `tool_name`, required nullable/non-empty `tool_version`, direct `model_id` and `tool_version` values (no session-model fallback), required `u64` millisecond `time`, same-tool-idempotent stored `session_id` prefixing (`oc_` for OpenCode, `cc_` for Claude, `pi_` for Pi), non-lossy AgentTraceDb `time_ms` conversion, and no parsed-payload artifact fallback. This behavior is documented in `context/sce/agent-trace-hooks-command-routing.md`. The removed `sce hooks claude-capture` raw capture route is documented in `context/sce/claude-raw-hook-capture.md` as a removed feature. diff --git a/context/plans/agent-trace-git-notes-auto-push.md b/context/plans/agent-trace-git-notes-auto-push.md index e90ecfc1..ae9fe754 100644 --- a/context/plans/agent-trace-git-notes-auto-push.md +++ b/context/plans/agent-trace-git-notes-auto-push.md @@ -61,8 +61,11 @@ The push is best-effort and silent on failure: if the push cannot complete, the - Done when: helper constructs a deterministic `git push `-equivalent invocation for the configured notes ref, returns a structured success/failure outcome, does not emit user-facing output directly, and focused tests cover success and git-command failure. - Verification notes (commands or checks): targeted hook/helper tests if appropriate; `nix develop -c sh -c 'cd cli && cargo fmt'`; `nix flake check`. -- [ ] T03: `Wire silent auto-push into post-commit Agent Trace flow` (status:todo) +- [x] T03: `Wire silent auto-push into post-commit Agent Trace flow` (status:done) - Task ID: T03 + - Completed: 2026-07-15 + - Files changed: `cli/src/services/hooks/mod.rs`, `context/overview.md`, `context/glossary.md`, `context/context-map.md`, `context/cli/config-precedence-contract.md`, `context/sce/agent-trace-hooks-command-routing.md`, `context/sce/setup-githooks-hook-asset-packaging.md` + - Evidence: Direct targeted `cargo test post_commit_agent_trace_flow` was blocked by repository bash policy; `nix develop -c sh -c 'cd cli && cargo fmt'`; `nix flake check --print-build-logs` passed (150 Rust tests, clippy/fmt/parity checks clean); `git diff --check`; `nix run .#pkl-check-generated` passed. - Goal: After successful local Agent Trace git-note persistence, conditionally attempt a best-effort notes push when auto-push config is enabled. - Boundaries (in/out of scope): In - post-commit flow ordering, config gate, git-only behavior, existing remote context reuse, fail-open/silent handling, tests proving enabled/default attempt, disabled skip, configured ref use, and push failure does not change hook success. Out - retry queue, user-facing command output changes, fetch/backfill, non-git VCS note pushing. - Done when: default git post-commit flow attempts the push after local note write; explicit config disable skips the push; configured notes ref is used; push failure is swallowed from hook success/output and can be retried by a later hook invocation. diff --git a/context/sce/agent-trace-hooks-command-routing.md b/context/sce/agent-trace-hooks-command-routing.md index 23d97ff5..7eaeef92 100644 --- a/context/sce/agent-trace-hooks-command-routing.md +++ b/context/sce/agent-trace-hooks-command-routing.md @@ -61,7 +61,8 @@ - When validation passes, the payload is serialized and inserted into Agent Trace DB `agent_traces` using `commit_id` from flow-result commit metadata, `commit_time_ms` from flow-result post-commit timestamp metadata, a derived non-null `url` value formatted as `sce.crocoder.dev/trace/`, and the validated runtime `--remote-url` value persisted to nullable `agent_traces.remote_url`. - After Agent Trace DB insertion succeeds, git post-commit contexts also write the same full serialized Agent Trace JSON to a git note on the committed SHA. The default ref is `refs/notes/sce-agent-trace`, resolved through `policies.agent_trace.git_notes_ref`; explicit non-git `--vcs` values skip the note write. - Git-note writes use replace/upsert semantics (`git notes --ref add -f -F - `) and preserve multiline JSON by piping content through stdin. - - Post-commit Agent Trace success requires both schema validation and Agent Trace DB `agent_traces` persistence to succeed. Git-note write failures are best-effort: they are logged with `sce.hooks.post_commit.agent_trace_git_note_write_failed` and do not fail the hook after DB persistence succeeded. + - When the local git-note write succeeds and `policies.agent_trace.push_notes.enabled` resolves to its default `true`, the post-commit flow immediately attempts `git push ` using the validated `--remote-url` handoff value and the same configured notes ref. Explicit `enabled: false` skips this push. + - Post-commit Agent Trace success requires both schema validation and Agent Trace DB `agent_traces` persistence to succeed. Git-note write failures are best-effort: they are logged with `sce.hooks.post_commit.agent_trace_git_note_write_failed` and do not fail the hook after DB persistence succeeded. Git-notes push failures are silent fail-open: no user-facing warning or hook failure is emitted, and a later post-commit hook invocation can try the push again without a retry queue. - Current command-surface success output is: `post-commit hook processed intersection: commit=, intersection_files=`. - `post-rewrite` is a deterministic no-op entrypoint. - `diff-trace` reads STDIN JSON and classifies the payload: @@ -117,7 +118,7 @@ ## Explicit non-goals in the current baseline - No checkpoint handoff file -- No git-notes push/fetch/backfill behavior +- No git-notes fetch/backfill behavior - No backfill/import of existing `context/tmp/*-diff-trace.json` artifacts into AgentTraceDb - No retry queue replay - No rewrite remap ingestion diff --git a/context/sce/setup-githooks-hook-asset-packaging.md b/context/sce/setup-githooks-hook-asset-packaging.md index 02cfe35f..5861655f 100644 --- a/context/sce/setup-githooks-hook-asset-packaging.md +++ b/context/sce/setup-githooks-hook-asset-packaging.md @@ -19,7 +19,7 @@ Current `post-commit` template behavior is: - resolve `origin` with `git remote get-url origin`; if `sce` is not on `PATH`, print `sce CLI not found. Install it from https://sce.crocoder.dev/docs/getting-started#install-cli` to stderr and exit successfully so missing local CLI installation does not block the commit - if the remote lookup returns a non-empty URL, invoke `sce hooks post-commit --vcs git --remote-url "$remote_url" "$@"` - otherwise still invoke `sce hooks post-commit --vcs git "$@"`; Rust-side validation fails this missing-URL path without blocking git commit completion under the hook script policy. -- the Rust `post-commit` runtime handles Agent Trace persistence after this handoff: DB insertion remains the required persistence path, and successful git contexts also write the validated full Agent Trace JSON best-effort to git notes under the configured Agent Trace notes ref (default `refs/notes/sce-agent-trace`). +- the Rust `post-commit` runtime handles Agent Trace persistence after this handoff: DB insertion remains the required persistence path, successful git contexts write the validated full Agent Trace JSON best-effort to git notes under the configured Agent Trace notes ref (default `refs/notes/sce-agent-trace`), and successful local note writes attempt a silent fail-open push to the same `remote_url` unless `policies.agent_trace.push_notes.enabled` is `false`. ## Setup-service accessor surface From dede184ba9fd813d84e993c1c5c92121ac93aee9 Mon Sep 17 00:00:00 2001 From: David Abram Date: Wed, 15 Jul 2026 12:06:57 +0200 Subject: [PATCH 8/8] context: Document Agent Trace notes auto-push validation Cites plan agent-trace-git-notes-auto-push; updates T04 and T05. Co-authored-by: SCE --- context/architecture.md | 2 +- context/patterns.md | 2 +- .../plans/agent-trace-git-notes-auto-push.md | 39 +++++++++++++++++-- .../sce/agent-trace-hooks-command-routing.md | 4 +- 4 files changed, 40 insertions(+), 7 deletions(-) diff --git a/context/architecture.md b/context/architecture.md index 423601b1..436f1f1d 100644 --- a/context/architecture.md +++ b/context/architecture.md @@ -129,7 +129,7 @@ The repository includes a new placeholder Rust binary crate at `cli/`. - `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. Checkout DB discovery no longer lives in `doctor`; it moved to the `trace` group (`sce trace db list`) in `cli/src/services/trace/`. 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 enabled-by-default attribution-hooks config/env control is not opted out, `SCE_DISABLED` is false, and the staged-diff AI-overlap preflight confirms AI/editor evidence (`StagedDiffAiOverlapResult::Overlap`); the preflight is wired into `run_commit_msg_subcommand_in_repo` and logs `sce.hooks.commit_msg.ai_overlap_error` on error paths; `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 and best-effort git notes 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`), required nullable/non-empty `tool_version` plus required `u64` `time` (Unix epoch milliseconds), rejects values that cannot fit AgentTraceDb signed `time_ms` storage, prefixes the stored `diff_traces.session_id` before insert construction (`oc_` for OpenCode, `cc_` for Claude, `pi_` for Pi, same-tool idempotent), and inserts the parsed payload fields into AgentTraceDb without creating a parsed-payload `context/tmp` artifact; Claude structured `PostToolUse` diff-trace intake extracts direct model metadata from top-level or nested `model` fields and normalizes it with the `claude/` prefix when present. `session-model` is no longer a supported hook route. +- `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 enabled-by-default attribution-hooks config/env control is not opted out, `SCE_DISABLED` is false, and the staged-diff AI-overlap preflight confirms AI/editor evidence (`StagedDiffAiOverlapResult::Overlap`); the preflight is wired into `run_commit_msg_subcommand_in_repo` and logs `sce.hooks.commit_msg.ai_overlap_error` on error paths; `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, writes best-effort git notes, and attempts default-enabled silent fail-open notes push controlled by `policies.agent_trace.push_notes.enabled` 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`), required nullable/non-empty `tool_version` plus required `u64` `time` (Unix epoch milliseconds), rejects values that cannot fit AgentTraceDb signed `time_ms` storage, prefixes the stored `diff_traces.session_id` before insert construction (`oc_` for OpenCode, `cc_` for Claude, `pi_` for Pi, same-tool idempotent), and inserts the parsed payload fields into AgentTraceDb without creating a parsed-payload `context/tmp` artifact; Claude structured `PostToolUse` diff-trace intake extracts direct model metadata from top-level or nested `model` fields and normalizes it with the `claude/` prefix when present. `session-model` is no longer a supported hook route. - Generated Claude settings no longer register `SessionStart` for Agent Trace model attribution, and `sce hooks session-model` is no longer a supported hook command. The `session_models` table/API and diff-trace fallback lookup were removed in T02 of the `remove-session-models-direct-claude-model-id` plan; `diff-trace` now uses only direct payload `model_id` and `tool_version` values. - `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 flow through the doctor surface and checkout DB discovery flows through the `trace` group (`sce trace db list`). diff --git a/context/patterns.md b/context/patterns.md index 085538ec..7dbb7783 100644 --- a/context/patterns.md +++ b/context/patterns.md @@ -133,7 +133,7 @@ - For cross-service CLI dependencies exposed through the borrowed `AppContext` view, prefer shared capability/accessor traits over one-off per-service abstractions; keep production wrappers thin over `std::fs` and `git` process execution until call-site migration tasks approve deeper service refactors, and keep command execution generic over the narrow accessors each command needs where practical. - For future CLI domains, define trait-first service contracts with request/plan models in `cli/src/services/*` and keep placeholder implementations explicitly non-runnable until production behavior is approved. - Model deferred integration boundaries with concrete event/capability data structures (for example hook-runtime attribution snapshots/policies and cloud-sync checkpoints) so later tasks can implement behavior without reshaping public seams. -- For the current local-hook baseline, keep `pre-commit` and `post-rewrite` as deterministic no-op entrypoints; keep `post-commit` as the active bounded recent-diff-trace intersection entrypoint with validated `--remote-url` plumbed through Agent Trace flow and any direct diagnostics printed to stderr; keep `diff-trace` as an explicit STDIN intake path with deterministic required-field validation for `sessionID`, `diff`, `time`, `tool_name`, optional `model_id` (absent/`null` → `None`), and `tool_version` (present and either `null` or non-empty string), same-tool-idempotent stored `diff_traces.session_id` prefixing (`oc_` for OpenCode, `cc_` for Claude, `pi_` for Pi), non-lossy AgentTraceDb `time_ms` conversion, and AgentTraceDb insertion whose failure is logged and reflected in deterministic success text without creating a `context/tmp` artifact fallback; keep `conversation-trace` as the active message/part intake path. `session-model` is no longer a supported hook intake path. +- For the current local-hook baseline, keep `pre-commit` and `post-rewrite` as deterministic no-op entrypoints; keep `post-commit` as the active bounded recent-diff-trace intersection entrypoint with validated `--remote-url` plumbed through Agent Trace flow, best-effort git-note persistence, default-enabled silent fail-open notes push, and any direct diagnostics printed to stderr; keep `diff-trace` as an explicit STDIN intake path with deterministic required-field validation for `sessionID`, `diff`, `time`, `tool_name`, optional `model_id` (absent/`null` → `None`), and `tool_version` (present and either `null` or non-empty string), same-tool-idempotent stored `diff_traces.session_id` prefixing (`oc_` for OpenCode, `cc_` for Claude, `pi_` for Pi), non-lossy AgentTraceDb `time_ms` conversion, and AgentTraceDb insertion whose failure is logged and reflected in deterministic success text without creating a `context/tmp` artifact fallback; keep `conversation-trace` as the active message/part intake path. `session-model` is no longer a supported hook intake path. - For `diff-trace` hook intake, keep producer-facing failure behavior fail-open: STDIN read, parse/validation, and setup/persistence failures are logged with `sce.hooks.diff_trace.error` and converted into command success; preserve the existing valid-payload success text and the AgentTraceDb write-warning success path. - For `conversation-trace` hook intake, keep producer-facing failure behavior fail-open: STDIN read, top-level parse/validation, unsupported raw Claude hook events, and AgentTraceDb setup/persistence failures are logged with `sce.hooks.conversation_trace.error` and converted into command success; preserve valid-payload mixed-batch accounting, skipped-item logging, and batch-insert warning behavior. - For diff-trace attribution persistence, persist direct payload `model_id` and `tool_version` values as-is; missing attribution fields are stored as `NULL` in `diff_traces`. The former `session_models` fallback lookup was removed. diff --git a/context/plans/agent-trace-git-notes-auto-push.md b/context/plans/agent-trace-git-notes-auto-push.md index ae9fe754..0428da18 100644 --- a/context/plans/agent-trace-git-notes-auto-push.md +++ b/context/plans/agent-trace-git-notes-auto-push.md @@ -71,20 +71,53 @@ The push is best-effort and silent on failure: if the push cannot complete, the - Done when: default git post-commit flow attempts the push after local note write; explicit config disable skips the push; configured notes ref is used; push failure is swallowed from hook success/output and can be retried by a later hook invocation. - Verification notes (commands or checks): targeted post-commit hook tests if appropriate; manual local dry-run/review of command construction; `nix flake check`. -- [ ] T04: `Document Agent Trace notes auto-push behavior` (status:todo) +- [x] T04: `Document Agent Trace notes auto-push behavior` (status:done) - Task ID: T04 + - Completed: 2026-07-15 + - Files changed: `context/sce/agent-trace-hooks-command-routing.md`, `context/architecture.md`, `context/patterns.md`, `context/plans/agent-trace-git-notes-auto-push.md` + - Evidence: `rg "git-notes|git notes|push_notes|push notes|No git-notes" context/` reviewed current/historical matches; `git diff --check` passed; context sync verified root overview/architecture/glossary/patterns/context-map alignment. - Goal: Sync current-state context to describe default auto-push, disable config, and silent fail-open retry-on-next-commit behavior. - Boundaries (in/out of scope): In - focused updates to `context/sce/agent-trace-hooks-command-routing.md`, `context/cli/config-precedence-contract.md`, `context/sce/setup-githooks-hook-asset-packaging.md` if hook behavior text needs adjustment, `context/context-map.md`, and glossary entry if a new term is introduced. Out - broad narrative docs rewrites, completed-work summaries in durable context, implementation code. - Done when: context no longer states “No git-notes push/fetch/backfill behavior” as current behavior without qualification; documents that push is default-enabled, config-disableable, silent fail-open, and retried only by future post-commit invocations. - Verification notes (commands or checks): `rg "git-notes|git notes|push_notes|push notes|No git-notes" context/`; manual diff review; `git diff --check`. -- [ ] T05: `Validate notes auto-push and cleanup` (status:todo) +- [x] T05: `Validate notes auto-push and cleanup` (status:done) - Task ID: T05 + - Completed: 2026-07-15 + - Files changed: `context/plans/agent-trace-git-notes-auto-push.md` + - Evidence: `git diff --check` passed; `nix run .#pkl-check-generated` passed (generated outputs up to date); `rg "refs/notes/sce-agent-trace|push_notes|git notes.*push|No git-notes" cli/ config/ context/` reviewed current and historical matches; `nix flake check --print-build-logs` passed (150 Rust tests plus clippy/fmt/parity and other flake checks clean). - Goal: Run final validation for the complete plan and clean up temporary scaffolding. - Boundaries (in/out of scope): In - full repo validation, generated-output parity, formatting/lint/test checks, stale-string review, cleanup of temporary repos/remotes/notes refs used during testing, plan status/evidence updates. Out - new behavior beyond completed task stack. - Done when: `nix flake check` passes or any failure is documented as pre-existing/unrelated; `nix run .#pkl-check-generated` passes; context sync is verified; no temporary scaffolding remains. - Verification notes (commands or checks): `nix flake check`; `nix run .#pkl-check-generated`; `git diff --check`; `rg "refs/notes/sce-agent-trace|push_notes|git notes.*push|No git-notes" cli/ config/ context/`. +## Validation Report + +### Commands run + +- `git diff --check` -> exit 0 (no whitespace errors). +- `nix run .#pkl-check-generated` -> exit 0 (`Generated outputs are up to date.`). +- `rg "refs/notes/sce-agent-trace|push_notes|git notes.*push|No git-notes" cli/ config/ context/` -> exit 0; reviewed current implementation/config/context references plus historical plan/reference matches. +- `nix flake check --print-build-logs` -> exit 0 (`all checks passed`; 150 Rust tests passed; clippy/fmt/parity and other flake checks clean). +- `find context/tmp -mindepth 1 -maxdepth 1 -type f -newermt '2026-07-15 00:00:00' -print` -> exit 0 with no output; no task-created temporary scaffolding remained. + +### Success-criteria verification + +- [x] Successful post-commit Agent Trace flow writes the local git note then attempts a push by default -> covered by hook tests in `nix flake check` (`post_commit_agent_trace_flow_writes_git_note_after_db_insert`, push helper tests, and default enabled config resolver test). +- [x] Default notes ref remains `refs/notes/sce-agent-trace` unless overridden -> covered by config resolver and hook tests plus stale-string review. +- [x] Notes auto-push is default-enabled and config-disableable -> covered by `agent_trace_push_notes_enabled_uses_default`, `agent_trace_push_notes_enabled_uses_explicit_config_false`, and `post_commit_agent_trace_flow_skips_push_when_config_disabled` in flake check. +- [x] Push failures are silent/fail-open and retried only by later hook invocations -> covered by `post_commit_agent_trace_flow_swallows_git_notes_push_failure` and current-state context in `context/sce/agent-trace-hooks-command-routing.md`. +- [x] Tests cover command construction and configured ref behavior -> covered by `git_notes_push_helper_builds_push_command`, `git_notes_push_helper_honors_configured_ref`, and `post_commit_agent_trace_flow_honors_configured_git_notes_ref`. +- [x] Current-state context documents the default auto-push behavior, disable switch, and fail-open/no-retry-queue posture -> verified in `context/overview.md`, `context/architecture.md`, `context/patterns.md`, `context/context-map.md`, `context/glossary.md`, `context/cli/config-precedence-contract.md`, and `context/sce/agent-trace-hooks-command-routing.md`. + +### Failed checks and follow-ups + +None. + +### Residual risks + +None identified. + ## Open questions -None. Plan is ready for T01 execution. +None. Plan task stack is complete. diff --git a/context/sce/agent-trace-hooks-command-routing.md b/context/sce/agent-trace-hooks-command-routing.md index 7eaeef92..3aee6430 100644 --- a/context/sce/agent-trace-hooks-command-routing.md +++ b/context/sce/agent-trace-hooks-command-routing.md @@ -118,9 +118,9 @@ ## Explicit non-goals in the current baseline - No checkpoint handoff file -- No git-notes fetch/backfill behavior +- No git-notes fetch/backfill behavior; the only remote notes operation is the default-enabled, config-disableable, silent fail-open push after a successful local post-commit note write - No backfill/import of existing `context/tmp/*-diff-trace.json` artifacts into AgentTraceDb -- No retry queue replay +- No retry queue replay; failed git-notes pushes are retried only by later successful post-commit hook invocations - No rewrite remap ingestion - No `conversation-trace` retry/backfill path or `context/tmp` artifact persistence - No runtime Claude diff-trace persistence or AgentTraceDb writes from the capture route itself, and no direct artifact/DB writes from the Claude or OpenCode TypeScript runtimes