diff --git a/cli/src/services/structured_patch.rs b/cli/src/services/structured_patch.rs index c50cce5c..bf273eed 100644 --- a/cli/src/services/structured_patch.rs +++ b/cli/src/services/structured_patch.rs @@ -148,9 +148,7 @@ fn build_write_create_patch(payload: &Map) -> PatchBuildResult { return skipped_build(ClaudeStructuredPatchSkipReason::UnsupportedWritePayload); }; - if value_field(tool_response, &["originalFile", "original_file"]) != Some(&Value::Null) { - return skipped_build(ClaudeStructuredPatchSkipReason::UnsupportedWritePayload); - } + let structured_patch = value_field(tool_response, &["structuredPatch", "structured_patch"]); let file_path = normalize_patch_path( string_field(tool_input, &["file_path", "filePath"]) @@ -162,7 +160,15 @@ fn build_write_create_patch(payload: &Map) -> PatchBuildResult { return skipped_build(ClaudeStructuredPatchSkipReason::MissingFilePath); }; - let Some(content) = string_value_field(tool_input, &["content", "newFile", "new_file"]) else { + if let Some(structured_patch) = structured_patch.filter(|patch| !patch.is_null()) { + if let Some(patch) = + modified_patch_from_structured_hunks(file_path.clone(), structured_patch) + { + return PatchBuildResult::Built(patch); + } + } + + let Some(content) = string_value_field(tool_input, &["content"]) else { return skipped_build(ClaudeStructuredPatchSkipReason::MissingFileContent); }; @@ -187,29 +193,30 @@ fn build_edit_structured_patch(payload: &Map) -> PatchBuildResult } let file_path = normalize_patch_path( - string_field(tool_input, &["file_path", "filePath"]) - .or_else(|| { - structured_patch - .as_object() - .and_then(|patch| string_field(patch, &["file_path", "filePath", "path"])) - }) - .as_deref(), + string_field(tool_input, &["file_path", "filePath"]).as_deref(), string_field(payload, &["cwd"]).as_deref(), ); let Some(file_path) = file_path else { return skipped_build(ClaudeStructuredPatchSkipReason::MissingFilePath); }; + let Some(patch) = modified_patch_from_structured_hunks(file_path, structured_patch) else { + return skipped_build(ClaudeStructuredPatchSkipReason::UnsupportedEditPayload); + }; + + PatchBuildResult::Built(patch) +} + +fn modified_patch_from_structured_hunks( + file_path: String, + structured_patch: &Value, +) -> Option { let hunks: Vec = structured_patch_hunks(structured_patch) .into_iter() .filter_map(parse_structured_patch_hunk) .collect(); - if hunks.is_empty() { - return skipped_build(ClaudeStructuredPatchSkipReason::UnsupportedEditPayload); - } - - PatchBuildResult::Built(ParsedPatch { + (!hunks.is_empty()).then_some(ParsedPatch { files: vec![PatchFileChange { old_path: file_path.clone(), new_path: file_path, diff --git a/cli/src/services/structured_patch/fixtures/write_create_simple/claude-post-tool-use.json b/cli/src/services/structured_patch/fixtures/write_create_simple/claude-post-tool-use.json index d75b4dea..f1095636 100644 --- a/cli/src/services/structured_patch/fixtures/write_create_simple/claude-post-tool-use.json +++ b/cli/src/services/structured_patch/fixtures/write_create_simple/claude-post-tool-use.json @@ -1,12 +1,28 @@ { "hook_event_name": "PostToolUse", - "session_id": "test-write-create-simple", + "session_id": "test-write-update-structured-patch", "tool_name": "Write", "tool_input": { - "file_path": "hunks/hello.ts", - "content": "function helloWorld(): void {\n console.log(\"Hello World\");\n}\n\nhelloWorld();\n" + "file_path": "docs/status.md", + "content": "# Status\n\nThis full content fallback must not be used.\n" }, "tool_response": { - "originalFile": null + "originalFile": "# Status\n\nThe old state is pending.\n", + "structuredPatch": { + "hunks": [ + { + "oldStart": 1, + "oldCount": 3, + "newStart": 1, + "newCount": 3, + "lines": [ + " # Status", + " ", + "-The old state is pending.", + "+The new state is complete." + ] + } + ] + } } } diff --git a/cli/src/services/structured_patch/fixtures/write_create_simple/expected.patch b/cli/src/services/structured_patch/fixtures/write_create_simple/expected.patch index 496ddbf7..5d2fdef4 100644 --- a/cli/src/services/structured_patch/fixtures/write_create_simple/expected.patch +++ b/cli/src/services/structured_patch/fixtures/write_create_simple/expected.patch @@ -1,10 +1,9 @@ -diff --git a/hunks/hello.ts b/hunks/hello.ts -new file mode 100644 ---- /dev/null -+++ b/hunks/hello.ts -@@ -0,0 +1,5 @@ -+function helloWorld(): void { -+ console.log("Hello World"); -+} -+ -+helloWorld(); +Index: docs/status.md +=================================================================== +--- a/docs/status.md ++++ b/docs/status.md +@@ -1,3 +1,3 @@ + # Status + +-The old state is pending. ++The new state is complete. diff --git a/context/architecture.md b/context/architecture.md index c0d32a0f..4104e902 100644 --- a/context/architecture.md +++ b/context/architecture.md @@ -130,7 +130,7 @@ The repository includes a new placeholder Rust binary crate at `cli/`. - `cli/src/services/resilience.rs` defines bounded retry/timeout/backoff execution policy (`RetryPolicy`, `run_with_retry`) for transient operation hardening with deterministic failure messaging and retry observability. - No user-invocable `sce sync` command is wired in the current runtime; local DB bootstrap and setup-time per-checkout Agent Trace DB initialization flow through lifecycle providers aggregated by setup, while checkout/global DB health/repair and checkout DB discovery flow through the doctor surface. - `cli/src/services/patch.rs` defines the standalone patch domain model (`ParsedPatch`, `PatchFileChange`, `FileChangeKind`, `PatchHunk`, `TouchedLine`, `TouchedLineKind`) for in-memory parsed unified-diff representation, capturing only touched lines (added/removed) plus minimal per-file/per-hunk metadata while excluding non-hunk headers and unchanged context lines. All types are `serde`-serializable/deserializable with `snake_case` JSON field naming. The module also provides `parse_patch`, a public parser function that converts raw unified-diff text (both `Index:` SVN-style and `diff --git` git-style formats) into `ParsedPatch` structs, with `ParseError` for actionable malformed-input diagnostics. Storage-agnostic JSON load helpers (`load_patch_from_json` for string input, `load_patch_from_json_bytes` for byte input) reconstruct `ParsedPatch` from serialized JSON content with `PatchLoadError` for actionable deserialization diagnostics. Its patch-set operations now include deterministic ordered combination plus target-shaped intersection that prefers exact touched-line matches and falls back to historical `kind`+`content` matching when incremental diffs and canonical post-commit diffs have drifted line numbers; `parse_patch`, `combine_patches`, and `intersect_patches` are consumed by the active post-commit hook runtime. -- `cli/src/services/structured_patch.rs` defines the synchronous structured editor-hook derivation seam. It derives Claude `PostToolUse` `Write` create and `Edit` structured-patch payloads into canonical `ParsedPatch` values plus Claude session/tool metadata, returning deterministic skip reasons for unsupported events/tools/payload shapes. The module is pure and side-effect-free. It is wired into `sce hooks diff-trace` for Claude payload classification at intake (T04) and into `AgentTraceDb::recent_diff_trace_patches` for post-commit structured payload parsing dispatch at read time (T05). +- `cli/src/services/structured_patch.rs` defines the synchronous structured editor-hook derivation seam. It derives Claude `PostToolUse` `Write` structured-update hunks, `Write` `tool_input.content` create fallback, and `Edit` structured-patch payloads into canonical `ParsedPatch` values plus Claude session/tool metadata, returning deterministic skip reasons for unsupported events/tools/payload shapes. The module is pure and side-effect-free. It is wired into `sce hooks diff-trace` for Claude payload classification at intake (T04) and into `AgentTraceDb::recent_diff_trace_patches` for post-commit structured payload parsing dispatch at read time (T05). - `cli/src/services/` contains module boundaries for command_registry, lifecycle, auth_command, config, setup, doctor, hooks, checkout identity, bash_policy, version, completion, help, patch, SCE web URL helpers, shared database infrastructure, local DB adapters, encrypted auth DB adapters, and Agent Trace DB adapters with explicit trait seams for future implementations. `cli/src/services/checkout/` owns checkout ID file infrastructure and per-checkout Agent Trace DB lazy resolution for hook runtime; setup uses `AgentTraceDbLifecycle::setup()` to establish checkout identity and initialize the per-checkout DB, while `sce doctor` surfaces checkout identity and per-checkout Agent Trace DB status when a checkout ID exists and `sce doctor dbs` discovers checkouts via filesystem scan of `agent-trace-*.db` files on disk. `cli/src/services/bash_policy.rs` owns both the CLI-agnostic evaluator logic and the hidden `sce policy bash` command adapter used by OpenCode and Claude hook callers. `cli/src/services/command_registry.rs` defines the static `RuntimeCommand` enum, deterministic `CommandRegistry` name catalog, and `build_default_registry()` function for command dispatch metadata. Service-owned command modules own the runtime command payload structs for help/help-text, version, completion, auth, config, setup, doctor, hooks, and policy. - `cli/README.md` is the crate-local onboarding and usage source of truth for placeholder behavior, safety limitations, and roadmap mapping back to service contracts. - `flake.nix` applies `rust-overlay` (`oxalica/rust-overlay`) to nixpkgs, pins `rust-bin.stable.1.95.0.default` with `rustfmt` + `clippy`, reads the package/check version from repo-root `.version`, builds `packages.sce` through Crane (`buildDepsOnly` -> `buildPackage`) with a filtered repo-root source that preserves the Cargo tree plus `cli/assets/hooks`, then injects generated OpenCode/Claude config payloads and schema inputs into a temporary `cli/assets/generated/` mirror during derivation unpack so `cli/build.rs` can package the crate without requiring committed generated crate assets. The same `cli/build.rs` now scans `cli/migrations/*/*.sql` and writes `cli/src/generated_migrations.rs` with deterministic migration constants sorted by numeric filename prefix. diff --git a/context/cli/patch-service.md b/context/cli/patch-service.md index aa36e16c..b5888ac2 100644 --- a/context/cli/patch-service.md +++ b/context/cli/patch-service.md @@ -86,7 +86,7 @@ Patch reconstruction tests use deterministic fixture suites under `cli/src/servi - `claude-post-tool-use.json` containing a sanitized synthetic Claude `PostToolUse` payload shaped for `derive_claude_structured_patch(...)` - `expected.patch` containing the expected unified diff output - No `input.json` sidecar is part of the active fixture contract for this suite. - - Covered scenarios: simple/empty/no-newline/multiline Write create flows and single-hunk/multi-hunk/additions-only/deletions-only Edit flows + - Covered scenarios: Write structured-update hunk derivation, empty/no-newline/multiline Write content-create fallback flows, and single-hunk/multi-hunk/additions-only/deletions-only Edit flows - Rust golden tests in `cli/src/services/structured_patch/tests.rs` discover and validate all scenarios at test time, failing on missing or extra fixtures. - The current tmp-hunks scenario is materialized as `text_file_lifecycle_reconstruction/` with: - `incremental_01.patch` .. `incremental_26.patch` reconstructed from `tmp_hunks/*-message.part.json` in lexical filename order diff --git a/context/cli/structured-patch-service.md b/context/cli/structured-patch-service.md index 274753ee..0920f586 100644 --- a/context/cli/structured-patch-service.md +++ b/context/cli/structured-patch-service.md @@ -6,29 +6,33 @@ - Supported source: Claude `PostToolUse` structured payloads. - Supported tools: - - `Write` create payloads where `tool_response.originalFile` / `original_file` is `null`. + - `Write` payloads with non-empty `tool_response.structuredPatch` / `structured_patch` hunks. + - `Write` create payloads that omit usable structured hunks and provide `tool_input.content`. - `Edit` payloads with `tool_response.structuredPatch` / `structured_patch` hunks. - Output: `ClaudeStructuredPatch` with `session_id`, `ParsedPatch`, fixed caller-provided `time`, `tool_name="claude"`, and nullable `tool_version`. - Failure mode: `ClaudeStructuredPatchDerivationResult::Skipped(...)` with deterministic skip reasons for unsupported events/tools/payload shapes or missing required fields. ## ParsedPatch contract -- Write-create payloads produce `FileChangeKind::Added` file entries. +- Write payloads prefer structured hunks when present, including updates with non-null `tool_response.originalFile` / `original_file`. +- Write content fallback accepts only `tool_input.content`; missing content skips with `MissingFileContent` when no structured hunks are usable. +- Write-create content fallback produces `FileChangeKind::Added` file entries. - Empty Write content produces an added file with no hunks. -- Edit structured hunks produce `FileChangeKind::Modified` file entries. +- Write and Edit structured hunks produce `FileChangeKind::Modified` file entries. +- File paths come from `tool_input.file_path` / `tool_input.filePath`, with Write also accepting `tool_response.file_path` / `tool_response.filePath`; structured hunks are not path sources. - Structured hunk context lines advance old and new line counters but are not stored as touched lines. - Added/removed structured lines become `TouchedLineKind::Added` / `TouchedLineKind::Removed` entries with line numbers derived from hunk starts. - The service does not render unified-diff text; downstream consumers can serialize or intersect `ParsedPatch` directly. ## Runtime wiring status -The module is wired into `sce hooks diff-trace` for Claude payload classification at intake (T04): when `hook_event_name` is present and the event is a supported `PostToolUse` (`Write` create or `Edit` structured patch), the raw JSON is persisted as a `structured` payload type in `diff_traces` without conversion to unified-diff text. Unsupported Claude events (non-`PostToolUse`, unsupported tools) produce deterministic no-op results. OpenCode normalized payloads continue to be stored as `patch` payloads unchanged. +The module is wired into `sce hooks diff-trace` for Claude payload classification at intake (T04): when `hook_event_name` is present and the event is a supported `PostToolUse` (`Write` structured update, `Write` content create fallback, or `Edit` structured patch), the raw JSON is persisted as a `structured` payload type in `diff_traces` without conversion to unified-diff text. Unsupported Claude events (non-`PostToolUse`, unsupported tools) produce deterministic no-op results. OpenCode normalized payloads continue to be stored as `patch` payloads unchanged. Post-commit parsing dispatch through `structured_patch.rs` is implemented (T05): `AgentTraceDb::recent_diff_trace_patches` now reads `payload_type` from each `diff_traces` row and dispatches `patch` rows through existing `parse_patch` while dispatching `structured` rows through `derive_claude_structured_patch` at read time, producing `ParsedPatch` for both paths before hunk `model_id` injection and downstream combine/intersect operations. ## Test status -Golden fixture coverage lives in `cli/src/services/structured_patch/tests.rs` as `claude_derivation_golden_tests`. The test discovers all scenarios under `cli/src/services/structured_patch/fixtures/`, validates the expected eight scenarios, and asserts derived `ParsedPatch` equality against `parse_patch(expected.patch)` with fixed time/tool-version inputs. No generated helper tests are kept for the T01 derivation slice. +Golden fixture coverage lives in `cli/src/services/structured_patch/tests.rs` as `claude_derivation_golden_tests`. The test discovers all scenarios under `cli/src/services/structured_patch/fixtures/`, validates the expected eight scenarios, and asserts derived `ParsedPatch` equality against `parse_patch(expected.patch)` with fixed time/tool-version inputs. Current Write coverage includes structured update hunks plus content fallback cases. No generated helper tests are kept for the T01 derivation slice. ## See also diff --git a/context/context-map.md b/context/context-map.md index f30c9a52..03114b47 100644 --- a/context/context-map.md +++ b/context/context-map.md @@ -13,6 +13,7 @@ Feature/domain context: - `context/cli/default-path-catalog.md` (canonical production CLI path-ownership contract centered on `cli/src/services/default_paths.rs`, including persisted auth/config files, named DB paths for auth/local/Agent Trace databases, repo-relative, embedded-asset, install, hook, and context-path families plus the regression guard that keeps production path ownership centralized) - `context/cli/checkout-identity.md` (current checkout identity infrastructure in `cli/src/services/checkout/`, including `/sce/checkout-id` UUIDv7 storage, setup lifecycle integration that creates/reuses checkout identity and initializes the per-checkout Agent Trace DB, hook-runtime lazy DB fallback, `sce doctor` checkout identity display, and `sce doctor dbs` filesystem-based checkout discovery) - `context/cli/patch-service.md` (standalone patch domain model, parser, JSON load helpers, and set operations in `cli/src/services/patch.rs` for in-memory parsed unified-diff representation, capturing only touched lines plus minimal per-file/per-hunk metadata, supporting both `Index:` SVN-style and `diff --git` git-style formats, with `ParseError` for actionable malformed-input diagnostics, `PatchLoadError`/`load_patch_from_json`/`load_patch_from_json_bytes` for storage-agnostic JSON reconstruction, `intersect_patches` for target-shaped overlap with exact-match-first and historical `kind`+`content` fallback semantics plus matched-constructed-line `session_id` and matched-constructed-hunk `model_id` provenance inheritance, and `combine_patches` for ordered patch combination with later-wins conflict resolution plus winning-hunk `model_id` provenance inheritance; `parse_patch`, `intersect_patches`, and `combine_patches` are consumed by the active post-commit hook runtime) +- `context/cli/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/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/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) diff --git a/context/glossary.md b/context/glossary.md index d073a996..317eb78e 100644 --- a/context/glossary.md +++ b/context/glossary.md @@ -39,7 +39,7 @@ - `auth DB adapter`: Module in `cli/src/services/auth_db/mod.rs` that defines `AuthDbSpec` and exposes `AuthDb` as an `EncryptedTursoDb` alias. It resolves the canonical `/sce/auth.db` path with `auth_db_path()`, keeps encryption mandatory with `SCE_AUTH_DB_ENCRYPTION_KEY` env-secret precedence before OS keyring fallback and no plaintext mode, and embeds ordered auth migrations where baseline SQL creates `auth_credentials` without `user_id`, with `updated_at`, and a trigger that auto-refreshes `updated_at` on row updates. Auth runtime token-storage is now wired through `cli/src/services/token_storage.rs`, which persists tokens via the `auth_credentials` table in the encrypted auth DB instead of a JSON file. - `AuthDbLifecycle`: Lifecycle provider in `cli/src/services/auth_db/lifecycle.rs` that implements `ServiceLifecycle` for encrypted auth DB setup/doctor integration. `diagnose` collects auth DB path health problems, `fix` bootstraps missing auth DB parent directory, and `setup` calls `AuthDb::new()`. Registered as `LifecycleProviderId::AuthDb` in the shared lifecycle catalog. - `agent trace DB adapter`: Module in `cli/src/services/agent_trace_db/mod.rs` that defines `AgentTraceDbSpec`, exposes `AgentTraceDb` as a `TursoDb` alias, keeps `/sce/agent-trace.db` as the legacy/global fallback path, supports explicit per-checkout DB paths through `open_at(path)` and `open_for_hooks_without_migrations_at(path)`, embeds an ordered split fresh-start baseline migration set (`001..016`) covering `diff_traces`, `post_commit_patch_intersections`, `agent_traces`, `messages`, `parts`, `session_models`, and `diff_traces.payload_type` plus indexes and triggers; provides typed parameterized insert/upsert helpers for diff traces with `payload_type` discriminator, post-commit intersection rows, built agent-trace rows, message/part batch inserts, and session model attribution; exposes chronological recent `diff_traces` query/parse support with malformed-row skip accounting; has `AgentTraceDbLifecycle` for setup/doctor integration including setup-time per-checkout DB initialization; and is accessed by active Agent Trace hooks through checkout-scoped lazy DB resolution when setup has not prepared the DB. -- `structured patch service`: Pure synchronous Rust service in `cli/src/services/structured_patch.rs` that derives supported structured editor hook payloads into canonical `ParsedPatch` values. The current implemented source is Claude `PostToolUse` payloads for `Write` creates and `Edit` structured patches; wired into `sce hooks diff-trace` for Claude payload classification at intake (T04) and into `AgentTraceDb::recent_diff_trace_patches` for post-commit structured payload parsing dispatch at read time (T05). +- `structured patch service`: Pure synchronous Rust service in `cli/src/services/structured_patch.rs` that derives supported structured editor hook payloads into canonical `ParsedPatch` values. The current implemented source is Claude `PostToolUse` payloads for `Write` structured-update hunks, `Write` `tool_input.content` create fallback, and `Edit` structured patches; wired into `sce hooks diff-trace` for Claude payload classification at intake (T04) and into `AgentTraceDb::recent_diff_trace_patches` for post-commit structured payload parsing dispatch at read time (T05). - `Agent Trace SCE metadata`: Implementation-owned top-level metadata emitted by `build_agent_trace(...)` as `metadata.sce.version`; the value is sourced from the compiled `sce` CLI package version via `env!("CARGO_PKG_VERSION")`, is schema-validated with the rest of the payload, and is persisted in AgentTraceDb `agent_traces.trace_json` without changing the top-level Agent Trace payload/schema `version`. - `Agent Trace range content_hash`: Per-range `content_hash` emitted by `build_agent_trace(...)` inside every `ranges[]` entry as `murmur3:`, computed from the touched-line kind/content of the `post_commit_patch` or embedded-patch hunk used to emit that range while excluding positions, paths, metadata, and database IDs. - `DiffTraceInsert`: Insert payload in `cli/src/services/agent_trace_db/mod.rs` carrying `time_ms`, `session_id`, `patch`, `model_id`, `tool_name`, nullable `tool_version`, and `payload_type` for parameterized writes to the `diff_traces` table; `payload_type` uses `PAYLOAD_TYPE_PATCH` (`"patch"`) for `OpenCode` unified-diff payloads and `PAYLOAD_TYPE_STRUCTURED` (`"structured"`) for `Claude` `PostToolUse` structured payloads. diff --git a/context/sce/agent-trace-hooks-command-routing.md b/context/sce/agent-trace-hooks-command-routing.md index 89d1411f..54ddf41d 100644 --- a/context/sce/agent-trace-hooks-command-routing.md +++ b/context/sce/agent-trace-hooks-command-routing.md @@ -64,7 +64,7 @@ - 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: - - **Claude structured payloads** (detected by presence of top-level `hook_event_name`): the STDIN JSON is validated through `derive_claude_structured_patch`. Supported `PostToolUse` `Write` create and `Edit` structured-patch events produce a `DiffTracePayload` with `payload_type="structured"` and the raw event JSON stored as the `diff` column without conversion to unified-diff text. Unsupported Claude events (non-`PostToolUse`, unsupported tools, invalid payloads) produce a deterministic `NoOp` success result. + - **Claude structured payloads** (detected by presence of top-level `hook_event_name`): the STDIN JSON is validated through `derive_claude_structured_patch`. Supported `PostToolUse` `Write` structured-update hunks, `Write` `tool_input.content` create fallback, and `Edit` structured-patch events produce a `DiffTracePayload` with `payload_type="structured"` and the raw event JSON stored as the `diff` column without conversion to unified-diff text. Unsupported Claude events (non-`PostToolUse`, unsupported tools, invalid payloads) produce a deterministic `NoOp` success result. - **OpenCode normalized payloads** (no `hook_event_name`): existing flat `{ sessionID, diff, time, model_id?, tool_name, tool_version }` validation applies unchanged, with `payload_type="patch"`. - The `DiffTracePayload` struct carries a `payload_type: String` field consumed by `persist_diff_trace_payload_to_agent_trace_db_with` to pass the correct discriminator to `DiffTraceInsert`. - When `model_id` or `tool_version` is missing/nullable in the parsed payload, Rust looks up AgentTraceDb `session_models` by `(tool_name, session_id)` and uses the stored attribution values for missing fields when available. Direct payload `model_id` and `tool_version` values keep precedence over stored values. @@ -72,7 +72,7 @@ - Persistence: writes one parsed-payload artifact per invocation to `context/tmp/-000000-diff-trace.json` with atomic create-new retry semantics, resolves the current per-checkout AgentTraceDb lazily, and inserts the parsed payload fields via `DiffTraceInsert` + `insert_diff_trace()` using nullable/resolved `model_id` and `tool_version`. - Current TypeScript producers are the OpenCode agent-trace plugin and the generated Claude `sce hooks` command hooks (no TypeScript intermediary). - OpenCode forwards user-message `message` diffs with `tool_name="opencode"`, always including `model_id`, and nullable OpenCode client-version metadata. - - Claude forwards supported `PostToolUse` `Write` create and `Edit` structured-patch diffs with `tool_name="claude"` and no direct `model_id`; any explicit payload version metadata is preserved, and missing `model_id` / `tool_version` values are resolved from `session_models` when available. + - Claude forwards supported `PostToolUse` `Write` structured-update/content-create and `Edit` structured-patch diffs with `tool_name="claude"` and no direct `model_id`; any explicit payload version metadata is preserved, and missing `model_id` / `tool_version` values are resolved from `session_models` when available. - Neither TypeScript runtime writes `context/tmp/*-diff-trace.json` artifacts or AgentTraceDb rows directly. - `diff-trace` command success requires artifact persistence to succeed. AgentTraceDb open/insert failures are logged through `sce.hooks.diff_trace.agent_trace_db_write_failed` and reflected in the success text as failed DB persistence, while the parsed-payload artifact remains the durable fallback. - `conversation-trace` is a recognized hook subcommand routed through `HookSubcommand::ConversationTrace`. Rust intake classifies incoming STDIN JSON by the presence of a top-level `hook_event_name` field: raw Claude hook events are routed through `transform_claude_user_prompt_submit`, `transform_claude_stop`, or `transform_claude_post_tool_use` depending on the event name, while payloads without `hook_event_name` follow the existing mixed-batch `{ payloads: [...] }` path.