From 4f445434cce10e76f607713502c7570504b150c2 Mon Sep 17 00:00:00 2001 From: David Abram Date: Wed, 19 Aug 2026 09:55:04 +0200 Subject: [PATCH 1/2] hooks: Restore Claude transcript model attribution Resolve structured Claude diff traces from direct model metadata first, then fall back to the event's transcript and matching tool-use ID without restoring session-level state. Keep lookup failures nullable and document the event-local attribution contract. Plan: fix-claude-model-attribution T01 Co-authored-by: SCE --- cli/src/services/hooks/claude_transcript.rs | 156 ++++++++++++++++++ cli/src/services/hooks/mod.rs | 125 +++++++++++++- context/architecture.md | 4 +- context/context-map.md | 2 +- context/glossary.md | 6 +- context/overview.md | 8 +- context/patterns.md | 2 +- context/plans/fix-claude-model-attribution.md | 76 +++++++++ context/sce/agent-trace-db.md | 6 +- .../sce/agent-trace-hooks-command-routing.md | 6 +- 10 files changed, 373 insertions(+), 18 deletions(-) create mode 100644 cli/src/services/hooks/claude_transcript.rs create mode 100644 context/plans/fix-claude-model-attribution.md diff --git a/cli/src/services/hooks/claude_transcript.rs b/cli/src/services/hooks/claude_transcript.rs new file mode 100644 index 00000000..6464b117 --- /dev/null +++ b/cli/src/services/hooks/claude_transcript.rs @@ -0,0 +1,156 @@ +use std::fs::File; +use std::io::{self, BufRead, BufReader}; +use std::path::Path; + +use serde_json::Value; + +/// Extract the model identity from a Claude JSONL transcript by matching an +/// assistant message whose `tool_use` content block has the given ID. +/// +/// Transcript access and parsing are fail-open. Unreadable files, unreadable +/// lines, missing fields, and unmatched tool calls return `None`; malformed +/// unrelated JSONL records are skipped so later valid records can still match. +pub fn extract_claude_transcript_model( + transcript_path: &Path, + tool_use_id: &str, +) -> Option { + extract_claude_transcript_model_from_reader( + File::open(transcript_path).map(BufReader::new), + tool_use_id, + ) +} + +fn extract_claude_transcript_model_from_reader( + reader: io::Result, + tool_use_id: &str, +) -> Option { + let reader = reader.ok()?; + + for line in reader.lines() { + let line = line.ok()?; + if line.trim().is_empty() { + continue; + } + + let Ok(parsed) = serde_json::from_str::(&line) else { + continue; + }; + let Some(record) = parsed.as_object() else { + continue; + }; + + // Current Claude transcripts wrap the assistant message in `message`. + // Keep support for the earlier flat assistant-message shape as well. + let message = if let Some(message) = record.get("message").and_then(Value::as_object) { + let is_assistant = record + .get("type") + .and_then(Value::as_str) + .is_some_and(|value| value == "assistant") + || message + .get("role") + .and_then(Value::as_str) + .is_some_and(|value| value == "assistant"); + if !is_assistant { + continue; + } + message + } else { + if !record + .get("role") + .and_then(Value::as_str) + .is_some_and(|value| value == "assistant") + { + continue; + } + record + }; + + let Some(content) = message.get("content").and_then(Value::as_array) else { + continue; + }; + let has_matching_tool_use = content.iter().any(|block| { + block.as_object().is_some_and(|block| { + block + .get("type") + .and_then(Value::as_str) + .is_some_and(|value| value == "tool_use") + && block + .get("id") + .and_then(Value::as_str) + .is_some_and(|value| value == tool_use_id) + }) + }); + + if has_matching_tool_use { + return message + .get("model") + .and_then(Value::as_str) + .map(str::trim) + .filter(|model| !model.is_empty()) + .map(str::to_string); + } + } + + None +} + +#[cfg(test)] +mod tests { + use std::io::{Cursor, Error, ErrorKind}; + + use super::*; + + fn transcript_reader(content: &str) -> io::Result> { + Ok(Cursor::new(content.as_bytes())) + } + + #[test] + fn claude_transcript_reads_real_assistant_envelope_and_skips_malformed_records() { + let transcript = concat!( + "{malformed unrelated record}\n", + r#"{"type":"user","message":{"role":"user","content":[{"type":"tool_use","id":"tool-123"}]}}"#, + "\n", + r#"{"type":"assistant","message":{"role":"assistant","model":"claude-opus-4-1","content":[{"type":"text","text":"working"},{"type":"tool_use","id":"tool-123","name":"Write"}]}}"#, + "\n" + ); + + let model = + extract_claude_transcript_model_from_reader(transcript_reader(transcript), "tool-123"); + + assert_eq!(model.as_deref(), Some("claude-opus-4-1")); + } + + #[test] + fn claude_transcript_returns_none_when_transcript_cannot_be_read() { + let unavailable = Err(Error::new(ErrorKind::NotFound, "transcript unavailable")); + + assert_eq!( + extract_claude_transcript_model_from_reader::>(unavailable, "tool-123"), + None + ); + } + + #[test] + fn claude_transcript_returns_none_for_unmatched_tool_use_or_missing_model() { + let unmatched = concat!( + r#"{"type":"assistant","message":{"role":"assistant","model":"claude-opus-4-1","content":[{"type":"tool_use","id":"other-tool"}]}}"#, + "\n" + ); + let missing_model = concat!( + r#"{"type":"assistant","message":{"role":"assistant","content":[{"type":"tool_use","id":"tool-123"}]}}"#, + "\n" + ); + + assert_eq!( + extract_claude_transcript_model_from_reader(transcript_reader(unmatched), "tool-123"), + None + ); + assert_eq!( + extract_claude_transcript_model_from_reader( + transcript_reader(missing_model), + "tool-123" + ), + None + ); + } +} diff --git a/cli/src/services/hooks/mod.rs b/cli/src/services/hooks/mod.rs index 3f916bbd..7e9f172a 100644 --- a/cli/src/services/hooks/mod.rs +++ b/cli/src/services/hooks/mod.rs @@ -32,6 +32,7 @@ use crate::services::structured_patch::{ build_claude_post_tool_use_patch, derive_claude_structured_patch, ClaudeStructuredPatchDerivationResult, PatchBuildResult, }; +pub mod claude_transcript; pub mod command; pub mod lifecycle; @@ -952,7 +953,7 @@ fn parse_claude_diff_trace_payload( session_id: patch.session_id, diff: stdin_payload.to_string(), time: patch.time, - model_id: extract_direct_claude_model_id(payload), + model_id: resolve_claude_model_id(payload), tool_name: patch.tool_name, tool_version: patch.tool_version, payload_type: PAYLOAD_TYPE_STRUCTURED.to_string(), @@ -966,6 +967,26 @@ fn parse_claude_diff_trace_payload( } } +fn resolve_claude_model_id(payload: &serde_json::Map) -> Option { + resolve_claude_model_id_with(payload, claude_transcript::extract_claude_transcript_model) +} + +fn resolve_claude_model_id_with( + payload: &serde_json::Map, + transcript_lookup: F, +) -> Option +where + F: FnOnce(&Path, &str) -> Option, +{ + extract_direct_claude_model_id(payload).or_else(|| { + let transcript_path = non_empty_string(payload.get("transcript_path"))?; + let tool_use_id = non_empty_string(payload.get("tool_use_id"))?; + + transcript_lookup(Path::new(transcript_path), tool_use_id) + .and_then(|model| normalize_claude_model_id(&model)) + }) +} + fn extract_direct_claude_model_id(payload: &serde_json::Map) -> Option { direct_claude_model_id_string(payload, &["model", "model_id", "modelId"]) .or_else(|| { @@ -2542,6 +2563,108 @@ mod tests { } } + fn claude_model_test_event(transcript_path: &Path, tool_use_id: &str) -> Value { + json!({ + "hook_event_name": "PostToolUse", + "session_id": "session-123", + "tool_name": "Write", + "tool_use_id": tool_use_id, + "transcript_path": transcript_path, + "tool_input": { + "file_path": "docs/status.md", + "content": "# Status\n\nThe new state is complete.\n" + }, + "tool_response": { + "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." + ] + }] + } + } + }) + } + + fn parsed_claude_model_id(event: &Value) -> Option { + match parse_diff_trace_payload(&event.to_string()) + .expect("Claude PostToolUse diff-trace payload should parse") + { + DiffTraceParseResult::Persist(payload) => payload.model_id, + DiffTraceParseResult::NoOp(message) => { + panic!("Claude Write payload should persist, got no-op: {message}") + } + } + } + + fn resolved_claude_model_id_with(event: &Value, transcript_lookup: F) -> Option + where + F: FnOnce(&Path, &str) -> Option, + { + resolve_claude_model_id_with( + event.as_object().expect("test event should be an object"), + transcript_lookup, + ) + } + + #[test] + fn claude_model_direct_nested_metadata_wins_over_transcript_without_double_prefixing() { + let transcript_path = Path::new("/unused/direct-precedence.jsonl"); + let mut event = claude_model_test_event(transcript_path, "tool-123"); + event + .as_object_mut() + .expect("test event should be an object") + .insert("model".to_string(), json!({ "id": "claude/direct-model" })); + + let model_id = resolved_claude_model_id_with(&event, |_, _| { + panic!("transcript lookup must not run when direct metadata is present") + }); + + assert_eq!(model_id.as_deref(), Some("claude/direct-model")); + assert_eq!(parsed_claude_model_id(&event), model_id); + } + + #[test] + fn claude_model_falls_back_to_matching_transcript_and_normalizes_model() { + let transcript_path = Path::new("/virtual/transcript-fallback.jsonl"); + let event = claude_model_test_event(transcript_path, "tool-123"); + + let model_id = resolved_claude_model_id_with(&event, |path, tool_use_id| { + assert_eq!(path, transcript_path); + assert_eq!(tool_use_id, "tool-123"); + Some(String::from("claude/claude-opus-4-1")) + }); + + assert_eq!(model_id.as_deref(), Some("claude/claude-opus-4-1")); + } + + #[test] + fn claude_model_remains_none_when_transcript_lookup_cannot_succeed() { + let event = claude_model_test_event(Path::new("/virtual/missing.jsonl"), "tool-123"); + assert_eq!(resolved_claude_model_id_with(&event, |_, _| None), None); + + let mut event_without_lookup_fields = event; + let payload = event_without_lookup_fields + .as_object_mut() + .expect("test event should be an object"); + payload.remove("transcript_path"); + payload.remove("tool_use_id"); + assert_eq!( + resolved_claude_model_id_with(&event_without_lookup_fields, |_, _| { + panic!("lookup must not run without transcript event metadata") + }), + None + ); + } + #[test] fn prefixed_diff_trace_session_id_prefixes_fresh_pi_session_id() { assert_eq!( diff --git a/context/architecture.md b/context/architecture.md index 400e2759..ad444621 100644 --- a/context/architecture.md +++ b/context/architecture.md @@ -129,8 +129,8 @@ 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. Agent Trace database inspection is no longer a doctor-adjacent command surface; doctor owns repository-scoped DB health and checkout identity facts, while `sce sync` owns control-plane synchronization. Report fact collection preserves environment/repository/hook/integration display data, while service-owned lifecycle providers own config validation, local DB and repository-scoped Agent Trace DB readiness/bootstrap, and hook rollout diagnosis/repair. Integration inspection in `doctor/inspect.rs` is scoped twice over: `resolve_doctor_integration_targets` picks which targets to inspect, and `persisted_optional_workflows` (reused from setup) resolves which optional workflows the repository selected, which the OpenCode/Claude/Pi child collectors apply through `iter_embedded_assets_for_setup_target_with_selection`. An unselected optional workflow therefore contributes no expected children at all, so no row and no missing/mismatch problem can be produced for it, while a selected one keeps the unchanged presence and content-hash checks. - `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 repository-level `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 the repository-scoped Agent Trace DB 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 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 `RepositoryAgentTraceDb` 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/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 repository-level `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 the repository-scoped Agent Trace DB 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 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 `RepositoryAgentTraceDb` without creating a parsed-payload `context/tmp` artifact; Claude structured `PostToolUse` diff-trace intake resolves model attribution event-locally: direct top-level or nested metadata wins, otherwise the event's `transcript_path` is scanned for the assistant envelope whose `tool_use.id` matches `tool_use_id`; either source is normalized once with the `claude/` prefix and lookup failures remain nullable. `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 session-level fallback lookup were removed in T02 of the `remove-session-models-direct-claude-model-id` plan; `diff-trace` now uses direct-first/event-transcript-second Claude `model_id` resolution and direct `tool_version` values, without restoring session-level state. - `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. - `cli/src/services/sync/progress.rs` owns the sync-local, consumer-typed progress seam: generic `ProgressReporter` supports event delivery plus explicit successful finalization, closure-based collectors, and a no-op implementation alongside the fixed `indicatif` stderr presentation adapter. `cli/src/services/sync/sync.rs` owns `SyncProgressEvent` and its four-stream payload semantics, while `sync/command.rs` selects the terminal adapter for text and the no-op reporter for JSON. There is no top-level `cli/src/services/progress/` module; sync orchestration depends only on its sync-owned contract, so terminal-library details stay at the sync presentation boundary. - `sce sync [--format text|json]` is implemented: `cli/src/services/sync/sync.rs` resolves repository-scoped Agent Trace storage, authenticates against the control plane with stored WorkOS credentials, uses the config-resolved `control_plane_base_url` with baked default `https://sce.crocoderlab.dev`, calls the ingestion `/state` endpoint once, then starts the `messages`/`parts`/`diff_traces`/`agent_traces` capture-stream state machines concurrently via `AgentTraceExportReader` and a shared per-stream reconciliation engine. Batches and cursor refreshes remain sequential within each stream, while fixed stream order is retained for final and stream-completion reporting; `cli/src/services/sync/render_sync.rs` renders the converged `AgentTraceSyncReport` as concise per-stream text or `camelCase` JSON without a nested subcommand field (see `context/cli/sync-command.md`). Local DB bootstrap and setup-time repository-scoped Agent Trace DB initialization otherwise still flow through lifecycle providers aggregated by setup, while repository-scoped DB health/repair flows through the doctor surface. The former trace database inspection and nested sync surfaces are unavailable. diff --git a/context/context-map.md b/context/context-map.md index 3f40802b..e0252a44 100644 --- a/context/context-map.md +++ b/context/context-map.md @@ -68,7 +68,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) while hook diagnostics route with unprefixed producer-native sessions when available, 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 with per-item and first-valid-insert batch diagnostic routing; 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 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) while hook diagnostics route with unprefixed producer-native sessions when available, nullable event-local `model_id`/direct `tool_version` persistence without session fallback, Claude direct-first model metadata extraction from top-level or nested fields followed by fail-open `transcript_path` + `tool_use_id` JSONL lookup when direct metadata is absent, `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 with per-item and first-valid-insert batch diagnostic routing; 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, custom-policy `satisfied_by` wrapper exemption, fixed preset catalog/messages, and precedence rules) - `context/sce/bash-policy-satisfied-by-wrapper-exemption.md` (custom-policy `satisfied_by` field: optional list of wrapper argv prefixes that exempt a policy from firing when the matched command was unwrapped from one of them; `NormalizedSegment` wrapper-chain tracking, matching model, examples, and scope) diff --git a/context/glossary.md b/context/glossary.md index b932e9e7..8d053b56 100644 --- a/context/glossary.md +++ b/context/glossary.md @@ -80,6 +80,7 @@ - `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 `RepositoryAgentTraceDb::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. +- `event-local Claude model attribution`: Diff-trace enrichment rule where one Claude `PostToolUse` event resolves its model from direct top-level/nested metadata first, then only from that event's `transcript_path` by matching `tool_use_id` to an assistant envelope's `tool_use.id`; either source receives one `claude/` normalization step, failures remain `NULL`, and no `session_models` table or session-level cache participates. - `DiffTraceInsert`: Insert payload in `cli/src/services/agent_trace_db/mod.rs` carrying `time_ms`, tool-prefixed `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_traces payload_type discriminator`: `TEXT NOT NULL DEFAULT 'patch'` column in `diff_traces` added by migration `015_add_diff_traces_payload_type`; values are `PAYLOAD_TYPE_PATCH` (`"patch"`) for `OpenCode` unified-diff source payloads and `PAYLOAD_TYPE_STRUCTURED` (`"structured"`) for `Claude` `PostToolUse` structured source payloads; existing rows default to `"patch"` for backward compatibility. - `bash policy satisfied_by`: Optional field on a custom `policies.bash` entry listing wrapper argv prefixes that already satisfy the policy. When the matched command was unwrapped from one of these wrappers (outermost first, tracked by `NormalizedSegment.wrappers` in `cli/src/services/bash_policy.rs`), the policy does not fire, so a policy steering `rg` toward nix stays quiet for `nix shell nixpkgs#ripgrep -c rg ...` while still blocking a bare `rg`. Custom-policy-only; presets cannot declare satisfying wrappers. Exact argv-prefix matching only. See `context/sce/bash-tool-policy-enforcement-contract.md`. @@ -163,12 +164,11 @@ - `sce policy command adapter`: Hidden/internal `sce policy bash` command in `cli/src/services/bash_policy.rs` that exposes the Rust bash-policy evaluator to hook callers. It reads JSON from STDIN, resolves bash-policy config from the project root (git root with current-directory fallback), evaluates the command against active policies, and emits hook-safe output: Claude Code deny JSON (`hookSpecificOutput` with `permissionDecision: "deny"`) or empty string for allowed commands in `--output claude-hook` mode (default), and structured `{"status","decision","command","normalized_argv","reason","policy_id"}` JSON in `--output json` mode. Input modes are `--input claude-pre-tool-use` (default, parses Claude `PreToolUse` event JSON with `tool_name`/`tool_input.command`) and `--input normalized` (parses `{"command":...}` for OpenCode delegation). The command uses explicit `--input`/`--output` flags rather than auto-detection; Claude Code hooks invoke `sce policy bash` with defaults, while OpenCode plugin delegation passes `--input normalized --output json`. Invalid invocation/input returns deterministic validation diagnostics without executing target commands. - `bash policy redundancy warning`: Non-fatal config validation output emitted when `forbid-git-all` and `forbid-git-commit` are enabled together; the config remains valid, but `sce config show|validate` reports the overlap deterministically as a warning instead of an error. - `auth config baked default`: Optional key-declared fallback in `cli/src/services/config/mod.rs` (with schema/parsing in `schema.rs`) used only after env and config-file inputs are absent; the first implemented case is `workos_client_id`, which currently falls back to `client_01KZE4DDA8HM1JHZGF2QCF49RP`. -- `setup install engine`: Installer in `cli/src/services/setup/mod.rs` (`install_embedded_setup_assets`) that writes each embedded setup asset into its own staged file next to its final destination under repository-root `.opencode/`/`.claude/`/`.pi/`, removing only that exact destination file if present, then swapping the staged content into place. It never removes an integration target directory as a whole, so files a repository owns inside an SCE-managed target directory survive a setup run untouched. After the per-asset install loop, it prunes stale SCE-owned paths (see `setup catalog-derived pruning`). Two assets, the Claude target's `settings.json` and the OpenCode target's `opencode.json`, stage merged content instead of the embedded asset's bytes verbatim (see `setup config-merge seam`). - `setup install engine`: Installer in `cli/src/services/setup/mod.rs` (`install_embedded_setup_assets`) that writes each embedded setup asset into its own staged file next to its final destination under repository-root `.opencode/`/`.claude/`/`.pi/`, then swaps it into place via the `setup atomic-swap` policy (see `setup atomic-swap`) — renaming the staging file directly over the destination without unlinking it first. It never removes an integration target directory as a whole, so files a repository owns inside an SCE-managed target directory survive a setup run untouched. After the per-asset install loop, it prunes stale SCE-owned paths (see `setup catalog-derived pruning`). Two assets, the Claude target's `settings.json` and the OpenCode target's `opencode.json`, stage merged content instead of the embedded asset's bytes verbatim (see `setup config-merge seam`). - `setup catalog-derived pruning`: `prune_stale_assets_for_concrete_target` in `cli/src/services/setup/mod.rs`, run after per-asset install, deletes every path the full embedded catalog for a concrete target claims but the resolved selection did not install (a deselected optional workflow, or an asset a newer catalog renamed or dropped), then `remove_empty_ancestor_directories` removes any parent directory left empty by that deletion, walking upward until it reaches the target root or a directory that still holds something (a user file inside an SCE-owned skill directory keeps that directory from being removed). Pruning is stateless and derives only from the compiled-in catalog; no install manifest is persisted, so an orphaned file installed by an older `sce` binary under a name the current catalog no longer knows is left alone. - `setup config-merge seam`: Pure module `cli/src/services/setup/config_merge.rs`, covering `.claude/settings.json` and `.opencode/opencode.json`. Both `merge_or_create_claude_settings` and `merge_or_create_opencode_config` share the shape `(existing_bytes: Option<&[u8]>, generated_bytes: &[u8], source_path: &str) -> Result>`: each returns `generated_bytes` verbatim when no existing file is present; otherwise each parses both as JSON (an existing-file parse failure is a hard error naming `source_path`, with no write) and delegates to a pure merge function that copies the SCE-owned `$schema` key from the generated document and preserves every other top-level key from the existing document untouched. `merge_claude_settings` additionally replaces, per hook event key the generated `hooks` object declares, only the entries whose `hooks[].command` contains the SCE ownership marker `run-sce-or-show-install-guidance.sh`, appended after the surviving non-SCE entries; event keys the generated document does not declare are left untouched. `merge_opencode_config` additionally merges the `plugin` array as a set: existing entries whose path starts with the SCE ownership marker `./plugins/sce-` are dropped structurally (so a stale plugin path an older or renamed catalog installed is recognized and pruned even after the current generated document stops declaring it), then the generated document's `plugin` entries are appended. `install_single_asset_with_rename` (`mod install` in `cli/src/services/setup/mod.rs`) detects each merge target via `is_claude_settings_merge_target` / `is_opencode_config_merge_target` and stages the merged bytes instead of the embedded asset's bytes before the shared stage/swap step. The same module also exposes `claude_settings_fragment_is_current` / `opencode_config_fragment_is_current`, which merge `existing_bytes` into `generated_bytes` and compare the merged `Value` against the existing one: a no-op merge means the existing file already carries a current copy of the SCE-owned fragment, whatever else it holds. `cli/src/services/doctor/inspect.rs` uses these two functions (instead of byte-exact `sha256`) to inspect `.claude/settings.json` and `.opencode/opencode.json`, and `crate::services::setup::repair_merge_target_asset` lets `sce doctor --fix` repair a drifted merge target by reinstalling just that one asset through the same merge-install path. - `setup atomic-swap`: Per-file replacement choreography in `cli/src/services/setup/mod.rs` where staged content is renamed directly over an existing destination file, without ever unlinking that destination first — `fs::rename` replaces it atomically on both Unix and Windows, so a rename failure leaves the prior destination content and permissions untouched; on swap failure, the engine cleans that file's staging artifact and returns deterministic recovery guidance naming its path (recover from version control). No backup artifacts are created. Config install (`install_embedded_setup_assets`) applies this at individual-asset granularity and never removes an integration target directory as a whole; required-hook install applies it per hook file. Formerly called "setup remove-and-replace"; that name predates the removal of the pre-swap unlink step. -- `hooks command routing contract`: Current hook command parser/dispatcher plus runtime wiring in `cli/src/services/hooks/mod.rs` (`HookSubcommand`, `run_hooks_subcommand`) supports `pre-commit`, `commit-msg `, `post-commit`, `post-rewrite `, `diff-trace`, and `conversation-trace` with deterministic invocation validation/usage errors; `session-model` is no longer supported. `commit-msg` is the only active attribution path behind the attribution hooks gate and staged-diff AI-overlap preflight; `pre-commit`/`post-rewrite` are no-ops; `post-commit` persists intersections and built Agent Trace payloads; `diff-trace` persists DB-only AgentTraceDb rows using tool-prefixed `session_id` values plus direct `model_id` and `tool_version` values (session-model fallback was removed in the `remove-session-models-direct-claude-model-id` plan). +- `hooks command routing contract`: Current hook command parser/dispatcher plus runtime wiring in `cli/src/services/hooks/mod.rs` (`HookSubcommand`, `run_hooks_subcommand`) supports `pre-commit`, `commit-msg `, `post-commit`, `post-rewrite `, `diff-trace`, and `conversation-trace` with deterministic invocation validation/usage errors; `session-model` is no longer supported. `commit-msg` is the only active attribution path behind the attribution hooks gate and staged-diff AI-overlap preflight; `pre-commit`/`post-rewrite` are no-ops; `post-commit` persists intersections and built Agent Trace payloads; `diff-trace` persists DB-only AgentTraceDb rows using tool-prefixed `session_id`, event-local model attribution (Claude direct-first then event-transcript fallback), and direct `tool_version`, without session-level state. - `Claude raw hook capture (removed)`: Former hidden/internal `sce hooks claude-capture ` intake path removed in T05 of the `claude-typescript-model-cache-remove-rust-capture` plan. Rust now exposes `diff-trace` and `conversation-trace` intakes for active Claude/OpenCode editor runtimes; `session-model` is also removed from the supported hook command surface. The removed route previously wrote pretty-printed JSON artifacts under `context/tmp/claude/` without AgentTraceDb writes. See `context/sce/claude-raw-hook-capture.md`. - `deferred sync command` (historical): An earlier implementation note deferred a user-invocable sync command; it was superseded first by nested `sce trace sync` and now by the top-level `sce sync` command (see `context/cli/sync-command.md` and `context/cli/agent-trace-sync-command.md`). Local DB initialization and health ownership remain split between setup and doctor. - `sce CLI onboarding guide`: Crate-local documentation at `cli/README.md` that defines runnable placeholder commands, non-goals/safety limits, and roadmap mapping to service modules. @@ -184,7 +184,7 @@ - `agent trace historical reference docs`: Retained `context/sce/agent-trace-*.md` artifacts that describe the removed pre-v0.3 Agent Trace design and task slices; they are reference-only and do not describe the active local-hook runtime. - `agent trace commit-msg co-author policy`: Current contract in `cli/src/services/hooks/mod.rs` (`apply_commit_msg_coauthor_policy`) that applies exactly one canonical trailer (`Co-authored-by: SCE `) only when attribution hooks are enabled, SCE is not disabled, and the staged-diff AI-overlap preflight confirms AI/editor evidence (`StagedDiffAiOverlapResult::Overlap`); `NoOverlap` and `Error` both suppress the trailer, with `Error` logged via `sce.hooks.commit_msg.ai_overlap_error`; duplicate canonical trailers are deduped idempotently. - `local DB migration contract`: `cli/src/services/local_db/mod.rs` delegates migration execution to `TursoDb` through the `DbSpec::migrations()` contract. The current `LocalDbSpec` migration list is empty, so `LocalDb::new()` opens/creates the canonical local DB without creating local tables. -- `hook no-op baseline`: Current `cli/src/services/hooks/mod.rs` runtime posture where `pre-commit` and `post-rewrite` return deterministic no-op status text, `commit-msg` is a gated mutating path, `post-commit` persists intersections and built Agent Trace payloads without post-commit file artifacts, `diff-trace` validates STDIN payloads, uses only direct payload `model_id` and `tool_version` (session-model fallback removed), applies stored `session_id` prefixes (`oc_`/`cc_`/`pi_`), and inserts DB-only AgentTraceDb rows, and `conversation-trace` is the active message/part intake path. `session-model` is no longer a supported hook route. +- `hook no-op baseline`: Current `cli/src/services/hooks/mod.rs` runtime posture where `pre-commit` and `post-rewrite` return deterministic no-op status text, `commit-msg` is a gated mutating path, `post-commit` persists intersections and built Agent Trace payloads without post-commit file artifacts, `diff-trace` validates STDIN payloads, resolves Claude `model_id` event-locally with direct metadata before fail-open transcript lookup, persists direct `tool_version`, applies stored `session_id` prefixes (`oc_`/`cc_`/`pi_`), and inserts DB-only AgentTraceDb rows, and `conversation-trace` is the active message/part intake path. `session-model` is no longer a supported hook route. - `sce doctor` operator-health contract: `cli/src/services/doctor/mod.rs` is the stable doctor entrypoint, with focused `doctor/{inspect,render,fixes,types}.rs` submodules implementing the current approved operator-health surface in `context/sce/agent-trace-hook-doctor.md`: `sce doctor --fix` selects repair intent, Agent Trace DB discovery is repository-scoped only (the checkout-scoped `sce trace --legacy` surface was removed by the `retire-legacy-agent-trace-db` plan), and output exposes deterministic doctor mode, readiness, stable problem taxonomy/fixability fields, checkout/database records, and fix-result records. The runtime validates state-root resolution, global and repo-local `sce/config.json` readability/schema health, local DB and repository-scoped Agent Trace DB path/health, DB-parent readiness barriers, git availability, non-repo vs bare-repo targeting failures, effective hook-path source resolution, required hook presence/executable/content drift against canonical embedded hook assets, and repo-root installed OpenCode, Claude, plus Pi integration content health. Human text mode uses the approved sectioned layout (`Environment`, `Configuration` with checkout identity plus repository-scoped Agent Trace DB rows when available, `Repository`, `Git Hooks`, `Integrations`), `SCE doctor diagnose` / `SCE doctor fix` headers, bracketed `[PASS]`/`[FAIL]`/`[MISS]` status tokens with shared-style green/red colorization when enabled, simplified `label (path)` row formatting, top-level-only hook rows, and integration parent/child rows where missing files surface as `[MISS]`, mismatches/read failures as `[FAIL]`, and affected parent groups as `[FAIL]`. Agent Trace DB rows include repository ID, identity source, safe canonical identity, configured remote name, and never raw remote URLs. Current integration groups include `OpenCode plugins`, `OpenCode agents`, `OpenCode commands`, `OpenCode skills`, `ClaudeCode plugins`, `ClaudeCode commands`, `ClaudeCode skills`, `Pi prompts`, and `Pi skills`; Claude `settings.json` plus `hooks/**` belong to `ClaudeCode plugins`, including `.claude/hooks/run-sce-or-show-install-guidance.sh`, while Pi `prompts/**` and `skills/**` map to the Pi groups. Fix mode reuses canonical setup hook installation for missing/stale/non-executable required hooks and missing hooks directories and can bootstrap canonical missing SCE-owned DB parent directories. - `cli warnings-denied lint policy`: `cli/Cargo.toml` sets `warnings = "deny"`, so plain `cargo clippy --manifest-path cli/Cargo.toml` already fails on warnings without needing an extra `-- -D warnings` tail. - `agent trace local DB schema migration contract`: Retired `apply_core_schema_migrations` behavior removed from the current runtime during `agent-trace-removal-and-hook-noop-reset` T01; the local DB baseline is now file open/create only. diff --git a/context/overview.md b/context/overview.md index a1435037..a19650aa 100644 --- a/context/overview.md +++ b/context/overview.md @@ -38,7 +38,7 @@ The current user-facing synchronization entrypoint is `sce sync`; references to Sync owns the complete progress boundary in `cli/src/services/sync/progress.rs`: the consumer-typed `ProgressReporter` contract, no-op reporter, focused contract tests, and fixed `indicatif` terminal adapter. `SyncProgressEvent` remains owned by `cli/src/services/sync/sync.rs`; `sync/command.rs` selects the adapter or no-op implementation by output format, there is no top-level `cli/src/services/progress/` module, and JSON callers use the sync-owned no-op reporter. The same config resolver now also owns the attribution-hooks gate used by local hook runtime: opt-out env `SCE_ATTRIBUTION_HOOKS_DISABLED` overrides `policies.attribution_hooks.enabled` with inverted semantics, and the gate defaults to enabled unless explicitly disabled. The config service split now includes `cli/src/services/config/resolver.rs` as the focused owner for config-file discovery, file-layer merging, env/flag/default precedence, auth-key resolution, observability resolution, attribution-hooks resolution, and default-discovered invalid-file degradation; `cli/src/services/config/mod.rs` remains the facade/rendering orchestration surface while preserving existing `services::config` imports. -Generated config now includes repo-local OpenCode plugin assets: `sce-bash-policy.ts` plus `sce-agent-trace.ts` are emitted under `config/.opencode/plugins/`; the OpenCode agent-trace plugin extracts `{ sessionID, diff, time, model_id }` from user `message.updated` events with diffs, tracks per-session OpenCode client version from `session.created`/`session.updated`, and sends payloads to `sce hooks diff-trace` with `tool_name="opencode"` plus optional `tool_version`. Claude generated config now routes supported `PostToolUse Write|Edit|MultiEdit|NotebookEdit` events directly to `sce hooks diff-trace`; it no longer registers a `SessionStart` hook or calls `sce hooks session-model`. Rust handles extraction, validation, and persistence without a TypeScript intermediary; the former `config/.claude/plugins/sce-agent-trace.ts` Bun runtime was removed in T07 of the `claude-rust-diff-trace` plan. The Rust hook validates required fields, uses only direct payload `model_id` and `tool_version` (no longer resolves from `session_models`), and persists tool-prefixed `session_id` values (`oc_` for OpenCode, `cc_` for Claude, `pi_` for Pi), `model_id`, `tool_name`, and nullable `tool_version` into `diff_traces` through AgentTraceDb. Bash-policy now delegates OpenCode enforcement to the Rust `sce policy bash` command: the generated OpenCode plugin at `config/.opencode/plugins/sce-bash-policy.ts` is a thin wrapper that calls `sce policy bash --input normalized --output json` via `spawnSync` and throws on deny decisions; it no longer contains independent TypeScript policy logic. The former `bash-policy/runtime.ts` TypeScript runtime has been removed. Preset... +Generated config now includes repo-local OpenCode plugin assets: `sce-bash-policy.ts` plus `sce-agent-trace.ts` are emitted under `config/.opencode/plugins/`; the OpenCode agent-trace plugin extracts `{ sessionID, diff, time, model_id }` from user `message.updated` events with diffs, tracks per-session OpenCode client version from `session.created`/`session.updated`, and sends payloads to `sce hooks diff-trace` with `tool_name="opencode"` plus optional `tool_version`. Claude generated config now routes supported `PostToolUse Write|Edit|MultiEdit|NotebookEdit` events directly to `sce hooks diff-trace`; it no longer registers a `SessionStart` hook or calls `sce hooks session-model`. Rust handles extraction, validation, and persistence without a TypeScript intermediary; the former `config/.claude/plugins/sce-agent-trace.ts` Bun runtime was removed in T07 of the `claude-rust-diff-trace` plan. The Rust hook validates required fields, resolves Claude `model_id` event-locally with direct metadata first and matching `transcript_path`/`tool_use_id` JSONL fallback while keeping `tool_version` direct (with no `session_models` runtime), and persists tool-prefixed `session_id` values (`oc_` for OpenCode, `cc_` for Claude, `pi_` for Pi), `model_id`, `tool_name`, and nullable `tool_version` into `diff_traces` through AgentTraceDb. Bash-policy now delegates OpenCode enforcement to the Rust `sce policy bash` command: the generated OpenCode plugin at `config/.opencode/plugins/sce-bash-policy.ts` is a thin wrapper that calls `sce policy bash --input normalized --output json` via `spawnSync` and throws on deny decisions; it no longer contains independent TypeScript policy logic. The former `bash-policy/runtime.ts` TypeScript runtime has been removed. Preset... Claude bash-policy enforcement is also generated through `.claude/settings.json` as a `PreToolUse` `Bash` command hook running `sce policy bash`, so Claude and OpenCode both delegate to the Rust policy evaluator without a Claude TypeScript runtime. Pi bash-policy enforcement is delegated the same way through a project-local Pi extension (`config/lib/pi-plugin/sce-pi-extension.ts`, emitted to `config/.pi/extensions/sce/index.ts`) whose `tool_call` handler blocks denied bash commands via `sce policy bash` and fails open when the policy check cannot run (see `context/sce/pi-extension-runtime.md`). Local database bootstrap is now owned by `LocalDbLifecycle::setup` and `AgentTraceDbLifecycle::setup` aggregated by the setup command. Agent Trace setup creates/reuses the current checkout ID for diagnostics and initializes the repository-scoped `/sce/repos//agent-trace.db` with the repository schema; hook runtime lazily creates or upgrades that repository DB when setup has not run or schema metadata is incomplete. Doctor validates the repository-scoped DB path/health and can bootstrap missing parent directories; outside a Git repository it reports an actionable "requires a Git repository" diagnostic instead of probing a sentinel path. `sce sync` is fully implemented: it resolves repository-scoped storage, authenticates against the control plane with stored WorkOS credentials, fetches authoritative cursors once, synchronizes the four Agent Trace capture streams concurrently while preserving sequential batches within each stream, and renders the documented concise text/JSON output (see `context/cli/sync-command.md`). The former `sce trace` command group and its database inspection surfaces are unavailable. The repository-root flake (`flake.nix`) applies a Rust overlay-backed stable toolchain pinned to `1.95.0` (with `rustfmt` and `clippy`), reads package/check version from the repo-root `.version` file, and builds `packages.sce` through a Crane `buildDepsOnly` + `buildPackage` pipeline. One deterministic pre-Cargo Nix derivation invokes the shared generated-input producer and supplies its validated `SCE_CLI_GENERATED_INPUT_DIR` store path to native, release, test, and Clippy Cargo derivations. Pkl is absent from those Cargo environments; dependency-only and format derivations do not receive the handoff, so canonical generation changes invalidate compiling outputs without invalidating dependency artifacts or formatting. `cli-tests`, `cli-clippy`, and `cli-fmt` remain Crane-backed check derivations. @@ -64,10 +64,10 @@ Every target preserves the same gates and lifecycle semantics through six render Context sync uses an important-change gate: cross-cutting/policy/architecture/terminology changes require root shared-file edits, while localized tasks run verify-only root checks without default churn. OpenCode and Claude no longer generate legacy bootstrap or context-sync skills; `/commit` and `/handover` are generated only as catalog-registered composite workflow packages. OpenCode retains only thin routing agents, while Claude emits no agents. The superseded grouped Markdown catalog and automated OpenCode profile have been removed from Pkl ownership and generated outputs. 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`, 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 direct-first/event-transcript-second Claude `model_id` resolution plus direct `tool_version` without any `session_models` runtime, and continues with `None` when event-local lookup cannot resolve 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 prefers direct top-level or nested model metadata, then matches the event's `tool_use_id` in its JSONL `transcript_path`, normalizes either source once with the `claude/` prefix, and fails open to `None`. 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 the sole `RepositoryAgentTraceDb = TursoDb` adapter at `/sce/repos//agent-trace.db`, with a one-file repository schema for `repository_metadata`, `diff_traces`, `post_commit_patch_intersections`, `agent_traces`, `messages`, `parts`, indexes/triggers, and no `checkout_id` columns on trace rows. The checkout-scoped `AgentTraceDb = TursoDb` adapter, its `agent_trace_db_path()`/`agent_trace_db_path_for_checkout()` helpers, and the 15-file `cli/migrations/agent-trace/` chain were removed by the `retire-legacy-agent-trace-db` plan; 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. +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 the sole `RepositoryAgentTraceDb = TursoDb` adapter at `/sce/repos//agent-trace.db`, with a one-file repository schema for `repository_metadata`, `diff_traces`, `post_commit_patch_intersections`, `agent_traces`, `messages`, `parts`, indexes/triggers, and no `checkout_id` columns on trace rows. The checkout-scoped `AgentTraceDb = TursoDb` adapter, its `agent_trace_db_path()`/`agent_trace_db_path_for_checkout()` helpers, and the 15-file `cli/migrations/agent-trace/` chain were removed by the `retire-legacy-agent-trace-db` plan; active hook runtime writes nullable event-local 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-first/event-transcript-second Claude `model_id` plus direct `tool_version` values (no session-model fallback or cache), 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. The setup service now also exposes deterministic required-hook embedded asset accessors (`iter_required_hook_assets`, `get_required_hook_asset`) backed by canonical templates in `cli/assets/hooks/` for `pre-commit`, `commit-msg`, and `post-commit`; this behavior is documented in `context/sce/setup-githooks-hook-asset-packaging.md`. The setup service now also includes required-hook install orchestration (`install_required_git_hooks`) that resolves repository root and effective hooks path from git truth, computes the bytes to stage by merging the canonical hook template with any existing hook (preserving a foreign hook's content as an exact prefix with the SCE managed block appended, or bringing an SCE-owned block current in place) rather than writing canonical bytes verbatim, enforces deterministic per-hook outcomes (`Installed`/`Updated`/`Skipped`) against that merged content, surfaces a deterministic advisory when an appended block would be unreachable, and uses a unified atomic-swap policy that renames staged content directly over existing hooks without unlinking them first, with deterministic recovery guidance on swap failures; this behavior is documented in `context/sce/setup-githooks-install-flow.md`. The setup command parser/dispatch now also supports composable setup+hooks runs (`sce setup --opencode|--claude|--pi|--all --hooks`) plus hooks-only mode (`sce setup --hooks` with optional `--repo `), enforces deterministic compatibility validation (`--repo` requires `--hooks`; target flags remain mutually exclusive), and emits deterministic setup/hook outcome messaging (`installed`/`updated`/`skipped`); this behavior is documented in `context/sce/setup-githooks-cli-ux.md`. diff --git a/context/patterns.md b/context/patterns.md index 4b59fa2c..50d5cc42 100644 --- a/context/patterns.md +++ b/context/patterns.md @@ -160,7 +160,7 @@ - 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 `diff-trace` hook intake, keep producer-facing failure behavior fail-open: STDIN read and parse/validation failures use `sce.hooks.diff_trace.error`; AgentTraceDb open failures use the error-level `sce.hooks.diff_trace.agent_trace_db_open_failed`; later conversion/insert failures retain `sce.hooks.diff_trace.agent_trace_db_write_failed`. Preserve existing output text and emit only the most specific persistence diagnostic for one failure. - For `conversation-trace` hook intake, keep producer-facing failure behavior fail-open: STDIN read, top-level parse/validation, and unsupported raw Claude hook events use `sce.hooks.conversation_trace.error`; AgentTraceDb open failures use the error-level `sce.hooks.conversation_trace.agent_trace_db_open_failed`; later batch inserts retain their existing warning event. Preserve valid-payload mixed-batch accounting, skipped-item logging, output text, and one diagnostic per DB-open failure. -- 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. +- For diff-trace attribution persistence, keep Claude model resolution event-local: prefer direct payload metadata, then use only that event's `transcript_path` and `tool_use_id` for fail-open JSONL lookup, normalize either source through the `claude/` convention, and store unresolved attribution as `NULL` in `diff_traces`. Persist `tool_version` directly. Do not restore the former `session_models` fallback or any session-level cache. - For commit-msg co-author policy seams, gate canonical trailer insertion on runtime controls (`SCE_DISABLED` plus the shared attribution-hooks enablement gate) plus the staged-diff AI-overlap evidence gate (`StagedDiffAiOverlapResult::Overlap` maps to `ai_contribution_present = true`; `NoOverlap` and `Error` both map to `false`), and enforce idempotent dedupe so allowed cases end with exactly one `Co-authored-by: SCE ` trailer. - For local hook attribution flows, resolve the top-level enablement gate through the shared config precedence model (`SCE_ATTRIBUTION_HOOKS_DISABLED` opt-out env over `policies.attribution_hooks.enabled`, default `true`) so commit-msg attribution is enabled by default while explicit config `enabled = false` and truthy env opt-out still suppress it without adding hook-specific config parsing. - Do not assume conversation-trace retry/backfill/artifact persistence, retry replay, remap ingestion, or rewrite trace transformation are active in the current local-hook runtime; those paths are removed from or deferred beyond the current baseline. diff --git a/context/plans/fix-claude-model-attribution.md b/context/plans/fix-claude-model-attribution.md new file mode 100644 index 00000000..cab6c489 --- /dev/null +++ b/context/plans/fix-claude-model-attribution.md @@ -0,0 +1,76 @@ +# Plan: Fix Claude Model Attribution + +## Change summary + +Restore event-local Claude model enrichment for structured diff traces by resolving direct `PostToolUse` model metadata first and then, when direct metadata is absent, looking up the matching `tool_use_id` in the event's Claude JSONL transcript. The resolved value remains nullable, is normalized with the existing `claude/` convention, and is persisted only in `diff_traces.model_id`; the retired `session_models` table and runtime remain absent. + +Also repair structured diff-trace reconstruction so every touched line receives the persisted canonical row session ID while each hunk retains the persisted row model ID. This lets downstream Agent Trace generation emit both `contributor.model_id` and canonical `cc_...` related-session URLs without changing OpenCode, Pi, the Agent Trace schema, or database migrations. + +## Acceptance criteria + +How this plan is proven complete. Each criterion is observable and names the check that proves it. `/validate` runs these checks; no task in the stack performs final validation. + +- [ ] AC1: A supported Claude `PostToolUse` event uses direct model metadata when present; otherwise it resolves the model from the real Claude assistant-message JSONL envelope by matching `tool_use.id` to `tool_use_id`, normalizes the result without double-prefixing, and leaves `model_id` null when lookup cannot succeed. + - Validate: `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml claude_transcript`; `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml claude_model`. +- [ ] AC2: Transcript lookup is fail-open for missing or unreadable files, unmatched tool calls, missing models, and malformed unrelated JSONL lines, and direct metadata always wins over transcript-derived metadata. + - Validate: focused transcript and resolver unit tests cover all named branches and pass under the commands in AC1. +- [ ] AC3: Reconstructing a `payload_type="structured"` diff-trace row assigns the persisted `row.model_id` to every relevant hunk and the persisted canonical `row.session_id` to every touched line, without reusing the raw unprefixed session from the stored Claude payload. + - Validate: `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml structured_diff_trace`. +- [ ] AC4: A Claude event with no direct model, a transcript match, and persisted `cc_...` session provenance produces Agent Trace output containing both `contributor.model_id` and a related resource with `type="session"` and the canonical `https://sce.crocoder.dev/sessions/cc_...` URL; existing OpenCode and Pi attribution behavior remains unchanged. + - Validate: `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml claude_model_attribution`; existing OpenCode/Pi hook and Agent Trace regression tests pass in `nix flake check`. +- [ ] AC5: Current-state Agent Trace documentation describes direct-first/transcript-second/NULL Claude attribution as event-local enrichment, explicitly excludes `session_models` from runtime design, and records canonical persisted-session propagation during structured reconstruction. + - Validate: inspect the focused Agent Trace hook, DB, patch, and generator context documents for those statements and confirm no current-state document describes an active `session_models` runtime. + +### Full validation + +Repository-wide checks `/validate` runs after the last task, regardless of which criterion they map to. + +- `nix run .#pkl-check-generated` +- `nix flake check` + +### Context sync + +- Update `context/sce/agent-trace-hooks-command-routing.md` and `context/sce/agent-trace-db.md` for event-local direct-first/transcript-second Claude attribution and the explicit absence of `session_models` runtime behavior. +- Update `context/cli/patch-service.md`, `context/cli/structured-patch-service.md`, and `context/sce/agent-trace-minimal-generator.md` for persisted canonical session propagation and Agent Trace related-session generation. +- Refresh `context/context-map.md`, `context/overview.md`, `context/architecture.md`, `context/patterns.md`, and `context/glossary.md` where their current-state summaries or contracts would otherwise remain direct-only or omit the structured provenance rule. + +## Constraints and non-goals + +- **In scope:** `cli/src/services/hooks/` transcript/model resolution, Claude structured diff-trace parsing, repository Agent Trace recent-row reconstruction, focused Rust tests, and current-state Agent Trace context documentation. +- **Out of scope:** DWH changes, historical backfill or repair, Agent Trace schema changes, unrelated migrations, and changes to OpenCode or Pi attribution semantics. +- **Constraints:** direct Claude model metadata must win; transcript lookup must use only the event's `transcript_path` plus `tool_use_id`, skip malformed unrelated lines where practical, normalize through the existing `claude/` convention, remain nullable/fail-open, and use no new database abstraction or table. +- **Non-goal:** restoring `session_models`, any session-level model cache/runtime, placeholder model IDs, or raw unprefixed Claude sessions in reconstructed touched-line provenance. + +## Assumptions + +- The focused transcript helper will live at `cli/src/services/hooks/claude_transcript.rs` and use the existing standard-library buffered file reading plus `serde_json`; no new dependency is needed. +- The historical implementation at `18afa0ac402134b132820a37f42e600cf6639644` is behavioral reference only; its whole-transcript failure on any malformed JSONL line is intentionally tightened to skip malformed unrelated lines. +- The existing repository DB test seams and public patch/Agent Trace builder APIs make the requested end-to-end regression practical without adding production abstractions. + +## Task stack + +- [x] T01: `Restore event-local Claude transcript model resolution` (status:done) + - Task ID: T01 + - Scope: In — add the focused Claude JSONL transcript helper and unit tests; refactor Claude diff-trace model resolution to direct-first/transcript-second; retain existing nested direct fields and `claude/` normalization; cover missing/unreadable/malformed/unmatched inputs and direct precedence. Out — database schema/API changes, structured-row session propagation, OpenCode/Pi parser behavior, and session-level attribution storage. + - Dependencies: none + - Done when: supported Claude structured diff-trace payloads persist the direct normalized model when available, otherwise use the matching assistant transcript model, and otherwise keep `model_id=None` without rejecting the hook; all requested helper/resolver tests pass and no `session_models` runtime/API is introduced. + - Verify: `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml claude_transcript`; `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml claude_model`. + - Context synchronization: synced + - Context synchronization handoff: Changed files: `cli/src/services/hooks/claude_transcript.rs`, `cli/src/services/hooks/mod.rs`; Implementation summary: Added a buffered, fail-open Claude JSONL transcript reader that recognizes real wrapped assistant-message envelopes (while retaining flat-message compatibility), matches `tool_use.id` to the event's `tool_use_id`, skips malformed unrelated records, and returns no model for missing/unreadable/unmatched/missing-model inputs. Updated structured Claude diff-trace parsing to resolve existing direct model fields first and lazily fall back to the event's `transcript_path` plus `tool_use_id`, normalizing either source through the existing `claude/` convention. Added focused helper and resolver tests for the real envelope, malformed records, inaccessible and unmatched inputs, direct precedence, nested direct metadata, normalization, and nullable fallback.; Verification: `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml claude_transcript` (pass: 3 passed); `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml claude_model` (pass: 3 passed).; Done checks: All satisfied — supported structured Claude payloads retain direct normalized models when present, otherwise use a matching transcript model, otherwise persist nullable model attribution without rejecting the payload; no dependency, database API, schema, or `session_models` runtime was added.; Context impact: domain — update current-state Agent Trace hook and DB context for event-local direct-first/transcript-second/NULL Claude model resolution and the continued absence of `session_models`; review the five root context files for stale direct-only summaries. + - Completed: 2026-08-19 + - Files changed: `cli/src/services/hooks/claude_transcript.rs`, `cli/src/services/hooks/mod.rs` + - Result: Restored event-local Claude transcript model fallback with direct metadata precedence, existing model normalization, fail-open behavior, and focused regression coverage. + - Verify: `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml claude_transcript` — pass (3 tests); `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml claude_model` — pass (3 tests). + - Context impact: domain — synchronized current-state Agent Trace hook and DB documentation plus root summaries/patterns/glossary entries that previously described direct-only Claude model attribution. + +- [ ] T02: `Preserve structured diff-trace model and session provenance` (status:todo) + - Task ID: T02 + - Scope: In — update `parse_recent_diff_trace_patch_rows` so structured-row hunks retain persisted `model_id` and every touched line receives persisted canonical `session_id`; add focused reconstruction coverage and a practical persisted-row-to-Agent-Trace regression proving model plus canonical related session; preserve patch-row behavior. Out — schema/migration changes, backfill, Agent Trace schema changes, OpenCode/Pi attribution changes, and unrelated post-commit refactors. + - Dependencies: T01 + - Done when: a persisted Claude structured row with `cc_session-123` and a Claude model reconstructs with that model on every relevant hunk and that canonical session on every touched line, and downstream Agent Trace output emits both model attribution and the canonical related-session URL; focused and existing regressions pass. + - Verify: `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml structured_diff_trace`; `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml claude_model_attribution`. + - Context synchronization: pending + +## Open questions + +None. The request fixes a production attribution loss with explicit precedence, failure, persistence, provenance, compatibility, documentation, and validation contracts; the existing code and historical helper provide sufficient implementation seams without an architecture decision. diff --git a/context/sce/agent-trace-db.md b/context/sce/agent-trace-db.md index d429f80c..288f7174 100644 --- a/context/sce/agent-trace-db.md +++ b/context/sce/agent-trace-db.md @@ -180,10 +180,10 @@ Both triggers compare `OLD.*` vs `NEW.*` for all mutable columns (excluding `upd `sce hooks diff-trace` is the current runtime writer for `diff_traces`. -- The hook path validates required STDIN `{ sessionID, diff, time, tool_name, tool_version }` before persistence, with `model_id` accepted as optional (absent or `null`) and `tool_version` accepted as nullable. Missing attribution remains `None`; `diff_traces.model_id` is the only active model-attribution storage for diff traces and there is no session-level fallback lookup. -- Direct payload `model_id` and `tool_version` values pass into `DiffTraceInsert` as-is. The stored `session_id` is tool-prefixed before insert construction: `opencode` payloads store `oc_`, `claude` structured payloads store `cc_`, `pi` normalized payloads store `pi_`, and same-tool-prefixed values are not double-prefixed. The `payload_type` field is set to `PAYLOAD_TYPE_PATCH` for `OpenCode` normalized diff-trace payloads and `PAYLOAD_TYPE_STRUCTURED` for Claude structured `PostToolUse` payloads. Claude structured intake best-effort extracts direct `model`/`model_id`/`modelId` metadata, including nested `model.id` / `model.model` / `model.name`, normalizes values with the `claude/` prefix when present, and leaves `model_id` nullable when metadata is absent. +- The hook path validates required normalized STDIN `{ sessionID, diff, time, tool_name, tool_version }` fields and supported raw Claude structured events before persistence, with `model_id` accepted as optional (absent or `null`) and `tool_version` accepted as nullable. Missing attribution remains `None`; `diff_traces.model_id` is the only active model-attribution storage for diff traces and there is no session-level fallback lookup or cache. +- The resolved `model_id` and direct `tool_version` pass into `DiffTraceInsert`. The stored `session_id` is tool-prefixed before insert construction: `opencode` payloads store `oc_`, `claude` structured payloads store `cc_`, `pi` normalized payloads store `pi_`, and same-tool-prefixed values are not double-prefixed. The `payload_type` field is set to `PAYLOAD_TYPE_PATCH` for `OpenCode` normalized diff-trace payloads and `PAYLOAD_TYPE_STRUCTURED` for Claude structured `PostToolUse` payloads. Claude structured intake resolves direct `model`/`model_id`/`modelId` metadata, including nested `model.id` / `model.model` / `model.name`, before optionally matching the event's `tool_use_id` in its `transcript_path` JSONL assistant-message envelopes. Direct metadata always wins; transcript access and matching fail open; either resolved source is normalized once with the `claude/` prefix; and unresolved attribution remains `NULL`. - `time` is accepted as a `u64` Unix epoch millisecond input and must fit the signed `i64` `time_ms` column before any persistence starts. -- The hook inserts the parsed payload fields plus nullable direct attribution through `RepositoryAgentTraceDb::insert_diff_trace()` without writing a parsed-payload artifact under `context/tmp`. +- The hook inserts the parsed payload fields plus nullable event-local attribution through `RepositoryAgentTraceDb::insert_diff_trace()` without writing a parsed-payload artifact under `context/tmp`. - AgentTraceDb open failures are logged at error level through `sce.hooks.diff_trace.agent_trace_db_open_failed`; later conversion/insert failures retain `sce.hooks.diff_trace.agent_trace_db_write_failed`. Both failure classes preserve deterministic failed-persistence success text and create no artifact fallback. Open failures use the producer-native unprefixed session and do not also emit the write-failure event. - Existing artifact files are not backfilled into the database. diff --git a/context/sce/agent-trace-hooks-command-routing.md b/context/sce/agent-trace-hooks-command-routing.md index 4ef6d33f..3310c740 100644 --- a/context/sce/agent-trace-hooks-command-routing.md +++ b/context/sce/agent-trace-hooks-command-routing.md @@ -63,12 +63,12 @@ - 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. Direct model metadata is extracted best-effort from top-level `model`, `model_id`, or `modelId`, or from nested `model.id`, `model.model`, or `model.name`; non-empty values are normalized with the `claude/` prefix. If Claude omits model metadata, `model_id` remains nullable and downstream Agent Trace JSON omits contributor `model_id`. 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` 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. Model attribution is resolved event-locally and direct-first: top-level `model`, `model_id`, or `modelId`, or nested `model.id`, `model.model`, or `model.name`, wins when present. Otherwise, when the event provides both `transcript_path` and `tool_use_id`, Rust scans that Claude JSONL transcript for the assistant-message envelope whose `tool_use.id` matches, skipping malformed unrelated records. Either source is normalized once with the `claude/` prefix. Missing/unreadable transcripts, unmatched tool calls, missing models, or absent lookup fields leave `model_id` nullable without rejecting the hook, and downstream Agent Trace JSON omits contributor `model_id`. No session-level cache or lookup participates. 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`. - Before `DiffTraceInsert` construction, Rust prefixes the stored `diff_traces.session_id` by source tool: OpenCode normalized payloads store `oc_`, Claude structured payloads store `cc_`, Pi normalized payloads (`tool_name: "pi"`) store `pi_`, and already same-tool-prefixed values are left unchanged. Unknown `tool_name` values pass the raw session ID through unprefixed. Raw non-empty session-ID validation still happens before prefixing. - - Missing `model_id` or `tool_version` stays nullable; Rust does not perform session-level fallback attribution. Direct payload values are persisted as-is after payload-specific validation/normalization, making `diff_traces.model_id` the only active model-attribution storage for diff traces. - - Persistence: resolves the current repository-scoped `RepositoryAgentTraceDb` lazily and inserts the parsed payload fields via `DiffTraceInsert` + `insert_diff_trace()` using tool-prefixed `session_id` plus nullable direct `model_id` and `tool_version`. No parsed-payload artifact is written under `context/tmp`. + - Missing `model_id` or `tool_version` stays nullable. Claude's event-local transcript fallback is used only when direct event metadata is absent; Rust performs no session-level fallback attribution. The resolved event model and direct tool-version value are persisted after payload-specific validation/normalization, making `diff_traces.model_id` the only active model-attribution storage for diff traces. + - Persistence: resolves the current repository-scoped `RepositoryAgentTraceDb` lazily and inserts the parsed payload fields via `DiffTraceInsert` + `insert_diff_trace()` using tool-prefixed `session_id` plus nullable event-resolved `model_id` and direct `tool_version`. No parsed-payload artifact is written under `context/tmp`. - Current 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 generated settings no longer register `SessionStart`; supported `PostToolUse` `Write|Edit|MultiEdit|NotebookEdit` events are routed directly to `sce hooks diff-trace`. Runtime persistence currently derives structured diff traces for `Write` create and `Edit` structured-patch payloads; unsupported Claude payload shapes are no-ops. From e72ddc20186d11a05120dc4c59e56ab9a6cb3020 Mon Sep 17 00:00:00 2001 From: David Abram Date: Wed, 19 Aug 2026 10:14:32 +0200 Subject: [PATCH 2/2] agent-trace: Preserve structured Claude attribution provenance Apply persisted model IDs to reconstructed hunks and canonical session IDs to every touched line, so downstream Agent Trace output retains contributor and related-session attribution. Add persisted-row regression coverage, simplify transcript fixture handling, and synchronize validated context. Plan: fix-claude-model-attribution (T02) Co-authored-by: SCE --- cli/src/services/agent_trace_db/mod.rs | 120 ++++++++++++++++++ cli/src/services/hooks/claude_transcript.rs | 23 ++-- context/architecture.md | 2 +- context/cli/patch-service.md | 2 + context/cli/structured-patch-service.md | 2 +- context/context-map.md | 8 +- context/glossary.md | 2 +- context/overview.md | 2 +- context/patterns.md | 1 + context/plans/fix-claude-model-attribution.md | 50 +++++++- context/sce/agent-trace-db.md | 2 +- context/sce/agent-trace-minimal-generator.md | 2 +- 12 files changed, 188 insertions(+), 28 deletions(-) diff --git a/cli/src/services/agent_trace_db/mod.rs b/cli/src/services/agent_trace_db/mod.rs index 15b8426f..258afec3 100644 --- a/cli/src/services/agent_trace_db/mod.rs +++ b/cli/src/services/agent_trace_db/mod.rs @@ -377,6 +377,7 @@ fn parse_recent_diff_trace_patch_rows(rows: Vec) -> RecentDif let mut skipped = Vec::new(); for row in rows { + let is_structured = row.payload_type == PAYLOAD_TYPE_STRUCTURED; let parse_result = match row.payload_type.as_str() { PAYLOAD_TYPE_PATCH => parse_patch(&row.patch, Some(row.session_id.as_str())) .map_err(|error| skipped_diff_trace_patch_reason(&error)), @@ -402,6 +403,11 @@ fn parse_recent_diff_trace_patch_rows(rows: Vec) -> RecentDif for file in &mut patch.files { for hunk in &mut file.hunks { hunk.model_id.clone_from(&row.model_id); + if is_structured { + for line in &mut hunk.lines { + line.session_id = Some(row.session_id.clone()); + } + } } } @@ -441,6 +447,7 @@ mod tests { use super::repository::RepositoryAgentTraceDb; use super::*; + use crate::services::agent_trace::{build_agent_trace, AgentTraceMetadataInput}; fn unique_test_db_path() -> PathBuf { let nonce = SystemTime::now() @@ -479,6 +486,119 @@ mod tests { .expect("diff trace insert should succeed"); } + #[test] + fn structured_diff_trace_reconstruction_uses_persisted_model_and_session_provenance() { + let db_path = unique_test_db_path(); + let db = RepositoryAgentTraceDb::new_at(&db_path).expect("test DB should open"); + let payload = + include_str!("../structured_patch/fixtures/edit_multi_hunk/claude-post-tool-use.json"); + + db.insert_diff_trace(DiffTraceInsert { + time_ms: 1_000, + session_id: "cc_session-123", + patch: payload, + model_id: Some("claude/claude-sonnet-4-5"), + tool_name: "claude", + tool_version: Some("1.0.0"), + payload_type: PAYLOAD_TYPE_STRUCTURED, + }) + .expect("structured diff trace insert should succeed"); + + let result = db + .recent_diff_trace_patches(0, 2_000) + .expect("structured diff trace should load"); + assert_eq!(result.loaded_count(), 1); + assert_eq!(result.skipped_count(), 0); + + let patch = &result.patches[0].patch; + assert!( + patch + .files + .iter() + .flat_map(|file| &file.hunks) + .all(|hunk| hunk.model_id.as_deref() == Some("claude/claude-sonnet-4-5")), + "every reconstructed hunk should use the persisted row model" + ); + assert!( + patch + .files + .iter() + .flat_map(|file| &file.hunks) + .flat_map(|hunk| &hunk.lines) + .all(|line| line.session_id.as_deref() == Some("cc_session-123")), + "every reconstructed touched line should use the persisted canonical row session" + ); + + drop(db); + if let Some(parent) = db_path.parent() { + fs::remove_dir_all(parent).expect("test DB directory should be removed"); + } + } + + #[test] + fn claude_model_attribution_flows_from_persisted_structured_row_to_agent_trace() { + let db_path = unique_test_db_path(); + let db = RepositoryAgentTraceDb::new_at(&db_path).expect("test DB should open"); + let payload = + include_str!("../structured_patch/fixtures/edit_single_hunk/claude-post-tool-use.json"); + let post_commit_patch = parse_patch( + include_str!("../structured_patch/fixtures/edit_single_hunk/expected.patch"), + None, + ) + .expect("post-commit fixture should parse"); + + db.insert_diff_trace(DiffTraceInsert { + time_ms: 1_000, + session_id: "cc_session-123", + patch: payload, + model_id: Some("claude/claude-sonnet-4-5"), + tool_name: "claude", + tool_version: Some("1.0.0"), + payload_type: PAYLOAD_TYPE_STRUCTURED, + }) + .expect("structured diff trace insert should succeed"); + + let mut result = db + .recent_diff_trace_patches(0, 2_000) + .expect("structured diff trace should load"); + let constructed_patch = result + .patches + .pop() + .expect("one structured diff trace should load") + .patch; + let agent_trace = build_agent_trace( + &constructed_patch, + &post_commit_patch, + AgentTraceMetadataInput { + commit_timestamp: "2026-04-23T10:20:30Z", + commit_revision: "a0b1c2d3e4f5a6b7c8d9e0f11223344556677889", + vcs_type: None, + tool_name: Some("claude"), + tool_version: Some("1.0.0"), + }, + ) + .expect("Agent Trace should build"); + let agent_trace_json = + serde_json::to_value(agent_trace).expect("Agent Trace should serialize"); + + assert_eq!( + agent_trace_json["files"][0]["conversations"][0]["contributor"]["model_id"], + "claude/claude-sonnet-4-5" + ); + assert_eq!( + agent_trace_json["files"][0]["conversations"][0]["related"], + serde_json::json!([{ + "type": "session", + "url": "https://sce.crocoder.dev/sessions/cc_session-123" + }]) + ); + + drop(db); + if let Some(parent) = db_path.parent() { + fs::remove_dir_all(parent).expect("test DB directory should be removed"); + } + } + #[test] fn recent_diff_trace_patches_applies_bounded_window_ordering_and_parse_accounting() { let db_path = unique_test_db_path(); diff --git a/cli/src/services/hooks/claude_transcript.rs b/cli/src/services/hooks/claude_transcript.rs index 6464b117..8ea3a5de 100644 --- a/cli/src/services/hooks/claude_transcript.rs +++ b/cli/src/services/hooks/claude_transcript.rs @@ -55,11 +55,7 @@ fn extract_claude_transcript_model_from_reader( } message } else { - if !record - .get("role") - .and_then(Value::as_str) - .is_some_and(|value| value == "assistant") - { + if record.get("role").and_then(Value::as_str) != Some("assistant") { continue; } record @@ -100,8 +96,8 @@ mod tests { use super::*; - fn transcript_reader(content: &str) -> io::Result> { - Ok(Cursor::new(content.as_bytes())) + fn transcript_reader(content: &str) -> Cursor<&[u8]> { + Cursor::new(content.as_bytes()) } #[test] @@ -114,8 +110,10 @@ mod tests { "\n" ); - let model = - extract_claude_transcript_model_from_reader(transcript_reader(transcript), "tool-123"); + let model = extract_claude_transcript_model_from_reader( + Ok(transcript_reader(transcript)), + "tool-123", + ); assert_eq!(model.as_deref(), Some("claude-opus-4-1")); } @@ -142,12 +140,15 @@ mod tests { ); assert_eq!( - extract_claude_transcript_model_from_reader(transcript_reader(unmatched), "tool-123"), + extract_claude_transcript_model_from_reader( + Ok(transcript_reader(unmatched)), + "tool-123" + ), None ); assert_eq!( extract_claude_transcript_model_from_reader( - transcript_reader(missing_model), + Ok(transcript_reader(missing_model)), "tool-123" ), None diff --git a/context/architecture.md b/context/architecture.md index ad444621..3b846676 100644 --- a/context/architecture.md +++ b/context/architecture.md @@ -122,7 +122,7 @@ The repository includes a new placeholder Rust binary crate at `cli/`. - `cli/src/services/db/mod.rs` provides the shared generic Turso infrastructure seam: `DbSpec` supplies a service-specific name, path, ordered embedded migrations, and config-file lookup key (`db_config_key()`), while `TursoDb` owns parent-directory creation, `Builder::new_local(...)` initialization (with `experimental_multiprocess_wal(true)` for safe concurrent access), Turso connection setup, tokio current-thread runtime bridging, retry-backed blocking `execute`/`query`/`query_values`/`query_map` wrappers, and generic migration execution with per-database `__sce_migrations` metadata. `TursoDb::new()` and `EncryptedTursoDb::new()` wrap only their local open/connect block in `run_with_retry_sync` using a config-driven connection-open policy resolved from the `DATABASE_RETRY_CONFIG` `OnceLock` with fallback to hardcoded defaults, while operation methods use a config-driven operation policy from the same source. `query_values()` returns fully fetched column names plus raw `turso::Value` rows for deterministic operator-facing rendering; `query_map()` retries the initial query and row-fetch loop, then applies caller row mapping after retry completion. Migration execution is not retried and uses batch execution so one migration file may contain multiple SQL statements while still recording one migration ID. The same module also provides `EncryptedTursoDb`, a structurally parallel encrypted adapter that resolves the encryption key through `encryption_key::get_or_create_encryption_key()`, enables Turso local encryption with strict `aegis256` cipher selection, and exposes retry-backed synchronous wrappers plus migration execution. `cli/src/services/db/encryption_key.rs` first derives a Turso-compatible 64-character hex key from non-empty `SCE_AUTH_DB_ENCRYPTION_KEY` env-secret text when present, otherwise falls back to keyring-backed credential-store get-or-create behavior; no plaintext auth DB fallback exists. - `cli/src/services/local_db/mod.rs` provides the concrete local DB spec and `LocalDb` type alias over the shared generic `TursoDb` adapter. `LocalDbSpec` resolves the deterministic persistent runtime DB target through the shared default-path seam and declares no local migrations; `TursoDb` supplies retry-backed blocking `execute`/`query`, parent-directory creation, Turso connection setup, tokio current-thread runtime bridging, and generic migration execution. - `cli/src/services/auth_db/mod.rs` provides the encrypted auth DB spec and `AuthDb` type alias over `EncryptedTursoDb`. `AuthDbSpec` resolves `/sce/auth.db` through the shared default-path seam and embeds ordered auth migrations. Auth DB lifecycle setup/doctor integration is wired through `AuthDbLifecycle`; auth command/token-storage reads/writes are directed through `token_storage.rs`. -- `cli/src/services/agent_trace_db/mod.rs` owns the shared Agent Trace insert payloads, SQL constants, and typed row helpers (diff-trace/intersection/Agent Trace/message/part) plus `ensure_schema_ready_for_hooks()` consumed by the repository adapter. `cli/src/services/agent_trace_db/repository.rs` defines the sole `RepositoryAgentTraceDb` adapter over `TursoDb` with one fresh `agent-trace-repository/001_repository_schema.sql` baseline for `diff_traces` (including `payload_type`), `post_commit_patch_intersections`, `agent_traces`, `messages`, `parts`, indexes, and triggers, `repository_metadata` validation, no trace-table `checkout_id` columns, `agent_traces.agent_trace_id NOT NULL UNIQUE`, and `recent_diff_trace_patches(cutoff_time_ms, end_time_ms)` using the inclusive chronological parser without checkout filtering. Active hook runtime, setup/lifecycle storage, and `sce sync` resolve through `agent_trace_storage` and use `RepositoryAgentTraceDb`. The checkout-scoped `AgentTraceDb`/`AgentTraceDbSpec` adapter, its `agent_trace_db_path()`/`agent_trace_db_path_for_checkout()` helpers, the 15-file `cli/migrations/agent-trace/` chain, and the former `sce trace --legacy` surface were removed by the `retire-legacy-agent-trace-db` plan. +- `cli/src/services/agent_trace_db/mod.rs` owns the shared Agent Trace insert payloads, SQL constants, and typed row helpers (diff-trace/intersection/Agent Trace/message/part) plus `ensure_schema_ready_for_hooks()` consumed by the repository adapter. `cli/src/services/agent_trace_db/repository.rs` defines the sole `RepositoryAgentTraceDb` adapter over `TursoDb` with one fresh `agent-trace-repository/001_repository_schema.sql` baseline for `diff_traces` (including `payload_type`), `post_commit_patch_intersections`, `agent_traces`, `messages`, `parts`, indexes, and triggers, `repository_metadata` validation, no trace-table `checkout_id` columns, `agent_traces.agent_trace_id NOT NULL UNIQUE`, and `recent_diff_trace_patches(cutoff_time_ms, end_time_ms)` using the inclusive chronological parser without checkout filtering; structured-row reconstruction applies the persisted row `model_id` to every hunk and the persisted canonical `session_id` to every touched line before downstream combination and intersection. Active hook runtime, setup/lifecycle storage, and `sce sync` resolve through `agent_trace_storage` and use `RepositoryAgentTraceDb`. The checkout-scoped `AgentTraceDb`/`AgentTraceDbSpec` adapter, its `agent_trace_db_path()`/`agent_trace_db_path_for_checkout()` helpers, the 15-file `cli/migrations/agent-trace/` chain, and the former `sce trace --legacy` surface were removed by the `retire-legacy-agent-trace-db` plan. - `cli/src/services/setup/mod.rs` defines the setup command contract (`SetupMode`, `SetupTarget`, `SetupRequest`, CLI flag parser/validator), an `inquire`-backed interactive target prompter (`InquireSetupTargetPrompter`), setup dispatch outcomes (proceed/cancelled), additive durable-context bootstrap (`bootstrap_context_baseline` for standalone `--bootstrap-context` and every normal successful setup path), and compile-time embedded asset access (`EmbeddedAsset`, target-scoped iterators, required-hook asset iterators/lookups). It also owns the install-time optional-workflow seam: the `OptionalWorkflow` type plus the build-generated `OPTIONAL_WORKFLOWS` catalog, a per-target `WorkflowAssetLayout` built from the existing `default_paths` command/skill directory constants (`command`/`commands`/`prompts` plus `skills`), and `iter_embedded_assets_for_setup_target_with_selection(target, selection)`, which yields every embedded asset except the `{command_dir}/{command_slug}.md` file and `{skills_dir}/{skill_slug}/` subtree of each optional workflow the selection omits. Membership is derived from the catalog's slugs rather than an enumerated file list, so a new optional workflow needs no Rust change. This filtered iterator is the only way embedded assets are enumerated; setup installs through it and doctor inspects through it, so there is no unfiltered enumeration path that could reintroduce an unselected workflow. The non-interactive selection flows through the repeatable `--workflow ` flag into `SetupRequest.optional_workflows: Option>` (`None` meaning the flag was absent); `validate_optional_workflow_slugs` checks each slug against `OPTIONAL_WORKFLOWS` during request resolution, before any write, and `run_setup_for_mode` resolves `None` to the persisted `integrations.optional_workflows` (exported as `persisted_optional_workflows`) before installing through the filtered iterator and persisting the resolved selection. The interactive selection flows through the same seam: `SetupTargetPrompter` carries `prompt_target` plus `prompt_optional_workflows(defaults)` (returning `None` for a cancelled prompt), `SetupDispatch::Proceed { mode, optional_workflows }` carries a prompted selection alongside the resolved mode, and `resolve_setup_dispatch(mode, prompter, defaults)` runs the workflow prompt only after an interactive target prompt, mapping either cancellation to `SetupDispatch::Cancelled`. The prompt module builds its `inquire::MultiSelect` from `optional_workflow_prompt_inputs(catalog, defaults)`, which returns `None` for an empty catalog (skipping the prompt) and otherwise catalog-ordered rows plus the indices to pre-check, ignoring ids absent from the catalog. `setup/command.rs` therefore resolves the repository root before dispatch, so a non-git directory fails before any prompt. For repository builds, `cli/build.rs` validates the `SCE_CLI_GENERATED_INPUT_DIR` payload and canonical-input inventories, copies the payload into Cargo `OUT_DIR/pkl-generated`, stages `cli/assets/hooks/**` under `OUT_DIR/static`, requires the staged `config/optional-workflows.json`, and generates both the setup manifest and the optional-workflow catalog (`optional_workflows.rs`, rejecting a manifest whose `schemaVersion` is not 1 or whose entries lack a non-empty `id`/`title`/`description`/`commandSlug`/`skillSlug`) in `OUT_DIR`; focused internal seams separate install-flow from prompt-flow logic; `cli/src/services/setup/command.rs` owns the `SetupCommand` payload used by the static `RuntimeCommand` enum and executes against any context implementing repo-root scoping. Its install engine/orchestrator (`install_assets_for_concrete_target_with_rename`) installs each embedded asset individually: it stages the asset's content next to its final destination and swaps it into place by renaming the staging file directly over the destination — never unlinking the destination first, since `fs::rename` already replaces an existing file atomically — with deterministic recovery guidance naming the failing asset's path on swap failure (the pre-existing destination content, if any, is untouched) and no backup artifact creation; it never removes an integration target directory as a whole, so files a repository owns inside `.opencode`/`.claude`/`.pi` — including nested inside an SCE-owned subdirectory such as `skills/` or `commands/` — survive a setup run. For the two assets that are merge targets — the Claude target's `settings.json`, detected by `is_claude_settings_merge_target`, and the OpenCode target's `opencode.json`, detected by `is_opencode_config_merge_target` — the content staged is not the embedded asset's bytes but the result of `cli/src/services/setup/config_merge.rs::merge_or_create_claude_settings`/`merge_or_create_opencode_config(existing_bytes, generated_bytes, source_path)`: each returns the generated document verbatim when no file exists yet, otherwise parses the existing file as JSON (a parse failure is a hard error naming `source_path`, and nothing is written) and merges it with a pure per-shape function that copies `$schema` from the generated document and preserves every other key from the existing file untouched. `merge_claude_settings` replaces, per hook event key the generated document declares, only the entries whose command contains the ownership marker `run-sce-or-show-install-guidance.sh`, preserving every event key and non-SCE hook entry from the existing file. `merge_opencode_config` merges the `plugin` array as a set: existing entries whose path starts with the ownership marker `./plugins/sce-` are dropped structurally — so a stale plugin path an older or renamed catalog once installed is still recognized and pruned even after the current generated document stops declaring it — and the generated document's `plugin` entries are appended after the surviving entries. After that per-asset install loop, `prune_stale_assets_for_concrete_target` deletes every path the full embedded catalog for the concrete target claims but the resolved selection did not install (a deselected optional workflow, or an asset a newer catalog renamed or dropped), and `remove_empty_ancestor_directories` removes any parent directory left empty by that deletion, stopping at the target root or at a directory that still holds something such as a user file. It formats deterministic completion messaging; required-hook install orchestration (`install_required_git_hooks`, backed by the rename-injectable `install_required_git_hooks_with_rename`) is a third content-computation seam alongside the two JSON merge targets: `install_single_required_hook_with_rename` computes the bytes to stage with `cli/src/services/setup/hook_merge.rs::merge_or_create_hook(existing_bytes, canonical_bytes, hook_name)` rather than writing `hook_asset.bytes` verbatim — a foreign hook (no SCE managed block, no legacy guidance-URL marker) is kept as an exact byte prefix with the canonical block appended after it, an SCE-owned hook has only its block spliced in place or left unchanged, and a legacy pre-marker hook is replaced wholesale — then follows the same per-file stage/atomic-swap choreography as config-asset install: the staging file is renamed directly over the existing hook without unlinking it first, so a rename failure leaves the prior hook's bytes and executable bit intact, with deterministic recovery guidance on swap failure. `Installed`/`Updated`/`Skipped` are decided against the merged bytes plus the executable bit rather than the canonical asset's raw bytes, so an already-current foreign-plus-block hook reports `Skipped`; `RequiredHookInstallResult.unreachable_block_advisory` is set, and rendered as a named advisory line in setup's hook output, when an appended block follows a foreign hook's zero-indent `exec`/`exit` and so would never run. After the Git gate, setup always ensures the context baseline; context-only requests return there, while normal modes derive a repo-root-scoped context before aggregating static lifecycle provider `setup` dispatch across providers (config → local_db → auth_db → agent_trace_db → hooks when requested), so setup providers consume only repo-root access from the scoped context. - `cli/src/services/setup/mod.rs` keeps those responsibilities inside one file for now, but the current ownership split is explicit: the inline `install` module owns repository-path normalization, staging/swap install behavior, required-hook installation, and filesystem safety guards, while the inline `prompt` module owns interactive target selection and prompt styling. - `cli/src/services/security.rs` provides shared security utilities for deterministic secret redaction (`redact_sensitive_text`) and directory write-permission probes (`ensure_directory_is_writable`) used by app/setup/observability surfaces. diff --git a/context/cli/patch-service.md b/context/cli/patch-service.md index b5888ac2..f40202f0 100644 --- a/context/cli/patch-service.md +++ b/context/cli/patch-service.md @@ -75,6 +75,8 @@ Both functions wrap `serde_json::from_str`/`serde_json::from_slice` and map serd | `intersect_patches` | Post-commit hook runtime | Combines recent patches then intersects with current commit patch | | `combine_patches` | Post-commit hook runtime | Combines chronological recent patches before intersection | +For `payload_type="structured"` database rows, the repository adapter enriches the derived `ParsedPatch` before these set operations: every hunk receives the persisted row `model_id`, and every touched line receives the persisted tool-prefixed canonical row `session_id`. The raw unprefixed session carried inside the stored Claude payload is not reconstructed as touched-line provenance. + Public types consumed by the parser or load helpers have `#[allow(dead_code)]` removed; other module internals that are not yet consumed outside the crate retain `#[allow(dead_code)]`. ## Reconstruction fixture suites diff --git a/context/cli/structured-patch-service.md b/context/cli/structured-patch-service.md index f4835a4a..007f1612 100644 --- a/context/cli/structured-patch-service.md +++ b/context/cli/structured-patch-service.md @@ -28,7 +28,7 @@ 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): `RepositoryAgentTraceDb::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. +Post-commit parsing dispatch through `structured_patch.rs` is implemented: `RepositoryAgentTraceDb::recent_diff_trace_patches` 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. Before downstream combine/intersect operations, every structured-row hunk receives the persisted row `model_id` and every touched line receives the persisted tool-prefixed canonical row `session_id`; reconstruction does not reuse the raw unprefixed session inside the stored Claude payload. ## Test status diff --git a/context/context-map.md b/context/context-map.md index e0252a44..af40c926 100644 --- a/context/context-map.md +++ b/context/context-map.md @@ -14,8 +14,8 @@ Feature/domain context: - `context/cli/repository-identity.md` (repository identity module in `cli/src/services/repository_identity/`: pure scheme-neutral `host[:port]/path` canonicalization for SCP/`ssh://`/HTTPS/`git://` remote URLs with credential stripping, hostname lowercasing, default-port removal, and query/fragment/trailing-`.git` cleanup, trim-only explicit-identity handling, `sha256("sce-repository-id-v1\0" + canonical_identity)` repository IDs, credential-safe fieldless errors, plus the `resolve` runtime submodule applying explicit-config-then-configured-remote precedence with `git config --get remote..url` lookup, `RepositoryIdentitySource` provenance, and `.sce/config.json`-guidance resolution errors that never echo URLs; consumed by the T04 `agent_trace_storage` resolver) - `context/cli/agent-trace-storage.md` (repository-scoped Agent Trace storage resolver in `cli/src/services/agent_trace_storage/`: `AgentTraceStorageContext` inputs mirroring the `agent_trace.*` config keys, `ResolvedAgentTraceStorage` carrying repository identity + checkout ID + `/sce/repos//agent-trace.db` path + open `RepositoryAgentTraceDb` + typed `RepositoryMetadata`, `resolve_agent_trace_storage{,_at_state_root}` setup/lifecycle entrypoints with idempotent concurrent-safe first open via bounded fast-path-then-migrate retry plus narrow one-file schema migration-metadata repair and repository metadata validation, a separate no-migration `resolve_agent_trace_storage_for_hook_runtime{,_at_state_root}` pair that high-frequency hook callers use exclusively and that never runs migration `002` or any migration, path-unsafe repository ID rejection in `default_paths::agent_trace_db_path_for_repository{,_at}`, strict never-touch boundary for any pre-migration checkout-scoped/global DB files, and active hook/runtime plus Agent Trace lifecycle setup call-site consumption after T08) - `context/cli/checkout-identity.md` (current checkout identity infrastructure in `cli/src/services/checkout/`, including `/sce/checkout-id` UUIDv7 storage, setup/hook integration that creates/reuses checkout identity as repository-scoped Agent Trace diagnostic metadata, the removed per-checkout DB opener/path helper, `sce doctor` checkout identity display, and the never-touch on-disk handling of pre-migration checkout-scoped DB files that are no longer inspectable via the CLI) -- `context/cli/patch-service.md` (standalone patch domain model, parser, JSON load helpers, and set operations in `cli/src/services/patch.rs` for in-memory parsed unified-diff representation, capturing only touched lines plus minimal per-file/per-hunk metadata, supporting both `Index:` SVN-style and `diff --git` git-style formats, with `ParseError` for actionable malformed-input diagnostics, `PatchLoadError`/`load_patch_from_json`/`load_patch_from_json_bytes` for storage-agnostic JSON reconstruction, `intersect_patches` for target-shaped overlap with exact-match-first and historical `kind`+`content` fallback semantics plus matched-constructed-line `session_id` and matched-constructed-hunk `model_id` provenance inheritance, and `combine_patches` for ordered patch combination with later-wins conflict resolution plus winning-hunk `model_id` provenance inheritance; `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/patch-service.md` (standalone patch domain model, parser, JSON load helpers, and set operations in `cli/src/services/patch.rs` for in-memory parsed unified-diff representation, capturing only touched lines plus minimal per-file/per-hunk metadata, supporting both `Index:` SVN-style and `diff --git` git-style formats, with `ParseError` for actionable malformed-input diagnostics, `PatchLoadError`/`load_patch_from_json`/`load_patch_from_json_bytes` for storage-agnostic JSON reconstruction, `intersect_patches` for target-shaped overlap with exact-match-first and historical `kind`+`content` fallback semantics plus matched-constructed-line `session_id` and matched-constructed-hunk `model_id` provenance inheritance, and `combine_patches` for ordered patch combination with later-wins conflict resolution plus winning-hunk `model_id` provenance inheritance; repository structured-row reconstruction supplies persisted hunk-model and canonical touched-line-session provenance before these operations; `parse_patch`, `intersect_patches`, and `combine_patches` are consumed by the active post-commit hook runtime) +- `context/cli/structured-patch-service.md` (Claude structured editor-hook derivation in `cli/src/services/structured_patch.rs`, including `Write` structured-update hunks, `Write` `tool_input.content` create fallback, `Edit` structured patches, deterministic skip reasons, `ParsedPatch` output semantics, Rust golden fixture coverage, and repository read-time enrichment that assigns persisted row `model_id` to each hunk and canonical row `session_id` to each touched line) - `context/cli/styling-service.md` (CLI text-mode output styling with `owo-colors`, TTY/`NO_COLOR` policy, shared helper API for human-facing surfaces including sync completion markers, and per-column right-to-left RGB gradient banner rendering) - `context/cli/sync-command.md` (the top-level `sce sync` command: repository-scoped Agent Trace storage resolution, WorkOS-authenticated four-stream control-plane synchronization through the sync-owned consumer-typed `services::sync::progress` reporter contract with sync-owned events, its generic/no-op contract and `indicatif` presentation adapter for aligned stderr progress with independent stream completion, explicit successful finalization, JSON stdout silence, and rejection of the removed `sce trace` command group) - `context/cli/agent-trace-sync-command.md` (composed local-to-control-plane `sce sync` architecture: the `hooks/plugins → repository Agent Trace DB → AgentTraceExportReader → sce sync → HTTPS + WorkOS Bearer → control plane` data flow, the `sce auth login` / `cd ` / `sce sync` user flow, the no-local-cursor/no-`agent-trace-sync.db`/no-Turso-Sync/no-`BridgeLock`/no-local-DWH invariants, and `401`/`409`/ambiguous-batch-failure recovery semantics) @@ -62,12 +62,12 @@ Feature/domain context: - `context/sce/local-db.md` (implemented `cli/src/services/local_db/mod.rs` local database spec with `LocalDb = TursoDb`, canonical local DB path resolution, zero local migrations, and inherited retry-backed blocking `execute`/`query`/`query_map` methods using the shared Turso adapter) - `context/sce/shared-turso-db.md` (current shared `cli/src/services/db/mod.rs` Turso database infrastructure seam, including `DbSpec`, generic `TursoDb`, encrypted `EncryptedTursoDb`, build-time generated migration constants from `cli/build.rs`/Cargo `OUT_DIR`, config-driven constructor/open-connect retry via `run_with_retry_sync`, no-migration `TursoDb::open_without_migrations()` / explicit-path `open_without_migrations_at(path)` for hot runtime paths, migration-running `new()` / explicit-path `new_at(path)` / `run_migrations()` with per-database `__sce_migrations` tracking, config-driven operation retry for `execute`/`query`/`query_values`/`query_map` with a `<= 2_000ms` default query failure budget, raw-value row fetching for deterministic operator-facing rendering, row-mapping excluded from retry, generic embedded migration execution, non-mutating `migration_metadata_problems()` and `ensure_schema_ready(setup_guidance)` readiness methods on `TursoDb`, and concrete wrappers for `LocalDb`, `AuthDb`, plus `RepositoryAgentTraceDb`) - `context/sce/auth-db.md` (encrypted `AuthDb = EncryptedTursoDb` adapter, canonical `/sce/auth.db` path, build-time generated `AUTH_MIGRATIONS` from `cli/migrations/auth/`, auth credential schema and updated-at trigger baseline, lifecycle setup/doctor integration, encrypted token-storage persistence, and `SCE_AUTH_DB_ENCRYPTION_KEY`/OS credential-store key handling) -- `context/sce/agent-trace-db.md` (implemented Agent Trace database adapter: the sole repository-scoped `RepositoryAgentTraceDb` backed by the fresh multi-statement baseline schema plus the additive `source_instance_id` migration, with `repository_metadata` carrying both `repository_id` and a concurrency-safe atomic-claim `source_instance_id` (physical database identity, independent of `repository_id`), narrow concurrent-first-open repair for missing one-file baseline migration metadata after all required schema tables exist, no trace-table `checkout_id` columns, repository-level typed insert helpers for diff traces, post-commit intersections, Agent Trace rows, messages, and parts, repository-level recent diff-trace reads without checkout filtering, on-demand command/hook initialization with no daemon/background service, and the never-touch on-disk boundary for any pre-migration checkout-scoped/global DB files; the checkout-scoped `AgentTraceDb` adapter, its `agent_trace_db_path()`/`agent_trace_db_path_for_checkout()` helpers, and the 15-file `cli/migrations/agent-trace/` chain were removed by the `retire-legacy-agent-trace-db` plan; active hook writers/readers and Agent Trace setup/lifecycle resolve repository storage through `agent_trace_storage`) +- `context/sce/agent-trace-db.md` (implemented Agent Trace database adapter: the sole repository-scoped `RepositoryAgentTraceDb` backed by the fresh multi-statement baseline schema plus the additive `source_instance_id` migration, with `repository_metadata` carrying both `repository_id` and a concurrency-safe atomic-claim `source_instance_id` (physical database identity, independent of `repository_id`), narrow concurrent-first-open repair for missing one-file baseline migration metadata after all required schema tables exist, no trace-table `checkout_id` columns, repository-level typed insert helpers for diff traces, post-commit intersections, Agent Trace rows, messages, and parts, repository-level recent diff-trace reads without checkout filtering including persisted hunk-model plus canonical touched-line-session enrichment for structured rows, on-demand command/hook initialization with no daemon/background service, and the never-touch on-disk boundary for any pre-migration checkout-scoped/global DB files; the checkout-scoped `AgentTraceDb` adapter, its `agent_trace_db_path()`/`agent_trace_db_path_for_checkout()` helpers, and the 15-file `cli/migrations/agent-trace/` chain were removed by the `retire-legacy-agent-trace-db` plan; active hook writers/readers and Agent Trace setup/lifecycle resolve repository storage through `agent_trace_storage`) - `context/sce/agent-trace-export-readers.md` (implemented `AgentTraceExportReader<'a>` in `cli/src/services/agent_trace_export/mod.rs`: the read-only local export boundary over `RepositoryAgentTraceDb` — `read_messages_after`/`read_parts_after`/`read_diff_traces_after`/`read_agent_traces_after`, each cursor/limit/JS-safe-integer validated, materialized into owned camelCase `serde::Serialize` DTOs; composes directly with `ResolvedAgentTraceStorage` without owning `source_instance_id`; no local sync cursor, no `agent-trace-sync.db`, no Turso Sync, no ETL, no DWH) - `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 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-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, optional canonical session links derived from matched touched-line provenance, 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) while hook diagnostics route with unprefixed producer-native sessions when available, nullable event-local `model_id`/direct `tool_version` persistence without session fallback, Claude direct-first model metadata extraction from top-level or nested fields followed by fail-open `transcript_path` + `tool_use_id` JSONL lookup when direct metadata is absent, `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 with per-item and first-valid-insert batch diagnostic routing; 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, custom-policy `satisfied_by` wrapper exemption, fixed preset catalog/messages, and precedence rules) diff --git a/context/glossary.md b/context/glossary.md index 8d053b56..7447bb3f 100644 --- a/context/glossary.md +++ b/context/glossary.md @@ -77,7 +77,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`: Modules under `cli/src/services/agent_trace_db/` that define the sole repository-scoped `RepositoryAgentTraceDb = TursoDb` adapter (the checkout-scoped `AgentTraceDb`/`AgentTraceDbSpec` adapter and its 15-file migration chain were removed by the `retire-legacy-agent-trace-db` plan). The repository adapter uses the `agent-trace-repository` migration set (fresh baseline schema plus the additive `source_instance_id` migration) with `repository_metadata`, repository-level `diff_traces`, `post_commit_patch_intersections`, `agent_traces`, `messages`, and `parts` tables, no row-level `checkout_id`, typed parameterized insert helpers, and chronological recent `diff_traces` query/parse support. `AgentTraceDbLifecycle` initializes/checks repository-scoped storage through `agent_trace_storage`. -- `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 `RepositoryAgentTraceDb::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` creates and `Edit` structured patches; wired into `sce hooks diff-trace` for Claude payload classification at intake and into `RepositoryAgentTraceDb::recent_diff_trace_patches` for post-commit structured payload parsing, where persisted row `model_id` is assigned to every hunk and persisted canonical row `session_id` to every touched line before downstream reconstruction. - `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. - `event-local Claude model attribution`: Diff-trace enrichment rule where one Claude `PostToolUse` event resolves its model from direct top-level/nested metadata first, then only from that event's `transcript_path` by matching `tool_use_id` to an assistant envelope's `tool_use.id`; either source receives one `claude/` normalization step, failures remain `NULL`, and no `session_models` table or session-level cache participates. diff --git a/context/overview.md b/context/overview.md index a19650aa..9a071b62 100644 --- a/context/overview.md +++ b/context/overview.md @@ -64,7 +64,7 @@ Every target preserves the same gates and lifecycle semantics through six render Context sync uses an important-change gate: cross-cutting/policy/architecture/terminology changes require root shared-file edits, while localized tasks run verify-only root checks without default churn. OpenCode and Claude no longer generate legacy bootstrap or context-sync skills; `/commit` and `/handover` are generated only as catalog-registered composite workflow packages. OpenCode retains only thin routing agents, while Claude emits no agents. The superseded grouped Markdown catalog and automated OpenCode profile have been removed from Pkl ownership and generated outputs. 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 direct-first/event-transcript-second Claude `model_id` resolution plus direct `tool_version` without any `session_models` runtime, and continues with `None` when event-local lookup cannot resolve 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 prefers direct top-level or nested model metadata, then matches the event's `tool_use_id` in its JSONL `transcript_path`, normalizes either source once with the `claude/` prefix, and fails open to `None`. +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, then assigning each structured hunk the persisted row model and every structured touched line the persisted canonical `cc_...` row session), 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 direct-first/event-transcript-second Claude `model_id` resolution plus direct `tool_version` without any `session_models` runtime, and continues with `None` when event-local lookup cannot resolve 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 prefers direct top-level or nested model metadata, then matches the event's `tool_use_id` in its JSONL `transcript_path`, normalizes either source once with the `claude/` prefix, and fails open to `None`. 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 the sole `RepositoryAgentTraceDb = TursoDb` adapter at `/sce/repos//agent-trace.db`, with a one-file repository schema for `repository_metadata`, `diff_traces`, `post_commit_patch_intersections`, `agent_traces`, `messages`, `parts`, indexes/triggers, and no `checkout_id` columns on trace rows. The checkout-scoped `AgentTraceDb = TursoDb` adapter, its `agent_trace_db_path()`/`agent_trace_db_path_for_checkout()` helpers, and the 15-file `cli/migrations/agent-trace/` chain were removed by the `retire-legacy-agent-trace-db` plan; active hook runtime writes nullable event-local 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-first/event-transcript-second Claude `model_id` plus direct `tool_version` values (no session-model fallback or cache), 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/patterns.md b/context/patterns.md index 50d5cc42..07dc6ede 100644 --- a/context/patterns.md +++ b/context/patterns.md @@ -161,6 +161,7 @@ - For `diff-trace` hook intake, keep producer-facing failure behavior fail-open: STDIN read and parse/validation failures use `sce.hooks.diff_trace.error`; AgentTraceDb open failures use the error-level `sce.hooks.diff_trace.agent_trace_db_open_failed`; later conversion/insert failures retain `sce.hooks.diff_trace.agent_trace_db_write_failed`. Preserve existing output text and emit only the most specific persistence diagnostic for one failure. - For `conversation-trace` hook intake, keep producer-facing failure behavior fail-open: STDIN read, top-level parse/validation, and unsupported raw Claude hook events use `sce.hooks.conversation_trace.error`; AgentTraceDb open failures use the error-level `sce.hooks.conversation_trace.agent_trace_db_open_failed`; later batch inserts retain their existing warning event. Preserve valid-payload mixed-batch accounting, skipped-item logging, output text, and one diagnostic per DB-open failure. - For diff-trace attribution persistence, keep Claude model resolution event-local: prefer direct payload metadata, then use only that event's `transcript_path` and `tool_use_id` for fail-open JSONL lookup, normalize either source through the `claude/` convention, and store unresolved attribution as `NULL` in `diff_traces`. Persist `tool_version` directly. Do not restore the former `session_models` fallback or any session-level cache. +- For recent structured diff-trace reconstruction, treat persisted row attribution as canonical: assign the row `model_id` to every reconstructed hunk and the tool-prefixed row `session_id` to every reconstructed touched line before combination/intersection. Never reuse the raw unprefixed Claude payload session as touched-line provenance. - For commit-msg co-author policy seams, gate canonical trailer insertion on runtime controls (`SCE_DISABLED` plus the shared attribution-hooks enablement gate) plus the staged-diff AI-overlap evidence gate (`StagedDiffAiOverlapResult::Overlap` maps to `ai_contribution_present = true`; `NoOverlap` and `Error` both map to `false`), and enforce idempotent dedupe so allowed cases end with exactly one `Co-authored-by: SCE ` trailer. - For local hook attribution flows, resolve the top-level enablement gate through the shared config precedence model (`SCE_ATTRIBUTION_HOOKS_DISABLED` opt-out env over `policies.attribution_hooks.enabled`, default `true`) so commit-msg attribution is enabled by default while explicit config `enabled = false` and truthy env opt-out still suppress it without adding hook-specific config parsing. - Do not assume conversation-trace retry/backfill/artifact persistence, retry replay, remap ingestion, or rewrite trace transformation are active in the current local-hook runtime; those paths are removed from or deferred beyond the current baseline. diff --git a/context/plans/fix-claude-model-attribution.md b/context/plans/fix-claude-model-attribution.md index cab6c489..4e3c35c3 100644 --- a/context/plans/fix-claude-model-attribution.md +++ b/context/plans/fix-claude-model-attribution.md @@ -10,15 +10,15 @@ Also repair structured diff-trace reconstruction so every touched line receives How this plan is proven complete. Each criterion is observable and names the check that proves it. `/validate` runs these checks; no task in the stack performs final validation. -- [ ] AC1: A supported Claude `PostToolUse` event uses direct model metadata when present; otherwise it resolves the model from the real Claude assistant-message JSONL envelope by matching `tool_use.id` to `tool_use_id`, normalizes the result without double-prefixing, and leaves `model_id` null when lookup cannot succeed. +- [x] AC1: A supported Claude `PostToolUse` event uses direct model metadata when present; otherwise it resolves the model from the real Claude assistant-message JSONL envelope by matching `tool_use.id` to `tool_use_id`, normalizes the result without double-prefixing, and leaves `model_id` null when lookup cannot succeed. - Validate: `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml claude_transcript`; `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml claude_model`. -- [ ] AC2: Transcript lookup is fail-open for missing or unreadable files, unmatched tool calls, missing models, and malformed unrelated JSONL lines, and direct metadata always wins over transcript-derived metadata. +- [x] AC2: Transcript lookup is fail-open for missing or unreadable files, unmatched tool calls, missing models, and malformed unrelated JSONL lines, and direct metadata always wins over transcript-derived metadata. - Validate: focused transcript and resolver unit tests cover all named branches and pass under the commands in AC1. -- [ ] AC3: Reconstructing a `payload_type="structured"` diff-trace row assigns the persisted `row.model_id` to every relevant hunk and the persisted canonical `row.session_id` to every touched line, without reusing the raw unprefixed session from the stored Claude payload. +- [x] AC3: Reconstructing a `payload_type="structured"` diff-trace row assigns the persisted `row.model_id` to every relevant hunk and the persisted canonical `row.session_id` to every touched line, without reusing the raw unprefixed session from the stored Claude payload. - Validate: `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml structured_diff_trace`. -- [ ] AC4: A Claude event with no direct model, a transcript match, and persisted `cc_...` session provenance produces Agent Trace output containing both `contributor.model_id` and a related resource with `type="session"` and the canonical `https://sce.crocoder.dev/sessions/cc_...` URL; existing OpenCode and Pi attribution behavior remains unchanged. +- [x] AC4: A Claude event with no direct model, a transcript match, and persisted `cc_...` session provenance produces Agent Trace output containing both `contributor.model_id` and a related resource with `type="session"` and the canonical `https://sce.crocoder.dev/sessions/cc_...` URL; existing OpenCode and Pi attribution behavior remains unchanged. - Validate: `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml claude_model_attribution`; existing OpenCode/Pi hook and Agent Trace regression tests pass in `nix flake check`. -- [ ] AC5: Current-state Agent Trace documentation describes direct-first/transcript-second/NULL Claude attribution as event-local enrichment, explicitly excludes `session_models` from runtime design, and records canonical persisted-session propagation during structured reconstruction. +- [x] AC5: Current-state Agent Trace documentation describes direct-first/transcript-second/NULL Claude attribution as event-local enrichment, explicitly excludes `session_models` from runtime design, and records canonical persisted-session propagation during structured reconstruction. - Validate: inspect the focused Agent Trace hook, DB, patch, and generator context documents for those statements and confirm no current-state document describes an active `session_models` runtime. ### Full validation @@ -63,14 +63,50 @@ Repository-wide checks `/validate` runs after the last task, regardless of which - Verify: `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml claude_transcript` — pass (3 tests); `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml claude_model` — pass (3 tests). - Context impact: domain — synchronized current-state Agent Trace hook and DB documentation plus root summaries/patterns/glossary entries that previously described direct-only Claude model attribution. -- [ ] T02: `Preserve structured diff-trace model and session provenance` (status:todo) +- [x] T02: `Preserve structured diff-trace model and session provenance` (status:done) - Task ID: T02 - Scope: In — update `parse_recent_diff_trace_patch_rows` so structured-row hunks retain persisted `model_id` and every touched line receives persisted canonical `session_id`; add focused reconstruction coverage and a practical persisted-row-to-Agent-Trace regression proving model plus canonical related session; preserve patch-row behavior. Out — schema/migration changes, backfill, Agent Trace schema changes, OpenCode/Pi attribution changes, and unrelated post-commit refactors. - Dependencies: T01 - Done when: a persisted Claude structured row with `cc_session-123` and a Claude model reconstructs with that model on every relevant hunk and that canonical session on every touched line, and downstream Agent Trace output emits both model attribution and the canonical related-session URL; focused and existing regressions pass. - Verify: `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml structured_diff_trace`; `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml claude_model_attribution`. - - Context synchronization: pending + - Context synchronization: synced + - Completed: 2026-08-19 + - Files changed: `cli/src/services/agent_trace_db/mod.rs` + - Result: Structured diff-trace reconstruction now assigns the persisted row model to every hunk and the persisted canonical row session to every touched line, with persisted-row regressions proving downstream Agent Trace model and related-session output while preserving patch-row behavior. + - Verify: `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml structured_diff_trace` — pass (1 test); `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml claude_model_attribution` — pass (1 test); `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml agent_trace_db::tests` — pass (3 tests). + - Context impact: domain — update current-state patch reconstruction and Agent Trace generator documentation for persisted structured-row model and canonical session propagation; review all five root context files for stale provenance summaries or contracts. ## Open questions None. The request fixes a production attribution loss with explicit precedence, failure, persistence, provenance, compatibility, documentation, and validation contracts; the existing code and historical helper provide sufficient implementation seams without an architecture decision. + +## Validation Report + +**Status:** validated +**Date:** 2026-08-19 + +### Commands run + +- `nix run .#pkl-check-generated` -> exit 0 (ephemeral generation passed for 107 files) +- `nix flake check` -> exit 0 (all x86_64-linux flake checks passed, including CLI tests, Clippy, and formatting) +- `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml claude_transcript` -> exit 0 (3 focused transcript tests passed) +- `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml claude_model` -> exit 0 (4 matching model-attribution tests passed) +- `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml structured_diff_trace` -> exit 0 (1 structured reconstruction test passed) +- `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml claude_model_attribution` -> exit 0 (1 persisted-row Agent Trace attribution test passed) +- `grep -R -n --include='*.md' 'session_models' context | head -200; git status --short` -> exit 0 (current-state references describe the runtime as removed or absent, and no untracked validation artifacts were present) + +### Success-criteria verification + +- [x] AC1: A supported Claude event uses direct-first/transcript-second normalized nullable model attribution -> both focused test commands passed, including real assistant-envelope lookup, direct precedence, normalization, and unresolved `None` behavior. +- [x] AC2: Transcript lookup is fail-open across the named failure branches -> focused tests passed for unreadable, unmatched, missing-model, malformed-record, and direct-precedence cases. +- [x] AC3: Structured reconstruction propagates persisted model and canonical session provenance -> the focused reconstruction test passed. +- [x] AC4: Agent Trace emits Claude model and canonical related-session attribution while OpenCode/Pi regressions remain unchanged -> the focused persisted-row test and repository-wide flake checks passed. +- [x] AC5: Current-state documentation records event-local Claude enrichment, excludes active `session_models`, and documents canonical persisted-session reconstruction -> inspected the focused hook, DB, patch, structured-patch, and generator documents; the context search found no current-state claim of an active `session_models` runtime. + +### Failed checks and follow-ups + +- None. + +### Residual risks + +- Default `nix flake check` validated x86_64-linux; Nix reported that aarch64-darwin, aarch64-linux, and x86_64-darwin checks were omitted as incompatible with the current host. diff --git a/context/sce/agent-trace-db.md b/context/sce/agent-trace-db.md index 288f7174..81c8705b 100644 --- a/context/sce/agent-trace-db.md +++ b/context/sce/agent-trace-db.md @@ -209,7 +209,7 @@ Post-commit intersection rows are written by the active `post-commit` hook flow - SQL reads `id`, `time_ms`, `session_id`, `patch`, nullable `model_id` + `tool_name` + `tool_version`, and `payload_type` from `diff_traces` where `time_ms >= cutoff_time_ms AND time_ms <= end_time_ms`. - Rows are ordered by `time_ms ASC, id ASC` for deterministic chronological processing. - Valid row patches are parsed through `cli/src/services/patch.rs` `parse_patch` for `payload_type="patch"` rows (OpenCode unified-diff payloads), while `payload_type="structured"` rows (Claude `PostToolUse` structured payloads) are parsed from stored JSON through `cli/src/services/structured_patch.rs` `derive_claude_structured_patch` at read time to produce `ParsedPatch` without pre-rendered unified-diff text. -- Each produced `PatchHunk` is annotated with the originating row `model_id` (`Some(value)` propagated verbatim, `NULL` propagated as `None`) for both patch and structured paths; parsed row records also carry nullable `tool_name`/`tool_version` and `payload_type` from the same source row and are returned as `ParsedDiffTracePatch` records. +- Each produced `PatchHunk` is annotated with the originating row `model_id` (`Some(value)` propagated verbatim, `NULL` propagated as `None`) for both patch and structured paths. For structured rows, every produced touched line is also assigned the persisted tool-prefixed canonical row `session_id`; the raw unprefixed session inside the stored Claude payload is not used as reconstructed touched-line provenance. Parsed row records carry nullable `tool_name`/`tool_version` and `payload_type` from the same source row and are returned as `ParsedDiffTracePatch` records. - Malformed recent row patches (invalid unified-diff text, invalid structured JSON, unsupported payload types, or unsupported Claude structured payloads) are returned as `SkippedDiffTracePatch` records with deterministic parse-error or derivation-skip reasons; malformed historical rows do not fail the operation. - `RecentDiffTracePatches::loaded_count()` and `skipped_count()` expose accounting for later hook output and persistence metadata. diff --git a/context/sce/agent-trace-minimal-generator.md b/context/sce/agent-trace-minimal-generator.md index c9ffe245..d0c3e4f9 100644 --- a/context/sce/agent-trace-minimal-generator.md +++ b/context/sce/agent-trace-minimal-generator.md @@ -13,7 +13,7 @@ Given a `constructed_patch` (AI candidate) and a `post_commit_patch` (canonical - **`mixed`** — `intersection_patch` hunk exists at the same slot but content differs. - **`unknown`** — no `intersection_patch` hunk at the same `old_start` slot. 4. Map `Conversation.contributor.model_id` from the matched `intersection_patch` hunk when contributor type is `ai` or `mixed`; omit `model_id` when provenance is missing (`None`). -5. For each emitted conversation, derive optional `conversation.related` entries from non-empty `session_id` values on touched lines in the matched `intersection_patch` hunk; emit related entries as `{ "type": "session", "url": "https://sce.crocoder.dev/sessions/" }`, deduplicated by session ID with deterministic ordering, and omit `related` when no included lines provide `session_id`. +5. For each emitted conversation, derive optional `conversation.related` entries from non-empty `session_id` values on touched lines in the matched `intersection_patch` hunk; emit related entries as `{ "type": "session", "url": "https://sce.crocoder.dev/sessions/" }`, deduplicated by session ID with deterministic ordering, and omit `related` when no included lines provide `session_id`. Structured diff-trace reconstruction supplies the persisted canonical `cc_...` row session on every touched line, so Claude related-session URLs use canonical persisted provenance rather than the raw payload session. 6. Emit one `Conversation` per `post_commit_patch` hunk, each carrying the trace lookup `url`, one `TraceFile` per `post_commit_patch` file, and one range per hunk with a deterministic `content_hash` computed from that hunk's touched-line kind/content. ## Domain types