diff --git a/cli/assets/hooks/post-commit b/cli/assets/hooks/post-commit index 0ad697aa..0336572e 100644 --- a/cli/assets/hooks/post-commit +++ b/cli/assets/hooks/post-commit @@ -1,4 +1,4 @@ #!/bin/sh set -eu -exec sce hooks post-commit "$@" +exec sce hooks post-commit --vcs git "$@" diff --git a/cli/src/cli_schema.rs b/cli/src/cli_schema.rs index eecc88b9..8d1b258a 100644 --- a/cli/src/cli_schema.rs +++ b/cli/src/cli_schema.rs @@ -264,7 +264,10 @@ pub enum HooksSubcommand { CommitMsg { message_file: PathBuf }, #[command(about = "Run post-commit hook")] - PostCommit, + PostCommit { + #[arg(long = "vcs")] + vcs: Option, + }, #[command(about = "Run post-rewrite hook (reads pairs from STDIN)")] PostRewrite { rewrite_method: String }, diff --git a/cli/src/services/agent_trace.rs b/cli/src/services/agent_trace.rs index ab5fc8c5..c8ff766b 100644 --- a/cli/src/services/agent_trace.rs +++ b/cli/src/services/agent_trace.rs @@ -48,13 +48,23 @@ fn generate_agent_trace_id(commit_time: DateTime) -> Result pub struct AgentTraceMetadataInput<'a> { pub commit_timestamp: &'a str, pub commit_revision: &'a str, + pub vcs_type: Option, +} + +#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum AgentTraceVcsType { + Git, + Jj, + Hg, + Svn, } #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "snake_case")] pub struct AgentTraceVcs { #[serde(rename = "type")] - pub kind: String, + pub r#type: AgentTraceVcsType, pub revision: String, } @@ -217,7 +227,8 @@ pub struct AgentTrace { #[serde(default)] pub timestamp: String, /// Version control metadata sourced from caller-provided commit metadata. - pub vcs: AgentTraceVcs, + #[serde(skip_serializing_if = "Option::is_none")] + pub vcs: Option, /// File-level trace entries, one per file present in `post_commit_patch`. pub files: Vec, } @@ -401,10 +412,10 @@ pub fn build_agent_trace( version: default_agent_trace_version(), id: generate_agent_trace_id(commit_time)?, timestamp, - vcs: AgentTraceVcs { - kind: "git".to_owned(), + vcs: metadata.vcs_type.map(|vcs_type| AgentTraceVcs { + r#type: vcs_type, revision: metadata.commit_revision.to_owned(), - }, + }), files, }) } diff --git a/cli/src/services/agent_trace/tests.rs b/cli/src/services/agent_trace/tests.rs index 4a930da3..7188b98e 100644 --- a/cli/src/services/agent_trace/tests.rs +++ b/cli/src/services/agent_trace/tests.rs @@ -1,6 +1,6 @@ use super::{ - build_agent_trace, validate_agent_trace_value, AgentTraceMetadataInput, LineRange, - AGENT_TRACE_VERSION, + build_agent_trace, validate_agent_trace_value, AgentTraceMetadataInput, AgentTraceVcsType, + LineRange, AGENT_TRACE_VERSION, }; use crate::services::patch::{combine_patches, parse_patch, ParsedPatch}; use serde_json::{json, Value}; @@ -62,13 +62,19 @@ fn assert_builds_expected_agent_trace(scenario: AgentTraceScenario) { AgentTraceMetadataInput { commit_timestamp: TEST_COMMIT_TIMESTAMP, commit_revision: TEST_COMMIT_REVISION, + vcs_type: Some(AgentTraceVcsType::Git), }, ) .expect("agent trace should build"); assert_eq!(actual.version, AGENT_TRACE_VERSION); assert_eq!(actual.timestamp, TEST_COMMIT_TIMESTAMP); - assert_eq!(actual.vcs.kind, "git"); - assert_eq!(actual.vcs.revision, TEST_COMMIT_REVISION); + assert_eq!( + actual.vcs, + Some(super::AgentTraceVcs { + r#type: AgentTraceVcsType::Git, + revision: TEST_COMMIT_REVISION.to_string(), + }) + ); let actual_json = serde_json::to_value(&actual).expect("agent trace should serialize"); validate_agent_trace_value(&actual_json).expect("actual json should validate against schema"); assert_eq!(actual_json["vcs"], golden["vcs"]); @@ -146,6 +152,7 @@ fn poem_edit_reconstruction_maps_each_hunk_to_one_range() { AgentTraceMetadataInput { commit_timestamp: TEST_COMMIT_TIMESTAMP, commit_revision: TEST_COMMIT_REVISION, + vcs_type: Some(AgentTraceVcsType::Git), }, ) .expect("agent trace should build"); diff --git a/cli/src/services/hooks/mod.rs b/cli/src/services/hooks/mod.rs index 4c7698dc..3db0b051 100644 --- a/cli/src/services/hooks/mod.rs +++ b/cli/src/services/hooks/mod.rs @@ -12,6 +12,7 @@ use serde_json::{json, to_string as serialize_to_json, Value}; use crate::services::agent_trace::{ build_agent_trace, validate_agent_trace_value, AgentTrace, AgentTraceMetadataInput, + AgentTraceVcsType, }; use crate::services::agent_trace_db::{ AgentTraceDb, AgentTraceInsert, DiffTraceInsert, PostCommitPatchIntersectionInsert, @@ -36,7 +37,7 @@ const MAX_TRACE_FILE_CREATE_ATTEMPTS: u64 = 1_000_000; pub enum HookSubcommand { PreCommit, CommitMsg { message_file: PathBuf }, - PostCommit, + PostCommit { vcs_type: Option }, PostRewrite { rewrite_method: String }, DiffTrace, } @@ -74,7 +75,9 @@ fn run_hooks_subcommand_in_repo( HookSubcommand::CommitMsg { message_file } => { run_commit_msg_subcommand_with_trace(repository_root, subcommand, message_file) } - HookSubcommand::PostCommit => run_post_commit_subcommand_with_trace(repository_root), + HookSubcommand::PostCommit { vcs_type } => { + run_post_commit_subcommand_with_trace(repository_root, *vcs_type) + } HookSubcommand::PostRewrite { rewrite_method } => { run_post_rewrite_subcommand_with_trace(repository_root, subcommand, rewrite_method) } @@ -439,9 +442,13 @@ fn run_commit_msg_subcommand_with_trace( run_commit_msg_subcommand_in_repo(repository_root, message_file) } -fn run_post_commit_subcommand(repository_root: &Path) -> Result { +fn run_post_commit_subcommand( + repository_root: &Path, + vcs_type: Option, +) -> Result { run_post_commit_subcommand_with( repository_root, + vcs_type, run_post_commit_intersection_flow, run_post_commit_agent_trace_flow, ) @@ -449,15 +456,20 @@ fn run_post_commit_subcommand(repository_root: &Path) -> Result { fn run_post_commit_subcommand_with( repository_root: &Path, + vcs_type: Option, run_intersection_flow: F, run_agent_trace_flow: B, ) -> Result where F: FnOnce(&Path) -> Result, - B: FnOnce(&Path, &PostCommitIntersectionFlowResult) -> Result, + B: FnOnce( + &Path, + &PostCommitIntersectionFlowResult, + Option, + ) -> Result, { let result = run_intersection_flow(repository_root)?; - let _agent_trace = run_agent_trace_flow(repository_root, &result)?; + let _agent_trace = run_agent_trace_flow(repository_root, &result, vcs_type)?; Ok(format!( "post-commit hook processed intersection: commit={}, intersection_files={}", @@ -469,11 +481,13 @@ where fn run_post_commit_agent_trace_flow( _repository_root: &Path, flow_result: &PostCommitIntersectionFlowResult, + vcs_type: Option, ) -> Result { let db = AgentTraceDb::new().context("Failed to open Agent Trace DB for post-commit trace.")?; run_post_commit_agent_trace_flow_with( flow_result, + vcs_type, |trace_value| { validate_agent_trace_value(trace_value) .map_err(|error| anyhow!(error.to_string())) @@ -492,6 +506,7 @@ fn run_post_commit_agent_trace_flow( fn run_post_commit_agent_trace_flow_with( flow_result: &PostCommitIntersectionFlowResult, + vcs_type: Option, validate_agent_trace: V, persist_agent_trace: I, ) -> Result @@ -515,6 +530,7 @@ where AgentTraceMetadataInput { commit_timestamp: &commit_timestamp, commit_revision: &flow_result.post_commit_data.commit_oid, + vcs_type, }, ) .context("Failed to build Agent Trace payload from post-commit intersection flow result.")?; @@ -629,10 +645,13 @@ fn current_unix_time_ms() -> Result { .context("Current time exceeds i64 range for post-commit intersection.") } -fn run_post_commit_subcommand_with_trace(repository_root: &Path) -> Result { - let subcommand = HookSubcommand::PostCommit; +fn run_post_commit_subcommand_with_trace( + repository_root: &Path, + vcs_type: Option, +) -> Result { + let subcommand = HookSubcommand::PostCommit { vcs_type }; let input = build_hook_trace_input_for_post_commit(repository_root); - let outcome = run_post_commit_subcommand(repository_root); + let outcome = run_post_commit_subcommand(repository_root, vcs_type); let _ = persist_hook_trace(repository_root, &subcommand, &input, &outcome); @@ -662,7 +681,7 @@ fn hook_runtime_invocation_name(subcommand: &HookSubcommand) -> &'static str { match subcommand { HookSubcommand::PreCommit => "pre-commit runtime invocation", HookSubcommand::CommitMsg { .. } => "commit-msg runtime invocation", - HookSubcommand::PostCommit => "post-commit runtime invocation", + HookSubcommand::PostCommit { .. } => "post-commit runtime invocation", HookSubcommand::PostRewrite { .. } => "post-rewrite runtime invocation", HookSubcommand::DiffTrace => "diff-trace runtime invocation", } @@ -704,7 +723,7 @@ fn hook_trace_name(subcommand: &HookSubcommand) -> &'static str { match subcommand { HookSubcommand::PreCommit => "pre-commit", HookSubcommand::CommitMsg { .. } => "commit-msg", - HookSubcommand::PostCommit => "post-commit", + HookSubcommand::PostCommit { .. } => "post-commit", HookSubcommand::PostRewrite { .. } => "post-rewrite", HookSubcommand::DiffTrace => "diff-trace", } diff --git a/cli/src/services/parse/command_runtime.rs b/cli/src/services/parse/command_runtime.rs index 318b8b92..d5b63ff7 100644 --- a/cli/src/services/parse/command_runtime.rs +++ b/cli/src/services/parse/command_runtime.rs @@ -384,9 +384,12 @@ fn convert_hooks_subcommand( subcommand: services::hooks::HookSubcommand::CommitMsg { message_file }, })) } - cli_schema::HooksSubcommand::PostCommit => { + cli_schema::HooksSubcommand::PostCommit { vcs } => { + let vcs_type = parse_optional_hook_vcs_type(vcs.as_deref()) + .map_err(ClassifiedError::validation)?; + Ok(Box::new(services::hooks::command::HooksCommand { - subcommand: services::hooks::HookSubcommand::PostCommit, + subcommand: services::hooks::HookSubcommand::PostCommit { vcs_type }, })) } cli_schema::HooksSubcommand::PostRewrite { rewrite_method } => { @@ -401,3 +404,23 @@ fn convert_hooks_subcommand( } } } + +fn parse_optional_hook_vcs_type( + vcs: Option<&str>, +) -> Result, String> { + let Some(vcs) = vcs else { + return Ok(None); + }; + + let normalized = vcs.trim().to_ascii_lowercase(); + + match normalized.as_str() { + "git" => Ok(Some(services::agent_trace::AgentTraceVcsType::Git)), + "jj" => Ok(Some(services::agent_trace::AgentTraceVcsType::Jj)), + "hg" => Ok(Some(services::agent_trace::AgentTraceVcsType::Hg)), + "svn" => Ok(Some(services::agent_trace::AgentTraceVcsType::Svn)), + _ => Err(format!( + "Unsupported value for '--vcs': '{vcs}'. Supported values: git, jj, hg, svn." + )), + } +} diff --git a/context/context-map.md b/context/context-map.md index c5a9cb42..f46a7db8 100644 --- a/context/context-map.md +++ b/context/context-map.md @@ -46,8 +46,8 @@ Feature/domain context: - `context/sce/agent-trace-core-schema-migrations.md` (historical reference for removed local DB schema bootstrap behavior; T03 now implements the actual local DB with migrations) - `context/sce/agent-trace-retry-queue-observability.md` (inactive local-hook retry path plus historical retry/metrics reference) - `context/sce/agent-trace-local-hooks-mvp-contract-gap-matrix.md` (T01 Local Hooks MVP production contract freeze and deterministic gap matrix for `agent-trace-local-hooks-production-mvp`) -- `context/sce/agent-trace-minimal-generator.md` (implemented minimal Agent Trace builder 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 UUIDv7 `id` derived from commit-time metadata, caller-provided commit-time `timestamp`, top-level `vcs` metadata (`type = git`, `revision` from metadata input), and 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 as nested `contributor.type` plus optional `contributor.model_id` omitted when provenance is missing and one derived `ranges[{start_line,end_line}]` entry per post-commit hunk) -- `context/sce/agent-trace-hooks-command-routing.md` (implemented `sce hooks` command routing plus current runtime behavior: disabled-default commit-msg attribution, no-op `pre-commit`/`post-rewrite` entrypoints, active `post-commit` intersection entrypoint capturing current commit patch, querying recent `diff_traces` from past 7 days, combining valid patches via `patch::combine_patches`, intersecting via `patch::intersect_patches`, persisting results to `post_commit_patch_intersections`, building and schema-validating post-commit Agent Trace payloads, and persisting validated payloads to AgentTraceDb `agent_traces` (DB-only), plus `diff-trace` STDIN intake with required non-empty `sessionID`/`diff`/`model_id` and required `u64` `time` validation, dual persistence to AgentTraceDb, and collision-safe `context/tmp/-000000-diff-trace.json` artifacts) +- `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`, and 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`), 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 as nested `contributor.type` with optional `contributor.model_id` omitted when provenance is missing, and one derived `ranges[{start_line,end_line}]` entry per post-commit hunk) +- `context/sce/agent-trace-hooks-command-routing.md` (implemented `sce hooks` command routing plus current runtime behavior: disabled-default commit-msg attribution, no-op `pre-commit`/`post-rewrite` entrypoints, active `post-commit` intersection entrypoint capturing current commit patch, querying recent `diff_traces` from past 7 days, combining valid patches via `patch::combine_patches`, intersecting via `patch::intersect_patches`, persisting results to `post_commit_patch_intersections`, building and schema-validating post-commit Agent Trace payloads, and persisting validated payloads to AgentTraceDb `agent_traces` (DB-only), plus `diff-trace` STDIN intake with required non-empty `sessionID`/`diff`/`model_id` and required `u64` `time` validation, and dual persistence to AgentTraceDb and collision-safe `context/tmp/-000000-diff-trace.json` artifacts) - `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 the implementation target for generated OpenCode enforcement, including config schema, argv-prefix matching, 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`, and TypeScript source ownership; Claude bash-policy enforcement has been removed from generated outputs) diff --git a/context/glossary.md b/context/glossary.md index e1bdd859..14c8e31b 100644 --- a/context/glossary.md +++ b/context/glossary.md @@ -147,11 +147,12 @@ - `HunkContributor`: Enum in `cli/src/services/agent_trace.rs` classifying a `post_commit_patch` hunk's origin relative to the intersection patch `intersection_patch = intersect_patches(constructed_patch, post_commit_patch)`: `Ai` (exact line-by-line match), `Mixed` (same slot but different content), `Unknown` (no corresponding slot in `intersection_patch`); serialized as `snake_case` JSON strings - `Conversation`: Struct in `cli/src/services/agent_trace.rs` representing one per-hunk entry in the minimal agent-trace payload, carrying a nested `contributor` object (`type` plus optional `model_id`) plus `ranges`, where the current implementation emits exactly one `{ start_line, end_line }` entry derived from the `post_commit_patch` hunk - `TraceFile`: Struct in `cli/src/services/agent_trace.rs` representing one per-file entry in the minimal agent-trace payload, carrying `path` (from `post_commit_patch`'s `new_path`) plus `conversations` (one per `post_commit_patch` hunk) -- `AgentTraceVcs`: Top-level VCS metadata struct in `cli/src/services/agent_trace.rs` carrying `type` and `revision`; current builder behavior fixes `type` to `"git"` and maps revision from caller metadata. -- `AgentTrace`: Top-level struct in `cli/src/services/agent_trace.rs` representing the minimal agent-trace payload, carrying top-level `version` (fixed to `0.1.0`, strict numeric `x.y.z`), `id` (UUIDv7 string derived from the same commit-time moment used for `timestamp` in `build_agent_trace(...)`), `timestamp` (caller-provided commit timestamp via `AgentTraceMetadataInput.commit_timestamp`, validated as RFC 3339), `vcs` (`AgentTraceVcs`), and `files` (`Vec`, one per `post_commit_patch` file); `serde`-serializable with `snake_case` field naming +- `AgentTraceVcs`: Top-level VCS metadata struct in `cli/src/services/agent_trace.rs` carrying `type` and `revision`; builder behavior maps `type` from caller metadata (`AgentTraceMetadataInput.vcs_type`, enum-backed) and maps revision from caller metadata when VCS metadata is present. +- `AgentTrace`: Top-level struct in `cli/src/services/agent_trace.rs` representing the minimal agent-trace payload, carrying top-level `version` (fixed to `0.1.0`, strict numeric `x.y.z`), `id` (UUIDv7 string derived from the same commit-time moment used for `timestamp` in `build_agent_trace(...)`), `timestamp` (caller-provided commit timestamp via `AgentTraceMetadataInput.commit_timestamp`, validated as RFC 3339), optional `vcs` (`Option`, omitted from serialized JSON when `None`), and `files` (`Vec`, one per `post_commit_patch` file); `serde`-serializable with `snake_case` field naming - `classify_hunk`: Public function in `cli/src/services/agent_trace.rs` that classifies a single `post_commit_patch` hunk against `intersection_patch` hunks by matching on `old_start` slot, returning `HunkContributor::Ai` for exact line-by-line match, `Mixed` for same-slot-but-different-content, or `Unknown` when no matching slot exists -- `AgentTraceMetadataInput`: Metadata input struct in `cli/src/services/agent_trace.rs` that carries `commit_timestamp` (RFC 3339 commit-time value used as `AgentTrace.timestamp`) and `commit_revision` (mapped to `AgentTrace.vcs.revision`). -- `build_agent_trace`: Public function in `cli/src/services/agent_trace.rs` that computes `intersection_patch = intersect_patches(constructed_patch, post_commit_patch)`, iterates over `post_commit_patch`'s files and hunks, classifies each hunk against `intersection_patch`, validates `AgentTraceMetadataInput.commit_timestamp` as RFC 3339, derives UUIDv7 `AgentTrace.id` from that same commit-time moment, and returns `Result` with top-level metadata fields plus one `Conversation` per `post_commit_patch` hunk; consumed by the active post-commit hook flow, with no standalone `sce agent-trace` command surface -- `agent-trace plugin diff extraction seam`: Exported helper `extractDiffTracePayload` in `config/lib/agent-trace-plugin/opencode-sce-agent-trace-plugin.ts` that reads `input.event` and returns `{ sessionID, diff, time, model_id }` only when the event is `message.updated` with `properties.info.role === "user"`; extracts `sessionID` from `info.sessionID` (falling back to `"unknown"` when absent or empty), joins object-entry `patch` fields from `info.summary?.diffs[]` with `\n` while preserving empty patch strings for Rust-side validation, uses `Date.now()` for `time`, and builds `model_id` directly as `info.model.providerID/info.model.modelID`; returns `undefined` for non-`message.updated` events, non-user messages, messages without a non-empty `summary.diffs` array, or diffs arrays without object entries. +- `AgentTraceMetadataInput`: Metadata input struct in `cli/src/services/agent_trace.rs` that carries `commit_timestamp` (RFC 3339 commit-time value used as `AgentTrace.timestamp`), `commit_revision` (mapped to `AgentTrace.vcs.revision` when VCS metadata is emitted), and optional `vcs_type` (`Option`, mapped to `AgentTrace.vcs.type` and controlling whether top-level `vcs` is emitted). +- `AgentTraceVcsType`: Schema-aligned VCS enum in `cli/src/services/agent_trace.rs` (`Git`, `Jj`, `Hg`, `Svn`) serialized as `snake_case` JSON values (`git`, `jj`, `hg`, `svn`) for `AgentTrace.vcs.type`. +- `build_agent_trace`: Public function in `cli/src/services/agent_trace.rs` that computes `intersection_patch = intersect_patches(constructed_patch, post_commit_patch)`, iterates over `post_commit_patch` files and hunks, classifies each hunk against `intersection_patch`, validates `AgentTraceMetadataInput.commit_timestamp` as RFC 3339, derives UUIDv7 `AgentTrace.id` from that same commit-time moment, and returns `Result` with top-level metadata fields plus one `Conversation` per `post_commit_patch` hunk; consumed by the active post-commit hook flow, with no standalone `sce agent-trace` command surface. +- `agent-trace plugin diff extraction seam`: Exported helper `extractDiffTracePayload` in `config/lib/agent-trace-plugin/opencode-sce-agent-trace-plugin.ts` that reads `input.event` and returns `{ sessionID, diff, time, model_id }` only when the event is `message.updated` with `properties.info.role === "user"`; extracts `sessionID` from `info.sessionID` (falling back to `"unknown"` when missing/empty), joins object-entry `patch` fields from `info.summary?.diffs[]` with `\n` while preserving empty patch strings for Rust-side validation, uses `Date.now()` for `time`, and builds `model_id` as `info.model.providerID/info.model.modelID`; returns `undefined` for non-`message.updated` events, non-user messages, messages without a non-empty `summary.diffs` array, or diffs arrays without object entries. - `agent-trace plugin diff-trace hook handoff seam`: Internal helper `runDiffTraceHook` in `config/lib/agent-trace-plugin/opencode-sce-agent-trace-plugin.ts` that invokes `sce hooks diff-trace`, streams extracted `{ sessionID, diff, time, model_id }` to STDIN JSON, and surfaces deterministic invocation failures. - `agent-trace plugin secondary diff artifact ownership`: Current runtime contract where `buildTrace` no longer writes diff-trace artifacts or database rows directly; extracted diff payloads are forwarded to CLI `diff-trace` intake and the Rust hook runtime owns AgentTraceDb insertion plus collision-safe per-invocation artifact persistence. diff --git a/context/sce/agent-trace-hooks-command-routing.md b/context/sce/agent-trace-hooks-command-routing.md index 66a275ed..ab1f3ae1 100644 --- a/context/sce/agent-trace-hooks-command-routing.md +++ b/context/sce/agent-trace-hooks-command-routing.md @@ -7,13 +7,14 @@ ## Implemented command surface - `sce hooks pre-commit` - `sce hooks commit-msg ` -- `sce hooks post-commit` +- `sce hooks post-commit [--vcs ]` - `sce hooks post-rewrite ` - `sce hooks diff-trace` ## Parser and dispatch behavior - `cli/src/app.rs` routes `hooks` through dedicated hook-subcommand parsing. - `cli/src/services/hooks/mod.rs` owns deterministic runtime dispatch through `HookSubcommand` + `run_hooks_subcommand`. +- `post-commit` now parses optional `--vcs` input tolerantly at the command boundary: recognized values (`git|jj|hg|svn`) map to `Some(AgentTraceVcsType)`, while unknown values map to `None` without parse failure. - Invalid and ambiguous invocations return deterministic actionable errors pointing to `sce hooks --help`. ## Current runtime behavior @@ -36,7 +37,9 @@ - Persists the serialized intersection result to `post_commit_patch_intersections` table with commit metadata (OID, timestamp), window bounds (cutoff_ms, end_ms), and loaded/skipped counts. - Empty recent patch set produces deterministic empty intersection result (no crash). - Internal orchestration now returns a typed `PostCommitIntersectionFlowResult` (`combined_recent_patch`, `post_commit_data`) from `run_post_commit_intersection_flow_with()`. - - `run_post_commit_subcommand(...)` passes that typed flow result through `run_post_commit_agent_trace_flow(...)` (run-flow naming), which maps commit-time metadata to RFC3339 and calls `agent_trace::build_agent_trace(...)`. + - `run_post_commit_subcommand(...)` now threads the parsed optional `vcs_type` through `run_post_commit_agent_trace_flow(...)` into `run_post_commit_agent_trace_flow_with(...)`. +- At the current runtime boundary, optional parsed `vcs_type` is forwarded unchanged into `agent_trace::build_agent_trace(...)`; when absent, the built payload omits top-level `vcs`. + - The run-flow path maps commit-time metadata to RFC3339 and calls `agent_trace::build_agent_trace(...)`. - 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 and `commit_time_ms` from flow-result post-commit timestamp metadata. diff --git a/context/sce/agent-trace-minimal-generator.md b/context/sce/agent-trace-minimal-generator.md index 7b4b38dc..fd1e6066 100644 --- a/context/sce/agent-trace-minimal-generator.md +++ b/context/sce/agent-trace-minimal-generator.md @@ -24,8 +24,8 @@ Given a `constructed_patch` (AI candidate) and a `post_commit_patch` (canonical | `LineRange` | New-file line span with `start_line` + `end_line` | | `Conversation` | Per-hunk entry: nested contributor + `ranges` (currently exactly one range derived from `post_commit_patch`) | | `TraceFile` | Per-file entry: path + conversations | -| `AgentTraceVcs` | Top-level VCS metadata object carrying `type` + `revision` | -| `AgentTrace` | Top-level payload: `version`, `id`, `timestamp`, `vcs`, `files` | +| `AgentTraceVcs` | Optional top-level VCS metadata object carrying `type` + `revision` when present | +| `AgentTrace` | Top-level payload: `version`, `id`, `timestamp`, optional `vcs`, `files` | All types are `serde`-serializable with `snake_case` field naming. `Conversation.contributor` serializes as a nested object with a JSON field named `type`; `model_id` is present only when a concrete value exists. @@ -36,8 +36,8 @@ Current output includes top-level metadata fields with this contract: - `version` is fixed to `"0.1"` - `id` is generated per `build_agent_trace(...)` call as a UUIDv7 string derived from the same commit-time moment used for `timestamp` - `timestamp` is sourced from explicit commit metadata input (`AgentTraceMetadataInput.commit_timestamp`) and must be RFC 3339 -- `vcs.type` is fixed to `"git"` -- `vcs.revision` is sourced from explicit commit metadata input (`AgentTraceMetadataInput.commit_revision`) +- `vcs` is emitted only when explicit commit metadata input includes `AgentTraceMetadataInput.vcs_type` +- when `vcs` is emitted, `vcs.type` is sourced from the schema-aligned enum (`git | jj | hg | svn`) and `vcs.revision` is sourced from `AgentTraceMetadataInput.commit_revision` ```json { @@ -70,7 +70,7 @@ Current output includes top-level metadata fields with this contract: ## Public API - `classify_hunk(post_commit_hunk, intersection_hunks) -> HunkContributor` — classify a single `post_commit_patch` hunk against `intersection_patch` hunks. -- `build_agent_trace(constructed_patch, post_commit_patch, metadata) -> Result` — full generator entrypoint that validates `metadata.commit_timestamp` as RFC 3339, uses it as top-level `timestamp`, derives a UUIDv7 `id` from that same commit-time moment, sets `vcs.type = "git"`, and maps `metadata.commit_revision` to `vcs.revision`. +- `build_agent_trace(constructed_patch, post_commit_patch, metadata) -> Result` — full generator entrypoint that validates `metadata.commit_timestamp` as RFC 3339, uses it as top-level `timestamp`, derives a UUIDv7 `id` from that same commit-time moment, and conditionally emits `vcs` only when `metadata.vcs_type` is present (mapping `vcs.type` from metadata and `vcs.revision` from `metadata.commit_revision`). ## Test fixture contract diff --git a/context/sce/setup-githooks-hook-asset-packaging.md b/context/sce/setup-githooks-hook-asset-packaging.md index c014ace2..541c9691 100644 --- a/context/sce/setup-githooks-hook-asset-packaging.md +++ b/context/sce/setup-githooks-hook-asset-packaging.md @@ -14,6 +14,10 @@ Task `sce-setup-githooks-any-repo` `T02` defines how required git-hook templates These templates are emitted into `OUT_DIR/setup_embedded_assets.rs` as `HOOK_EMBEDDED_ASSETS` with deterministic sorted relative paths. +Current `post-commit` template invocation is: + +- `exec sce hooks post-commit --vcs git "$@"` + ## Setup-service accessor surface `cli/src/services/setup/mod.rs` exposes hook-template access through: