diff --git a/claude-plugin/skills/using-tracedecay/SKILL.md b/claude-plugin/skills/using-tracedecay/SKILL.md index b22a15f9c4..3cd9dda7b4 100644 --- a/claude-plugin/skills/using-tracedecay/SKILL.md +++ b/claude-plugin/skills/using-tracedecay/SKILL.md @@ -12,6 +12,16 @@ code, reading code, tracing calls, estimating blast radius, recalling prior context — try the matching tracedecay tool BEFORE Grep, Glob, codebase search, or file reads. You cannot rationalize your way out of this. +## Scope and priority + +- SUBAGENT-STOP: if you are a scoped subagent that was handed the exact files, + symbols, or excerpts to act on, do NOT re-run discovery — act on what you + were given. This mandate governs open-ended exploration, not narrow handoffs. +- Priority ladder: explicit user instructions and project rules + (CLAUDE.md / AGENTS.md) win over this skill; this skill wins over the host's + default "just grep it" habit. Never fight a direct instruction to satisfy + the mandate. + ## Moment → mandatory action | The moment you are in | Do this first | diff --git a/codex-plugin/skills/using-tracedecay/SKILL.md b/codex-plugin/skills/using-tracedecay/SKILL.md index b22a15f9c4..3cd9dda7b4 100644 --- a/codex-plugin/skills/using-tracedecay/SKILL.md +++ b/codex-plugin/skills/using-tracedecay/SKILL.md @@ -12,6 +12,16 @@ code, reading code, tracing calls, estimating blast radius, recalling prior context — try the matching tracedecay tool BEFORE Grep, Glob, codebase search, or file reads. You cannot rationalize your way out of this. +## Scope and priority + +- SUBAGENT-STOP: if you are a scoped subagent that was handed the exact files, + symbols, or excerpts to act on, do NOT re-run discovery — act on what you + were given. This mandate governs open-ended exploration, not narrow handoffs. +- Priority ladder: explicit user instructions and project rules + (CLAUDE.md / AGENTS.md) win over this skill; this skill wins over the host's + default "just grep it" habit. Never fight a direct instruction to satisfy + the mandate. + ## Moment → mandatory action | The moment you are in | Do this first | diff --git a/cursor-plugin/skills/using-tracedecay/SKILL.md b/cursor-plugin/skills/using-tracedecay/SKILL.md index b22a15f9c4..3cd9dda7b4 100644 --- a/cursor-plugin/skills/using-tracedecay/SKILL.md +++ b/cursor-plugin/skills/using-tracedecay/SKILL.md @@ -12,6 +12,16 @@ code, reading code, tracing calls, estimating blast radius, recalling prior context — try the matching tracedecay tool BEFORE Grep, Glob, codebase search, or file reads. You cannot rationalize your way out of this. +## Scope and priority + +- SUBAGENT-STOP: if you are a scoped subagent that was handed the exact files, + symbols, or excerpts to act on, do NOT re-run discovery — act on what you + were given. This mandate governs open-ended exploration, not narrow handoffs. +- Priority ladder: explicit user instructions and project rules + (CLAUDE.md / AGENTS.md) win over this skill; this skill wins over the host's + default "just grep it" habit. Never fight a direct instruction to satisfy + the mandate. + ## Moment → mandatory action | The moment you are in | Do this first | diff --git a/src/agents/kiro.rs b/src/agents/kiro.rs index 1e3cd9fed7..1b145c7123 100644 --- a/src/agents/kiro.rs +++ b/src/agents/kiro.rs @@ -30,7 +30,7 @@ use super::{ /// Kiro agent. pub struct KiroIntegration; -const PROMPT_MARKER: &str = "## Prefer tracedecay MCP tools"; +const PROMPT_MARKER: &str = "## TraceDecay: mandatory tool routing"; const PROMPT_END_MARKER: &str = ""; const KIRO_AGENT_NAME: &str = "tracedecay"; const OWNED_AGENT_DESCRIPTION: &str = @@ -615,28 +615,36 @@ fn prompt_rules_text() -> String { fn prompt_rules_text_without_end_marker() -> String { format!( - "## Prefer tracedecay MCP tools\n\n\ -Before reading source files or scanning the codebase, use the tracedecay MCP tools \ -(`tracedecay_context`, `tracedecay_search`, `tracedecay_callers`, `tracedecay_callees`, \ -`tracedecay_impact`, `tracedecay_node`, `tracedecay_files`, `tracedecay_affected`). \ -They provide semantic results from a pre-built local knowledge graph and are faster \ -than broad file reads.\n\n\ + "## TraceDecay: mandatory tool routing\n\n\ +This project has a live TraceDecay code graph. If there is even a 1% chance a \ +tracedecay tool applies, you MUST use it BEFORE Kiro's file reads, codebase scan, \ +or `delegate`. This is a mandate, not a preference — you cannot rationalize your \ +way out of it.\n\n\ +| The moment you are in | Do this first |\n\ +|---|---|\n\ +| Searching for a symbol or concept | `tracedecay_search` (names) or `tracedecay_context` (concepts) |\n\ +| About to read a source file | `tracedecay_outline` -> `tracedecay_body` -> `tracedecay_read` slices |\n\ +| \"Who calls X\" / \"what does X call\" / \"trace this\" | `tracedecay_callers` / `tracedecay_callees` |\n\ +| About to change code, wondering what breaks | `tracedecay_impact` / `tracedecay_diff_context` / `tracedecay_affected` |\n\ +| Project / storage identity question | `tracedecay_active_project` / `tracedecay_storage_status` |\n\ +| A prior decision or past conversation is referenced | `tracedecay_message_search` / `tracedecay_lcm_expand_query` |\n\n\ +| Red-flag thought | Reality |\n\ +|---|---|\n\ +| \"Grep is faster for this\" | `tracedecay_search` is one call and pre-ranked. |\n\ +| \"I'll just read the whole file\" | `tracedecay_outline` / `tracedecay_body` answer at a fraction of the tokens. |\n\ +| \"This is a simple lookup\" | Simple lookups are exactly what the graph is for. |\n\ +| \"I already know this codebase\" | The graph is fresher than your memory. Check it. |\n\n\ +SUBAGENT-STOP: if you were handed the exact files, symbols, or excerpts to act on, \ +do NOT re-run discovery — act on what you were given. Explicit user instructions and \ +project rules (CLAUDE.md / AGENTS.md) win over this mandate; the mandate wins over the \ +default \"just grep it\" habit. Never fight a direct instruction to satisfy it.\n\n\ Do not use Kiro's `delegate` tool for codebase exploration, architecture mapping, \ call graph work, symbol lookup, or other code research until tracedecay MCP tools \ have been tried. Delegation is still appropriate for long-running execution work \ such as builds, tests, generated reports, or independent implementation tasks.\n\n\ -For project/storage identity questions, use `tracedecay_active_project` or \ -`tracedecay_storage_status` instead of inferring from marker files or \ -direct DB paths.\n\n\ -If a code analysis question cannot be fully answered by tracedecay MCP tools, prefer \ -built-in MCP tools first. If the user explicitly needs raw store inspection, use the \ -resolved graph DB path reported by `tracedecay_storage_status` rather than a hardcoded \ -repo path. Use SQL for structural queries that go beyond the MCP tools.\n\n\ For durable project/user facts, prefer `tracedecay_fact_store`, \ -`tracedecay_fact_feedback`, and `tracedecay_memory_status` over ad-hoc notes. Use \ -`tracedecay_message_search` for active-project transcript recall when prior \ -conversation context matters. Do not store secrets, credentials, or unnecessary PII \ -in persistent facts.\n\n\ +`tracedecay_fact_feedback`, and `tracedecay_memory_status` over ad-hoc notes. Do not \ +store secrets, credentials, or unnecessary PII in persistent facts.\n\n\ {cli_fallback}\n\n\ If you discover a gap where an extractor, schema, or tracedecay tool could answer a \ question natively, propose opening an issue at \ diff --git a/src/hooks/claude.rs b/src/hooks/claude.rs index 2ef993a4fb..30bb1e4d52 100644 --- a/src/hooks/claude.rs +++ b/src/hooks/claude.rs @@ -11,8 +11,8 @@ use super::codex::{ }; use super::post_tool_use::{notify_post_tool_use, CLAUDE_POST_TOOL_USE_SPEC}; use super::steering::{ - append_context_recovery_hint, cursor_index_signals_for_root, index_status_line, - session_start_from_compaction, + append_context_recovery_hint, append_tracedecay_bootstrap_context, + cursor_index_signals_for_root, index_status_line, session_start_from_compaction, }; use super::tool_hints::{decide_hint, HintAgent, ToolHintInput}; use super::{ @@ -122,13 +122,20 @@ pub async fn hook_claude_session_start() -> i32 { 0 } -/// Builds the lean Claude `SessionStart` context for code workspaces. -async fn claude_session_context_for_event(event_json: &str) -> String { +/// Builds the Claude `SessionStart` context for code workspaces. +/// +/// On an initialized project this injects the full `using-tracedecay` +/// adoption contract (the `` bootstrap) after the index +/// status line, matching the Cursor and Codex session hooks so Claude's +/// `SessionStart` channel carries the same mandate. +pub async fn claude_session_context_for_event(event_json: &str) -> String { let parsed = serde_json::from_str::(event_json).unwrap_or(Value::Null); match codex_project_root_from_parsed_event(&parsed) { Some(root) => { let (staleness, _) = cursor_index_signals_for_root(&root).await; - index_status_line(true, staleness.as_deref()) + let mut context = index_status_line(true, staleness.as_deref()); + append_tracedecay_bootstrap_context(&mut context); + context } None if event_cwd_from_parsed(&parsed) .as_deref() diff --git a/src/hooks/mod.rs b/src/hooks/mod.rs index f55f42b548..087a4010ed 100644 --- a/src/hooks/mod.rs +++ b/src/hooks/mod.rs @@ -33,8 +33,8 @@ mod steering; pub mod tool_hints; pub use claude::{ - evaluate_hook_decision, hook_claude_post_tool_use, hook_claude_session_start, - hook_pre_tool_use, hook_prompt_submit, hook_stop, + claude_session_context_for_event, evaluate_hook_decision, hook_claude_post_tool_use, + hook_claude_session_start, hook_pre_tool_use, hook_prompt_submit, hook_stop, }; pub use codex::{ codex_additional_context_json, codex_apply_patch_rel_paths, codex_project_root_from_event, diff --git a/src/hooks/steering.rs b/src/hooks/steering.rs index 1721acd5bb..ca083e892b 100644 --- a/src/hooks/steering.rs +++ b/src/hooks/steering.rs @@ -39,7 +39,7 @@ pub const CURSOR_PLUGIN_SKILLS: &[&str] = &[ const TRACEDECAY_BOOTSTRAP_SKILL: &str = include_str!("../../cursor-plugin/skills/using-tracedecay/SKILL.md"); -fn append_tracedecay_bootstrap_context(s: &mut String) { +pub(super) fn append_tracedecay_bootstrap_context(s: &mut String) { s.push_str( "\n\ This project has TraceDecay code-graph support. Below is the full `tracedecay:using-tracedecay` \ diff --git a/tests/agent_suite/kiro_agent_test.rs b/tests/agent_suite/kiro_agent_test.rs index 451eda2e92..b685bac03e 100644 --- a/tests/agent_suite/kiro_agent_test.rs +++ b/tests/agent_suite/kiro_agent_test.rs @@ -131,7 +131,7 @@ fn test_install_creates_global_mcp_steering_agent_and_default() { "global Kiro tracedecay.md should exist" ); let steering = std::fs::read_to_string(&steering_path).unwrap(); - assert!(steering.contains("## Prefer tracedecay MCP tools")); + assert!(steering.contains("## TraceDecay: mandatory tool routing")); assert!(steering.contains("delegate")); let agent_path = home.join(".kiro/agents/tracedecay.json"); @@ -333,18 +333,18 @@ fn test_install_and_uninstall_preserve_existing_steering_content() { let user_steering = std::fs::read_to_string(&user_steering_path).unwrap(); assert!(user_steering.contains("## Existing Kiro guidance")); assert!(user_steering.contains("Keep this user-authored guidance.")); - assert!(!user_steering.contains("## Prefer tracedecay MCP tools")); + assert!(!user_steering.contains("## TraceDecay: mandatory tool routing")); let tracedecay_steering_path = home.join(".kiro/steering/tracedecay.md"); let installed = std::fs::read_to_string(&tracedecay_steering_path).unwrap(); - assert!(installed.contains("## Prefer tracedecay MCP tools")); + assert!(installed.contains("## TraceDecay: mandatory tool routing")); KiroIntegration.uninstall(&ctx).unwrap(); let uninstalled = std::fs::read_to_string(&user_steering_path).unwrap(); assert!(uninstalled.contains("## Existing Kiro guidance")); assert!(uninstalled.contains("Keep this user-authored guidance.")); - assert!(!uninstalled.contains("## Prefer tracedecay MCP tools")); + assert!(!uninstalled.contains("## TraceDecay: mandatory tool routing")); assert!(!tracedecay_steering_path.exists()); } @@ -368,7 +368,7 @@ fn test_uninstall_preserves_user_steering_after_tracedecay_block() { let uninstalled = std::fs::read_to_string(&steering_path).unwrap(); assert!(uninstalled.contains("User guidance appended after setup without a new heading.")); - assert!(!uninstalled.contains("## Prefer tracedecay MCP tools")); + assert!(!uninstalled.contains("## TraceDecay: mandatory tool routing")); } #[test] @@ -401,7 +401,7 @@ fn test_uninstall_removes_tracedecay_and_preserves_other_mcp_servers() { ); let steering = std::fs::read_to_string(home.join(".kiro/steering/tracedecay.md")).unwrap_or_default(); - assert!(!steering.contains("## Prefer tracedecay MCP tools")); + assert!(!steering.contains("## TraceDecay: mandatory tool routing")); } #[test] @@ -525,7 +525,7 @@ fn test_healthcheck_fails_when_steering_lacks_owned_end_marker() { let steering_path = home.join(".kiro/steering/tracedecay.md"); std::fs::write( &steering_path, - "## Prefer tracedecay MCP tools\n\nEdited tracedecay guidance without ownership marker.\n", + "## TraceDecay: mandatory tool routing\n\nEdited tracedecay guidance without ownership marker.\n", ) .unwrap(); diff --git a/tests/agent_suite/prompt_rules_parity_test.rs b/tests/agent_suite/prompt_rules_parity_test.rs index dc42fe69c9..cf439f60a6 100644 --- a/tests/agent_suite/prompt_rules_parity_test.rs +++ b/tests/agent_suite/prompt_rules_parity_test.rs @@ -11,6 +11,7 @@ use crate::common::{EnvVarGuard, PROCESS_ENV_LOCK}; const STANDARD_MARKER: &str = "## Prefer tracedecay MCP tools"; const CLAUDE_MARKER: &str = "## MANDATORY: No Explore Agents When Tracedecay Is Available"; +const KIRO_MARKER: &str = "## TraceDecay: mandatory tool routing"; const KIRO_END_MARKER: &str = ""; struct HostCase { @@ -61,7 +62,7 @@ fn hosts() -> Vec { HostCase { id: "kiro", rules_path: |home| home.join(".kiro/steering/tracedecay.md"), - marker: STANDARD_MARKER, + marker: KIRO_MARKER, stale_block_tail: "\n\n", }, ] diff --git a/tests/hooks_lsp_suite/hooks_test.rs b/tests/hooks_lsp_suite/hooks_test.rs index 152f2f9fb9..c3e8bcf8f2 100644 --- a/tests/hooks_lsp_suite/hooks_test.rs +++ b/tests/hooks_lsp_suite/hooks_test.rs @@ -2,8 +2,8 @@ use crate::common::{EnvVarGuard, GLOBAL_DB_ENV, GLOBAL_DB_ENV_LOCK}; use std::path::Path; use tracedecay::config::USER_DATA_DIR_ENV; use tracedecay::hooks::{ - build_cursor_session_context, codex_additional_context_json, codex_apply_patch_rel_paths, - codex_project_root_from_event, codex_subagent_start_log_line, + build_cursor_session_context, claude_session_context_for_event, codex_additional_context_json, + codex_apply_patch_rel_paths, codex_project_root_from_event, codex_subagent_start_log_line, codex_user_prompt_submit_context_for_event, codex_workspace_status_from_event, cursor_branch_switch_target, cursor_project_root_from_event, cursor_session_start_json, cursor_shell_command_targets_project, cursor_shell_sync_plan, @@ -828,6 +828,60 @@ fn test_build_codex_session_context_carries_full_steering() { assert!(uninit.contains("tracedecay_message_search")); } +#[tokio::test] +async fn test_claude_session_context_injects_bootstrap_when_initialized() { + // On an initialized project Claude's SessionStart additionalContext must + // carry the full using-tracedecay adoption contract, not just the index + // status line (matching Cursor and Codex). + let dir = tempfile::tempdir().unwrap(); + std::fs::create_dir_all(dir.path().join(".tracedecay")).unwrap(); + std::fs::write(dir.path().join(".tracedecay/tracedecay.db"), "").unwrap(); + let event = serde_json::json!({ + "hook_event_name": "SessionStart", + "cwd": dir.path().to_str().unwrap(), + }) + .to_string(); + + let context = claude_session_context_for_event(&event).await; + assert!( + context.contains("tracedecay index status: "), + "initialized Claude context keeps the index status line: {context}" + ); + assert!( + context.contains(""), + "initialized Claude context must inject the bootstrap contract: {context}" + ); + assert!(context.contains("tracedecay:using-tracedecay")); + assert!(context.contains("Grep is faster for this")); + assert!( + context.contains("SUBAGENT-STOP"), + "bootstrap must carry the scoped-subagent guard: {context}" + ); + // The additionalContext channel wraps it as SessionStart context. + let json = codex_additional_context_json("SessionStart", &context); + assert!(json.contains("")); +} + +#[tokio::test] +async fn test_claude_session_context_omits_bootstrap_for_unindexed_project() { + // A project-like workspace without an index gets the init nudge, not the + // full contract. + let dir = tempfile::tempdir().unwrap(); + std::fs::write(dir.path().join("Cargo.toml"), "[package]\n").unwrap(); + let event = serde_json::json!({ + "hook_event_name": "SessionStart", + "cwd": dir.path().to_str().unwrap(), + }) + .to_string(); + + let context = claude_session_context_for_event(&event).await; + assert!(context.contains("tracedecay init")); + assert!( + !context.contains(""), + "unindexed workspaces should not inject the full contract: {context}" + ); +} + #[test] fn test_build_codex_session_context_for_unindexed_project_suggests_init() { let context = tracedecay::hooks::build_codex_session_context_for_workspace(