diff --git a/crates/tui/src/core/engine.rs b/crates/tui/src/core/engine.rs index c8a92b26e8..0198d0cbf2 100644 --- a/crates/tui/src/core/engine.rs +++ b/crates/tui/src/core/engine.rs @@ -146,7 +146,7 @@ fn agent_list_event(manager: &SubAgentManager, active_session_id: &str) -> Event } const MCP_REGISTRY_FIRST_INSTRUCTION_SOURCE: &str = "runtime:mcp-registry-first"; -const MCP_REGISTRY_FIRST_INSTRUCTION: &str = "## MCP Registry-first policy\n\nFor any task centered on a specialized capability, including media or document conversion, data transformation, browser automation, database or service access, or a developer utility, you must call `registry_sync` with a `query` describing that capability before `exec_shell`, `fetch_url`, code execution, local programs, custom code, or a manual implementation. It scores the local Registry snapshot host-side and returns at most eight matches; the full catalog never enters the conversation. Treat a returned server as a match when it plausibly covers the core capability; wording need not be exact. If any plausible match exists, you must call `start_registry_mcp_server` with its exact name and inspect its tools before considering a local alternative. If nothing matches, refine the query once; a still-empty refined result means every Registry entry is clearly irrelevant. An installed or familiar shell command is not a reason to skip Registry discovery. Use local tools directly only for ordinary repo-native work and simple file operations, or after the matching server fails to start."; +const MCP_REGISTRY_FIRST_INSTRUCTION: &str = "## MCP Registry-first policy\n\nFor any task centered on a specialized capability, including media or document conversion, data transformation, browser automation, database or service access, or a developer utility, you must call `registry_sync` with a `query` describing that capability before `bash`, the `Web` tool, code execution, local programs, custom code, or a manual implementation. It scores the local Registry snapshot host-side and returns at most eight matches; the full catalog never enters the conversation. Treat a returned server as a match when it plausibly covers the core capability; wording need not be exact. If any plausible match exists, you must call `start_registry_mcp_server` with its exact name and inspect its tools before considering a local alternative. If nothing matches, refine the query once; a still-empty refined result means every Registry entry is clearly irrelevant. An installed or familiar shell command is not a reason to skip Registry discovery. Use local tools directly only for ordinary repo-native work and simple file operations, or after the matching server fails to start."; const ISOLATED_CHAT_ENGINE_PROMPT: &str = "You are Codewhale Chat. Answer the user's request directly and conversationally. This isolated chat-only session has no local workspace, project, memory, skill, account, credential, path, runtime context, or tools."; fn sanitize_isolated_chat_attachments(mut text: String) -> String { diff --git a/crates/tui/src/core/engine/tests.rs b/crates/tui/src/core/engine/tests.rs index 2a6788cd4a..d50c9f1a43 100644 --- a/crates/tui/src/core/engine/tests.rs +++ b/crates/tui/src/core/engine/tests.rs @@ -387,6 +387,36 @@ fn ordinary_engine_default_has_a_finite_step_budget() { assert_eq!(EngineConfig::default().max_steps, DEFAULT_MODEL_STEPS); } +/// The Registry-first instruction is injected into new-session prompts, so +/// every tool name it cites must be in the published catalog: hidden +/// compatibility aliases (`Bash`, `File`) and fully retired names are never +/// offered to new models (same rule as +/// `tools::canonical_action::no_advertised_tool_teaches_a_retired_name`). +#[test] +fn registry_first_instruction_only_names_published_tools() { + for unpublished in [ + "Bash", + "File", + "exec_shell", + "exec_shell_wait", + "exec_shell_cancel", + "fetch_url", + "web_search", + "run_verifiers", + "read_file", + "write_file", + "edit_file", + ] { + assert!( + !MCP_REGISTRY_FIRST_INSTRUCTION.contains(unpublished), + "Registry-first instruction cites `{unpublished}`, \ + which new-session catalogs never publish" + ); + } + assert!(MCP_REGISTRY_FIRST_INSTRUCTION.contains("`bash`")); + assert!(MCP_REGISTRY_FIRST_INSTRUCTION.contains("`Web`")); +} + #[test] fn registry_first_scenario() { // Scenario consolidation of: registry_first_policy_is_in_the_initial_prompt_only_when_mcp_is_enabled, registry_first_guidance_is_attached_to_the_shell_fallback_once diff --git a/crates/tui/src/prompts.rs b/crates/tui/src/prompts.rs index a0c83e51b8..988b44b256 100644 --- a/crates/tui/src/prompts.rs +++ b/crates/tui/src/prompts.rs @@ -111,7 +111,8 @@ Only output English for:\n\ - Technical terms that lack a standard translation in {target_language}\n\ - Code blocks the user explicitly requests in English\n\n\ This is a hard display requirement: the user does not read English, \ -so any English prose in your response will block their decision-making." +so any English prose in your response will block their decision-making. \ +This overrides the ## Language rule for this session." ) } @@ -455,6 +456,60 @@ pub fn set_base_prompt_override(s: String) -> Result<(), String> { set_prompt_override(&BASE_PROMPT_OVERRIDE, s) } +/// Replace the Simplified Chinese locale preamble. First call wins; later +/// calls return the rejected string. Set before spawning any engine. +pub fn set_locale_preamble_zh_hans_override(s: String) -> Result<(), String> { + set_prompt_override(&LOCALE_PREAMBLE_ZH_HANS_OVERRIDE, s) +} + +/// Replace the Japanese locale preamble. First call wins; later calls return +/// the rejected string. Set before spawning any engine. +pub fn set_locale_preamble_ja_override(s: String) -> Result<(), String> { + set_prompt_override(&LOCALE_PREAMBLE_JA_OVERRIDE, s) +} + +/// Replace the Brazilian Portuguese locale preamble. First call wins; later +/// calls return the rejected string. Set before spawning any engine. +pub fn set_locale_preamble_pt_br_override(s: String) -> Result<(), String> { + set_prompt_override(&LOCALE_PREAMBLE_PT_BR_OVERRIDE, s) +} + +/// Replace the Vietnamese locale preamble. First call wins; later calls +/// return the rejected string. Set before spawning any engine. +pub fn set_locale_preamble_vi_override(s: String) -> Result<(), String> { + set_prompt_override(&LOCALE_PREAMBLE_VI_OVERRIDE, s) +} + +/// Replace the Simplified Chinese locale closer. First call wins; later calls +/// return the rejected string. Set before spawning any engine. +pub fn set_locale_closer_zh_hans_override(s: String) -> Result<(), String> { + set_prompt_override(&LOCALE_CLOSER_ZH_HANS_OVERRIDE, s) +} + +/// Replace the Japanese locale closer. First call wins; later calls return +/// the rejected string. Set before spawning any engine. +pub fn set_locale_closer_ja_override(s: String) -> Result<(), String> { + set_prompt_override(&LOCALE_CLOSER_JA_OVERRIDE, s) +} + +/// Replace the Brazilian Portuguese locale closer. First call wins; later +/// calls return the rejected string. Set before spawning any engine. +pub fn set_locale_closer_pt_br_override(s: String) -> Result<(), String> { + set_prompt_override(&LOCALE_CLOSER_PT_BR_OVERRIDE, s) +} + +/// Replace the Vietnamese locale closer. First call wins; later calls return +/// the rejected string. Set before spawning any engine. +pub fn set_locale_closer_vi_override(s: String) -> Result<(), String> { + set_prompt_override(&LOCALE_CLOSER_VI_OVERRIDE, s) +} + +/// Replace the authority-recap trailer. First call wins; later calls return +/// the rejected string. Set before spawning any engine. +pub fn set_authority_recap_override(s: String) -> Result<(), String> { + set_prompt_override(&AUTHORITY_RECAP_OVERRIDE, s) +} + // ── Config-directory prompt overrides (issue #3638) ── // Bridge the embedder override hooks above to a user-facing source: an // optional file in the Codewhale config directory. This lets users repurpose @@ -705,6 +760,14 @@ pub(crate) fn effective_authority_recap() -> &'static str { effective_prompt_override(&AUTHORITY_RECAP_OVERRIDE, AUTHORITY_RECAP) } +/// Whether the authority-recap trailer is appended after WorldState. When an +/// embedder composer owns the static prompt prefix, the bundled +/// `### Whose word wins` section the recap points at no longer exists, so +/// appending the recap would leave a dangling cross-reference. +fn authority_recap_trailer_appended(composer_installed: bool) -> bool { + !composer_installed +} + /// Optional locale-native reinforcement preamble prepended to the system /// prompt when the user's UI locale is non-English. /// @@ -810,7 +873,7 @@ const LOCALE_PREAMBLE_ZH_HANS: &str = "## 语言要求\n\n\ 你正在 codewhale 中运行。无论任务上下文(代码、错误日志、文件名)\ 是英文,无论系统提示的其余部分是英文,你都必须用简体中文进行 \ `reasoning_content`(内部思考)和最终回复。代码、文件路径、工具名称\ -(例如 `File`、`Bash`)、环境变量、命令行参数和 URL \ +(例如 `bash`、`Web`)、环境变量、命令行参数和 URL \ 保持原样 —— 只有自然语言散文要切换到简体中文。\n\n\ 如果用户在会话中切换到另一种语言,从下一轮开始跟随切换。\ 如果用户明确要求(例如 \"think in English\"),则覆盖此规则。"; @@ -819,8 +882,8 @@ const LOCALE_PREAMBLE_JA: &str = "## 言語要件\n\n\ codewhale を実行しています。タスクコンテキスト(コード、エラーログ、\ ファイル名)が英語であっても、システムプロンプトの他の部分が英語で\ あっても、`reasoning_content`(内部思考)と最終的な返信は日本語で\ -行ってください。コード、ファイルパス、ツール名(例:`File`、\ -`Bash`)、環境変数、コマンドライン引数、URL は元のまま —— \ +行ってください。コード、ファイルパス、ツール名(例:`bash`、\ +`Web`)、環境変数、コマンドライン引数、URL は元のまま —— \ 自然言語の文章のみ日本語に切り替えます。\n\n\ ユーザーがセッション中に別の言語に切り替えた場合は、次のターンから\ それに従ってください。ユーザーが明示的に要求した場合(例:\ @@ -832,8 +895,8 @@ Você está rodando dentro do codewhale. Escreva tanto \ em português do Brasil, mesmo quando o contexto da tarefa (código, \ logs de erro, nomes de arquivos) estiver em inglês e mesmo quando o \ resto do system prompt for em inglês. Mantenha código, caminhos de \ -arquivos, nomes de ferramentas (por exemplo `File`, \ -`Bash`), variáveis de ambiente, flags de linha de comando e \ +arquivos, nomes de ferramentas (por exemplo `bash`, \ +`Web`), variáveis de ambiente, flags de linha de comando e \ URLs no formato original — apenas a prosa em linguagem natural muda \ para português do Brasil.\n\n\ Se o usuário mudar de idioma no meio da sessão, mude no próximo turno. \ @@ -873,7 +936,7 @@ const LOCALE_PREAMBLE_VI: &str = "## Yêu cầu ngôn ngữ\n\n\ Bạn đang chạy trong codewhale. Cho dù ngữ cảnh tác vụ (mã nguồn, nhật ký lỗi, tên tệp) \ là tiếng Anh, cho dù phần còn lại của system prompt là tiếng Anh, bạn đều phải sử dụng \ tiếng Việt cho phần `reasoning_content` (suy nghĩ nội bộ) và câu trả lời cuối cùng. Các từ \ -mã nguồn, đường dẫn tệp, tên công cụ (ví dụ `File`, `Bash`), biến môi trường, \ +mã nguồn, đường dẫn tệp, tên công cụ (ví dụ `bash`, `Web`), biến môi trường, \ tham số dòng lệnh và URL giữ nguyên dạng gốc —— chỉ các văn bản giải thích bằng ngôn ngữ \ tự nhiên mới được chuyển sang tiếng Việt.\n\n\ Nếu người dùng chuyển sang ngôn ngữ khác trong phiên làm việc, hãy chuyển theo từ lượt tiếp theo. \ @@ -1312,7 +1375,10 @@ pub(crate) fn system_prompt_for_mode_with_context_skills_session_and_approval_fo .to_system_blocks(); // Trailers keep recency bias after WorldState: authority, then locale. - if !bundled_headless { + // When an embedder composer owns the static prefix, the bundled + // `### Whose word wins` section the recap points at no longer exists, + // so appending the recap would leave a dangling cross-reference. + if !bundled_headless && authority_recap_trailer_appended(static_composer_installed) { blocks.push(SystemBlock { block_type: "text".to_string(), text: effective_authority_recap().trim().to_string(), @@ -1414,6 +1480,15 @@ mod tests { /// agent prompt's own discussion of the convention). const HANDOFF_BLOCK_MARKER: &str = "left a relay artifact at `.codewhale/handoff.md`"; + /// The recap points at the bundled `### Whose word wins` section; an + /// embedder composer that owns the static prefix retires that section, + /// so the trailer must be skipped instead of dangling in the blocks. + #[test] + fn authority_recap_trailer_skipped_when_static_composer_owns_prefix() { + assert!(authority_recap_trailer_appended(false)); + assert!(!authority_recap_trailer_appended(true)); + } + // Config-directory prompt override resolution (#3638). These exercise the // pure file resolver only; the global install path is intentionally not // unit-tested here because `set_base_prompt_override` writes a process-wide @@ -2188,9 +2263,10 @@ mod tests { "zh preamble must steer reasoning_content: {preamble:?}" ); assert!( - preamble.contains("`File`"), - "zh preamble must call out tool-name immutability with a LIVE tool \ - name; `read_file` is retired (registry.rs:2067): {preamble:?}" + preamble.contains("`bash`") && preamble.contains("`Web`"), + "zh preamble must call out tool-name immutability with LIVE tool \ + names; `File`/`Bash` are hidden replay aliases (registry.rs \ + with_file_tools/with_foreground_shell_tools): {preamble:?}" ); assert!( !preamble.contains("read_file") && !preamble.contains("exec_shell"), diff --git a/crates/tui/src/prompts/text.rs b/crates/tui/src/prompts/text.rs index c30a4e4afe..8720022242 100644 --- a/crates/tui/src/prompts/text.rs +++ b/crates/tui/src/prompts/text.rs @@ -286,13 +286,12 @@ capability. Then stop. "#; /// Scout output contract — scaled down for small children (see #5189 F5). -/// Keeps the parseable spine (SUMMARY+EVIDENCE + sentinel) but drops +/// Keeps the parseable spine (SUMMARY+EVIDENCE) but drops /// CHANGES/RISKS/BLOCKERS ceremony; scouts are read-only explorers. pub const SUBAGENT_SCOUT_OUTPUT_FORMAT: &str = r#"## Output contract (scout) End with these exact Markdown headings: `### SUMMARY` and `### EVIDENCE`. Keep each section compact. Cite only files you actually inspected and distinguish child reports from evidence you verified. Write `None.` where -a section has no entries. If blocked, name the missing fact. Then stop -with ``. +a section has no entries. If blocked, name the missing fact. Then stop. "#; diff --git a/crates/tui/src/tools/apply_patch.rs b/crates/tui/src/tools/apply_patch.rs index 7620b4e0cd..ecfb7c5397 100644 --- a/crates/tui/src/tools/apply_patch.rs +++ b/crates/tui/src/tools/apply_patch.rs @@ -13,9 +13,7 @@ use serde_json::{Value, json}; use thiserror::Error; use super::diff_format::make_unified_diff; -use super::file::{ - EXPECTED_HASH_DESCRIPTION, PATCH_PARAMS, PATH_ALIASES, apply_param_aliases, content_hash, -}; +use super::file::{PATCH_PARAMS, PATH_ALIASES, apply_param_aliases, content_hash}; use super::spec::{ ApprovalRequirement, ToolCapability, ToolContext, ToolError, ToolResult, ToolSpec, lsp_diagnostics_for_paths, optional_bool, optional_str, optional_u64, @@ -319,7 +317,7 @@ impl ToolSpec for ApplyPatchTool { } fn description(&self) -> &'static str { - "Apply a transactional unified-diff patch across one or more files, with fuzzy context matching and a rendered diff." + "Apply a unified-diff patch (multi-hunk, multi-file) or full-file replacements via `replace`. Use this instead of `git apply` or `patch` in `bash`, or repeated `edit` calls — single transactional change with fuzzy matching and a rendered diff." } fn input_schema(&self) -> Value { @@ -368,9 +366,7 @@ impl ToolSpec for ApplyPatchTool { }, "expected_hash": { "type": "string", - "description": format!( - "{EXPECTED_HASH_DESCRIPTION} Verifies the patch target — the `path` argument when given, otherwise the first file the patch touches; other files in a multi-file patch are not hash-checked." - ) + "description": "Optional sha256: of the complete target file bytes; a mismatch refuses the patch without writing. Compute it with `bash`; `read` does not return a hash. Verifies the `path` argument when given, otherwise the first file the patch touches; other files in a multi-file patch are not hash-checked." } }, "oneOf": [ @@ -395,8 +391,8 @@ impl ToolSpec for ApplyPatchTool { async fn execute(&self, input: Value, context: &ToolContext) -> Result { let mut input = input; - apply_param_aliases(&mut input, PATH_ALIASES, "File patch")?; - PATCH_PARAMS.reject_unknown(&input)?; + apply_param_aliases(&mut input, PATH_ALIASES, "apply_patch")?; + PATCH_PARAMS.reject_unknown_named(&input, "apply_patch")?; let input = input; let fuzz = optional_u64(&input, "fuzz", DEFAULT_FUZZ as u64)?.min(MAX_FUZZ as u64); @@ -524,7 +520,7 @@ fn verify_patch_expected_hash( .or_else(|| summary.touched_files.first().map(String::as_str)) else { return Err(ToolError::execution_failed( - "File `patch` refused: expected_hash was supplied but the patch names no target file to verify it against, so nothing was written.".to_string(), + "`apply_patch` refused: expected_hash was supplied but the patch names no target file to verify it against, so nothing was written.".to_string(), )); }; @@ -533,13 +529,13 @@ fn verify_patch_expected_hash( // Fail closed, matching `write`: a hash describes a file that was // read, so a missing target means the guard cannot be honored. return Err(ToolError::execution_failed(format!( - "File `patch` refused: expected_hash was supplied but {target} does not exist, so there is no snapshot to verify and nothing was written. Recovery: drop `expected_hash` when creating files." + "`apply_patch` refused: expected_hash was supplied but {target} does not exist, so there is no snapshot to verify and nothing was written. Recovery: drop `expected_hash` when creating files." ))); } let current = fs::read(&resolved).map_err(|e| { ToolError::execution_failed(format!( - "File `patch` refused: could not read {target} to verify expected_hash ({e}); nothing was written." + "`apply_patch` refused: could not read {target} to verify expected_hash ({e}); nothing was written." )) })?; let actual = content_hash(¤t); @@ -547,10 +543,10 @@ fn verify_patch_expected_hash( return Ok(()); } Err(ToolError::execution_failed(format!( - "File `patch` refused: {target} changed since it was read. \ + "`apply_patch` refused: {target} changed since it was read. \ expected_hash was {expected} but the file is now {actual}, so nothing was written. \ - Recovery: call File with action=\"read\" path=\"{target}\" to get the current contents \ - and its content_hash, then rebuild the patch against them." + Recovery: call `read` path=\"{target}\" to get the current contents \ + then rebuild the patch. If retaining expected_hash, recompute the SHA-256 of the complete file bytes with `bash` and use the sha256: prefix; `read` does not return a hash." ))) } @@ -1413,7 +1409,7 @@ fn format_hunk_no_match_error( let expected_preview = preview_expected_lines(hunk, HUNK_PREVIEW_LINES).join("\n"); let file_preview = snippet_around(lines, *adjusted_line, SNIPPET_RADIUS).join("\n"); format!( - "could not find matching context near line {expected_line} (searched around line {adjusted_line} with offset {offset:+} and fuzz up to {max_fuzz}). Expected context preview:\n{expected_preview}\nFile snippet near line {adjusted_line}:\n{file_preview}\nHints: the line numbers may be stale after earlier edits — call File with action=\"read\" to re-check the current contents, ensure the patch matches the file, increase `fuzz`, or regenerate the patch." + "could not find matching context near line {expected_line} (searched around line {adjusted_line} with offset {offset:+} and fuzz up to {max_fuzz}). Expected context preview:\n{expected_preview}\nFile snippet near line {adjusted_line}:\n{file_preview}\nHints: the line numbers may be stale after earlier edits — call `read` to re-check the current contents, ensure the patch matches the file, increase `fuzz`, or regenerate the patch." ) } ApplyHunkError::ContextAmbiguous { @@ -1426,7 +1422,7 @@ fn format_hunk_no_match_error( .collect::>() .join(", "); format!( - "could not find matching context near line {expected_line}: the hunk's context appears at multiple locations (lines {candidates}), and the line numbers may be stale after earlier edits, so it is not safe to relocate automatically. Hints: call File with action=\"read\" to inspect the candidate locations above, then regenerate the patch with more surrounding context lines that uniquely identify the target block." + "could not find matching context near line {expected_line}: the hunk's context appears at multiple locations (lines {candidates}), and the line numbers may be stale after earlier edits, so it is not safe to relocate automatically. Hints: call `read` to inspect the candidate locations above, then regenerate the patch with more surrounding context lines that uniquely identify the target block." ) } } @@ -2724,6 +2720,8 @@ diff --git a/b.txt b/b.txt message.contains("multiple locations"), "expected ambiguity error, got: {message}" ); + assert!(message.contains("call `read`"), "{message}"); + assert!(!message.contains("call File"), "{message}"); // The two duplicate blocks start at 1-based lines 4 and 9. assert!( message.contains("lines 4, 9"), @@ -2871,6 +2869,12 @@ diff --git a/b.txt b/b.txt let message = err.to_string(); assert!(message.contains("changed since it was read"), "{message}"); assert!(message.contains("nothing was written"), "{message}"); + assert!(message.contains("call `read`"), "{message}"); + assert!( + message.contains("`read` does not return a hash"), + "{message}" + ); + assert!(!message.contains("File"), "{message}"); assert_eq!( fs::read_to_string(tmp.path().join("test.txt")).expect("read"), body, diff --git a/crates/tui/src/tools/canonical_action.rs b/crates/tui/src/tools/canonical_action.rs index a4f3c1c746..5f09580ea5 100644 --- a/crates/tui/src/tools/canonical_action.rs +++ b/crates/tui/src/tools/canonical_action.rs @@ -206,16 +206,16 @@ mod tests { use super::*; use serde_json::json; - /// Names the v0.9.3 consolidation retired. None of them can dispatch — - /// `ToolRegistry::resolve` has no fuzzy step — so any one of them inside a - /// model-visible description or schema teaches a call that cannot work. + /// Names the v0.9.3 consolidation retired from the advertised catalog. + /// Fully removed spellings cannot dispatch at all — `ToolRegistry::resolve` + /// has no fuzzy step — and the rest survive only as `model_visible=false` + /// replay aliases, so any one of them inside a model-visible description or + /// schema teaches a name the model is never offered. + // list_dir, file_search and grep_files are published standalone tools again. const RETIRED_TOOL_NAMES: &[&str] = &[ "read_file", "write_file", "edit_file", - "list_dir", - "file_search", - "grep_files", "git_status", "git_diff", "git_log", @@ -232,6 +232,13 @@ mod tests { "exec_shell_cancel", ]; + /// Uppercase action-family aliases that still dispatch for saved v0.9.x + /// transcript replay but are `model_visible=false`: new sessions publish + /// `bash` and the independent `read`/`write`/`edit` primitives instead + /// (`canonical_runtime_tools_hide_compatibility_aliases`). Matching is + /// whole-token so prose like "BashHistory" or lowercase `bash` never trips. + const HIDDEN_COMPAT_TOOL_NAMES: &[&str] = &["Bash", "File"]; + /// The catalog is re-sent on every request, so a retired name in it is a /// per-turn lie to every model. `verifier.rs` already guarded one such /// description by hand; this covers the whole advertised surface at once. @@ -249,9 +256,46 @@ mod tests { .with_test_runner_tool() .with_web_tools() .with_patch_tools() + .with_shell_tools() + .with_diagnostics_tool() + .with_tui_help_tool() + .with_pandoc_tools() + .with_image_ocr_tools() + .with_read_media_tool() + .with_skill_tools() + .with_project_tools() + .with_validation_tools() + .with_tool_result_retrieval_tool() + .with_runtime_task_tools() + .with_runtime_task_shell_tools() + .with_user_input_tool() + .with_revert_turn_tool() + .with_harness_tool() + .with_handle_tools() + .with_note_tool() + .with_remember_tool() + .with_verify_tool(None, "test-model".to_string()) + .with_registry_mcp_sync_tool() + .with_runtime_mcp_tool(std::sync::Arc::new(tokio::sync::Mutex::new( + crate::mcp::McpPool::new(crate::mcp::McpConfig::default()), + ))) + .with_registry_mcp_start_tool(std::sync::Arc::new(tokio::sync::Mutex::new( + crate::mcp::McpPool::new(crate::mcp::McpConfig::default()), + ))) .build(ToolContext::new(tmp.path().to_path_buf())); - for tool in registry.to_api_tools() { + let api_tools = registry.to_api_tools(); + let skill = api_tools + .iter() + .find(|tool| tool.name == "load_skill") + .unwrap(); + for reference in skill.description.split('`').skip(1).step_by(2) { + assert!( + api_tools.iter().any(|tool| tool.name == reference), + "load_skill cites unpublished tool `{reference}`" + ); + } + for tool in api_tools { let advertised = format!("{} {}", tool.description, tool.input_schema); for retired in RETIRED_TOOL_NAMES { assert!( @@ -261,6 +305,17 @@ mod tests { tool.name ); } + let advertised_tokens = advertised + .split(|c: char| !c.is_ascii_alphanumeric()) + .collect::>(); + for hidden in HIDDEN_COMPAT_TOOL_NAMES { + assert!( + !advertised_tokens.contains(hidden), + "tool `{}` advertises the hidden compatibility name `{hidden}`; \ + name the published catalog form instead", + tool.name + ); + } } } diff --git a/crates/tui/src/tools/fetch_url.rs b/crates/tui/src/tools/fetch_url.rs index 84594bff4f..fd1e480c63 100644 --- a/crates/tui/src/tools/fetch_url.rs +++ b/crates/tui/src/tools/fetch_url.rs @@ -97,7 +97,7 @@ impl ToolSpec for FetchUrlTool { } fn description(&self) -> &'static str { - "Fetch a known URL directly (HTTP GET) and return its content with a session-scoped citation ref_id. Use this instead of `curl` in `exec_shell` — sandboxed, network-policy aware, and properly decoded. Plain-text endpoints (`.md`, `.txt`, `.json`, `.yaml`, `raw.githubusercontent.com`, public APIs) prefer this over the browser/automation stack. For unknown queries, use `web_search` first. If a login or authorization wall is returned, treat the wall as the result; do not claim the protected page was read." + "Fetch a known URL directly (HTTP GET) and return its content with a session-scoped citation ref_id. Use this instead of `curl` in `Bash` — sandboxed, network-policy aware, and properly decoded. Plain-text endpoints (`.md`, `.txt`, `.json`, `.yaml`, `raw.githubusercontent.com`, public APIs) prefer this over the browser/automation stack. For unknown queries, use the Web tool with action=search first. If a login or authorization wall is returned, treat the wall as the result; do not claim the protected page was read." } fn input_schema(&self) -> Value { diff --git a/crates/tui/src/tools/file.rs b/crates/tui/src/tools/file.rs index 1f0908fd55..53ea9506b3 100644 --- a/crates/tui/src/tools/file.rs +++ b/crates/tui/src/tools/file.rs @@ -355,6 +355,10 @@ impl ActionParams { /// continuing would mean guessing which argument was intended — so it /// hard-errors rather than dropping the argument and reporting success. pub(super) fn reject_unknown(&self, input: &Value) -> Result<(), ToolError> { + self.reject_unknown_named(input, &format!("File {}", self.action)) + } + + pub(super) fn reject_unknown_named(&self, input: &Value, tool: &str) -> Result<(), ToolError> { let action = self.action; let required = if self.required_is_choice { format!("one of {}", quoted_list(self.required, "or")) @@ -364,7 +368,7 @@ impl ActionParams { let Some(obj) = input.as_object() else { return Err(ToolError::invalid_input(format!( - "File {action} input must be an object. Allowed parameters are {}. Required: {required}. The {action} was not performed.", + "{tool} input must be an object. Allowed parameters are {}. Required: {required}. The {action} was not performed.", quoted_list(self.allowed, "and"), ))); }; @@ -376,7 +380,7 @@ impl ActionParams { .collect(); if !unexpected.is_empty() { return Err(ToolError::invalid_input(format!( - "unexpected File {action} parameter(s): {}. Allowed parameters are {}. Required: {required}. The {action} was not performed.", + "unexpected {tool} parameter(s): {}. Allowed parameters are {}. Required: {required}. The {action} was not performed.", unexpected.join(", "), quoted_list(self.allowed, "and"), ))); @@ -1985,7 +1989,7 @@ impl ToolSpec for EditFileTool { } fn description(&self) -> &'static str { - "Replace text in a single file via exact search/replace after the file has been read with File `read` in this session. Use this instead of `sed -i` in `Bash` for one unambiguous in-place edit. `search` must match exactly one location by default; when no exact match is found the tool retries with leading-whitespace-tolerant fuzzy matching automatically. Returns a compact unified diff, not the full file. Pass `expected_hash` (the `content_hash` from that `read`) to have the edit refused, with the file untouched, if it changed in between. For structural, multi-block, or cross-file changes, use File `patch` or `write` instead." + "Replace text in a single file via exact search/replace after the file has been read with File `read` in this session. Use this instead of `sed -i` in `Bash` for one unambiguous in-place edit. `search` must match exactly one location by default; when no exact match is found the tool retries with leading-whitespace-tolerant fuzzy matching plus punctuation/line-ending normalization fallbacks automatically. Returns a compact unified diff, not the full file. Pass `expected_hash` (the `content_hash` from that `read`) to have the edit refused, with the file untouched, if it changed in between. For structural, multi-block, or cross-file changes, use File `patch` or `write` instead." } fn input_schema(&self) -> Value { diff --git a/crates/tui/src/tools/file/tests/tools.rs b/crates/tui/src/tools/file/tests/tools.rs index 095019a6fc..4b310f60ca 100644 --- a/crates/tui/src/tools/file/tests/tools.rs +++ b/crates/tui/src/tools/file/tests/tools.rs @@ -2410,15 +2410,21 @@ async fn write_without_expected_hash_still_creates_files() { #[tokio::test] async fn expected_hash_is_advertised_on_every_mutating_action() { - for schema in [ - WriteFileTool.input_schema(), - EditFileTool.input_schema(), - crate::tools::apply_patch::ApplyPatchTool.input_schema(), + for (schema, hash_format) in [ + (WriteFileTool.input_schema(), "content_hash"), + (EditFileTool.input_schema(), "content_hash"), + // Public `read` has no content_hash field; apply_patch documents how + // to compute the full-file SHA-256 instead of promising that field. + ( + crate::tools::apply_patch::ApplyPatchTool.input_schema(), + "sha256:", + ), ] { + assert_eq!(schema["properties"]["expected_hash"]["type"], "string"); let description = schema["properties"]["expected_hash"]["description"] .as_str() .expect("expected_hash must be advertised"); - assert!(description.contains("content_hash"), "{description}"); + assert!(description.contains(hash_format), "{description}"); } } diff --git a/crates/tui/src/tools/file_search.rs b/crates/tui/src/tools/file_search.rs index f8756dcf70..8509fb5a60 100644 --- a/crates/tui/src/tools/file_search.rs +++ b/crates/tui/src/tools/file_search.rs @@ -40,7 +40,7 @@ impl ToolSpec for FileSearchTool { } fn description(&self) -> &'static str { - "Find workspace files by name using fuzzy matching with score-based ranking. Pass extensions to filter by suffix." + "Find workspace files by name using fuzzy matching with score-based ranking. Use this instead of `find -name` or `fd` in `bash` for filename search. Respects .gitignore; by default skips target/**, node_modules/**, lock files, and similar generated artifacts unless `exclude` overrides them. `limit` accepts at most 200. Pass `extensions` to filter by suffix." } fn input_schema(&self) -> Value { diff --git a/crates/tui/src/tools/file_tool/tests.rs b/crates/tui/src/tools/file_tool/tests.rs index 8424dacd7a..a12a6b2c9c 100644 --- a/crates/tui/src/tools/file_tool/tests.rs +++ b/crates/tui/src/tools/file_tool/tests.rs @@ -288,8 +288,14 @@ async fn every_action_refuses_an_unknown_parameter() { message.contains("bogus_param"), "{action} must name the offending parameter: {message}" ); + // The compatibility patch action delegates to the public apply_patch tool. + let tool_name = if action == "patch" { + "apply_patch".to_string() + } else { + format!("File {action}") + }; assert!( - message.contains(&format!("unexpected File {action} parameter")), + message.contains(&format!("unexpected {tool_name} parameter")), "{action} must name the action it refused: {message}" ); assert!( diff --git a/crates/tui/src/tools/fim.rs b/crates/tui/src/tools/fim.rs index 1c43164553..ac4ad38813 100644 --- a/crates/tui/src/tools/fim.rs +++ b/crates/tui/src/tools/fim.rs @@ -70,7 +70,9 @@ impl ToolSpec for FimEditTool { prefix_anchor (text that appears before the section to replace), and \ suffix_anchor (text that appears after the section to replace). The tool \ calls the active route's fill-in-the-middle completion endpoint to \ - generate replacement content." + generate replacement content; this requires the active provider to \ + expose a fill-in-the-middle completions endpoint, and the call fails \ + otherwise." } fn input_schema(&self) -> Value { diff --git a/crates/tui/src/tools/git_tool.rs b/crates/tui/src/tools/git_tool.rs index 975034e1fd..3f44d06303 100644 --- a/crates/tui/src/tools/git_tool.rs +++ b/crates/tui/src/tools/git_tool.rs @@ -82,7 +82,7 @@ impl ToolSpec for GitTool { }, "unified": { "type": "integer", - "description": "Number of context lines for diff or show output" + "description": "Number of context lines for diff or show output (default 3, max 50)" }, "max_count": { "type": "integer", diff --git a/crates/tui/src/tools/github/mod.rs b/crates/tui/src/tools/github/mod.rs index 05540aed2f..c54a843704 100644 --- a/crates/tui/src/tools/github/mod.rs +++ b/crates/tui/src/tools/github/mod.rs @@ -138,16 +138,16 @@ impl ToolSpec for GithubTool { "Post an evidence-backed GitHub issue/PR comment with gh. Requires approval. Use blocker comments for partial work; do not claim closure without evidence." } Some("close_issue") => { - "Close a GitHub issue only when structured acceptance evidence is present and approved. For pull requests use github_close_pr; do not call PRs issues in user-facing output. Never close merely because the agent is stopping." + "Close a GitHub issue only when structured acceptance evidence is present and approved. Rejected when the worktree is dirty unless allow_dirty=true. For pull requests use github_close_pr; do not call PRs issues in user-facing output. Never close merely because the agent is stopping." } Some("close_pr") => { - "Close a GitHub pull request only when structured acceptance evidence is present and approved. Use this for PRs instead of github_close_issue so the UI, audit trail, and comments keep PR wording clear." + "Close a GitHub pull request only when structured acceptance evidence is present and approved. Rejected when the worktree is dirty unless allow_dirty=true. Use this for PRs instead of github_close_issue so the UI, audit trail, and comments keep PR wording clear." } _ if self.read_only => { "Read GitHub issue/PR context using gh. Actions: \"issue_context\" and \"pr_context\"; bodies/comments/labels/state are summarized and large bodies become task artifacts when a durable task is active." } _ => { - "Read and guardedly mutate GitHub issues/PRs using gh. Actions: \"issue_context\", \"pr_context\" (read-only; large bodies become task artifacts when a durable task is active), \"comment\" (approval; evidence-backed), \"close_issue\", \"close_pr\" (approval; only with structured acceptance evidence — never close merely because the agent is stopping). No push/merge." + "Read and guardedly mutate GitHub issues/PRs using gh. Actions: \"issue_context\", \"pr_context\" (read-only; large bodies become task artifacts when a durable task is active), \"comment\" (approval; evidence-backed), \"close_issue\", \"close_pr\" (approval; only with structured acceptance evidence — never close merely because the agent is stopping; rejected when the worktree is dirty unless allow_dirty=true). No push/merge." } } } diff --git a/crates/tui/src/tools/github/schema.rs b/crates/tui/src/tools/github/schema.rs index 365f923c84..f091ede26a 100644 --- a/crates/tui/src/tools/github/schema.rs +++ b/crates/tui/src/tools/github/schema.rs @@ -61,7 +61,7 @@ pub(super) fn canonical_schema(allowed_actions: &[&str], read_only: bool) -> Val ); properties.insert( "allow_dirty".to_string(), - json!({ "type": "boolean", "default": false, "description": "(action=close_issue/close_pr)" }), + json!({ "type": "boolean", "default": false, "description": "(action=close_issue/close_pr) Close is rejected when the worktree is dirty unless this is true." }), ); properties.insert( "dry_run".to_string(), diff --git a/crates/tui/src/tools/goal.rs b/crates/tui/src/tools/goal.rs index 249704d8c1..66cdfb5881 100644 --- a/crates/tui/src/tools/goal.rs +++ b/crates/tui/src/tools/goal.rs @@ -440,7 +440,7 @@ impl GoalState { ) -> Result<(), &'static str> { if self.objective.is_some() && self.status != Some(GoalStatus::Complete) { return Err( - "An unfinished goal already exists. Complete or clear it before creating another.", + "An unfinished goal already exists. Complete it before creating another; only the user/host can clear an unfinished goal (for example with /goal clear).", ); } self.objective = Some(objective); @@ -790,7 +790,7 @@ pub fn thread_goal_status_projection( pub fn render_continuation_prompt(snapshot: &GoalSnapshot, continuation_index: u32) -> String { let goal_json = serde_json::to_string_pretty(snapshot).unwrap_or_else(|_| "{}".to_string()); format!( - "{}\n\n## Active Goal State\n\n```json\n{}\n```\n\nContinuation pass #{}.\nIf a critical verifier finds remaining work, call `update_goal` with `status: \"not_achieved\"` and its concrete `verification.gaps`; repeated equivalent gap sets pause the loop for inspection instead of spending indefinitely. If the goal is complete, first run or cite a concrete verifier/check when one applies, then call `update_goal` with `status: \"complete\"`, concrete evidence, and `verification: {{\"status\":\"passed\",\"check\":\"...\",\"summary\":\"...\"}}`. For non-verifiable work (docs, research, writing), use `verification: {{\"status\":\"not_applicable\",\"check\":\"...\",\"summary\":\"...\"}}` with a clear rationale instead of fabricating a verifier receipt. If it is blocked, call `update_goal` with `status: \"blocked\"` and the blocker. Otherwise continue making progress toward the objective.", + "{}\n\n## Active Goal State\n\n```json\n{}\n```\n\nContinuation pass #{}.\nIf a critical verifier finds remaining work, call `update_goal` with `status: \"not_achieved\"` and its concrete `verification.gaps`; repeating an equivalent gap set only increments `repeated_gap_count` in the goal snapshot — the loop pauses at the continuation run limit, not because of repetition. If the goal is complete, first run or cite a concrete verifier/check when one applies, then call `update_goal` with `status: \"complete\"`, concrete evidence, and `verification: {{\"status\":\"passed\",\"check\":\"...\",\"summary\":\"...\"}}`. For non-verifiable work (docs, research, writing), use `verification: {{\"status\":\"not_applicable\",\"check\":\"...\",\"summary\":\"...\"}}` with a clear rationale instead of fabricating a verifier receipt. If it is blocked, call `update_goal` with `status: \"blocked\"` and the blocker. Otherwise continue making progress toward the objective.", crate::prompts::GOAL_CONTINUATION_PROMPT.trim(), goal_json, continuation_index, @@ -909,7 +909,7 @@ impl ToolSpec for CreateGoalTool { } fn description(&self) -> &'static str { - "Create the session's one persistent goal: a completion objective Codewhale keeps working toward across turns until it is verified complete, blocked, or the user stops it. Call this only when the user explicitly asks to use `/goal`, make an objective the goal, or otherwise explicitly requests persistent goal tracking. When the request is explicit, call `create_goal` before doing the rest of the work; acknowledging it in prose is not sufficient. Never infer a goal from an ordinary task, its apparent length, a question, or a one-file edit. Keep the user's full objective, not a shortened one-turn version. Set token_budget only when the user explicitly provides one. Creating a goal shows the user a one-line receipt (they can /goal pause or /goal clear); do not also ask for confirmation. Only one unfinished goal exists at a time: complete or clear it before creating another." + "Create the session's one persistent goal: a completion objective Codewhale keeps working toward across turns until it is verified complete, blocked, or the user stops it. Call this only when the user explicitly asks to use `/goal`, make an objective the goal, or otherwise explicitly requests persistent goal tracking. When the request is explicit, call `create_goal` before doing the rest of the work; acknowledging it in prose is not sufficient. Never infer a goal from an ordinary task, its apparent length, a question, or a one-file edit. Keep the user's full objective, not a shortened one-turn version. Set token_budget only when the user explicitly provides one. Creating a goal shows the user a one-line receipt (they can /goal pause or /goal clear); do not also ask for confirmation. Only one unfinished goal exists at a time: complete it before creating another; only the user can clear a goal (for example with /goal clear). Root agent only; sub-agents inspect with get_goal." } fn input_schema(&self) -> Value { @@ -1029,7 +1029,7 @@ impl ToolSpec for UpdateGoalTool { } fn description(&self) -> &'static str { - "Update the runtime goal completion gate. Critical verification may seal one immutable completion contract. Advisory review is append-only context and never completes, blocks, or pauses the goal. Mark blocked when progress requires user input." + "Update the runtime goal completion gate. Critical verification may seal one immutable completion contract. Advisory review is append-only context and never completes, blocks, or pauses the goal. Mark blocked when progress requires user input. Root agent only; sub-agents inspect with get_goal." } fn input_schema(&self) -> Value { @@ -1070,7 +1070,7 @@ impl ToolSpec for UpdateGoalTool { "gaps": { "type": "array", "items": {"type": "string"}, - "description": "Concrete remaining gaps. Required for critical not_achieved reviews; order and duplicate wording do not affect the stall fingerprint." + "description": "Concrete remaining gaps. Required for critical not_achieved reviews; order and duplicate wording do not affect the stall fingerprint. Repeating an identical gap set increments repeated_gap_count in the goal snapshot; it does not by itself pause the goal — automatic pause comes only from the continuation run limit." } }, "required": ["status", "check", "summary"], @@ -1916,6 +1916,25 @@ mod tests { assert!(prompt.contains("Continuation pass #2")); } + /// `GoalPauseReason::NoProgress` is never constructed: repeating an + /// equivalent gap set only bumps the snapshot counter, and the single + /// automatic pause is the continuation run limit (`goal_loop` backoff). + /// The prompt must not revive the fabricated repetition-pause claim that + /// the `update_goal` gaps schema already corrects. + #[test] + fn continuation_prompt_does_not_claim_gap_repetition_pauses_the_loop() { + let snapshot = GoalSnapshot { + objective: Some("finish issue 2199".to_string()), + status: "active".to_string(), + ..Default::default() + }; + + let prompt = render_continuation_prompt(&snapshot, 2); + assert!(!prompt.contains("pause the loop for inspection")); + assert!(prompt.contains("repeated_gap_count")); + assert!(prompt.contains("continuation run limit")); + } + #[test] fn update_goal_contract_treats_required_user_input_as_blocking() { let update = UpdateGoalTool::new(new_shared_goal_state()); diff --git a/crates/tui/src/tools/handle.rs b/crates/tui/src/tools/handle.rs index fcc57e66cf..d4ee926fe4 100644 --- a/crates/tui/src/tools/handle.rs +++ b/crates/tui/src/tools/handle.rs @@ -251,10 +251,11 @@ impl ToolSpec for HandleReadTool { as RLM sessions or sub-agents. This does not read artifact ids \ (`art_...`), tool-call ids (`call_...`), SHA refs, or files; use \ retrieve_tool_result for spilled tool results/artifacts and \ - File action=\"read\" for workspace files. Provide \ + the `read` tool for workspace files. Provide \ exactly one projection: `slice` for char/line slices, `range` for \ - one-based line ranges, `count` for metadata counts, or `jsonpath` \ - for a small JSON-path projection. This retrieves from the handle's \ + one-based line ranges, `count` for metadata counts, `jsonpath` \ + for a small JSON-path projection, or `introspect` for the \ + handle's supported projections, size hints, and examples. This retrieves from the handle's \ backing environment instead of asking the parent transcript to hold \ the full payload." } diff --git a/crates/tui/src/tools/image_ocr.rs b/crates/tui/src/tools/image_ocr.rs index 14e7f8fe7f..54fdeef797 100644 --- a/crates/tui/src/tools/image_ocr.rs +++ b/crates/tui/src/tools/image_ocr.rs @@ -7,7 +7,7 @@ //! //! Surfacing OCR as a model-callable tool means the model can read an //! asset the user drops into the workspace without bouncing through -//! `exec_shell`. +//! a shell. use std::path::Path; use std::process::{Command, Stdio}; @@ -28,7 +28,7 @@ impl ToolSpec for ImageOcrTool { } fn description(&self) -> &'static str { - "Extract text from an image (PNG, JPEG, or TIFF) via local OCR. On macOS this uses the built-in Vision framework; otherwise it uses local tesseract when available. Use this for screenshots, scanned receipts/whiteboards, image-only PDFs, or any visual that contains text the model needs to read. Returns the extracted text inline; no file is written." + "Extract text from an image (PNG, JPEG, or TIFF) via local OCR. On macOS this uses the built-in Vision framework; otherwise it uses local tesseract when available. Use this for screenshots, scanned receipts/whiteboards, or any visual that contains text the model needs to read. Returns the extracted text inline; no file is written." } fn input_schema(&self) -> Value { diff --git a/crates/tui/src/tools/mcp_registry.rs b/crates/tui/src/tools/mcp_registry.rs index 9bb1046a2d..af3c1428e5 100644 --- a/crates/tui/src/tools/mcp_registry.rs +++ b/crates/tui/src/tools/mcp_registry.rs @@ -883,7 +883,7 @@ impl ToolSpec for McpSyncRegistry { variables or API keys. If a match plausibly covers the task's core \ specialized capability, call start_registry_mcp_server with its \ exact name and inspect its tools before choosing a local \ - alternative; do not run its package command through exec_shell." + alternative; do not run its package command through `bash`." } fn input_schema(&self) -> Value { diff --git a/crates/tui/src/tools/pandoc.rs b/crates/tui/src/tools/pandoc.rs index 7a534a40d0..60e12fb813 100644 --- a/crates/tui/src/tools/pandoc.rs +++ b/crates/tui/src/tools/pandoc.rs @@ -70,7 +70,7 @@ impl ToolSpec for PandocConvertTool { } fn description(&self) -> &'static str { - "Convert a document between formats via pandoc. Reads `source_path` (any pandoc-supported input format — pandoc autodetects from extension), converts to `target_format`, and either writes the result to `output_path` (when provided) or returns the converted text inline. Supported targets: markdown, gfm, commonmark, html, rst, latex, docx, odt, epub, plain, asciidoc. Use this instead of shelling out to pandoc via `Bash` — no approval prompt for output_path-less reads, structured errors, and a curated format whitelist." + "Convert a document between formats via pandoc. Reads `source_path` (any pandoc-supported input format — pandoc autodetects from extension), converts to `target_format`, and either writes the result to `output_path` (when provided) or returns the converted text inline. Supported targets: markdown, gfm, commonmark, html, rst, latex, docx, odt, epub, plain, asciidoc. Use this instead of shelling out to pandoc via `bash` — no approval prompt for output_path-less reads, structured errors, and a curated format whitelist." } fn input_schema(&self) -> Value { diff --git a/crates/tui/src/tools/plan.rs b/crates/tui/src/tools/plan.rs index b40607c0bd..0a1b253d18 100644 --- a/crates/tui/src/tools/plan.rs +++ b/crates/tui/src/tools/plan.rs @@ -403,12 +403,12 @@ impl ToolSpec for UpdatePlanTool { } fn description(&self) -> &'static str { - "Legacy compatibility tool for loading older Plan artifacts. New work uses the canonical work_update list and a normal Plan-mode response." + "Legacy compatibility tool for loading older Plan artifacts. New work uses the canonical todo_write list and a normal Plan-mode response." } fn model_visible(&self) -> bool { // Older transcripts and sessions can still replay this tool, but new - // model turns get one progress model (`work_update`) instead of the + // model turns get one progress model (`todo_write`) instead of the // retired Strategy/Plan surface. false } @@ -466,7 +466,7 @@ impl ToolSpec for UpdatePlanTool { }, "plan": { "type": "array", - "description": "Legacy replay field; new work must use work_update", + "description": "Legacy replay field; new work must use todo_write", "deprecated": true, "items": { "type": "object" } } @@ -602,7 +602,7 @@ mod tests { assert!(!tool.model_visible()); assert!(description.contains("Legacy compatibility")); - assert!(description.contains("canonical work_update list")); + assert!(description.contains("canonical todo_write list")); } #[tokio::test] diff --git a/crates/tui/src/tools/remember.rs b/crates/tui/src/tools/remember.rs index 6933b78167..91821557da 100644 --- a/crates/tui/src/tools/remember.rs +++ b/crates/tui/src/tools/remember.rs @@ -72,7 +72,7 @@ impl ToolSpec for RememberTool { "scope": { "type": "string", "enum": ["global", "workspace"], - "description": "Native backend scope; defaults to global." + "description": "Native backend scope; defaults to global. workspace requires a git repository with an origin." } }, "required": [] diff --git a/crates/tui/src/tools/runtime_mcp.rs b/crates/tui/src/tools/runtime_mcp.rs index d602021f4c..926891caee 100644 --- a/crates/tui/src/tools/runtime_mcp.rs +++ b/crates/tui/src/tools/runtime_mcp.rs @@ -218,6 +218,7 @@ impl ToolSpec for StartRuntimeMcpServer { (like 'https://...'), call this tool immediately to start the server \ and register its tools. Do NOT suggest editing config files. \ Accepts a local command (stdio) or a remote URL (HTTP/SSE). \ + Local commands must be a known runtime (npx/npm/pnpm/yarn/bunx/bun/node/python/python3/uvx/uv/deno/ruby/cargo); shell wrappers are rejected. \ After the server starts, the response lists each tool's callable name. \ You MUST copy those exact names when calling the tools. \ Do NOT construct or guess tool names yourself." diff --git a/crates/tui/src/tools/search.rs b/crates/tui/src/tools/search.rs index 0f8b65ba7b..2e0647b260 100644 --- a/crates/tui/src/tools/search.rs +++ b/crates/tui/src/tools/search.rs @@ -56,7 +56,7 @@ impl ToolSpec for GrepFilesTool { } fn description(&self) -> &'static str { - "Search for a regex pattern in workspace files. The pure-Rust search skips common non-code directories by default and returns matching lines with context." + "Search for a regex pattern in workspace files. Use this instead of `grep -r`, `rg`, or `find ... -exec grep` in `bash` — pure-Rust; skips common non-code directories (node_modules, .git, target, ...) by default; it does not apply .gitignore (built-in default exclusions only). Returns matching lines with context (default: 2 lines before/after each match)." } fn input_schema(&self) -> Value { @@ -83,7 +83,7 @@ impl ToolSpec for GrepFilesTool { }, "context_lines": { "type": "integer", - "description": "Number of context lines before and after each match (default: 2)" + "description": "Context lines before and after each match (default: 2; 1 returns strings, not arrays)." }, "case_insensitive": { "type": "boolean", diff --git a/crates/tui/src/tools/send_later.rs b/crates/tui/src/tools/send_later.rs index 07c45be8e0..3e405f5516 100644 --- a/crates/tui/src/tools/send_later.rs +++ b/crates/tui/src/tools/send_later.rs @@ -104,6 +104,7 @@ Actions: \"schedule\" (create a pending trigger; requires approval), \ \"list\" (recent triggers), \"read\" (one trigger by trigger_id), \ \"cancel\" (cancel a pending trigger before it fires; requires approval). \ Use delay_minutes or fire_at (ISO 8601 UTC) — not both. \ +message is required for action=schedule; fire_at must be strictly in the future. \ Returns trigger_id and resolved fire_at." } } diff --git a/crates/tui/src/tools/shell.rs b/crates/tui/src/tools/shell.rs index 5f744764c6..d4ab8adf59 100644 --- a/crates/tui/src/tools/shell.rs +++ b/crates/tui/src/tools/shell.rs @@ -4366,7 +4366,7 @@ impl ToolSpec for LowercaseBashTool { json!({ "type": "object", "properties": { - "command": { "type": "string", "description": "Bash command to execute." }, + "command": { "type": "string", "description": "Shell command to execute." }, "timeout": { "type": "number", "description": "Optional timeout in seconds; when omitted the command is killed after 120 seconds." }, "sandbox_permissions": { "type": "string", @@ -4540,7 +4540,7 @@ impl ToolSpec for BashTool { if self.read_only { "Inspect the workspace with the bounded read-only command subset. Commands run directly as argv, never through a shell; only action=run plus command, cwd, and timeout_ms are accepted." } else { - "Execute a shell command in the workspace. Action \"run\" (default) executes a command; \"wait\" blocks for a background task until completion or timeout; \"interact\" sends stdin to a background task; \"cancel\" kills a background task. Pass wait=false for a nonblocking task snapshot. Foreground mode is for bounded commands; use background=true for work expected to take >5 seconds. Commands run via the user's login shell ($SHELL); when that shell is zsh, a bare word starting with `=` undergoes `=command` PATH expansion (e.g. `echo ===` fails) — quote such arguments, e.g. `echo '==='`." + "Execute a shell command in the workspace. Action \"run\" (default) executes a command; \"wait\" blocks for a background task until completion or timeout; \"interact\" sends stdin to a background task; \"cancel\" kills a background task. Pass wait=false for a nonblocking task snapshot. Foreground mode is for bounded commands; use background=true for work expected to take >5 seconds. Output is truncated per stream (~30KB: head/tail kept, middle summarized; see metadata flags). Commands run via the user's login shell ($SHELL); when that shell is zsh, a bare word starting with `=` undergoes `=command` PATH expansion (e.g. `echo ===` fails) — quote such arguments, e.g. `echo '==='`." } } @@ -4562,7 +4562,7 @@ impl ToolSpec for BashTool { }, "timeout_ms": { "type": "integer", - "description": "Timeout in milliseconds. The default depends on the action: action=run 120000 (the standalone Bash tool caps it at 600000), action=wait 30000, action=interact 1000. A foreground action=run that omits this is bounded by that default and killed with a background-rerun hint; pass an explicit value for longer foreground work, or background=true. For action=wait, `timeout_secs` (seconds) and `timeout` (milliseconds) are accepted aliases." + "description": "Timeout in milliseconds. The default depends on the action: action=run 120000 (the standalone Bash tool caps it at 600000), action=wait 30000, action=interact 1000. A foreground action=run that omits this is bounded by that default and killed with a background-rerun hint; pass an explicit value for longer foreground work, or background=true. For action=wait, `timeout_secs` (seconds) and `timeout` (milliseconds) are accepted aliases. Background=true tasks are not bounded by it — stop them with action=cancel." }, "background": { "type": "boolean", diff --git a/crates/tui/src/tools/skill.rs b/crates/tui/src/tools/skill.rs index 261481bfea..5a232cc2d5 100644 --- a/crates/tui/src/tools/skill.rs +++ b/crates/tui/src/tools/skill.rs @@ -39,7 +39,7 @@ impl ToolSpec for LoadSkillTool { "Load a skill (SKILL.md body + companion file list) into the next turn's context. \ Use name=\"list\" to discover the complete enabled catalogue, then load an exact \ skill when the user names it or the task clearly matches its description. Faster \ - than File action=\"read\" plus File action=\"list\"." + than separate `read` and `list_dir` calls." } fn input_schema(&self) -> Value { @@ -285,7 +285,7 @@ fn format_skill_body(skill: &Skill) -> String { if !companions.is_empty() { out.push_str("\n## Companion files\n\n"); out.push_str( - "Sibling files in the skill directory. Open one with File action=\"read\" when the task requires it; a skill stored outside the workspace has to be read through Bash instead.\n\n", + "Sibling files in the skill directory. Open one with the `read` tool when the task requires it; a skill stored outside the workspace has to be read through `bash` instead.\n\n", ); for path in &companions { out.push_str(&format!("- `{}`\n", path.display())); diff --git a/crates/tui/src/tools/subagent/mod.rs b/crates/tui/src/tools/subagent/mod.rs index c558a029d7..7d9fec2662 100644 --- a/crates/tui/src/tools/subagent/mod.rs +++ b/crates/tui/src/tools/subagent/mod.rs @@ -15744,8 +15744,7 @@ const EXPLORE_AGENT_INTRO: &str = concat!( "Use `read` for bounded file reads and `bash` only for the allowed read-only inspection subset: navigation/rg, safe Git reads (for example `git log -n 5`), and read-only GitHub views such as `gh issue view`. Builds, tests, writes, and shell control actions are unavailable.\n", "Use your private `todo_write` list as editable working notes when useful; it is agent-owned state, not permission to write project files. Those tool calls remain in the complete transcript artifact returned to the parent.\n", "Honor QUESTION, SCOPE, ALREADY_KNOWN, and STOP_CONDITION. Do not repeat ALREADY_KNOWN work unless evidence contradicts it; do not broaden once QUESTION is answered.\n", - "Your value is compressed evidence: cite `path:line-range` for each finding and stop once evidence is sufficient. Return partial findings if the next step would be speculative or duplicative.\n", - "CHANGES will almost always be \"None.\" for a scout.\n\n" + "Your value is compressed evidence: cite `path:line-range` for each finding and stop once evidence is sufficient. Return partial findings if the next step would be speculative or duplicative.\n\n" ); const PLAN_AGENT_INTRO: &str = concat!( diff --git a/crates/tui/src/tools/tasks.rs b/crates/tui/src/tools/tasks.rs index fa34735a23..85b73fa4ea 100644 --- a/crates/tui/src/tools/tasks.rs +++ b/crates/tui/src/tools/tasks.rs @@ -184,23 +184,23 @@ impl ToolSpec for TasksTool { "Cancel a queued or running durable task through TaskManager. Requires approval because it changes work state." } Some("gate_run") => { - "Run an approved verification gate command and return structured evidence. When inside a durable task, the gate result and log artifact are attached to that task." + "Run an approved verification gate command and return structured evidence. When inside a durable task, the gate result and log artifact are attached to that task. Dangerous commands are BLOCKED unless auto-approve is enabled; default timeout 120s." } Some("pr_attempt_record") => { - "Capture current git diff as a durable PR work attempt with patch artifact, changed files, and verification notes." + "Capture current git diff as a durable PR work attempt with patch artifact, changed files, and verification notes. Requires approval because it records work state." } Some("pr_attempt_list") => "List PR attempts recorded on a durable task.", Some("pr_attempt_read") => { "Read one recorded PR attempt and its patch artifact reference." } Some("pr_attempt_preflight") => { - "Run `git apply --check` for a recorded attempt patch. This is a no-mutation preflight; actual apply remains explicit and approval-gated elsewhere." + "Run `git apply --check` for a recorded attempt patch. This is a no-mutation preflight and itself requires approval; the actual apply stays a separate explicit step." } _ if self.read_only => { "Inspect durable tasks and their PR attempts. Actions: \"list\", \"read\", \"pr_attempt_list\", \"pr_attempt_read\"." } _ => { - "Manage durable background tasks through TaskManager. Durable tasks are restart-aware executable work, distinct from sub-agents. Actions: \"create\" (enqueue; approval), \"list\", \"read\", \"cancel\" (approval), \"gate_run\" (run an approved verification gate command and return structured evidence; approval), \"pr_attempt_record\", \"pr_attempt_list\", \"pr_attempt_read\", \"pr_attempt_preflight\". Use task_shell_start for long-running shell work." + "Manage durable background tasks through TaskManager. Durable tasks are restart-aware executable work, distinct from sub-agents. Actions: \"create\" (enqueue; approval), \"list\", \"read\", \"cancel\" (approval), \"gate_run\" (run an approved verification gate command and return structured evidence; approval), \"pr_attempt_record\" (approval), \"pr_attempt_list\", \"pr_attempt_read\", \"pr_attempt_preflight\" (approval). Use task_shell_start for long-running shell work." } } } @@ -885,7 +885,7 @@ impl ToolSpec for TaskShellStartTool { "properties": { "command": { "type": "string" }, "cwd": { "type": "string", "description": "Optional working directory within the workspace." }, - "timeout_ms": { "type": "integer", "minimum": 1000, "maximum": 600000 }, + "timeout_ms": { "type": "integer", "minimum": 1000, "maximum": 600000, "description": "Accepted for interface compatibility but not enforced: the command always starts in the background and is not bounded by this timeout. A running shell task cannot be cancelled from the model surface; it ends when the command finishes." }, "stdin": { "type": "string" }, "tty": { "type": "boolean" } }, @@ -949,7 +949,7 @@ impl ToolSpec for TaskShellWaitTool { json!({ "type": "object", "properties": { - "task_id": { "type": "string", "description": "Background shell task id returned by task_shell_start or `Bash`." }, + "task_id": { "type": "string", "description": "Background shell task id returned by task_shell_start." }, "wait": { "type": "boolean", "default": false }, "timeout_ms": { "type": "integer", "minimum": 1000, "maximum": 600000 }, "gate": { "type": "string", "enum": ["fmt", "check", "clippy", "test", "custom"] }, diff --git a/crates/tui/src/tools/terminal_session.rs b/crates/tui/src/tools/terminal_session.rs index decb5fee0f..aae5f7e294 100644 --- a/crates/tui/src/tools/terminal_session.rs +++ b/crates/tui/src/tools/terminal_session.rs @@ -714,7 +714,7 @@ pub struct TerminalRunTool; impl ToolSpec for TerminalRunTool { terminal_tool_common!( "terminal/run", - "Run a command in a persistent PTY shell session. cd, exports, shell functions, and activated environments persist across calls in this process. Identity and a non-secret last-known summary persist across restarts; prior shells are surfaced as stale/lost and are never reattached." + "Run a command in a persistent PTY shell session. cd, exports, shell functions, and activated environments persist across calls in this process. Identity and a non-secret last-known summary persist across restarts; prior shells are surfaced as stale/lost and are never reattached. On timeout the wait is abandoned but the command keeps running in the session (use terminal/wait or cancel). (Unix only)" ); fn input_schema(&self) -> serde_json::Value { json!({"type":"object","properties":{"command":{"type":"string"},"session":{"type":"string","default":"term-1"},"timeout_secs":{"type":"integer","default":120}},"required":["command"]}) @@ -761,7 +761,7 @@ pub struct TerminalSendTool; impl ToolSpec for TerminalSendTool { terminal_tool_common!( "terminal/send", - "Send raw input to a live persistent terminal session. Use a literal ETX control byte to interrupt an interactive process. A prior-process shell is reported as stale/lost rather than reattached." + "Send raw input to a live persistent terminal session. Use a literal ETX control byte to interrupt an interactive process. A prior-process shell is reported as stale/lost rather than reattached. (Unix only)" ); fn input_schema(&self) -> serde_json::Value { json!({"type":"object","properties":{"session":{"type":"string"},"text":{"type":"string"},"wait_ms":{"type":"integer","default":250}},"required":["session","text"]}) @@ -803,7 +803,7 @@ pub struct TerminalWaitTool; impl ToolSpec for TerminalWaitTool { terminal_tool_common!( "terminal/wait", - "Wait for the current foreground command in a live persistent terminal session and return buffered output. A prior-process shell is reported as stale/lost rather than reattached." + "Wait for the current foreground command in a live persistent terminal session and return buffered output. A prior-process shell is reported as stale/lost rather than reattached. Output buffer holds at most 512KiB; older bytes are dropped silently. (Unix only)" ); fn input_schema(&self) -> serde_json::Value { json!({"type":"object","properties":{"session":{"type":"string"},"timeout_secs":{"type":"integer","default":120}},"required":["session"]}) @@ -842,7 +842,7 @@ pub struct TerminalCancelTool; impl ToolSpec for TerminalCancelTool { terminal_tool_common!( "terminal/cancel", - "Interrupt the running foreground command with ETX. The live terminal session survives and can be reused; its non-secret summary persists." + "Interrupt the running foreground command with ETX. The live terminal session survives and can be reused; its non-secret summary persists. (Unix only)" ); fn input_schema(&self) -> serde_json::Value { json!({"type":"object","properties":{"session":{"type":"string"}},"required":["session"]}) @@ -886,7 +886,7 @@ pub struct TerminalResetTool; impl ToolSpec for TerminalResetTool { terminal_tool_common!( "terminal/reset", - "Kill and recreate a persistent terminal session with a fresh environment. This loses live cd, exports, functions, activated environments, and running work while retaining the prior historical summary." + "Kill and recreate a persistent terminal session with a fresh environment. This loses live cd, exports, functions, activated environments, and running work while retaining the prior historical summary. (Unix only)" ); fn input_schema(&self) -> serde_json::Value { json!({"type":"object","properties":{"session":{"type":"string"}},"required":["session"]}) diff --git a/crates/tui/src/tools/verify.rs b/crates/tui/src/tools/verify.rs index cbe5d1be72..c45fd06f94 100644 --- a/crates/tui/src/tools/verify.rs +++ b/crates/tui/src/tools/verify.rs @@ -318,7 +318,8 @@ elevated reasoning and tries to REFUTE it, returning structured findings (issue, suggested fix). Call this when it is worth spending extra thinking: before claiming a non-trivial \ change complete, after a risky or subtle edit, or when you are unsure the change fully satisfies \ the requirement and handles edge cases. Skip it for trivial or mechanical changes. This is not a \ -test runner (use run_verifiers) or a code review of an arbitrary target (use review) — it is a \ +test runner (use the Run tool with action=\"verifiers\") or a code review of an arbitrary \ +target (use review) — it is a \ self-check of whether what you just did is actually correct and complete." } diff --git a/crates/tui/src/tools/web_run.rs b/crates/tui/src/tools/web_run.rs index d93f564a06..5b5c252762 100644 --- a/crates/tui/src/tools/web_run.rs +++ b/crates/tui/src/tools/web_run.rs @@ -357,7 +357,7 @@ impl ToolSpec for WebRunTool { } fn description(&self) -> &'static str { - "Browse the web (search/open/click/find/screenshot/image_query) and return structured results with ref_ids for citations." + "Browse the web (search/open/click/find/screenshot/image_query) and return structured results with ref_ids for citations. ref_ids are session-cache references (≈30min TTL, ~256 pages); reopen by URL when evicted." } fn input_schema(&self) -> Value { @@ -384,7 +384,7 @@ impl ToolSpec for WebRunTool { "type": "object", "properties": { "q": { "type": "string" }, - "recency": { "type": "integer" }, + "recency": { "type": "integer", "description": "Freshness window in days (accepted but not enforced for image search)" }, "max_results": { "type": "integer" }, "timeout_ms": { "type": "integer" }, "domains": { "type": "array", "items": { "type": "string" } } @@ -397,7 +397,7 @@ impl ToolSpec for WebRunTool { "items": { "type": "object", "properties": { - "ref_id": { "type": "string" }, + "ref_id": { "type": "string", "description": "Page ref_id from an earlier result, or a raw http(s) URL" }, "lineno": { "type": "integer" } }, "required": ["ref_id"] @@ -427,11 +427,12 @@ impl ToolSpec for WebRunTool { }, "screenshot": { "type": "array", + "description": "Screenshot a PDF page. PDF refs only; returns the page's text lines (not an image), pageno is 0-based.", "items": { "type": "object", "properties": { "ref_id": { "type": "string" }, - "pageno": { "type": "integer" } + "pageno": { "type": "integer", "description": "0-based page number" } }, "required": ["ref_id", "pageno"] } diff --git a/crates/tui/src/tools/web_search.rs b/crates/tui/src/tools/web_search.rs index ce6df56c70..f8973f768b 100644 --- a/crates/tui/src/tools/web_search.rs +++ b/crates/tui/src/tools/web_search.rs @@ -178,7 +178,7 @@ impl ToolSpec for WebSearchTool { } fn description(&self) -> &'static str { - "Search the web and return ranked results with URLs, snippets, session-scoped ref_ids, and an execution receipt. Open a result ref_id with `web.run` when the short summary is not enough; fetch only the few sources needed. When the exact active route reports a documented first-party server-side search tool, it is tried first; otherwise keyless Firecrawl is the default. Configured API backends visibly degrade directly to keyless Bing when unavailable, and every hop is recorded. Configuration and network-policy errors fail closed. Explicit Bing and private DuckDuckGo-compatible routes do not cross providers. Set `[search] provider = \"firecrawl\" | \"bing\" | \"tavily\" | \"bocha\" | \"metaso\" | \"searxng\" | \"baidu\" | \"volcengine\" | \"sofya\"` in config.toml. Firecrawl Cloud works keyless with a bounded quota. For a known canonical URL, prefer `fetch_url` directly." + "Search the web and return ranked results with URLs, snippets, session-scoped ref_ids, and an execution receipt. Open a result ref_id with `web.run` when the short summary is not enough; fetch only the few sources needed. When the exact active route reports a documented first-party server-side search tool, it is tried first; otherwise keyless Firecrawl is the default. Configured API backends visibly degrade directly to keyless Bing when unavailable, and every hop is recorded. Configuration and network-policy errors fail closed. Explicit Bing and private DuckDuckGo-compatible routes do not cross providers. Set `[search] provider = \"firecrawl\" | \"bing\" | \"duckduckgo\" | \"tavily\" | \"bocha\" | \"metaso\" | \"searxng\" | \"baidu\" | \"volcengine\" | \"sofya\"` in config.toml. Firecrawl Cloud works keyless with a bounded quota. For a known canonical URL, prefer the Web tool with action=fetch." } fn input_schema(&self) -> Value { diff --git a/crates/tui/src/tools/web_tool.rs b/crates/tui/src/tools/web_tool.rs index 62513a4701..618efaf41c 100644 --- a/crates/tui/src/tools/web_tool.rs +++ b/crates/tui/src/tools/web_tool.rs @@ -71,7 +71,7 @@ impl ToolSpec for WebTool { } fn description(&self) -> &'static str { - "Search the web, fetch a known URL, or wait for a local dev server. Prefer fetch for a canonical URL and search when the source is unknown. Web actions are read-only and network-policy aware." + "Search the web, fetch a known URL, or wait for a local dev server. Prefer fetch for a canonical URL and search when the source is unknown. search/fetch are network-policy aware; wait only reaches loopback and does not evaluate network policy." } fn input_schema(&self) -> Value { @@ -159,7 +159,7 @@ impl ToolSpec for WebTool { }, "port": { "type": "integer", - "description": "TCP port to wait for (action=wait)" + "description": "TCP port to wait for (action=wait). action=wait requires `port`; the `url` port must match and the host must be loopback." }, "poll_interval_ms": { "type": "integer", diff --git a/crates/tui/src/tools/workflow/mod.rs b/crates/tui/src/tools/workflow/mod.rs index b4c5fcfcd0..fdaae5db68 100644 --- a/crates/tui/src/tools/workflow/mod.rs +++ b/crates/tui/src/tools/workflow/mod.rs @@ -995,19 +995,19 @@ impl ToolSpec for WorkflowTool { }, "script": { "type": "string", - "description": "Workflow JS source. The runtime provides args, task(...), parallel(thunks), pipeline(thunks), log(...), phase(...), and budget. Fan-out syntax: await parallel([() => task({...}), () => task({...})]). parallel() requires one array of zero-argument thunks, not variadic task promises." + "description": "Workflow JS source. The runtime provides args, task(...), parallel(thunks), pipeline(items, ...stages), log(...), phase(...), and budget. Fan-out syntax: await parallel([() => task({...}), () => task({...})]). parallel() requires one array of zero-argument thunks, not variadic task promises. Date and Math.random are unavailable (deterministic replay)." }, "source_path": { "type": "string", - "description": "Path to a .workflow.js script inside the workspace. Use instead of script for checked-in workflows." + "description": "Path to a workflow script (.workflow.js/.ts) inside the workspace or ~/.codewhale/workflows. Use instead of script for checked-in workflows." }, "fleet": { "type": "string", - "description": "Named Fleet from $CODEWHALE_HOME/fleets/ or workspace fleets/; qualified origin/name accepted. Exact Fleets freeze member identity, route, and reasoning. Runtime derives authority from role and live parent; per-task route/authority overrides are rejected." + "description": "Named Fleet to resolve task({ role }) declarations, loaded from $CODEWHALE_HOME/fleets/ or workspace fleets/. Accepts a qualified origin/name. A legacy roster maps roles to profiles. An exact Fleet (schema = \"exact\") is frozen at start: each member's provider, model, reasoning, and permission ceiling are fixed, and any per-task routing/stance override (model, model_strength, thinking, subagent_type, allowed_tools, write_authority) is rejected; write-role members must declare write scope. Runtime derives authority from role and live parent." }, "plan": { "type": "object", - "description": "Structured planner plan JSON (#4124). Alternative to script/source_path. Accepts goal, risk, max_children, token_budget, phases[], and/or children[] (or IR nodes). risk must be exactly read_only, writes, or elevated. For a child, prefer role/profile without an explicit type; do not combine a role/profile with a conflicting type. Lowered to Workflow JS with parallel() partial-success semantics." + "description": "Structured planner plan JSON (#4124). Alternative to script/source_path. Accepts goal, risk, max_children, token_budget, phases[], children[], gates[] (or IR nodes). gates[] are Workflow-owned lane gates that can pause roles pending APPROVE/PASS verdicts. risk: read_only | writes | elevated (common aliases accepted). For a child, prefer role/profile without an explicit type; do not combine a role/profile with a conflicting type. Lowered to Workflow JS with parallel() partial-success semantics." }, "args": { "anyOf": [ @@ -1036,7 +1036,7 @@ impl ToolSpec for WorkflowTool { "verify": { "type": "boolean", "default": false, - "description": "After a successful workflow completion, run quick workspace verifier gates (auto/quick profile)." + "description": "After a successful workflow completion, run quick workspace verifier gates (auto/quick profile); any failed or skipped gate flips the run's final status to Failed." } }, "required": [], diff --git a/crates/tui/src/tools/workflow_trigger.rs b/crates/tui/src/tools/workflow_trigger.rs index a2421b3f53..03fa649f17 100644 --- a/crates/tui/src/tools/workflow_trigger.rs +++ b/crates/tui/src/tools/workflow_trigger.rs @@ -4,11 +4,12 @@ //! saying the word "workflow". Policy here answers "should we orchestrate?" — //! the parent prompt still **tells the operator** the intended shape and may //! ask setup questions via `request_user_input` (TUI modal) before calling -//! `workflow` / `plan`. +//! `workflow` / emitting a Plan-mode response. //! -//! This remains Act/Agent guidance rather than a prose classifier at the host -//! boundary. Operate sends ordinary work to direct background workers and -//! reaches for Workflow only when its stronger orchestration properties help. +//! This is an offline policy probe / reserved classifier; the Act prompt layer +//! intentionally does not mention Workflow. Operate sends ordinary work to +//! direct background workers and reaches for Workflow only when its stronger +//! orchestration properties help. /// Signals the parent can supply without full conversation replay. #[derive(Debug, Clone, Default, PartialEq, Eq)]