diff --git a/.claude/hooks/run-sce-or-show-install-guidance.sh b/.claude/hooks/run-sce-or-show-install-guidance.sh new file mode 100644 index 00000000..a01dc897 --- /dev/null +++ b/.claude/hooks/run-sce-or-show-install-guidance.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +set -euo pipefail + +if ! command -v sce >/dev/null 2>&1; then + echo "sce CLI not found. Install it from https://sce.crocoder.dev/docs/getting-started#install-cli" >&2 + exit 0 +fi + +exec "$@" \ No newline at end of file diff --git a/.claude/settings.json b/.claude/settings.json index a143c4ba..51c102e7 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -6,7 +6,7 @@ "hooks": [ { "type": "command", - "command": "sce hooks session-model" + "command": "bash .claude/hooks/run-sce-or-show-install-guidance.sh sce hooks session-model" } ] } @@ -17,7 +17,7 @@ "hooks": [ { "type": "command", - "command": "sce policy bash" + "command": "bash .claude/hooks/run-sce-or-show-install-guidance.sh sce policy bash" } ] } @@ -28,7 +28,7 @@ "hooks": [ { "type": "command", - "command": "sce hooks diff-trace" + "command": "bash .claude/hooks/run-sce-or-show-install-guidance.sh sce hooks diff-trace" } ] }, @@ -36,7 +36,7 @@ "hooks": [ { "type": "command", - "command": "sce hooks conversation-trace" + "command": "bash .claude/hooks/run-sce-or-show-install-guidance.sh sce hooks conversation-trace" } ] } @@ -46,7 +46,7 @@ "hooks": [ { "type": "command", - "command": "sce hooks conversation-trace" + "command": "bash .claude/hooks/run-sce-or-show-install-guidance.sh sce hooks conversation-trace" } ] } @@ -56,7 +56,7 @@ "hooks": [ { "type": "command", - "command": "sce hooks conversation-trace" + "command": "bash .claude/hooks/run-sce-or-show-install-guidance.sh sce hooks conversation-trace" } ] } diff --git a/.opencode/plugins/sce-agent-trace.ts b/.opencode/plugins/sce-agent-trace.ts index ab5d6143..4a74bd3a 100644 --- a/.opencode/plugins/sce-agent-trace.ts +++ b/.opencode/plugins/sce-agent-trace.ts @@ -3,6 +3,9 @@ import type { Hooks, Plugin } from "@opencode-ai/plugin"; type OpenCodeEvent = Parameters>[0]["event"]; +const SCE_INSTALL_URL = + "https://sce.crocoder.dev/docs/getting-started#install-cli"; + const REQUIRED_EVENTS: Set = new Set([ "message.updated", "message.part.updated", @@ -323,26 +326,24 @@ async function runDiffTraceHook( tool_version: string | null; }, ): Promise { - await new Promise((resolve, reject) => { + await new Promise((resolve) => { const child = spawn("sce", ["hooks", "diff-trace"], { cwd: repoRoot, - stdio: ["pipe", "ignore", "inherit"], + // Fail-open: stderr is ignored so that sce intake errors + // (connection refused, timeout, etc.) do not leak into the + // OpenCode TUI. Resolve unconditionally on any outcome. + stdio: ["pipe", "ignore", "ignore"], }); - child.on("error", reject); - - child.on("close", (code, signal) => { - if (code === 0) { - resolve(); - return; + child.on("error", (err: NodeJS.ErrnoException) => { + if (err.code === "ENOENT") { + console.warn( + `sce CLI not found. Install it from ${SCE_INSTALL_URL}`, + ); } - - const reason = - signal === null ? `exit code ${String(code)}` : `signal ${signal}`; - reject( - new Error(`Command 'sce hooks diff-trace' failed with ${reason}.`), - ); + resolve(); }); + child.on("close", () => resolve()); child.stdin.end(`${JSON.stringify(payload)}\n`); }); @@ -352,28 +353,24 @@ async function runConversationTraceHook( repoRoot: string, payload: ConversationTracePayload, ): Promise { - await new Promise((resolve, reject) => { + await new Promise((resolve) => { const child = spawn("sce", ["hooks", "conversation-trace"], { cwd: repoRoot, - stdio: ["pipe", "ignore", "inherit"], + // Fail-open: stderr is ignored so that sce intake errors + // (connection refused, timeout, etc.) do not leak into the + // OpenCode TUI. Resolve unconditionally on any outcome. + stdio: ["pipe", "ignore", "ignore"], }); - child.on("error", reject); - - child.on("close", (code, signal) => { - if (code === 0) { - resolve(); - return; + child.on("error", (err: NodeJS.ErrnoException) => { + if (err.code === "ENOENT") { + console.warn( + `sce CLI not found. Install it from ${SCE_INSTALL_URL}`, + ); } - - const reason = - signal === null ? `exit code ${String(code)}` : `signal ${signal}`; - reject( - new Error( - `Command 'sce hooks conversation-trace' failed with ${reason}.`, - ), - ); + resolve(); }); + child.on("close", () => resolve()); child.stdin.end(`${JSON.stringify(payload)}\n`); }); diff --git a/.opencode/plugins/sce-bash-policy.ts b/.opencode/plugins/sce-bash-policy.ts index 6368d3fc..788929f3 100644 --- a/.opencode/plugins/sce-bash-policy.ts +++ b/.opencode/plugins/sce-bash-policy.ts @@ -10,6 +10,9 @@ interface JsonPolicyResult { policy_id?: string; } +const SCE_INSTALL_URL = + "https://sce.crocoder.dev/docs/getting-started#install-cli"; + /** * Evaluate a bash command against SCE bash-tool policy by delegating to the * Rust `sce policy bash` command. Returns the parsed JSON result, or null if @@ -28,6 +31,11 @@ function evaluateBashCommandPolicy(command: string): JsonPolicyResult | null { ); if (result.error) { + if ((result.error as NodeJS.ErrnoException).code === "ENOENT") { + console.warn( + `sce CLI not found. Install it from ${SCE_INSTALL_URL}`, + ); + } return null; } diff --git a/cli/assets/hooks/post-commit b/cli/assets/hooks/post-commit index a4d7935b..d8b83e50 100644 --- a/cli/assets/hooks/post-commit +++ b/cli/assets/hooks/post-commit @@ -3,6 +3,11 @@ set -eu remote_url="$(git remote get-url origin 2>/dev/null || true)" +if ! command -v sce >/dev/null 2>&1; then + echo "sce CLI not found. Install it from https://sce.crocoder.dev/docs/getting-started#install-cli" >&2 + exit 0 +fi + if [ -n "$remote_url" ]; then exec sce hooks post-commit --vcs git --remote-url "$remote_url" "$@" fi diff --git a/cli/src/services/hooks/mod.rs b/cli/src/services/hooks/mod.rs index bfbad9b3..7842377e 100644 --- a/cli/src/services/hooks/mod.rs +++ b/cli/src/services/hooks/mod.rs @@ -197,9 +197,9 @@ fn run_hooks_subcommand_in_repo( HookSubcommand::PostRewrite { rewrite_method } => { run_post_rewrite_subcommand_with_trace(repository_root, subcommand, rewrite_method) } - HookSubcommand::DiffTrace => run_diff_trace_subcommand(repository_root, logger), + HookSubcommand::DiffTrace => Ok(run_diff_trace_subcommand(repository_root, logger)), HookSubcommand::ConversationTrace => { - run_conversation_trace_subcommand(repository_root, logger) + Ok(run_conversation_trace_subcommand(repository_root, logger)) } HookSubcommand::SessionModel => run_session_model_subcommand(repository_root, logger), } @@ -208,20 +208,16 @@ fn run_hooks_subcommand_in_repo( fn run_conversation_trace_subcommand( repository_root: &Path, logger: Option<&dyn Logger>, -) -> Result { - let stdin_payload = read_hook_stdin()?; - let result = - run_conversation_trace_subcommand_from_payload(repository_root, &stdin_payload, logger); - if let Err(ref error) = result { - if let Some(log) = logger { - log.error( - "sce.hooks.conversation_trace.error", - &error.to_string(), - &[], - ); - } +) -> String { + let stdin_payload = match read_hook_stdin() { + Ok(payload) => payload, + Err(error) => return log_conversation_trace_fail_open(&error, logger), + }; + + match run_conversation_trace_subcommand_from_payload(repository_root, &stdin_payload, logger) { + Ok(output) => output, + Err(error) => log_conversation_trace_fail_open(&error, logger), } - result } fn run_conversation_trace_subcommand_from_payload( @@ -233,6 +229,18 @@ fn run_conversation_trace_subcommand_from_payload( persist_conversation_trace_payload_to_agent_trace_db(repository_root, payload, logger) } +fn log_conversation_trace_fail_open(error: &anyhow::Error, logger: Option<&dyn Logger>) -> String { + if let Some(log) = logger { + log.error( + "sce.hooks.conversation_trace.error", + &error.to_string(), + &[], + ); + } + + String::from("conversation-trace hook intake failed open; error logged.") +} + fn persist_conversation_trace_payload_to_agent_trace_db( repository_root: &Path, payload: ConversationTracePayload, @@ -666,18 +674,16 @@ fn conversation_trace_validation_error(detail: &str) -> String { format!("Invalid conversation-trace payload from STDIN: {detail}.") } -fn run_diff_trace_subcommand( - repository_root: &Path, - logger: Option<&dyn Logger>, -) -> Result { - let stdin_payload = read_hook_stdin()?; - let result = run_diff_trace_subcommand_from_payload(repository_root, &stdin_payload, logger); - if let Err(ref error) = result { - if let Some(log) = logger { - log.error("sce.hooks.diff_trace.error", &error.to_string(), &[]); - } +fn run_diff_trace_subcommand(repository_root: &Path, logger: Option<&dyn Logger>) -> String { + let stdin_payload = match read_hook_stdin() { + Ok(payload) => payload, + Err(error) => return log_diff_trace_fail_open(&error, logger), + }; + + match run_diff_trace_subcommand_from_payload(repository_root, &stdin_payload, logger) { + Ok(output) => output, + Err(error) => log_diff_trace_fail_open(&error, logger), } - result } fn run_diff_trace_subcommand_from_payload( @@ -711,6 +717,14 @@ fn run_diff_trace_subcommand_from_payload( ) } +fn log_diff_trace_fail_open(error: &anyhow::Error, logger: Option<&dyn Logger>) -> String { + if let Some(log) = logger { + log.error("sce.hooks.diff_trace.error", &error.to_string(), &[]); + } + + String::from("diff-trace hook intake failed open; error logged.") +} + fn run_diff_trace_subcommand_from_payload_with( repository_root: &Path, payload: &DiffTracePayload, diff --git a/config/.claude/hooks/run-sce-or-show-install-guidance.sh b/config/.claude/hooks/run-sce-or-show-install-guidance.sh new file mode 100644 index 00000000..a01dc897 --- /dev/null +++ b/config/.claude/hooks/run-sce-or-show-install-guidance.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +set -euo pipefail + +if ! command -v sce >/dev/null 2>&1; then + echo "sce CLI not found. Install it from https://sce.crocoder.dev/docs/getting-started#install-cli" >&2 + exit 0 +fi + +exec "$@" \ No newline at end of file diff --git a/config/.claude/settings.json b/config/.claude/settings.json index a143c4ba..51c102e7 100644 --- a/config/.claude/settings.json +++ b/config/.claude/settings.json @@ -6,7 +6,7 @@ "hooks": [ { "type": "command", - "command": "sce hooks session-model" + "command": "bash .claude/hooks/run-sce-or-show-install-guidance.sh sce hooks session-model" } ] } @@ -17,7 +17,7 @@ "hooks": [ { "type": "command", - "command": "sce policy bash" + "command": "bash .claude/hooks/run-sce-or-show-install-guidance.sh sce policy bash" } ] } @@ -28,7 +28,7 @@ "hooks": [ { "type": "command", - "command": "sce hooks diff-trace" + "command": "bash .claude/hooks/run-sce-or-show-install-guidance.sh sce hooks diff-trace" } ] }, @@ -36,7 +36,7 @@ "hooks": [ { "type": "command", - "command": "sce hooks conversation-trace" + "command": "bash .claude/hooks/run-sce-or-show-install-guidance.sh sce hooks conversation-trace" } ] } @@ -46,7 +46,7 @@ "hooks": [ { "type": "command", - "command": "sce hooks conversation-trace" + "command": "bash .claude/hooks/run-sce-or-show-install-guidance.sh sce hooks conversation-trace" } ] } @@ -56,7 +56,7 @@ "hooks": [ { "type": "command", - "command": "sce hooks conversation-trace" + "command": "bash .claude/hooks/run-sce-or-show-install-guidance.sh sce hooks conversation-trace" } ] } diff --git a/config/.opencode/plugins/sce-agent-trace.ts b/config/.opencode/plugins/sce-agent-trace.ts index ab5d6143..4a74bd3a 100644 --- a/config/.opencode/plugins/sce-agent-trace.ts +++ b/config/.opencode/plugins/sce-agent-trace.ts @@ -3,6 +3,9 @@ import type { Hooks, Plugin } from "@opencode-ai/plugin"; type OpenCodeEvent = Parameters>[0]["event"]; +const SCE_INSTALL_URL = + "https://sce.crocoder.dev/docs/getting-started#install-cli"; + const REQUIRED_EVENTS: Set = new Set([ "message.updated", "message.part.updated", @@ -323,26 +326,24 @@ async function runDiffTraceHook( tool_version: string | null; }, ): Promise { - await new Promise((resolve, reject) => { + await new Promise((resolve) => { const child = spawn("sce", ["hooks", "diff-trace"], { cwd: repoRoot, - stdio: ["pipe", "ignore", "inherit"], + // Fail-open: stderr is ignored so that sce intake errors + // (connection refused, timeout, etc.) do not leak into the + // OpenCode TUI. Resolve unconditionally on any outcome. + stdio: ["pipe", "ignore", "ignore"], }); - child.on("error", reject); - - child.on("close", (code, signal) => { - if (code === 0) { - resolve(); - return; + child.on("error", (err: NodeJS.ErrnoException) => { + if (err.code === "ENOENT") { + console.warn( + `sce CLI not found. Install it from ${SCE_INSTALL_URL}`, + ); } - - const reason = - signal === null ? `exit code ${String(code)}` : `signal ${signal}`; - reject( - new Error(`Command 'sce hooks diff-trace' failed with ${reason}.`), - ); + resolve(); }); + child.on("close", () => resolve()); child.stdin.end(`${JSON.stringify(payload)}\n`); }); @@ -352,28 +353,24 @@ async function runConversationTraceHook( repoRoot: string, payload: ConversationTracePayload, ): Promise { - await new Promise((resolve, reject) => { + await new Promise((resolve) => { const child = spawn("sce", ["hooks", "conversation-trace"], { cwd: repoRoot, - stdio: ["pipe", "ignore", "inherit"], + // Fail-open: stderr is ignored so that sce intake errors + // (connection refused, timeout, etc.) do not leak into the + // OpenCode TUI. Resolve unconditionally on any outcome. + stdio: ["pipe", "ignore", "ignore"], }); - child.on("error", reject); - - child.on("close", (code, signal) => { - if (code === 0) { - resolve(); - return; + child.on("error", (err: NodeJS.ErrnoException) => { + if (err.code === "ENOENT") { + console.warn( + `sce CLI not found. Install it from ${SCE_INSTALL_URL}`, + ); } - - const reason = - signal === null ? `exit code ${String(code)}` : `signal ${signal}`; - reject( - new Error( - `Command 'sce hooks conversation-trace' failed with ${reason}.`, - ), - ); + resolve(); }); + child.on("close", () => resolve()); child.stdin.end(`${JSON.stringify(payload)}\n`); }); diff --git a/config/.opencode/plugins/sce-bash-policy.ts b/config/.opencode/plugins/sce-bash-policy.ts index 6368d3fc..788929f3 100644 --- a/config/.opencode/plugins/sce-bash-policy.ts +++ b/config/.opencode/plugins/sce-bash-policy.ts @@ -10,6 +10,9 @@ interface JsonPolicyResult { policy_id?: string; } +const SCE_INSTALL_URL = + "https://sce.crocoder.dev/docs/getting-started#install-cli"; + /** * Evaluate a bash command against SCE bash-tool policy by delegating to the * Rust `sce policy bash` command. Returns the parsed JSON result, or null if @@ -28,6 +31,11 @@ function evaluateBashCommandPolicy(command: string): JsonPolicyResult | null { ); if (result.error) { + if ((result.error as NodeJS.ErrnoException).code === "ENOENT") { + console.warn( + `sce CLI not found. Install it from ${SCE_INSTALL_URL}`, + ); + } return null; } diff --git a/config/automated/.opencode/plugins/sce-agent-trace.ts b/config/automated/.opencode/plugins/sce-agent-trace.ts index ab5d6143..4a74bd3a 100644 --- a/config/automated/.opencode/plugins/sce-agent-trace.ts +++ b/config/automated/.opencode/plugins/sce-agent-trace.ts @@ -3,6 +3,9 @@ import type { Hooks, Plugin } from "@opencode-ai/plugin"; type OpenCodeEvent = Parameters>[0]["event"]; +const SCE_INSTALL_URL = + "https://sce.crocoder.dev/docs/getting-started#install-cli"; + const REQUIRED_EVENTS: Set = new Set([ "message.updated", "message.part.updated", @@ -323,26 +326,24 @@ async function runDiffTraceHook( tool_version: string | null; }, ): Promise { - await new Promise((resolve, reject) => { + await new Promise((resolve) => { const child = spawn("sce", ["hooks", "diff-trace"], { cwd: repoRoot, - stdio: ["pipe", "ignore", "inherit"], + // Fail-open: stderr is ignored so that sce intake errors + // (connection refused, timeout, etc.) do not leak into the + // OpenCode TUI. Resolve unconditionally on any outcome. + stdio: ["pipe", "ignore", "ignore"], }); - child.on("error", reject); - - child.on("close", (code, signal) => { - if (code === 0) { - resolve(); - return; + child.on("error", (err: NodeJS.ErrnoException) => { + if (err.code === "ENOENT") { + console.warn( + `sce CLI not found. Install it from ${SCE_INSTALL_URL}`, + ); } - - const reason = - signal === null ? `exit code ${String(code)}` : `signal ${signal}`; - reject( - new Error(`Command 'sce hooks diff-trace' failed with ${reason}.`), - ); + resolve(); }); + child.on("close", () => resolve()); child.stdin.end(`${JSON.stringify(payload)}\n`); }); @@ -352,28 +353,24 @@ async function runConversationTraceHook( repoRoot: string, payload: ConversationTracePayload, ): Promise { - await new Promise((resolve, reject) => { + await new Promise((resolve) => { const child = spawn("sce", ["hooks", "conversation-trace"], { cwd: repoRoot, - stdio: ["pipe", "ignore", "inherit"], + // Fail-open: stderr is ignored so that sce intake errors + // (connection refused, timeout, etc.) do not leak into the + // OpenCode TUI. Resolve unconditionally on any outcome. + stdio: ["pipe", "ignore", "ignore"], }); - child.on("error", reject); - - child.on("close", (code, signal) => { - if (code === 0) { - resolve(); - return; + child.on("error", (err: NodeJS.ErrnoException) => { + if (err.code === "ENOENT") { + console.warn( + `sce CLI not found. Install it from ${SCE_INSTALL_URL}`, + ); } - - const reason = - signal === null ? `exit code ${String(code)}` : `signal ${signal}`; - reject( - new Error( - `Command 'sce hooks conversation-trace' failed with ${reason}.`, - ), - ); + resolve(); }); + child.on("close", () => resolve()); child.stdin.end(`${JSON.stringify(payload)}\n`); }); diff --git a/config/automated/.opencode/plugins/sce-bash-policy.ts b/config/automated/.opencode/plugins/sce-bash-policy.ts index 6368d3fc..788929f3 100644 --- a/config/automated/.opencode/plugins/sce-bash-policy.ts +++ b/config/automated/.opencode/plugins/sce-bash-policy.ts @@ -10,6 +10,9 @@ interface JsonPolicyResult { policy_id?: string; } +const SCE_INSTALL_URL = + "https://sce.crocoder.dev/docs/getting-started#install-cli"; + /** * Evaluate a bash command against SCE bash-tool policy by delegating to the * Rust `sce policy bash` command. Returns the parsed JSON result, or null if @@ -28,6 +31,11 @@ function evaluateBashCommandPolicy(command: string): JsonPolicyResult | null { ); if (result.error) { + if ((result.error as NodeJS.ErrnoException).code === "ENOENT") { + console.warn( + `sce CLI not found. Install it from ${SCE_INSTALL_URL}`, + ); + } return null; } diff --git a/config/lib/agent-trace-plugin/opencode-sce-agent-trace-plugin.ts b/config/lib/agent-trace-plugin/opencode-sce-agent-trace-plugin.ts index ab5d6143..980ceb06 100644 --- a/config/lib/agent-trace-plugin/opencode-sce-agent-trace-plugin.ts +++ b/config/lib/agent-trace-plugin/opencode-sce-agent-trace-plugin.ts @@ -3,6 +3,9 @@ import type { Hooks, Plugin } from "@opencode-ai/plugin"; type OpenCodeEvent = Parameters>[0]["event"]; +const SCE_INSTALL_URL = + "https://sce.crocoder.dev/docs/getting-started#install-cli"; + const REQUIRED_EVENTS: Set = new Set([ "message.updated", "message.part.updated", @@ -323,26 +326,22 @@ async function runDiffTraceHook( tool_version: string | null; }, ): Promise { - await new Promise((resolve, reject) => { + await new Promise((resolve) => { const child = spawn("sce", ["hooks", "diff-trace"], { cwd: repoRoot, - stdio: ["pipe", "ignore", "inherit"], + // Fail-open: stderr is ignored so that sce intake errors + // (connection refused, timeout, etc.) do not leak into the + // OpenCode TUI. Resolve unconditionally on any outcome. + stdio: ["pipe", "ignore", "ignore"], }); - child.on("error", reject); - - child.on("close", (code, signal) => { - if (code === 0) { - resolve(); - return; + child.on("error", (err: NodeJS.ErrnoException) => { + if (err.code === "ENOENT") { + console.warn(`sce CLI not found. Install it from ${SCE_INSTALL_URL}`); } - - const reason = - signal === null ? `exit code ${String(code)}` : `signal ${signal}`; - reject( - new Error(`Command 'sce hooks diff-trace' failed with ${reason}.`), - ); + resolve(); }); + child.on("close", () => resolve()); child.stdin.end(`${JSON.stringify(payload)}\n`); }); @@ -352,28 +351,22 @@ async function runConversationTraceHook( repoRoot: string, payload: ConversationTracePayload, ): Promise { - await new Promise((resolve, reject) => { + await new Promise((resolve) => { const child = spawn("sce", ["hooks", "conversation-trace"], { cwd: repoRoot, - stdio: ["pipe", "ignore", "inherit"], + // Fail-open: stderr is ignored so that sce intake errors + // (connection refused, timeout, etc.) do not leak into the + // OpenCode TUI. Resolve unconditionally on any outcome. + stdio: ["pipe", "ignore", "ignore"], }); - child.on("error", reject); - - child.on("close", (code, signal) => { - if (code === 0) { - resolve(); - return; + child.on("error", (err: NodeJS.ErrnoException) => { + if (err.code === "ENOENT") { + console.warn(`sce CLI not found. Install it from ${SCE_INSTALL_URL}`); } - - const reason = - signal === null ? `exit code ${String(code)}` : `signal ${signal}`; - reject( - new Error( - `Command 'sce hooks conversation-trace' failed with ${reason}.`, - ), - ); + resolve(); }); + child.on("close", () => resolve()); child.stdin.end(`${JSON.stringify(payload)}\n`); }); diff --git a/config/lib/bash-policy-plugin/bash-policy-runtime.test.ts b/config/lib/bash-policy-plugin/bash-policy-runtime.test.ts index 32652f15..9e2edc96 100644 --- a/config/lib/bash-policy-plugin/bash-policy-runtime.test.ts +++ b/config/lib/bash-policy-plugin/bash-policy-runtime.test.ts @@ -34,11 +34,12 @@ mock.module("node:child_process", () => ({ } if (mockSpawnSyncResult.error) { + const err = mockSpawnSyncResult.error; return { status: 1, stdout: "", stderr: "", - error: mockSpawnSyncResult.error, + error: Object.assign(err, { code: "ENOENT" }), }; } diff --git a/config/lib/bash-policy-plugin/opencode-bash-policy-plugin.ts b/config/lib/bash-policy-plugin/opencode-bash-policy-plugin.ts index 6368d3fc..0b2b8d9c 100644 --- a/config/lib/bash-policy-plugin/opencode-bash-policy-plugin.ts +++ b/config/lib/bash-policy-plugin/opencode-bash-policy-plugin.ts @@ -10,6 +10,9 @@ interface JsonPolicyResult { policy_id?: string; } +const SCE_INSTALL_URL = + "https://sce.crocoder.dev/docs/getting-started#install-cli"; + /** * Evaluate a bash command against SCE bash-tool policy by delegating to the * Rust `sce policy bash` command. Returns the parsed JSON result, or null if @@ -28,6 +31,9 @@ function evaluateBashCommandPolicy(command: string): JsonPolicyResult | null { ); if (result.error) { + if ((result.error as NodeJS.ErrnoException).code === "ENOENT") { + console.warn(`sce CLI not found. Install it from ${SCE_INSTALL_URL}`); + } return null; } diff --git a/config/pkl/generate.pkl b/config/pkl/generate.pkl index 28ef5bf9..d79cdfe7 100644 --- a/config/pkl/generate.pkl +++ b/config/pkl/generate.pkl @@ -44,6 +44,9 @@ output { ["config/.claude/settings.json"] { text = claude.settings.rendered } + ["config/.claude/hooks/run-sce-or-show-install-guidance.sh"] { + text = claude.sceHookScript.rendered + } for (slug, document in opencode.skills) { ["config/.opencode/skills/\(slug)/SKILL.md"] { diff --git a/config/pkl/renderers/claude-content.pkl b/config/pkl/renderers/claude-content.pkl index 8bd50eb8..bc30e7e0 100644 --- a/config/pkl/renderers/claude-content.pkl +++ b/config/pkl/renderers/claude-content.pkl @@ -2,6 +2,12 @@ import "../base/shared-content.pkl" as shared import "common.pkl" as common import "claude-metadata.pkl" as metadata +local missingSceInstallMessage = "sce CLI not found. Install it from https://sce.crocoder.dev/docs/getting-started#install-cli" + +local claudeSceHookScriptPath = ".claude/hooks/run-sce-or-show-install-guidance.sh" + +local function sceHookCommand(arguments: String): String = "bash \(claudeSceHookScriptPath) \(arguments)" + local agentFrontmatterBySlug = new Mapping { for (unitSlug, _ in shared.agents) { [unitSlug] = """ @@ -61,7 +67,7 @@ settings = new common.RenderedTextFile { "hooks": [ { "type": "command", - "command": "sce hooks session-model" + "command": "\(sceHookCommand("sce hooks session-model"))" } ] } @@ -72,7 +78,7 @@ settings = new common.RenderedTextFile { "hooks": [ { "type": "command", - "command": "sce policy bash" + "command": "\(sceHookCommand("sce policy bash"))" } ] } @@ -83,7 +89,7 @@ settings = new common.RenderedTextFile { "hooks": [ { "type": "command", - "command": "sce hooks diff-trace" + "command": "\(sceHookCommand("sce hooks diff-trace"))" } ] }, @@ -91,7 +97,7 @@ settings = new common.RenderedTextFile { "hooks": [ { "type": "command", - "command": "sce hooks conversation-trace" + "command": "\(sceHookCommand("sce hooks conversation-trace"))" } ] } @@ -101,7 +107,7 @@ settings = new common.RenderedTextFile { "hooks": [ { "type": "command", - "command": "sce hooks conversation-trace" + "command": "\(sceHookCommand("sce hooks conversation-trace"))" } ] } @@ -111,7 +117,7 @@ settings = new common.RenderedTextFile { "hooks": [ { "type": "command", - "command": "sce hooks conversation-trace" + "command": "\(sceHookCommand("sce hooks conversation-trace"))" } ] } @@ -121,6 +127,21 @@ settings = new common.RenderedTextFile { """ } +sceHookScript = new common.RenderedTextFile { + slug = "sce-hook" + rendered = """ +#!/usr/bin/env bash +set -euo pipefail + +if ! command -v sce >/dev/null 2>&1; then + echo "\(missingSceInstallMessage)" >&2 + exit 0 +fi + +exec "$@" +""" +} + agents { for (unitSlug, unit in shared.agents) { [unitSlug] = new common.RenderedTargetDocument { diff --git a/context/architecture.md b/context/architecture.md index daf4c6e9..a2c55518 100644 --- a/context/architecture.md +++ b/context/architecture.md @@ -50,7 +50,7 @@ Renderer modules apply target-specific metadata/frontmatter rules while reusing - Target-specific metadata tables, including skill frontmatter descriptions, are isolated in `config/pkl/renderers/opencode-metadata.pkl`, `config/pkl/renderers/opencode-automated-metadata.pkl`, and `config/pkl/renderers/claude-metadata.pkl`. - Metadata key coverage is enforced by `config/pkl/renderers/metadata-coverage-check.pkl`, which resolves all required lookup keys for both targets and fails evaluation on missing entries. - Both renderers expose per-class rendered document objects (`agents`, `commands`, `skills`) consumed by `config/pkl/generate.pkl`. -- `config/pkl/generate.pkl` emits deterministic `output.files` mappings for all authored generated targets: OpenCode/Claude agents, commands, skills, Claude project settings, the Claude agent-trace plugin entrypoint under `config/.claude/plugins/`, shared bash-policy preset assets under `lib/`, the OpenCode plugin entrypoints under `plugins/` (currently `sce-bash-policy.ts` and `sce-agent-trace.ts`), generated OpenCode `package.json` and `opencode.json` manifests for manual and automated profiles, and the generated `sce/config.json` schema artifact at `config/schema/sce-config.schema.json`. +- `config/pkl/generate.pkl` emits deterministic `output.files` mappings for all authored generated targets: OpenCode/Claude agents, commands, skills, Claude project settings, the Claude hook helper script at `config/.claude/hooks/run-sce-or-show-install-guidance.sh`, shared bash-policy preset assets under `lib/`, the OpenCode plugin entrypoints under `plugins/` (currently `sce-bash-policy.ts` and `sce-agent-trace.ts`), generated OpenCode `package.json` and `opencode.json` manifests for manual and automated profiles, and the generated `sce/config.json` schema artifact at `config/schema/sce-config.schema.json`. - Generated-file warning markers are not injected by the generator: Markdown outputs render deterministic frontmatter + body, and shared library outputs are emitted without a leading generated warning header. - `config/pkl/check-generated.sh` is intentionally dev-shell scoped (`nix develop -c ...`): it requires `IN_NIX_SHELL`, runs `pkl eval -m config/pkl/generate.pkl`, and fails when generated-owned paths drift. @@ -125,6 +125,8 @@ The repository includes a new placeholder Rust binary crate at `cli/`. - `cli/src/services/version/mod.rs` defines the version command parser/rendering contract (`parse_version_request`, `render_version`) with deterministic text output and stable JSON runtime-identification fields; `cli/src/services/version/command.rs` owns the `VersionCommand` payload used by the static `RuntimeCommand` enum. - `cli/src/services/completion/mod.rs` defines completion parser/rendering contract (`parse_completion_request`, `render_completion`) with deterministic Bash/Zsh/Fish script output aligned to current parser-valid command/flag surfaces; `cli/src/services/completion/command.rs` owns the `CompletionCommand` payload used by the static `RuntimeCommand` enum. - `cli/src/services/hooks/mod.rs` defines the current local hook runtime parsing/dispatch (`HookSubcommand`, `run_hooks_subcommand`) plus a commit-msg co-author policy seam (`apply_commit_msg_coauthor_policy`) that injects one canonical SCE trailer only when the enabled-by-default attribution-hooks config/env control is not opted out, `SCE_DISABLED` is false, and the staged-diff AI-overlap preflight confirms AI/editor evidence (`StagedDiffAiOverlapResult::Overlap`); the preflight is wired into `run_commit_msg_subcommand_in_repo` and logs `sce.hooks.commit_msg.ai_overlap_error` on error paths; `cli/src/services/hooks/command.rs` owns the `HooksCommand` payload used by the static `RuntimeCommand` enum. In the current attribution-only baseline, `pre-commit` and `post-rewrite` are deterministic no-op surfaces; `post-commit` requires validated `--remote-url`, threads that URL through the Agent Trace flow, prints it to stderr, and remains an active intersection + Agent Trace persistence entrypoint (captures current commit patch, queries recent `diff_traces` from the bounded past-7-days window, combines valid patches via `patch::combine_patches`, intersects with post-commit patch via `patch::intersect_patches`, persists result to `post_commit_patch_intersections`, then persists built Agent Trace payloads with range-level `content_hash` values to `agent_traces` in AgentTraceDb without post-commit file artifacts); `diff-trace` performs STDIN JSON intake, validates required non-empty `sessionID`/`diff`/`tool_name`, optional `model_id` (absent/`null` → `None`, resolved from `session_models` by `tool_name` + `session_id` when absent) and required `tool_version` (present and either `null` or non-empty string) plus required `u64` `time` (Unix epoch milliseconds), rejects values that cannot fit AgentTraceDb signed `time_ms` storage, inserts the parsed payload fields into AgentTraceDb without writing a parsed-payload artifact under `context/tmp`; `session-model` performs STDIN JSON intake, validates required non-empty `sessionID`/`model_id`/`tool_name`, required `u64` `time` (Unix epoch milliseconds), and required nullable/non-empty `tool_version`, then upserts the parsed payload into AgentTraceDb `se... (line truncated to 2000 chars) +- `cli/src/services/hooks/mod.rs` defines the current local hook runtime parsing/dispatch (`HookSubcommand`, `run_hooks_subcommand`) plus a commit-msg co-author policy seam (`apply_commit_msg_coauthor_policy`) that injects one canonical SCE trailer only when the enabled-by-default attribution-hooks config/env control is not opted out, `SCE_DISABLED` is false, and the staged-diff AI-overlap preflight confirms AI/editor evidence (`StagedDiffAiOverlapResult::Overlap`); the preflight is wired into `run_commit_msg_subcommand_in_repo` and logs `sce.hooks.commit_msg.ai_overlap_error` on error paths; `cli/src/services/hooks/command.rs` owns the `HooksCommand` payload used by the static `RuntimeCommand` enum. In the current attribution-only baseline, `pre-commit` and `post-rewrite` are deterministic no-op surfaces; `post-commit` requires validated `--remote-url`, threads that URL through the Agent Trace flow, prints it to stderr, and remains an active intersection + Agent Trace persistence entrypoint (captures current commit patch, queries recent `diff_traces` from the bounded past-7-days window, combines valid patches via `patch::combine_patches`, intersects with post-commit patch via `patch::intersect_patches`, persists result to `post_commit_patch_intersections`, then persists built Agent Trace payloads with range-level `content_hash` values to `agent_traces` in AgentTraceDb without post-commit file artifacts); `diff-trace` performs STDIN JSON intake, validates required non-empty `sessionID`/`diff`/`tool_name`, optional `model_id` (absent/`null` → `None`, resolved from `session_models` by `tool_name` + `session_id` when absent) and required `tool_version` (present and either `null` or non-empty string) plus required `u64` `time` (Unix epoch milliseconds), rejects values that cannot fit AgentTraceDb signed `time_ms` storage, writes one collision-safe parsed-payload `context/tmp/-000000-diff-trace.json` artifact, and inserts the parsed payload fields into AgentTraceDb; `session-model` performs STDIN JSON intake, validates required non-empty `sessionID`/`model_id`/`tool_name`, required `u64` `time` (Unix epoch milliseconds), and required nullable/non-empty `tool_version`, then upserts the parsed payload into AgentTraceDb `se... (line truncated to 2000 chars) +- The external `diff-trace` and `conversation-trace` hook boundaries are fail-open for producer-facing intake failures: `run_diff_trace_subcommand` logs strict-path errors through `sce.hooks.diff_trace.error`, `run_conversation_trace_subcommand` logs strict-path errors through `sce.hooks.conversation_trace.error`, and both return hook success text while preserving valid-payload persistence output plus existing warning/skipped behavior. - Claude `SessionStart` session-model parsing in `cli/src/services/hooks/mod.rs` uses explicit payload version fields (`tool_version`/`claude_version`/`version`) when present; if no non-empty payload version is available, it best-effort runs `claude --version`, trims stdout, and leaves `tool_version` nullable without failing intake when the command is unavailable, fails, or returns empty output. - Diff-trace attribution resolution in `cli/src/services/hooks/mod.rs` looks up `session_models` when `model_id` or `tool_version` is missing/nullable, fills only missing fields from the stored row when available, preserves direct payload precedence, and continues persistence with `None` for unresolved attribution. - `cli/src/services/resilience.rs` defines bounded retry/timeout/backoff execution policy (`RetryPolicy`, `run_with_retry`) for transient operation hardening with deterministic failure messaging and retry observability. diff --git a/context/cli/cli-command-surface.md b/context/cli/cli-command-surface.md index 3d81002e..cf84fcf0 100644 --- a/context/cli/cli-command-surface.md +++ b/context/cli/cli-command-surface.md @@ -54,6 +54,7 @@ Operator onboarding currently comes from `sce --help`, command-local `--help` ou Deferred or gated command surfaces currently avoid claiming unimplemented behavior. `hooks` routes through implemented subcommand parsing/dispatch for `pre-commit`, `commit-msg`, `post-commit`, `post-rewrite`, `diff-trace`, and `session-model`; current behavior remains attribution-only and enabled by default for commit attribution unless explicitly opted out (via `SCE_ATTRIBUTION_HOOKS_DISABLED`, `SCE_DISABLED`, or `policies.attribution_hooks.enabled = false`), gated by the staged-diff AI-overlap preflight so the trailer is appended only when AI/editor evidence is found, while `post-commit` requires validated `--remote-url`, threads that value through Agent Trace flow, prints it to stderr, and remains the active intersection + Agent Trace DB path, `diff-trace` is active STDIN intake with 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) validation, missing/nullable attribution fallback from `session_models` by `tool_name` + `session_id` with direct payload values taking precedence, non-lossy AgentTraceDb `time_ms` conversion, and AgentTraceDb insertion including nullable/resolved `model_id` and `tool_version` without writing parsed-payload artifacts under `context/tmp`, and `session-model` performs STDIN intake for normalized model attribution upsert without raw artifact persistence, with Claude `SessionStart` parsing best-effort filling missing `tool_version` from `claude --version`. +`hooks` routes through implemented subcommand parsing/dispatch for `pre-commit`, `commit-msg`, `post-commit`, `post-rewrite`, `diff-trace`, `conversation-trace`, and `session-model`; current behavior remains attribution-only and enabled by default for commit attribution unless explicitly opted out (via `SCE_ATTRIBUTION_HOOKS_DISABLED`, `SCE_DISABLED`, or `policies.attribution_hooks.enabled = false`), gated by the staged-diff AI-overlap preflight so the trailer is appended only when AI/editor evidence is found, while `post-commit` requires validated `--remote-url`, threads that value through Agent Trace flow, prints it to stderr, and remains the active intersection + Agent Trace DB path, `diff-trace` is active STDIN intake with 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) validation, missing/nullable attribution fallback from `session_models` by `tool_name` + `session_id` with direct payload values taking precedence, non-lossy AgentTraceDb `time_ms` conversion, collision-safe per-invocation `context/tmp/-000000-diff-trace.json` parsed-payload writes, AgentTraceDb insertion including nullable/resolved `model_id` and `tool_version`, and fail-open logging through `sce.hooks.diff_trace.error` for intake failures, `conversation-trace` is active message/part intake with mixed-batch/raw-Claude-event parsing and fail-open logging through `sce.hooks.conversation_trace.error` for intake failures, and `session-model` performs STDIN intake for normalized model attribution upsert without raw artifact persistence, with Claude `SessionStart` parsing best-effort filling missing `tool_version` from `claude --version`. `config` exposes deterministic inspect/validate entrypoints (`sce config show`, `sce config validate`) with explicit precedence (`flags > env > config file > defaults`), a shared auth-runtime resolver for supported keys that declare env/config/optional baked-default inputs starting with `workos_client_id`, first-class `policies.bash` reporting for preset/custom blocked-command rules, and deterministic text/JSON output modes where `show` reports resolved values with provenance while `validate` reports pass/fail plus validation issues and warnings only. `version` exposes deterministic runtime identification output in text mode by default and JSON mode via `--format json`. `completion` exposes deterministic shell completion generation via `sce completion --shell `. @@ -92,6 +93,8 @@ A user-invocable `sync` command is not wired in the current CLI surface; local D - `cli/src/services/version/mod.rs` defines the version parser/output contract (`parse_version_request`, `render_version`) with deterministic text/JSON output modes; `cli/src/services/version/command.rs` owns the version runtime command handler. - `cli/src/services/completion/mod.rs` defines the completion output contract (`render_completion`) using clap_complete to generate deterministic shell scripts for Bash, Zsh, and Fish; `cli/src/services/completion/command.rs` owns the completion runtime command handler. - `cli/src/services/hooks/mod.rs` defines production local hook runtime parsing/dispatch (`HookSubcommand`, `run_hooks_subcommand`) for `pre-commit`, `commit-msg`, `post-commit`, `post-rewrite`, `diff-trace`, and `session-model`; `cli/src/services/hooks/command.rs` owns the hook runtime command handler. Current runtime behavior is commit-msg-only attribution behind the enabled-by-default attribution gate with explicit opt-out controls; `pre-commit` and `post-rewrite` are deterministic no-ops; `post-commit` requires validated `--remote-url`, threads that value through Agent Trace flow, prints it to stderr, and remains an active intersection + Agent Trace DB persistence path (captures current commit patch, combines/intersects recent `diff_traces`, persists intersection metadata to `post_commit_patch_intersections`, then persists built Agent Trace payload with range-level `content_hash` values to `agent_traces`); `diff-trace` performs STDIN JSON intake, required non-empty `sessionID`/`diff`/`tool_name`, optional `model_id`, required nullable/non-empty `tool_version`, plus required `u64` `time` (Unix epoch milliseconds) validation, missing/nullable attribution fallback from `session_models` by `tool_name` + `session_id` while preserving direct payload precedence, non-lossy AgentTraceDb `time_ms` conversion, and AgentTraceDb insertion whose failure is logged and reflected in deterministic success text without writing a `context/tmp` artifact fallback; and `session-model` performs STDIN JSON intake for normalized model attribution upsert without raw artifact persistence, with Claude `SessionStart` parsing best-effort filling missing `tool_version` from `claude --version`. `cli/src/services/hooks/lifecycle.rs` implements `ServiceLifecycle` for hook health checks, fix, and setup (hook rollout integrity and required-hook installation). +- `cli/src/services/hooks/mod.rs` defines production local hook runtime parsing/dispatch (`HookSubcommand`, `run_hooks_subcommand`) for `pre-commit`, `commit-msg`, `post-commit`, `post-rewrite`, `diff-trace`, and `session-model`; `cli/src/services/hooks/command.rs` owns the hook runtime command handler. Current runtime behavior is commit-msg-only attribution behind the enabled-by-default attribution gate with explicit opt-out controls; `pre-commit` and `post-rewrite` are deterministic no-ops; `post-commit` requires validated `--remote-url`, threads that value through Agent Trace flow, prints it to stderr, and remains an active intersection + Agent Trace DB persistence path (captures current commit patch, combines/intersects recent `diff_traces`, persists intersection metadata to `post_commit_patch_intersections`, then persists built Agent Trace payload with range-level `content_hash` values to `agent_traces`); `diff-trace` performs STDIN JSON intake, required non-empty `sessionID`/`diff`/`tool_name`, optional `model_id`, required nullable/non-empty `tool_version`, plus required `u64` `time` (Unix epoch milliseconds) validation, missing/nullable attribution fallback from `session_models` by `tool_name` + `session_id` while preserving direct payload precedence, non-lossy AgentTraceDb `time_ms` conversion, collision-safe parsed-payload `context/tmp/-000000-diff-trace.json` persistence, best-effort AgentTraceDb insertion whose failure is logged and reflected in success text, and fail-open conversion of intake failures to logged command success through `sce.hooks.diff_trace.error`; and `session-model` performs STDIN JSON intake for normalized model attribution upsert without raw artifact persistence, with Claude `SessionStart` parsing best-effort filling missing `tool_version` from `claude --version`. `cli/src/services/hooks/lifecycle.rs` implements `ServiceLifecycle` for hook health checks, fix, and setup (hook rollout integrity and required-hook installation). +- `conversation-trace` is also part of the production local hook runtime dispatch in `cli/src/services/hooks/mod.rs`; it persists valid mixed-batch/raw-Claude-event message and part payloads, keeps skipped-item and batch-insert warning behavior unchanged, and converts STDIN read, top-level parse/validation, unsupported raw Claude hook event, and AgentTraceDb setup/persistence failures into logged command success through `sce.hooks.conversation_trace.error`. - `cli/src/services/resilience.rs` defines shared bounded retry/timeout/backoff execution policy (`RetryPolicy`, `run_with_retry`) with deterministic failure messaging and retry observability hooks. - No `cli/src/services/sync.rs` module exists in the current codebase; `sce sync` command wiring is deferred, while local DB initialization and health ownership are split between setup and doctor. - `cli/src/services/default_paths.rs` defines the canonical per-user persisted-location seam for config/state/cache roots plus named default file paths for current persisted artifacts (`global config`, `auth tokens`, `local DB`, `agent trace DB`) used by config discovery, token storage, database adapters, and doctor diagnostics; its internal `roots` seam now owns the platform-aware root-directory resolution so non-test production modules consume shared path accessors instead of resolving owned roots directly. diff --git a/context/context-map.md b/context/context-map.md index fbb549e0..77bb790a 100644 --- a/context/context-map.md +++ b/context/context-map.md @@ -37,7 +37,7 @@ Feature/domain context: - `context/sce/doctor-human-text-contract.md` (implemented `sce doctor` human text layout contract: section order, `[PASS]`/`[FAIL]`/`[MISS]` status vocabulary, simplified hook rows, and OpenCode integration group rendering rules) - `context/sce/setup-githooks-install-contract.md` (T01 canonical `sce setup --hooks` install contract for target-path resolution, idempotent outcomes, remove-and-replace behavior, and doctor-readiness alignment) - `context/sce/setup-no-backup-policy-seam.md` (implemented unified remove-and-replace install policy for both config-install and required-hook install flows, with no backup creation and deterministic recovery guidance on swap failure) -- `context/sce/setup-githooks-hook-asset-packaging.md` (T02 compile-time `sce setup --hooks` required-hook template packaging contract, including current post-commit origin remote lookup, remote-URL forwarding/fallback behavior, and setup-service accessor surface) +- `context/sce/setup-githooks-hook-asset-packaging.md` (T02 compile-time `sce setup --hooks` required-hook template packaging contract, including current post-commit missing-`sce` install guidance, origin remote lookup, remote-URL forwarding/fallback behavior, and setup-service accessor surface) - `context/sce/setup-githooks-install-flow.md` (T03 setup-service required-hook install orchestration with git-truth hooks-path resolution, per-hook installed/updated/skipped outcomes, and remove-and-replace behavior with recovery guidance) - `context/sce/setup-githooks-cli-ux.md` (T04 composable `sce setup` target+`--hooks` / `--repo` command-surface contract, option compatibility validation, and deterministic setup/hook output semantics) - `context/sce/setup-repo-local-config-bootstrap.md` (setup local bootstrap behavior: repo-local `.sce/config.json` create-if-missing via config lifecycle plus lifecycle-owned local DB initialization before hooks/config asset dispatch) @@ -53,9 +53,11 @@ Feature/domain context: - `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 Agent Trace hook DB paths using no-migration readiness-gated AgentTraceDb access, active `post-commit` intersection entrypoint requiring validated `--remote-url`, threading that URL to the Agent Trace flow, printing it to stderr, capturing current commit patch, querying recent `diff_traces` from past 7 days, combining/intersecting patches via `patch::combine_patches` / `patch::intersect_patches`, persisting results to `post_commit_patch_intersections`, building/schema-validating post-commit Agent Trace payloads enriched with optional top-level `tool` metadata, `metadata.sce.version`, and range `content_hash`, and persisting validated payloads to AgentTraceDb `agent_traces` (DB-only), plus `diff-trace` STDIN intake with required non-empty `sessionID`/`diff`/`tool_name`, optional `model_id`, required nullable/non-empty `tool_version`, required `u64` `time` validation, AgentTraceDb-only persistence with no `context/tmp` parsed-payload artifacts, `session-model` STDIN intake for normalized model attribution and raw Claude `SessionStart` events, and `conversation-trace` STDIN intake that classifies by `hook_event_name` — raw Claude `UserPromptSubmit` events (`transform_claude_user_prompt_submit`) and `Stop` events (`transform_claude_stop`) are transformed into normalized `message` + `message.part` items (user or assistant role, text part) and forwarded through the existing mixed-batch parser, mixed-batch `message.part` accepts `text`/`reasoning`/`patch`/`question` with patch JSON normalization and question JSON-shape validation before persistence, unsupported raw Claude hook events fail deterministically with diagnostics listing supported events, and payloads without `hook_event_name` follow the existing `{ payloads: [{ type, ... }] }` mixed-batch validation/persistence path) +- `context/sce/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 Agent Trace hook DB paths using no-migration readiness-gated AgentTraceDb access, active `post-commit` intersection entrypoint requiring validated `--remote-url`, threading that URL to the Agent Trace flow, printing it to stderr, capturing current commit patch, querying recent `diff_traces` from past 7 days, combining/intersecting patches via `patch::combine_patches` / `patch::intersect_patches`, persisting results to `post_commit_patch_intersections`, building/schema-validating post-commit Agent Trace payloads enriched with optional top-level `tool` metadata, `metadata.sce.version`, and range `content_hash`, and persisting validated payloads to AgentTraceDb `agent_traces` (DB-only), plus `diff-trace` STDIN intake with required non-empty `sessionID`/`diff`/`tool_name`, optional `model_id`, required nullable/non-empty `tool_version`, required `u64` `time` validation, dual persistence to AgentTraceDb, collision-safe `context/tmp/-000000-diff-trace.json` artifacts, `session-model` STDIN intake for normalized model attribution and raw Claude `SessionStart` events, and `conversation-trace` STDIN intake that classifies by `hook_event_name` — raw Claude `UserPromptSubmit` events (`transform_claude_user_prompt_submit`) and `Stop` events (`transform_claude_stop`) are transformed into normalized `message` + `message.part` items (user or assistant role, text part) and forwarded through the existing mixed-batch parser, mixed-batch `message.part` accepts `text`/`reasoning`/`patch`/`question` with patch JSON normalization and question JSON-shape validation before persistence, unsupported raw Claude hook events fail deterministically with diagnostics listing supported events, and payloads without `hook_event_name` follow the existing `{ payloads: [{ type, ... }] }` mixed-batch validation/persistence path) +- `context/sce/agent-trace-hooks-command-routing.md` also owns the current `diff-trace` and `conversation-trace` fail-open intake contracts: diff-trace STDIN read, parse/validation, attribution lookup, artifact persistence, and setup/persistence failures log `sce.hooks.diff_trace.error`; conversation-trace STDIN read, parse/validation, unsupported raw Claude hook event, and AgentTraceDb setup/persistence failures log `sce.hooks.conversation_trace.error`; both return hook success while valid-payload output and existing warning/skipped behavior remain unchanged. Its conversation-trace section is canonical for the current unsupported raw-Claude-event fail-open behavior. - `context/sce/automated-profile-contract.md` (deterministic gate policy for automated OpenCode profile, including 10 gate categories, permission mappings, automated `/commit` single-commit execution behavior, and automated profile constraints) - `context/sce/bash-tool-policy-enforcement-contract.md` (approved bash-tool blocking contract plus current Rust evaluator seam and OpenCode/Claude delegation references, including config schema, argv-prefix matching, shell/nix unwrapping, fixed preset catalog/messages, and precedence rules) -- `context/sce/generated-opencode-plugin-registration.md` (current generated OpenCode plugin-registration contract, canonical Pkl ownership, generated manifest/plugin paths including `sce-bash-policy` + `sce-agent-trace`, TypeScript source ownership, and Claude generated settings boundary including Agent Trace hooks plus `PreToolUse` Bash policy hook registration) +- `context/sce/generated-opencode-plugin-registration.md` (current generated OpenCode plugin-registration contract, canonical Pkl ownership, generated manifest/plugin paths including `sce-bash-policy` + `sce-agent-trace`, TypeScript source ownership, and Claude generated settings boundary including Agent Trace hooks plus `PreToolUse` Bash policy hook registration through the missing-CLI install-guidance helper) - `context/sce/opencode-agent-trace-plugin-runtime.md` (current OpenCode agent-trace plugin runtime behavior, including captured `message.updated` handoff with `summary.diffs` branching: when diffs exist sends one `-patch` mixed batch containing a synthetic parent message plus per-diff `message.part` patch items, when no diffs sends the original `message.updated` payload; in-memory dedup `Set` keyed by `"${sessionID}:${messageID}"`; captured `message.part.updated` handoff to `sce hooks conversation-trace` for `text`/`reasoning` parts with non-empty text plus completed `question` tool parts emitted as `part_type: "question"` with JSON-stringified `{ question, answer }[]`; existing user-message diff extraction for `{ sessionID, diff, time, model_id }`; session-scoped OpenCode client version capture from `session.created`/`session.updated`; and CLI handoff to `sce hooks diff-trace` over STDIN JSON with required `tool_name="opencode"` plus required nullable `tool_version`; Rust hook parsing and AgentTraceDb insertion persist required payload fields including `model_id`) - `context/sce/cli-first-install-channels-contract.md` (current `sce` install/distribution contract covering supported Nix/Cargo/npm plus source-built Flatpak channel scope, implemented GitHub Release Flatpak source-manifest and source-built `.flatpak` bundle asset packaging, canonical `.version` release authority, manual GitHub Release `prerelease` checkbox behavior, Nix-owned build/release policy, Nix-owned Flatpak manifest generation via `pkgs.formats.yaml.generate` plus Nix-owned cargo-sources generation from `cli/Cargo.lock` plus dedicated Nix-built Bash validator scripts for static, version-parity, and local-manifest validation, thin imperative `packaging/flatpak/sce-flatpak.sh` orchestration around `flatpak-builder` / `flatpak build-bundle`, the reduced Linux flake app surface (`sce-flatpak`, `release-flatpak-package`, `release-flatpak-bundle`, plus `regenerate-flatpak-manifest` / `regenerate-cargo-sources` helpers) with `flatpak-static-validation` / `flatpak-manifest-parity` / `cargo-sources-parity` checks, the implemented `packaging/flatpak/dev.crocoder.sce.yml` + AppStream/Cargo-source packaging surface as generated artifacts, and the `dev.crocoder.sce` host-git bridge decision) - `context/sce/optional-install-channel-integration-test-entrypoint.md` (current opt-in flake app contract for existing binary install-channel integration coverage, including thin flake delegation to the Rust runner, shared harness ownership, real npm+Bun+Cargo install flows, channel selector semantics, the explicit non-default execution boundary, and the separate Flatpak source-build validation boundary) diff --git a/context/glossary.md b/context/glossary.md index 8e5e699b..3c3b322f 100644 --- a/context/glossary.md +++ b/context/glossary.md @@ -122,6 +122,8 @@ - `setup install engine`: Installer in `cli/src/services/setup/mod.rs` (`install_embedded_setup_assets`) that writes embedded setup assets into per-target staging directories and swaps them into repository-root `.opencode/`/`.claude/` destinations, using a unified remove-and-replace policy that removes existing targets before swapping staged content. - `setup remove-and-replace`: Replacement choreography in `cli/src/services/setup/mod.rs` where existing install targets are removed before staged content is promoted; on swap failure, the engine cleans temporary staging paths and returns deterministic recovery guidance (recover from version control). No backup artifacts are created. - `hooks command routing contract`: Current hook command parser/dispatcher plus runtime wiring in `cli/src/services/hooks/mod.rs` (`HookSubcommand`, `run_hooks_subcommand`) that supports `pre-commit`, `commit-msg `, `post-commit`, `post-rewrite `, `diff-trace`, and `session-model` with deterministic invocation validation/usage errors; `commit-msg` is the only active attribution path behind the attribution hooks gate AND the staged-diff AI-overlap preflight (trailer is appended only when `StagedDiffAiOverlapResult::Overlap` is returned; `NoOverlap` and `Error` suppress the trailer, with `Error` logged via `sce.hooks.commit_msg.ai_overlap_error`), `pre-commit`/`post-rewrite` are deterministic no-op entrypoints, `post-commit` requires validated `--remote-url`, threads that value through the Agent Trace flow, prints it to stderr, captures the current commit patch, queries recent `diff_traces` from the past 7 days, combines valid patches via `patch::combine_patches`, intersects with the post-commit patch via `patch::intersect_patches`, persists the intersection result to `post_commit_patch_intersections`, and persists built Agent Trace payloads to AgentTraceDb `agent_traces` (DB-only, no post-commit Agent Trace file artifact), `diff-trace` performs STDIN JSON intake with required non-empty `sessionID`/`diff`/`tool_name`, optional `model_id`, required nullable/non-empty `tool_version`, missing/nullable attribution fallback from `session_models` by `tool_name` + `session_id` while direct payload values keep precedence, required `u64` `time` validation, non-lossy AgentTraceDb `time_ms` conversion, and AgentTraceDb insertion with nullable/resolved attribution without writing parsed-payload artifacts under `context/tmp`, and `session-model` performs STDIN JSON intake for normalized model attribution upsert without raw artifact persistence, with Claude `SessionStart` parsing best-effort filling missing `tool_version` from `claude --version`. +- `diff-trace fail-open intake`: Current `sce hooks diff-trace` producer-facing error posture where STDIN read failures, JSON parse/validation failures, attribution lookup failures, parsed-payload artifact persistence failures, and setup/persistence failures are logged through `sce.hooks.diff_trace.error` and converted into command success text (`diff-trace hook intake failed open; error logged.`). Valid payload success text remains unchanged, and the existing post-artifact AgentTraceDb write-warning path still logs `sce.hooks.diff_trace.agent_trace_db_write_failed` and returns its established failed-DB-persistence success message. +- `hooks command routing contract`: Current hook command parser/dispatcher plus runtime wiring in `cli/src/services/hooks/mod.rs` (`HookSubcommand`, `run_hooks_subcommand`) that supports `pre-commit`, `commit-msg `, `post-commit`, `post-rewrite `, `diff-trace`, `conversation-trace`, and `session-model` with deterministic invocation validation/usage errors. `commit-msg` remains the only mutating attribution path behind the attribution-hooks gate and staged-diff AI-overlap preflight; `pre-commit`/`post-rewrite` are deterministic no-op entrypoints; `post-commit` captures and intersects recent diff traces before DB-only Agent Trace persistence; `diff-trace` and `conversation-trace` are active producer-facing STDIN intakes whose intake failures fail open through their dedicated logger event IDs while valid-payload persistence and warning/skipped behavior remain unchanged; `session-model` upserts normalized model attribution without raw artifact persistence. - `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 only normalized `session-model` and `diff-trace` intakes for Claude/OpenCode editor runtimes. The removed route previously wrote pretty-printed JSON artifacts under `context/tmp/claude/` without AgentTraceDb writes. See `context/sce/claude-raw-hook-capture.md`. - `cloud sync gateway placeholder`: Abstraction in `cli/src/services/sync.rs` (`CloudSyncGateway`) that returns deferred cloud-sync checkpoints while `sync` remains non-production. - `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. @@ -140,7 +142,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 behind the enabled-by-default attribution-hooks control with explicit opt-out, `post-commit` requires validated `--remote-url`, threads that value through the Agent Trace flow, prints it to stderr, captures current commit patch, queries recent `diff_traces` from past 7 days, combines/intersects patches, persists to `post_commit_patch_intersections`, and persists built Agent Trace payloads to `agent_traces` without post-commit file artifacts, `diff-trace` is an active intake path (validates required STDIN payload fields including `sessionID`/`diff`/`tool_name`, optional `model_id`, required nullable/non-empty `tool_version`, fills missing/nullable attribution from `session_models` when available while preserving direct payload precedence, and inserts parsed payload fields into AgentTraceDb with nullable/resolved attribution without writing `context/tmp` artifacts), and `session-model` is an active intake path (validates required STDIN payload fields including `sessionID`/`model_id`/`tool_name`, best-effort fills missing Claude `tool_version` from `claude --version`, and upserts into `session_models` without raw artifacts). +- `hook no-op baseline`: Current `cli/src/services/hooks/mod.rs` runtime posture where `pre-commit` and `post-rewrite` return deterministic no-op status text, `commit-msg` is a gated mutating path behind the enabled-by-default attribution-hooks control with explicit opt-out, `post-commit` requires validated `--remote-url`, threads that value through the Agent Trace flow, prints it to stderr, captures current commit patch, queries recent `diff_traces` from past 7 days, combines/intersects patches, persists to `post_commit_patch_intersections`, and persists built Agent Trace payloads to `agent_traces` without post-commit file artifacts, `diff-trace` is an active fail-open intake path (validates required STDIN payload fields including `sessionID`/`diff`/`tool_name`, optional `model_id`, required nullable/non-empty `tool_version`, fills missing/nullable attribution from `session_models` when available while preserving direct payload precedence, and inserts parsed payload fields into AgentTraceDb with nullable/resolved attribution without writing `context/tmp` artifacts), `conversation-trace` is an active fail-open intake path for mixed-batch/raw-Claude message and part payloads without `context/tmp` artifacts, and `session-model` is an active intake path (validates required STDIN payload fields including `sessionID`/`model_id`/`tool_name`, best-effort fills missing Claude `tool_version` from `claude --version`, and upserts into `session_models` without raw artifacts). - `sce doctor` operator-health contract: `cli/src/services/doctor/mod.rs` is the stable doctor entrypoint, with focused `doctor/{inspect,render,fixes,types}.rs` submodules implementing the current approved operator-health surface in `context/sce/agent-trace-hook-doctor.md`: `sce doctor --fix` selects repair intent, help/output expose deterministic doctor mode, JSON includes stable problem taxonomy/fixability fields plus checkout/database records and fix-result records, the runtime validates state-root resolution, global and repo-local `sce/config.json` readability/schema health, local DB and checkout/global Agent Trace DB path/health, DB-parent readiness barriers, git availability, non-repo vs bare-repo targeting failures, effective hook-path source resolution, required hook presence/executable/content drift against canonical embedded hook assets, and repo-root installed OpenCode integration presence for `OpenCode plugins`, `OpenCode agents`, `OpenCode commands`, and `OpenCode skills`. Human text mode uses the approved sectioned layout (`Environment`, `Configuration` with checkout identity + Agent Trace checkout DB rows when available, `Repository`, `Git Hooks`, `Integrations`), `SCE doctor diagnose` / `SCE doctor fix` headers, bracketed `[PASS]`/`[FAIL]`/`[MISS]` status tokens with shared-style green/red colorization when enabled, simplified `label (path)` row formatting, top-level-only hook rows, and presence-only integration parent/child rows where missing required files surface as `[MISS]` children and `[FAIL]` parent groups. Fix mode 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. Checkout DB discovery no longer lives in `doctor`; it moved to the `trace` group (`sce trace db list`). - `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. @@ -202,5 +204,9 @@ - `agent-trace plugin conversation-trace handoff seam`: Helper path in `config/lib/agent-trace-plugin/opencode-sce-agent-trace-plugin.ts` where `recordConversationTrace(repoRoot, event)` builds normalized snake_case mixed-batch envelopes for `sce hooks conversation-trace`: ordinary `message` and eligible `message.part` events each send one `payloads[0]` item with its own `type`, diff-backed `message` events send one envelope containing the synthetic parent `message` item plus patch `message.part` items, and completed OpenCode `question` tool parts are mapped to one `message.part` payload with `part_type: "question"` plus JSON-stringified `{ question, answer }[]` text; `message` handoff runs before the plugin's existing diff-trace flow, while part events do not invoke diff-trace. - `agent-trace plugin diff-trace hook handoff seam`: Internal helper `runDiffTraceHook` in `config/lib/agent-trace-plugin/opencode-sce-agent-trace-plugin.ts` that invokes `sce hooks diff-trace`, streams `{ sessionID, diff, time, model_id, tool_name, tool_version }` to STDIN JSON (`tool_name` fixed to `opencode`, `tool_version` session-derived when available), and surfaces deterministic invocation failures. - `agent-trace plugin secondary diff persistence ownership`: Current runtime contract where `buildTrace` no longer writes diff-trace artifacts or database rows directly; extracted diff payloads are forwarded to CLI `diff-trace` intake and the Rust hook runtime owns AgentTraceDb insertion without any `context/tmp` artifact fallback. +- `conversation-trace fail-open intake`: Current `sce hooks conversation-trace` producer-facing error posture where STDIN read failures, JSON parse/top-level validation failures, unsupported raw Claude hook events, and AgentTraceDb setup/persistence failures are logged through `sce.hooks.conversation_trace.error` and converted into command success text (`conversation-trace hook intake failed open; error logged.`). Valid mixed-batch/raw-event success text, skipped-item logging, and batch-insert warning behavior remain unchanged. +- `agent-trace plugin conversation-trace handoff seam`: Helper path in `config/lib/agent-trace-plugin/opencode-sce-agent-trace-plugin.ts` where `recordConversationTrace(repoRoot, event)` builds normalized snake_case mixed-batch envelopes for `sce hooks conversation-trace`: ordinary `message` and eligible `message.part` events each send one `payloads[0]` item with its own `type`, diff-backed `message` events send one envelope containing the synthetic parent `message` item plus patch `message.part` items, and completed OpenCode `question` tool parts are mapped to one `message.part` payload with `part_type: "question"` plus JSON-stringified `{ question, answer }[]` text; `message` handoff runs before the plugin's existing diff-trace flow, while part events do not invoke diff-trace. The helper function `runConversationTraceHook` fails open at the plugin level (ignored stderr, unconditional resolve). +- `agent-trace plugin diff-trace hook handoff seam`: Internal helper `runDiffTraceHook` in `config/lib/agent-trace-plugin/opencode-sce-agent-trace-plugin.ts` that invokes `sce hooks diff-trace` and streams `{ sessionID, diff, time, model_id, tool_name, tool_version }` to STDIN JSON (`tool_name` fixed to `opencode`, `tool_version` session-derived when available). The plugin fails open: stderr is ignored and the promise resolves unconditionally on any outcome, so spawn errors, non-zero exits, and sce intake errors do not surface as unhandled promise rejections or leak into the OpenCode TUI. +- `agent-trace plugin secondary diff artifact ownership`: Current runtime contract where `buildTrace` no longer writes diff-trace artifacts or database rows directly; extracted diff payloads are forwarded to CLI `diff-trace` intake and the Rust hook runtime owns AgentTraceDb insertion plus collision-safe per-invocation artifact persistence. - `messages table (Agent Trace DB)`: Agent Trace DB table created by migration `008_create_messages.sql`; stores session-scoped parent messages with columns `session_id`, `message_id`, `role` (`user`/`assistant` via CHECK constraint), `generated_at_unix_ms`, `created_at`, and `updated_at`. Message body text belongs to `parts.text`, not the parent `messages` row. Has a unique index on `(session_id, message_id)` for duplicate-ignore parent message inserts and a compound index on `(session_id, generated_at_unix_ms, id)` for chronological session message retrieval. No foreign keys to any other table. - `parts table (Agent Trace DB)`: Agent Trace DB table created by migration `009_create_parts.sql`; stores append-only message parts with columns `type` (typed by Rust as `text`/`reasoning`/`patch`/`question` and stored as unconstrained `TEXT NOT NULL`), `text`, `message_id`, `session_id`, `generated_at_unix_ms`, `created_at`, `updated_at`. Uses only the internal `id` for row identity (no upsert/dedup). Multiple parts can exist for the same `(session_id, message_id)`. A compound index on `(session_id, message_id, generated_at_unix_ms, id)` enables ordered joins. No foreign keys to `messages` or any other table, so parts may be inserted before their parent message exists. diff --git a/context/overview.md b/context/overview.md index d2dd22e4..b68edfac 100644 --- a/context/overview.md +++ b/context/overview.md @@ -26,9 +26,9 @@ The shared default path service in `cli/src/services/default_paths.rs` is now th The Rust CLI also centralizes SCE-owned web URI construction in `cli/src/services/agent_trace.rs`, with `SCE_WEB_BASE_URL` as the single Rust owner for `https://sce.crocoder.dev` and helpers consumed by Agent Trace conversation URLs, Agent Trace persisted trace URLs, Agent Trace session URLs, and setup-created repo-local config schema URLs. 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 plugin assets for both profiles: `sce-bash-policy.ts` plus `sce-agent-trace.ts` are emitted under `config/.opencode/plugins/` and `config/automated/.opencode/plugins/`; the OpenCode agent-trace plugin extracts `{ sessionID, diff, time, model_id }` from user `message.updated` events with diffs, tracks per-session OpenCode client version from `session.created`/`session.updated`, and sends payloads to `sce hooks diff-trace` with `tool_name="opencode"` plus optional `tool_version`. Claude generated config now routes agent-trace events through `.claude/settings.json` command hooks that call `sce hooks` directly: `SessionStart` pipes raw hook event JSON to `sce hooks session-model`, and matched `PostToolUse Write|Edit|MultiEdit|NotebookEdit` pipes raw hook event JSON to `sce hooks diff-trace`; the Rust `session-model` path uses explicit payload version fields when present and otherwise best-effort captures `tool_version` from trimmed `claude --version` stdout when available. Rust handles extraction, validation, and persistence without a TypeScript intermediary; the former `config/.claude/plugins/sce-agent-trace.ts` Bun runtime was removed in T07 of the `claude-rust-diff-trace` plan. The Rust hook validates required fields, resolves missing/nullable diff-trace attribution from `session_models` while preserving direct payload precedence, and persists `model_id`, `tool_name`, and nullable/resolved `tool_version` into `diff_traces` through AgentTraceDb. Bash-policy now delegates OpenCode enforcement to the Rust `sce policy bash` command: the generated OpenCode plugin at `config/.opencode/plugins/sce-bash-policy.ts` (and `config/automated/.opencode/plugins/sce-bash-policy.ts`) is a thin wrapper that calls `sce policy bash --input normalized --output json` via `spawnSync` and throws on deny decisions; it no longer contains independent TypeScript policy logic. The former `bash-policy/runtime.ts` TypeScript runtime has been removed. Preset... -The `doctor` command now exposes explicit inspection mode (`sce doctor`) and repair-intent mode (`sce doctor --fix`) at the CLI/help/schema level while keeping diagnosis mode read-only. Checkout discovery has moved out of `doctor` into the `trace` group (`sce trace db list`, `sce trace status`, `sce trace status --all`); see `context/cli/trace-command.md`. It now validates both current global operator health and the current repo/hook-integrity slice: state-root resolution, global config path resolution, global and repo-local `sce/config.json` readability/schema validity, local DB and checkout/global Agent Trace DB path + health, DB parent-directory readiness, git availability, non-repo vs bare-repo targeting failures, effective git hook-path source (default, per-repo `core.hooksPath`, or global `core.hooksPath`), hooks-directory health, required hook presence/executable permissions/content drift against canonical embedded SCE-managed hook assets, and repo-root OpenCode integration presence across the installed `plugins`, `agents`, `commands`, and `skills` inventories with embedded SHA-256 content verification for OpenCode assets. Text mode now renders the approved human-only layout with ordered `Environment` / `Configuration` / `Repository` / `Git Hooks` / `Integrations` sections, `SCE doctor diagnose` / `SCE doctor fix` headers, bracketed `[PASS]`/`[FAIL]`/`[MISS]` status tokens, shared-style green pass plus red fail/miss coloring when color output is enabled, simplified `label (path)` row formatting, top-level-only hook rows, and integration parent/child rows that reflect missing vs content-mismatch states; JSON output reports `checkout_identity` plus resolved Agent Trace DB health under `agent_trace_db` when a checkout ID exists, with global Agent Trace DB reporting retained outside checkout context. Fix mode reuses the canonical setup hook install flow to repair missing/stale/non-executable required hooks and can also bootstrap missing canonical DB parent directories while preserving manual-only guidance for unsupported issues. -Claude bash-policy enforcement is also generated through `.claude/settings.json` as a `PreToolUse` `Bash` command hook running `sce policy bash`, so Claude and OpenCode both delegate to the Rust policy evaluator without a Claude TypeScript runtime. +Generated config now includes repo-local plugin assets for both profiles: `sce-bash-policy.ts` plus `sce-agent-trace.ts` are emitted under `config/.opencode/plugins/` and `config/automated/.opencode/plugins/`; the OpenCode agent-trace plugin extracts `{ sessionID, diff, time, model_id }` from user `message.updated` events with diffs, tracks per-session OpenCode client version from `session.created`/`session.updated`, and sends payloads to `sce hooks diff-trace` with `tool_name="opencode"` plus optional `tool_version`. Claude generated config routes agent-trace and bash-policy events through `.claude/settings.json` command hooks that call the generated Bash helper `.claude/hooks/run-sce-or-show-install-guidance.sh` before invoking `sce`: `SessionStart` uses `sce hooks session-model`, matched `PostToolUse Write|Edit|MultiEdit|NotebookEdit` uses `sce hooks diff-trace`, supported conversation events use `sce hooks conversation-trace`, and `PreToolUse Bash` uses `sce policy bash`. The helper emits `sce CLI not found. Install it from https://sce.crocoder.dev/docs/getting-started#install-cli` and exits successfully when `sce` is missing, while `exec "$@"` preserves stdin/stdout/stderr and exit behavior when `sce` exists. 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. Bash-policy now delegates OpenCode enforcement to the Rust `sce policy bash` command through the generated OpenCode plugin wrappers, which call `sce policy bash --input normalized --output json` via `spawnSync` and no longer contain independent TypeScript policy logic. +The `doctor` command now exposes explicit inspection mode (`sce doctor`) and repair-intent mode (`sce doctor --fix`) at the CLI/help/schema level while keeping diagnosis mode read-only. Checkout discovery has moved out of `doctor` into the `trace` group (`sce trace db list`, `sce trace status`, `sce trace status --all`); see `context/cli/trace-command.md`. It now validates both current global operator health and the current repo/hook-integrity slice: state-root resolution, global config path resolution, global and repo-local `sce/config.json` readability/schema validity, local DB and checkout/global Agent Trace DB path + health, DB parent-directory readiness, git availability, non-repo vs bare-repo targeting failures, effective hook-path source (default, per-repo `core.hooksPath`, or global `core.hooksPath`), hooks-directory health, required hook presence/executable permissions/content drift against canonical embedded SCE-managed hook assets, and repo-root OpenCode integration presence across the installed `plugins`, `agents`, `commands`, and `skills` inventories with embedded SHA-256 content verification for OpenCode assets. Text mode now renders the approved human-only layout with ordered `Environment` / `Configuration` / `Repository` / `Git Hooks` / `Integrations` sections, `SCE doctor diagnose` / `SCE doctor fix` headers, bracketed `[PASS]`/`[FAIL]`/`[MISS]` status tokens, shared-style green pass plus red fail/miss coloring when color output is enabled, simplified `label (path)` row formatting, top-level-only hook rows, and integration parent/child rows that reflect missing vs content-mismatch states; JSON output reports `checkout_identity` plus resolved Agent Trace DB health under `agent_trace_db` when a checkout ID exists, with global Agent Trace DB reporting retained outside checkout context. Fix mode reuses the canonical setup hook install flow to repair missing/stale/non-executable required hooks and can also bootstrap missing canonical DB parent directories while preserving manual-only guidance for unsupported issues. +Claude bash-policy enforcement is also generated through `.claude/settings.json` as a `PreToolUse` `Bash` command hook routed through `.claude/hooks/run-sce-or-show-install-guidance.sh` before running `sce policy bash`, so Claude and OpenCode both delegate to the Rust policy evaluator without a Claude TypeScript runtime. Local database bootstrap is now owned by `LocalDbLifecycle::setup` and `AgentTraceDbLifecycle::setup` aggregated by the setup command. Agent Trace setup creates/reuses the current checkout ID and initializes the per-checkout `agent-trace-{checkout_id}.db` with embedded migrations; hook runtime still lazily creates or upgrades the per-checkout DB when setup has not run or schema metadata is incomplete. Doctor validates checkout/global DB paths/health and can bootstrap missing parent directories; checkout DB discovery now lives in the `trace` group (`sce trace db list`). Wiring a user-invocable `sce sync` command is deferred to `0.4.0`. The repository-root flake (`flake.nix`) now applies a Rust overlay-backed stable toolchain pinned to `1.95.0` (with `rustfmt` and `clippy`), reads package/check version from the repo-root `.version` file, builds `packages.sce` through a Crane `buildDepsOnly` + `buildPackage` pipeline with filtered package sources for the Cargo tree plus required embedded config/assets, and runs `cli-tests`, `cli-clippy`, and `cli-fmt` through Crane-backed check derivations (`cargoTest`, `cargoClippy`, `cargoFmt`) that reuse the same filtered source/toolchain setup. The root flake also exposes release install/run outputs directly as `packages.sce` (with `packages.default = packages.sce`) plus `apps.sce` and `apps.default`, so `nix build .#default`, `nix run . -- --help`, `nix run .#sce -- --help`, and `nix profile install github:crocoder-dev/shared-context-engineering` all target the packaged `sce` binary through the same flake-owned entrypoints. @@ -36,7 +36,7 @@ The CLI Cargo package metadata now includes crates.io publication-ready fields w The repository-root flake is now the single Nix entrypoint for both repo tooling and CLI packaging/checks, so root-level `nix flake check` evaluates the Crane-backed CLI checks (`cli-tests`, `cli-clippy`, `cli-fmt`), the dedicated `integrations/install` runner checks (`integrations-install-tests`, `integrations-install-clippy`, `integrations-install-fmt`), Linux-only `flatpak-static-validation`, `workflow-actionlint` (runs `actionlint` on all `.github/workflows/*.yml`), plus six split JavaScript check derivations: `npm-bun-tests`, `npm-biome-check`, `npm-biome-format`, `config-lib-bun-tests`, `config-lib-biome-check`, and `config-lib-biome-format`, without nested-flake indirection. The config-lib checks now consume `config/lib/` as the shared Bun/TypeScript package root for both `agent-trace-plugin/` and `bash-policy-plugin`, with dependencies resolved from `config/lib/package.json` and `config/lib/bun.lock`. For Cargo packaging/builds, the crate now compiles against a temporary `cli/assets/generated/` mirror prepared from canonical `config/` outputs during Nix builds and crates.io publish runs rather than from a committed crate-local snapshot. Config-lib JS flake checks execute from `config/lib/`, but the copied Nix check source is repo-shaped when tests require shared repo fixtures; the current Claude agent-trace golden tests are fully Rust-owned in `cli/src/services/structured_patch/fixtures` (Claude TypeScript plugin test removed in T07). Local developer Nix tuning guidance now lives in `AGENTS.md`, including optional user-level `~/.config/nix/nix.conf` recommendations for `max-jobs` and `cores` plus an explicit system-level-only note for `auto-optimise-store`. -The Pkl authoring layer owns generated OpenCode plugin registration for SCE-managed plugins: `config/pkl/base/opencode.pkl` defines the canonical plugin entries, `config/pkl/renderers/common.pkl` re-exports the shared plugin list for renderer use, and generated `config/.opencode/opencode.json` plus `config/automated/.opencode/opencode.json` register `./plugins/sce-bash-policy.ts` and `./plugins/sce-agent-trace.ts` through OpenCode's `plugin` field. Claude does not use an OpenCode-style plugin manifest; Claude bash-policy enforcement is registered through generated `.claude/settings.json` as a `PreToolUse` `Bash` command hook running `sce policy bash`. +The Pkl authoring layer owns generated OpenCode plugin registration for SCE-managed plugins: `config/pkl/base/opencode.pkl` defines the canonical plugin entries, `config/pkl/renderers/common.pkl` re-exports the shared plugin list for renderer use, and generated `config/.opencode/opencode.json` plus `config/automated/.opencode/opencode.json` register `./plugins/sce-bash-policy.ts` and `./plugins/sce-agent-trace.ts` through OpenCode's `plugin` field. Claude does not use an OpenCode-style plugin manifest; Claude bash-policy enforcement is registered through generated `.claude/settings.json` as a `PreToolUse` `Bash` command hook routed through `.claude/hooks/run-sce-or-show-install-guidance.sh` before running `sce policy bash`. The current CLI install/distribution contract for `sce` includes repo-flake Nix, Cargo, npm, and source-built Flatpak (`dev.crocoder.sce`) as supported channels, while `Homebrew` remains deferred from the current implementation stage. Nix-managed build/release entrypoints are the source of truth for existing binary rollout surfaces, npm consumes Nix-produced release artifacts, and repo-root `.version` is the canonical checked-in release version source that release packaging and downstream Cargo/npm publication must match. Flatpak is the approved source-built exception to binary artifact reuse: its package must build the Rust CLI from source inside Flatpak, use a Flathub-style release-source manifest plus a Nix-generated local checkout override for local builds, prepare generated CLI assets from checked-in `config/` inputs, and provide Git access through a `/app/bin/git` wrapper delegating to `flatpak-spawn --host git` with the required `org.freedesktop.Flatpak` permission. The active Flatpak release contract approves GitHub Release source-manifest assets (manifest tarball, checksum, and JSON metadata) and source-built `.flatpak` bundle assets (`sce-v-x86_64.flatpak` / `sce-v-aarch64.flatpak` plus `.sha256` / `.json`), with `.github/workflows/release-sce.yml` building/uploading those assets alongside CLI/npm assets, while still excluding automatic Flathub submission, prebuilt (non-source-built) Flatpak binaries/bundles, OSTree repositories, and release-version bumping. The shared release artifact foundation is now implemented through root-flake apps `release-artifacts` and `release-manifest`, which emit canonical `sce-v-.tar.gz` archives, SHA-256 checksum files, merged manifest outputs, and a detached `sce-v-release-manifest.json.sig` produced from a non-repo private signing key; the npm distribution surface is now implemented as a checked-in `npm/` launcher package plus root-flake `release-npm-package`, which packs `sce-v-npm.tgz`, refuses mismatched checked-in package metadata, and installs the native CLI by downloading the release manifest plus detached signature, verifying the manifest with the bundled npm public key, and only then checksum-verifying the matching GitHub release archive at npm `postinstall` time. GitHub Releases remain the canonical publication surface for binary release artifacts and approved Flatpak source-manifest package assets, while crates.io and npm registry publication are separate non-bumping publish stages under the approved release topology. GitHub CLI release automation now lives in dedicated `release-sce*.yml` workflows split by Linux, Linux ARM, and macOS ARM, and `.github/workflows/release-sce.yml` now orchestrates those three reusable platform lanes before assembling the signed release manifest, npm tarball, and GitHub release payload. The orchestrator tags/releases the checked-in `.version` directly and rejects version mismatches instead of generating a new semver during workflow execution; `.github/workflows/publish-crates.yml` and `.github/workflows/publish-npm.yml` own registry publication after release assets exist. The Linux root flake now also exposes `nix run .#release-flatpak-package -- --version --out-dir `, delegating to `packaging/flatpak/sce-flatpak.sh release-package` to emit deterministic Flatpak source-manifest tarball/checksum/JSON release assets from checked-in packaging source while running the Nix-built version-parity validator script across `.version`, `cli/Cargo.toml`, `npm/package.json`, and AppStream release metadata; `.github/workflows/release-sce.yml` runs that app into `dist/flatpak` and uploads `*.tar.gz`, `*.sha256`, and `*.json` Flatpak assets to the GitHub Release. Linux root flake also exposes `nix run .#release-flatpak-bundle -- --version --arch --out-dir `, delegating to `sce-flatpak.sh release-bundle` to build a source-built `.flatpak` bundle from the checkout using imperative `flatpak-builder` + `flatpak build-bundle` (network + bubblewrap, kept out of pure Nix), emitting per-architecture `.flatpak`/`.sha256`/`.json` files; `.github/workflows/release-sce-linux.yml` and `.github/workflows/release-sce-linux-arm.yml` build and upload x86_64/aarch64 bundles respectively, assembled by `.github/workflows/release-sce.yml`. The checked-in Flatpak packaging surface lives under `packaging/flatpak/` with Nix-owned generation: `dev.crocoder.sce.yml` is rendered from a Nix expression (`nix/flatpak/manifest.nix`) via the standard nixpkgs YAML formatter (`pkgs.formats.yaml.generate`) and regenerated by `nix run .#regenerate-flatpak-manifest`; `cargo-sources.json` is generated from `cli/Cargo.lock` by a Nix derivation wrapping `flatpak-builder-tools`/`flatpak-cargo-generator.py` and regenerated by `nix run .#regenerate-cargo-sources`; both are guarded by `flatpak-manifest-parity` and `cargo-sources-parity` flake checks. Static manifest validation is Bash-owned (`nix/flatpak/static-validate.sh`), and release-version parity validation is Bash-owned (`nix/flatpak/version-parity.sh`). AppStream metadata and the host-git wrapper source remain checked in, and `sce-flatpak.sh` is a thin imperative orchestrator (no manifest text rewriting, no embedded Python) around `flatpak-builder` and `flatpak build-bundle`, consumed by the reduced flake app surface and by Flatpak source-manifest release packaging. @@ -55,6 +55,10 @@ The hooks service now uses a minimal attribution-only runtime: `commit-msg` is t The CLI now also includes an approved operator-environment doctor contract documented in `context/sce/agent-trace-hook-doctor.md`; the runtime now matches the implemented T06 slice for `sce doctor --fix` parsing/help, stable problem/fix-result reporting, canonical hook-repair reuse, and bounded doctor-owned local-DB directory bootstrap for the missing SCE-owned DB parent path. The local DB service now provides `LocalDb` as a thin `TursoDb` alias in `cli/src/services/local_db/mod.rs`; `LocalDbSpec` resolves the canonical local DB path from the shared default-path catalog and currently declares zero migrations. Shared Turso infrastructure lives in `cli/src/services/db/mod.rs`, where `DbSpec` and generic `TursoDb` support dual-mode operation — local mode via `turso::Builder::new_local()` when `SCE_SYNC_URL`+`SCE_SYNC_TOKEN` are absent, or sync (Turso Cloud) mode via `turso::sync::Builder::new_remote()` when both are set. It owns parent-directory creation, connection setup, tokio current-thread runtime bridging, synchronous `execute`/`query`/`query_map`, generic migration execution, sync operations (`push`/`pull`/`checkpoint`/`stats`) that are no-ops in local mode (sync is never triggered automatically from `execute()`), and shared DB lifecycle helpers for service-specific database wrappers. Auth DB persistence now has a thin encrypted wrapper in `cli/src/services/auth_db/mod.rs`: `AuthDb = EncryptedTursoDb` resolves `/sce/auth.db` and embeds ordered `auth_tokens` table/index migrations, with lifecycle registration wired through `AuthDbLifecycle` in `cli/src/services/auth_db/lifecycle.rs`; auth runtime token-storage is now wired through `token_storage.rs`, which persists tokens via the `auth_credentials` table instead of a JSON file. Agent Trace persistence now has its own `cli/src/services/agent_trace_db/mod.rs` wrapper, legacy global `/sce/agent-trace.db` fallback plus active per-checkout `/sce/agent-trace-{checkout_id}.db` hook runtime paths, a split fresh-start baseline migration set (`001..008`) covering `diff_traces`, `post_commit_patch_intersections`, `agent_traces`, nullable `agent_traces.remote_url`, indexes (`idx_diff_traces_time_ms_id`, `idx_agent_traces_agent_trace_id`, `idx_agent_traces_remote_url`), and `session_models` keyed by `(tool_name, session_id)` without `AUTOINCREMENT`, plus `agent_traces.agent_trace_id` as `NOT NULL UNIQUE`; it also provides type... (line truncated to 2000 chars) The hooks command surface now also supports concrete runtime subcommand routing (`pre-commit`, `commit-msg`, `post-commit`, `post-rewrite`, `diff-trace`, and `session-model`) with deterministic argument/STDIN validation. Current runtime behavior keeps 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, `diff-trace` is the active intake path for parsed STDIN `{ sessionID, diff, time, model_id?, tool_name, tool_version }` payload persistence with optional `model_id`, required non-empty `tool_name`, required nullable/non-empty `tool_version`, missing/nullable attribution fallback from `session_models` by `tool_name` + `session_id` while direct payload values keep precedence, required `u64` millisecond `time`, non-lossy AgentTraceDb `time_ms` conversion, and AgentTraceDb-only insertion without parsed-payload artifacts under `context/tmp`; and `session-model` is the active STDIN intake for normalized model attribution upsert, including Claude `SessionStart` best-effort `claude --version` filling for missing version metadata. This behavior is documented in `context/sce/agent-trace-hooks-command-routing.md`. The removed `sce hooks claude-capture` raw capture route is documented in `context/sce/claude-raw-hook-capture.md` as a removed feature. +The 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`, resolves missing/nullable attribution from `session_models` by `tool_name` + `session_id` when available while direct payload values keep precedence, and continues with `None` for unresolved attribution, with non-lossy AgentTraceDb `time_ms` conversion, collision-safe timestamp+attempt artifact filenames, and fail-open logging through `sce.hooks.diff_trace.error` for intake failures; and `session-model` performs STDIN intake for normalized model attribution upsert without raw artifact persistence, with Claude `SessionStart` extracting `model_id` from the raw event and best-effort filling missing `tool_version` from `claude --version`. +The CLI now also includes an approved operator-environment doctor contract documented in `context/sce/agent-trace-hook-doctor.md`; the runtime now matches the implemented T06 slice for `sce doctor --fix` parsing/help, stable problem/fix-result reporting, canonical hook-repair reuse, and bounded doctor-owned local-DB directory bootstrap for the missing SCE-owned DB parent path. +The local DB service now provides `LocalDb` as a thin `TursoDb` alias in `cli/src/services/local_db/mod.rs`; `LocalDbSpec` resolves the canonical local DB path from the shared default-path catalog and currently declares zero migrations. Shared Turso infrastructure lives in `cli/src/services/db/mod.rs`, where `DbSpec` and generic `TursoDb` support dual-mode operation — local mode via `turso::Builder::new_local()` when `SCE_SYNC_URL`+`SCE_SYNC_TOKEN` are absent, or sync (Turso Cloud) mode via `turso::sync::Builder::new_remote()` when both are set. It owns parent-directory creation, connection setup, tokio current-thread runtime bridging, synchronous `execute`/`query`/`query_map`, generic migration execution, sync operations (`push`/`pull`/`checkpoint`/`stats`) that are no-ops in local mode (sync is never triggered automatically from `execute()`), and shared DB lifecycle helpers for service-specific database wrappers. Auth DB persistence now has a thin encrypted wrapper in `cli/src/services/auth_db/mod.rs`: `AuthDb = EncryptedTursoDb` resolves `/sce/auth.db` and embeds ordered `auth_tokens` table/index migrations, with lifecycle registration wired through `AuthDbLifecycle` in `cli/src/services/auth_db/lifecycle.rs`; auth runtime token-storage is now wired through `token_storage.rs`, which persists tokens via the `auth_credentials` table instead of a JSON file. Agent Trace persistence now has its own `cli/src/services/agent_trace_db/mod.rs` wrapper, legacy global `/sce/agent-trace.db` fallback plus active per-checkout `/sce/agent-trace-{checkout_id}.db` hook runtime paths, a split fresh-start baseline migration set (`001..008`) covering `diff_traces`, `post_commit_patch_intersections`, `agent_traces`, nullable `agent_traces.remote_url`, indexes (`idx_diff_traces_time_ms_id`, `idx_agent_traces_agent_trace_id`, `idx_agent_traces_remote_url`), and `session_models` keyed by `(tool_name, session_id)` without `AUTOINCREMENT`, plus `agent_traces.agent_trace_id` as `NOT NULL UNIQUE`; it also provides type... (line truncated to 2000 chars) +The hooks command surface now also supports concrete runtime subcommand routing (`pre-commit`, `commit-msg`, `post-commit`, `post-rewrite`, `diff-trace`, `conversation-trace`, and `session-model`) with deterministic argument/STDIN validation. 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, `diff-trace` is the active intake path for parsed STDIN `{ sessionID, diff, time, model_id?, tool_name, tool_version }` payload persistence with optional `model_id`, required non-empty `tool_name`, required nullable/non-empty `tool_version`, missing/nullable attribution fallback from `session_models` by `tool_name` + `session_id` while direct payload values keep precedence, required `u64` millisecond `time`, non-lossy AgentTraceDb `time_ms` conversion, collision-safe timestamp+attempt artifact filenames, and fail-open logging/success for intake failures; `conversation-trace` is the active message/part intake path with valid mixed-batch/raw-Claude-event persistence, skipped-item logging, batch-insert warnings, and fail-open logging/success for intake failures through `sce.hooks.conversation_trace.error`; and `session-model` is the active STDIN intake for normalized model attribution upsert, including Claude `SessionStart` best-effort `claude --version` filling for missing version metadata. This behavior is documented in `context/sce/agent-trace-hooks-command-routing.md`. The removed `sce hooks claude-capture` raw capture route is documented in `context/sce/claude-raw-hook-capture.md` as a removed feature. The setup service now also exposes deterministic required-hook embedded asset accessors (`iter_required_hook_assets`, `get_required_hook_asset`) backed by canonical templates in `cli/assets/hooks/` for `pre-commit`, `commit-msg`, and `post-commit`; this behavior is documented in `context/sce/setup-githooks-hook-asset-packaging.md`. The setup service now also includes required-hook install orchestration (`install_required_git_hooks`) that resolves repository root and effective hooks path from git truth, enforces deterministic per-hook outcomes (`Installed`/`Updated`/`Skipped`), and uses a unified remove-and-replace policy that removes existing hooks before swapping staged content with deterministic recovery guidance on swap failures; this behavior is documented in `context/sce/setup-githooks-install-flow.md`. The setup command parser/dispatch now also supports composable setup+hooks runs (`sce setup --opencode|--claude|--both --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 16332ba8..12063dea 100644 --- a/context/patterns.md +++ b/context/patterns.md @@ -98,7 +98,7 @@ - Keep generated-output parity anchored to `nix run .#pkl-check-generated` and the root `nix flake check` `pkl-parity` derivation; no dedicated generated-parity workflow is currently checked in. - Treat `nix run .#pkl-check-generated` and `nix flake check` as the lightweight post-task verification baseline and run both after each completed task. - For non-destructive verification during development, run `nix develop -c pkl eval -m context/tmp/t04-generated config/pkl/generate.pkl` and inspect emitted paths under `context/tmp/`. -- Keep `output.files` limited to generated-owned paths only (`config/{opencode_root}/{agent,command,skills,lib,plugins}`, generated `config/{opencode_root}/package.json`, and `config/{claude_root}/{agents,commands,skills,plugins,settings.json}`, where roots map to `.opencode` and `.claude`). +- Keep `output.files` limited to generated-owned paths only (`config/{opencode_root}/{agent,command,skills,lib,plugins}`, generated `config/{opencode_root}/package.json`, and `config/{claude_root}/{agents,commands,skills,hooks,settings.json}`, where roots map to `.opencode` and `.claude`). - For OpenCode pre-execution bash-policy hooks, keep the generated plugin entrypoint thin (`plugins/sce-bash-policy.ts`) and delegate policy evaluation to the Rust `sce policy bash --input normalized --output json` command so OpenCode and Claude share one evaluator. ## Internal subagent parity mapping @@ -154,6 +154,10 @@ - Model deferred integration boundaries with concrete event/capability data structures (for example hook-runtime attribution snapshots/policies and cloud-sync checkpoints) so later tasks can implement behavior without reshaping public seams. - For the current local-hook baseline, keep `pre-commit` and `post-rewrite` as deterministic no-op entrypoints; keep `post-commit` as the active bounded recent-diff-trace intersection entrypoint with validated `--remote-url` plumbed through Agent Trace flow and any direct diagnostics printed to stderr; keep `diff-trace` as an explicit STDIN intake path with deterministic required-field validation for `sessionID`, `diff`, `time`, `tool_name`, optional `model_id` (absent/`null` → `None`, resolved from `session_models` by `tool_name` + `session_id` when absent), and `tool_version` (present and either `null` or non-empty string), 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 `session-model` as an explicit STDIN intake path for normalized model attribution upsert with no raw artifact persistence. - For diff-trace attribution persistence, preserve direct payload `model_id` and `tool_version` values, query `session_models` only when either attribution field is missing/nullable, fill missing fields from the stored row when available, and persist unresolved attribution as `NULL` in AgentTraceDb rather than skipping DB insertion. +- 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`, resolved from `session_models` by `tool_name` + `session_id` when absent), and `tool_version` (present and either `null` or non-empty string), non-lossy AgentTraceDb `time_ms` conversion, collision-safe `context/tmp/-000000-diff-trace.json` persistence using atomic create-new retry semantics, and valid-path AgentTraceDb insertion whose post-artifact DB failure is logged and reflected in success text while preserving the artifact fallback; keep `conversation-trace` as an explicit STDIN intake path for mixed-batch/raw-Claude message and part payloads with no raw artifact persistence; keep `session-model` as an explicit STDIN intake path for normalized model attribution upsert with no raw artifact persistence. +- For `diff-trace` hook intake, keep producer-facing failure behavior fail-open: STDIN read, parse/validation, attribution lookup, artifact persistence, and setup/persistence failures are logged with `sce.hooks.diff_trace.error` and converted into command success; preserve the existing valid-payload success text and the post-artifact AgentTraceDb write-warning success path. +- For `conversation-trace` hook intake, keep producer-facing failure behavior fail-open: STDIN read, top-level parse/validation, unsupported raw Claude hook events, and AgentTraceDb setup/persistence failures are logged with `sce.hooks.conversation_trace.error` and converted into command success; preserve valid-payload mixed-batch accounting, skipped-item logging, and batch-insert warning behavior. +- For diff-trace attribution persistence, preserve direct payload `model_id` and `tool_version` values, query `session_models` only when either attribution field is missing/nullable, fill missing fields from the stored row when available, and persist unresolved attribution as `NULL` rather than skipping the artifact or DB row. - 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/sce/agent-trace-db.md b/context/sce/agent-trace-db.md index 1bb23990..3d92f605 100644 --- a/context/sce/agent-trace-db.md +++ b/context/sce/agent-trace-db.md @@ -193,6 +193,8 @@ Both triggers compare `OLD.*` vs `NEW.*` for all mutable columns (excluding `upd - `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 resolved nullable attribution through `AgentTraceDb::insert_diff_trace()` without writing a parsed-payload artifact under `context/tmp`. - AgentTraceDb open/insert failures are logged and reflected in deterministic success text as failed DB persistence; no artifact fallback is created. +- The hook writes the existing collision-safe `context/tmp/-000000-diff-trace.json` parsed-payload artifact, then attempts to insert the parsed payload fields plus resolved nullable attribution through `AgentTraceDb::insert_diff_trace()`. +- The strict valid-payload path writes the artifact before AgentTraceDb insertion. `diff-trace` intake failures, including artifact persistence failures and AgentTraceDb setup/persistence failures before the existing post-artifact DB-warning branch, are logged through `sce.hooks.diff_trace.error` and returned as command success; the existing post-artifact AgentTraceDb write-warning path still logs and returns the failed-DB-persistence success text. - Existing artifact files are not backfilled into the database. Post-commit intersection rows are written by the active `post-commit` hook flow through per-checkout lazy AgentTraceDb access, and the same flow now also inserts built Agent Trace payloads into `agent_traces` via `AgentTraceDb::insert_agent_trace()` (see [agent-trace-hooks-command-routing.md](agent-trace-hooks-command-routing.md)). The persisted `trace_json` is the schema-validated `build_agent_trace(...)` output and includes top-level `metadata.sce.version` from the compiled `sce` CLI package version plus `content_hash` on every emitted range. Range `content_hash` values are computed from the touched-line kind/content of the post-commit hunk that produced the persisted range, not from DB IDs, paths, line positions, or runtime metadata. @@ -203,7 +205,7 @@ Post-commit intersection rows are written by the active `post-commit` hook flow - `message` items validate and map payloads without message-level `text`, `agent`, or `summary_diffs` to `InsertMessageInsert`; valid rows are inserted through at most one multi-row `AgentTraceDb::insert_messages(...)` call per invocation so repeated `(session_id, message_id)` events are ignored without failing. - `message.part` items validate and map payloads with required part `text` to `InsertPartInsert`; valid rows are inserted through at most one multi-row `AgentTraceDb::insert_parts(...)` call per invocation so parts remain append-only and do not require a pre-existing message row. - Unsupported item types, missing/non-string item types, non-object items, and event-specific parser validation failures are retained as skipped-item diagnostics, logged, and counted as skipped while valid sibling items remain eligible for persistence. -- The hook opens one per-checkout `AgentTraceDb` per invocation through lazy checkout DB resolution before insertion; DB open/initialization failures remain command-failing because no rows can be attempted. +- The hook opens one per-checkout `AgentTraceDb` per invocation through lazy checkout DB resolution before insertion; DB open/initialization failures are logged through `sce.hooks.conversation_trace.error` and returned as hook success because conversation-trace intake is fail-open to producers. - Multi-row insert failures are logged once and count the whole valid-item batch as skipped without failing the command; the hook does not fall back to row-by-row insertion after a batch failure. Successful inserts contribute to deterministic success output counts (`attempted`, `persisted_messages`, `persisted_parts`, `skipped`). Duplicate parent message inserts preserve the existing `ON CONFLICT DO NOTHING` affected-row semantics. - No `context/tmp` artifact is written for conversation traces. - The generated OpenCode agent-trace plugin sends mixed-batch envelopes for conversation traces: regular `message` and `message.part` events each carry one per-item `type`, while diff-backed `message` events send one envelope containing the synthetic parent message item plus patch part items. diff --git a/context/sce/agent-trace-hooks-command-routing.md b/context/sce/agent-trace-hooks-command-routing.md index a88ae0e3..cd66b3d5 100644 --- a/context/sce/agent-trace-hooks-command-routing.md +++ b/context/sce/agent-trace-hooks-command-routing.md @@ -70,11 +70,16 @@ - When `model_id` or `tool_version` is missing/nullable in the parsed payload, Rust looks up AgentTraceDb `session_models` by `(tool_name, session_id)` and uses the stored attribution values for missing fields when available. Direct payload `model_id` and `tool_version` values keep precedence over stored values. - If no matching session row exists, missing attribution fields remain `None`; the hook still attempts the AgentTraceDb insert with nullable attribution. - Persistence: resolves the current per-checkout AgentTraceDb lazily and inserts the parsed payload fields via `DiffTraceInsert` + `insert_diff_trace()` using nullable/resolved `model_id` and `tool_version`. No parsed-payload artifact is written under `context/tmp`. + - If no matching session row exists, missing attribution fields remain `None`; the hook still persists the parsed-payload artifact and attempts the AgentTraceDb insert with nullable attribution. + - Persistence: writes one parsed-payload artifact per invocation to `context/tmp/-000000-diff-trace.json` with atomic create-new retry semantics, resolves the current per-checkout AgentTraceDb lazily, and inserts the parsed payload fields via `DiffTraceInsert` + `insert_diff_trace()` using nullable/resolved `model_id` and `tool_version`. + - Fail-open boundary: STDIN read failures, JSON parse/validation errors, attribution lookup failures, artifact persistence failures, and AgentTraceDb setup/persistence failures are logged through `sce.hooks.diff_trace.error` and converted to command success with `diff-trace hook intake failed open; error logged.` so hook callers do not receive app-level classified errors or non-zero exits for intake failures. + - Valid payload success output is unchanged: full artifact + AgentTraceDb success still returns `diff-trace hook intake persisted payload to AgentTraceDb and context/tmp.`, and the existing AgentTraceDb write-warning path still logs `sce.hooks.diff_trace.agent_trace_db_write_failed` and returns `diff-trace hook intake persisted payload to context/tmp; AgentTraceDb persistence failed.` - Current TypeScript producers are the OpenCode agent-trace plugin and the generated Claude `sce hooks` command hooks (no TypeScript intermediary). - OpenCode forwards user-message `message` diffs with `tool_name="opencode"`, always including `model_id`, and nullable OpenCode client-version metadata. - Claude forwards supported `PostToolUse` `Write` structured-update/content-create and `Edit` structured-patch diffs with `tool_name="claude"` and no direct `model_id`; any explicit payload version metadata is preserved, and missing `model_id` / `tool_version` values are resolved from `session_models` when available. - Neither TypeScript runtime writes `context/tmp/*-diff-trace.json` artifacts or AgentTraceDb rows directly. - `diff-trace` command success reports AgentTraceDb persistence only. AgentTraceDb open/insert failures are logged through `sce.hooks.diff_trace.agent_trace_db_write_failed` and reflected in deterministic success text as failed DB persistence; no parsed-payload artifact fallback is created. +- `diff-trace` command success no longer requires artifact persistence to succeed on intake failure paths. The strict valid-payload path still writes the parsed-payload artifact before AgentTraceDb insertion; failures before or during that strict path are logged through `sce.hooks.diff_trace.error` and returned as hook success, while the existing post-artifact AgentTraceDb write-warning success path remains unchanged. - `conversation-trace` is a recognized hook subcommand routed through `HookSubcommand::ConversationTrace`. Rust intake classifies incoming STDIN JSON by the presence of a top-level `hook_event_name` field: raw Claude hook events are routed through `transform_claude_user_prompt_submit`, `transform_claude_stop`, or `transform_claude_post_tool_use` depending on the event name, while payloads without `hook_event_name` follow the existing mixed-batch `{ payloads: [...] }` path. - **Raw Claude `UserPromptSubmit` events** (detected by `hook_event_name = "UserPromptSubmit"`): the raw event payload is validated and transformed by `transform_claude_user_prompt_submit` before being forwarded to `parse_conversation_trace_payloads`. - Validates that `hook_event_name` is exactly `"UserPromptSubmit"` and the required `session_id` and `prompt` fields are present and non-empty. @@ -96,18 +101,19 @@ - Produces one `message` item (with `role: "assistant"` and `generated_at_unix_ms`) plus one `message.part` item sharing the same `session_id` and generated `message_id`: - On `PatchBuildResult::Built(parsed_patch)`: produces one `message.part` with `part_type: "patch"` and `text` set to JSON-serialized `ParsedPatch`. - On `PatchBuildResult::Skipped(_)`: silently returns zero items (no-op, e.g. for unsupported tools or malformed payloads that would previously have been validation errors). - - Unsupported `hook_event_name` values (not `"UserPromptSubmit"`, `"Stop"`, or `"PostToolUse"`) fail deterministically with an `Invalid conversation-trace payload from STDIN: unsupported Claude hook event '...': supported events are 'UserPromptSubmit', 'Stop' and 'PostToolUse'` error that includes the event name for the existing error-logging path. + - Unsupported `hook_event_name` values (not `"UserPromptSubmit"`, `"Stop"`, or `"PostToolUse"`) produce an `Invalid conversation-trace payload from STDIN: unsupported Claude hook event '...': supported events are 'UserPromptSubmit', 'Stop' and 'PostToolUse'` error internally, then fail open through `sce.hooks.conversation_trace.error` with hook success text. - **Mixed-batch path** (no `hook_event_name`): Rust intake expects a top-level `payloads` array and per-item `type` discriminators. A top-level `type` field is ignored by the parser; old homogeneous `{ type, payloads }` envelopes are not a compatibility path because same-kind items without their own `type` are skipped rather than classified from the envelope. - `payloads[].type: "message"` parses that item into `InsertMessageInsert` with required non-empty `session_id`, `message_id`, valid `role` (`user|assistant`), and non-negative signed-64-bit `generated_at_unix_ms`; message-level `text`, `agent`, and `summary_diffs` are not required or mapped because body text belongs to `message.part` / `parts.text`. - `payloads[].type: "message.part"` parses that item into `InsertPartInsert` with required non-empty `session_id`, `message_id`, valid `part_type` (`text|reasoning|patch|question`), string `text`, and non-negative signed-64-bit `generated_at_unix_ms`. - `part_type: "text"` and `part_type: "reasoning"` store the raw `text` string unchanged. - `part_type: "patch"` first tries `load_patch_from_json` — if `payload.text` is already a valid JSON-serialized `ParsedPatch`, stores the raw text unchanged (no re-parse, no re-serialize). If JSON loading fails, falls back to the shared unified-diff parser (`parse_patch_from_text`) and stores JSON-serialized `ParsedPatch` in `parts.text`. If both fail, the item is skipped with a validation error mentioning both patch and patch-JSON formats. - `part_type: "question"` requires `text` to be a JSON string whose parsed value is an array of objects with string `question` and `answer` fields; valid question text is stored unchanged, while invalid question text is skipped through the same per-item validation path as malformed patch items. - - Unsupported item `type` values, missing/non-string item `type`, non-object items, and event-specific item validation failures are recorded as skipped-item diagnostics (`index`, `reason`) while valid sibling items remain eligible for persistence; skipped validation items are logged through `sce.hooks.conversation_trace.payload_skipped`. Top-level JSON/object/`payloads` shape failures fail deterministically with `Invalid conversation-trace payload from STDIN: ...` diagnostics. + - Unsupported item `type` values, missing/non-string item `type`, non-object items, and event-specific item validation failures are recorded as skipped-item diagnostics (`index`, `reason`) while valid sibling items remain eligible for persistence; skipped validation items are logged through `sce.hooks.conversation_trace.payload_skipped`. Top-level JSON/object/`payloads` shape failures produce `Invalid conversation-trace payload from STDIN: ...` diagnostics internally, then fail open through `sce.hooks.conversation_trace.error` with hook success text. - Shared persistence (both classification paths converge before DB writes): - Current persistence opens one per-checkout `AgentTraceDb` per hook invocation through lazy checkout DB resolution, then inserts the non-empty valid `message` batch through at most one multi-row `AgentTraceDb::insert_messages(...)` call and the non-empty valid `message.part` batch through at most one multi-row `AgentTraceDb::insert_parts(...)` call. - - DB open/initialization failures are command-failing runtime errors logged through `sce.hooks.conversation_trace.error`; valid-item multi-row insert failures are logged once through `sce.hooks.conversation_trace.agent_trace_db_batch_failed`, count the whole valid-item batch as skipped, and do not fail the command. The hook does not fall back to row-by-row insertion after a multi-row insert failure. - - Current success output reports deterministic mixed-batch accounting: `conversation-trace hook persisted mixed payload batch to AgentTraceDb: attempted=, persisted_messages=, persisted_parts=, skipped=.` The hook does not persist `context/tmp` artifacts. + - DB open/initialization failures fail open through `sce.hooks.conversation_trace.error` with hook success text; valid-item multi-row insert failures are logged once through `sce.hooks.conversation_trace.agent_trace_db_batch_failed`, count the whole valid-item batch as skipped, and do not fail the command. The hook does not fall back to row-by-row insertion after a multi-row insert failure. + - Current valid-payload success output reports deterministic mixed-batch accounting: `conversation-trace hook persisted mixed payload batch to AgentTraceDb: attempted=, persisted_messages=, persisted_parts=, skipped=.` The hook does not persist `context/tmp` artifacts. + - Fail-open output for conversation-trace intake failures is `conversation-trace hook intake failed open; error logged.` so hook callers do not receive app-level classified errors or non-zero exits for intake failures. - The generated OpenCode agent-trace plugin emits this mixed-batch shape for conversation-trace handoff: ordinary message/part events produce one-item mixed envelopes, completed question-tool parts produce `message.part` items with `part_type: "question"`, and diff-backed message events produce one envelope containing the synthetic parent `message` item plus patch `message.part` items. - `session-model` reads STDIN JSON and classifies the payload: - **Claude `SessionStart` payloads** (detected by presence of top-level `hook_event_name`): extracts `session_id` from `session_id`/`sessionID`, `model_id` from `model`/`model_id` (including nested `model.id`/`model.model`/`model.name` with `claude/` prefix normalization), `time` from `time`/`timestamp` (falls back to current system time), `tool_name="claude"`, and `tool_version` from `tool_version`/`claude_version`/`version`; when no non-empty payload version is present, Rust best-effort runs `claude --version`, trims stdout, and uses that value if non-empty, otherwise leaving `tool_version` nullable without failing intake. diff --git a/context/sce/bash-tool-policy-enforcement-contract.md b/context/sce/bash-tool-policy-enforcement-contract.md index 6f33a9ec..4af44ef0 100644 --- a/context/sce/bash-tool-policy-enforcement-contract.md +++ b/context/sce/bash-tool-policy-enforcement-contract.md @@ -131,7 +131,7 @@ The original contract intentionally excluded shell control operators (`|`, `&&`, - If ANY segment matches a blocking policy, the entire command is blocked - This applies to both preset policies (e.g., `forbid-git-all`) and custom policies -**Implementation:** `cli/src/services/bash_policy.rs` owns the canonical Rust evaluator and the hidden `sce policy bash` command adapter for hook callers. The OpenCode plugin at `config/lib/bash-policy-plugin/opencode-bash-policy-plugin.ts` is a thin wrapper that delegates to `sce policy bash --input normalized --output json` via `spawnSync`, while generated Claude settings register a `PreToolUse` `Bash` command hook that runs `sce policy bash` directly; neither target contains independent policy logic. Both preserve original single-command behavior for commands without operators. +**Implementation:** `cli/src/services/bash_policy.rs` owns the canonical Rust evaluator and the hidden `sce policy bash` command adapter for hook callers. The OpenCode plugin at `config/lib/bash-policy-plugin/opencode-bash-policy-plugin.ts` is a thin wrapper that delegates to `sce policy bash --input normalized --output json` via `spawnSync`, while generated Claude settings register a `PreToolUse` `Bash` command hook that calls `.claude/hooks/run-sce-or-show-install-guidance.sh` before `sce policy bash`; neither target contains independent policy logic. Both preserve original single-command behavior for commands without operators. **Examples:** - `cat abc | git diff` with `forbid-git-all` -> blocked (segment "git diff" matches) diff --git a/context/sce/claude-raw-hook-capture.md b/context/sce/claude-raw-hook-capture.md index 64b156af..690e1d92 100644 --- a/context/sce/claude-raw-hook-capture.md +++ b/context/sce/claude-raw-hook-capture.md @@ -8,6 +8,8 @@ Rust now exposes only normalized intakes for Claude/OpenCode editor runtimes: - `sce hooks session-model` — STDIN JSON intake for normalized model attribution upsert in `session_models`, keyed by `(tool_name, session_id)`. No raw hook artifacts are written. - `sce hooks diff-trace` — STDIN JSON intake for normalized or Claude structured diff-trace payloads with optional/nullable attribution. When `model_id` or `tool_version` is missing, Rust resolves available values from `session_models` by `(tool_name, session_id)` and otherwise persists nullable attribution to AgentTraceDb without writing raw hook artifacts. +- `sce hooks diff-trace` — STDIN JSON intake for normalized or Claude structured diff-trace payloads with optional/nullable attribution. When `model_id` or `tool_version` is missing, Rust resolves available values from `session_models` by `(tool_name, session_id)` and otherwise persists nullable attribution on the valid path; runtime intake failures log `sce.hooks.diff_trace.error` and fail open to the hook producer. +- `sce hooks conversation-trace` — STDIN JSON intake for normalized mixed-batch message/part payloads and supported raw Claude `UserPromptSubmit`, `Stop`, and `PostToolUse` events. Runtime intake failures log `sce.hooks.conversation_trace.error` and fail open to the hook producer. ## Historical artifact contract @@ -23,7 +25,7 @@ The generated Claude TypeScript runtime at `config/.claude/plugins/sce-agent-tra ## Current state -- Claude settings call `sce hooks` directly via generated `.claude/settings.json` command hooks: `SessionStart` pipes raw hook event JSON to `sce hooks session-model`, matched `PostToolUse Write|Edit|MultiEdit|NotebookEdit` pipes raw hook event JSON to `sce hooks diff-trace`. Rust handles extraction, validation, and persistence without a TypeScript intermediary. +- Claude settings call the generated Bash helper `.claude/hooks/run-sce-or-show-install-guidance.sh` via generated `.claude/settings.json` command hooks before invoking `sce`: `SessionStart` pipes raw hook event JSON to `sce hooks session-model`, matched `PostToolUse Write|Edit|MultiEdit|NotebookEdit` pipes raw hook event JSON to `sce hooks diff-trace`, supported conversation events pipe raw hook event JSON to `sce hooks conversation-trace`, and `PreToolUse Bash` pipes raw hook event JSON to `sce policy bash`. The helper emits `sce CLI not found. Install it from https://sce.crocoder.dev/docs/getting-started#install-cli` and exits successfully when `sce` is missing; when `sce` exists it `exec`s the original command arguments so Rust receives stdin and owns stdout/stderr/exit behavior. Rust handles extraction, validation, and persistence without a TypeScript intermediary. - The former Claude TypeScript runtime at `config/.claude/plugins/sce-agent-trace.ts` was removed in T07 of the `claude-rust-diff-trace` plan. - Rust owns normalized persistence: `session-model` upserts into `session_models`, `diff-trace` inserts into `diff_traces` with `payload_type` classification (`"patch"` for OpenCode, `"structured"` for Claude). - Claude `diff-trace` missing `model_id` and `tool_version` values are resolved from `session_models` at persistence time when available, otherwise stored as nullable attribution; OpenCode sends `model_id` directly and may send nullable `tool_version`. diff --git a/context/sce/generated-opencode-plugin-registration.md b/context/sce/generated-opencode-plugin-registration.md index 2c4ec940..cb8cab9e 100644 --- a/context/sce/generated-opencode-plugin-registration.md +++ b/context/sce/generated-opencode-plugin-registration.md @@ -24,9 +24,10 @@ The generated-config pipeline now has one canonical Pkl-authored source for Open ## Claude boundary - Claude does not consume the OpenCode `plugin` manifest surface. -- Claude agent-trace event handling is registered through generated `.claude/settings.json` command hooks that call `sce hooks` directly: `SessionStart` → `sce hooks session-model`, matched `PostToolUse Write|Edit|MultiEdit|NotebookEdit` → `sce hooks diff-trace`. +- Claude agent-trace event handling is registered through generated `.claude/settings.json` command hooks that call `.claude/hooks/run-sce-or-show-install-guidance.sh` before invoking `sce hooks`: `SessionStart` → `sce hooks session-model`, matched `PostToolUse Write|Edit|MultiEdit|NotebookEdit` → `sce hooks diff-trace`, and supported conversation events → `sce hooks conversation-trace`. - The Rust CLI receives raw Claude hook event JSON on STDIN and handles extraction, validation, and persistence without a TypeScript translation layer. -- Claude bash-policy enforcement is registered through generated `.claude/settings.json` as a `PreToolUse` `Bash` command hook that runs `sce policy bash` and passes raw hook event JSON on STDIN. +- Claude bash-policy enforcement is registered through generated `.claude/settings.json` as a `PreToolUse` `Bash` command hook that calls the same generated helper before running `sce policy bash` and passing raw hook event JSON on STDIN. +- The Claude helper emits `sce CLI not found. Install it from https://sce.crocoder.dev/docs/getting-started#install-cli` and exits successfully when `sce` is missing, preserving fail-open hook behavior; when `sce` exists it `exec`s the original command arguments. - OpenCode bash-policy enforcement delegates to the same Rust `sce policy bash` command through a thin generated plugin wrapper; the former TypeScript runtime (`bash-policy/runtime.ts`) has been removed from generated outputs. ## Ownership and edit policy @@ -39,6 +40,6 @@ The generated-config pipeline now has one canonical Pkl-authored source for Open - Inspect `config/.opencode/opencode.json` and `config/automated/.opencode/opencode.json` for the generated `plugin` field. - Inspect `config/.opencode/plugins/sce-bash-policy.ts`, `config/.opencode/plugins/sce-agent-trace.ts`, `config/automated/.opencode/plugins/sce-bash-policy.ts`, and `config/automated/.opencode/plugins/sce-agent-trace.ts` for the generated plugin implementations. -- Verify `config/.claude/settings.json` contains the generated `PreToolUse` `Bash` policy hook and that `config/.claude/` still contains no Claude bash-policy TypeScript runtime files. +- Verify `config/.claude/settings.json` contains the generated `PreToolUse` `Bash` policy hook, verify `config/.claude/hooks/run-sce-or-show-install-guidance.sh` contains the missing-CLI guidance path, and verify `config/.claude/` still contains no Claude bash-policy TypeScript runtime files. See also: [../overview.md](../overview.md), [../architecture.md](../architecture.md), [../glossary.md](../glossary.md) diff --git a/context/sce/opencode-agent-trace-plugin-runtime.md b/context/sce/opencode-agent-trace-plugin-runtime.md index b02742b6..368170c2 100644 --- a/context/sce/opencode-agent-trace-plugin-runtime.md +++ b/context/sce/opencode-agent-trace-plugin-runtime.md @@ -4,7 +4,7 @@ Current TypeScript runtime source: - `config/lib/agent-trace-plugin/opencode-sce-agent-trace-plugin.ts` -The Claude TypeScript agent-trace runtime was removed in T07 of the `claude-rust-diff-trace` plan. Claude now routes through generated `.claude/settings.json` command hooks that call `sce hooks` directly with raw hook event JSON on STDIN; Rust handles extraction, validation, and persistence without a TypeScript intermediary. +The Claude TypeScript agent-trace runtime was removed in T07 of the `claude-rust-diff-trace` plan. Claude now routes through generated `.claude/settings.json` command hooks that call `.claude/hooks/run-sce-or-show-install-guidance.sh` before invoking `sce hooks` with raw hook event JSON on STDIN; Rust handles extraction, validation, and persistence without a TypeScript intermediary. ## Event capture baseline @@ -14,8 +14,9 @@ The Claude TypeScript agent-trace runtime was removed in T07 of the `claude-rust - **When diffs exist**: builds one mixed `-patch` conversation-trace envelope containing the synthetic parent `message` item with `message_id = "${id}-patch"` plus all per-diff `message.part` patch items, then invokes `sce hooks conversation-trace` once. The original `message` event is replaced — no original `message` payload is sent. - **When no diffs exist**: builds one mixed envelope containing a single `message` item via `buildConversationTracePayload` and invokes `sce hooks conversation-trace` over STDIN JSON. - For captured `message.part` events, the plugin dispatches only supported part shapes to `sce hooks conversation-trace`: ordinary `text` and `reasoning` parts with non-empty `text`, plus completed OpenCode `question` tool parts emitted as first-class `part_type: "question"` payloads. +- Both `runConversationTraceHook` and `runDiffTraceHook` fail open at the plugin level: they ignore the child process stderr (`stdio: ["pipe", "ignore", "ignore"]`) and resolve unconditionally on error or close, so spawn errors, non-zero exits, and sce intake errors (connection refused, timeout, etc.) never produce unhandled promise rejections or leak sce stderr into the OpenCode TUI. - Existing diff-trace capture remains filtered to user messages with usable diffs. -- When diff extraction succeeds, the plugin invokes `sce hooks diff-trace` after conversation-trace handoff and sends `{ sessionID, diff, time, model_id, tool_name, tool_version }` over STDIN JSON (`tool_name` is always `"opencode"`; `tool_version` is captured from session lifecycle events when available). +- When diff extraction succeeds, the plugin invokes `sce hooks diff-trace` after conversation-trace handoff and sends `{ sessionID, diff, time, model_id, tool_name, tool_version }` over STDIN JSON (`tool_name` is always `"opencode"`; `tool_version` is captured from session lifecycle events when available). `runDiffTraceHook` fails open at the plugin level (ignored stderr, unconditional resolve), so callers do not need try/catch. - The plugin no longer writes diff-trace artifacts or database rows directly; the Rust `diff-trace` hook path owns AgentTraceDb insertion plus collision-safe timestamp+attempt artifact writes. ## In-memory dedup cache @@ -95,6 +96,7 @@ When extraction succeeds, `buildQuestionToolConversationTracePayload(eventPart)` ## Current usage boundary +- The plugin-level fail-open applies to both `runDiffTraceHook` and `runConversationTraceHook`: both functions ignore the child process stderr and resolve unconditionally on any outcome. This prevents sce intake errors from appearing as messages in the OpenCode TUI, since callers await these functions without try/catch. - `recordConversationTrace(repoRoot, event)` branches on event type: - For `message` events: calls `buildPatchConversationTracePayload` first. - If a patch payload is returned (diff entries exist), dispatches it once — the original `message` payload is not sent. @@ -106,12 +108,12 @@ When extraction succeeds, `buildQuestionToolConversationTracePayload(eventPart)` - The diff extraction seam is internal to the source module and is used by `buildTrace` at runtime. - `buildTrace` exits early when extraction returns `undefined` (non-user role, empty diffs array, or no usable patch entries), so no diff-trace hook invocation occurs for those events. - The plugin tracks OpenCode client version per session ID from `session.created` / `session.updated` events and forwards it as `tool_version` when available. -- When extraction succeeds, `buildTrace` forwards the extracted payload with required `tool_name="opencode"` and required `tool_version` (nullable when session version is unavailable) to `sce hooks diff-trace` via STDIN JSON; the Rust hook runtime validates required non-empty `sessionID`/`diff`/`tool_name`, optional `model_id`, required nullable/non-empty `tool_version`, plus required `time`, resolves missing/nullable attribution fields from `session_models` when available while preserving direct payload precedence, and persists DB-backed diff-trace fields through AgentTraceDb `diff_traces` insertion. +- When extraction succeeds, `buildTrace` forwards the extracted payload with required `tool_name="opencode"` and required `tool_version` (nullable when session version is unavailable) to `sce hooks diff-trace` via STDIN JSON; the Rust hook runtime validates required non-empty `sessionID`/`diff`/`tool_name`, optional `model_id`, required nullable/non-empty `tool_version`, plus required `time`, resolves missing/nullable attribution fields from `session_models` when available while preserving direct payload precedence, persists DB-backed diff-trace fields through AgentTraceDb `diff_traces` insertion on the valid path, and fails open for runtime intake failures by logging `sce.hooks.diff_trace.error` while returning hook success to the producer. ## Shared boundary with Claude runtime - OpenCode uses a generated TypeScript event runtime as an event-shape adapter before handing normalized diff-trace payloads to the shared Rust hook intake. -- Claude registration uses generated `.claude/settings.json` command hooks that call `sce hooks` directly (no TypeScript runtime intermediary): `SessionStart` pipes the raw Claude hook event JSON to `sce hooks session-model`, and matched `PostToolUse Write|Edit|MultiEdit|NotebookEdit` pipes the raw hook event to `sce hooks diff-trace`. +- Claude registration uses generated `.claude/settings.json` command hooks that call `.claude/hooks/run-sce-or-show-install-guidance.sh` before `sce hooks` (no TypeScript runtime intermediary): `SessionStart` pipes the raw Claude hook event JSON to `sce hooks session-model`, matched `PostToolUse Write|Edit|MultiEdit|NotebookEdit` pipes the raw hook event to `sce hooks diff-trace`, and supported conversation events pipe raw hook events to `sce hooks conversation-trace`. - Rust `diff-trace` intake detects Claude payloads via `hook_event_name` and derives structured patches from the raw JSON with `payload_type="structured"`; OpenCode normalized payloads (no `hook_event_name`) are stored as `payload_type="patch"`. - Rust `session-model` intake detects Claude `SessionStart` payloads via `hook_event_name`, extracts `session_id`/`model_id`/`time` from the raw Claude event format, uses explicit payload version fields (`tool_version`/`claude_version`/`version`) when present, and otherwise best-effort fills `tool_version` from trimmed `claude --version` stdout when available. - The shared Rust boundary is `sce hooks diff-trace` and `sce hooks session-model`: Rust remains the only writer of AgentTraceDb `diff_traces`/`session_models` rows and no longer writes parsed `context/tmp/*-diff-trace.json` artifacts. diff --git a/context/sce/setup-githooks-hook-asset-packaging.md b/context/sce/setup-githooks-hook-asset-packaging.md index 93b50280..ad592370 100644 --- a/context/sce/setup-githooks-hook-asset-packaging.md +++ b/context/sce/setup-githooks-hook-asset-packaging.md @@ -16,7 +16,8 @@ These templates are emitted into `OUT_DIR/setup_embedded_assets.rs` as `HOOK_EMB Current `post-commit` template behavior is: -- set `remote_name` to `origin`; if `git remote get-url "$remote_name"` returns a non-empty URL, invoke `sce hooks post-commit --vcs git --remote-url "$remote_url" "$@"` +- resolve `origin` with `git remote get-url origin`; if `sce` is not on `PATH`, print `sce CLI not found. Install it from https://sce.crocoder.dev/docs/getting-started#install-cli` to stderr and exit successfully so missing local CLI installation does not block the commit +- if the remote lookup returns a non-empty URL, invoke `sce hooks post-commit --vcs git --remote-url "$remote_url" "$@"` - otherwise still invoke `sce hooks post-commit --vcs git "$@"`; Rust-side validation fails this missing-URL path without blocking git commit completion under the hook script policy. ## Setup-service accessor surface