Skip to content
2 changes: 1 addition & 1 deletion crates/tui/src/core/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
30 changes: 30 additions & 0 deletions crates/tui/src/core/engine/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
98 changes: 87 additions & 11 deletions crates/tui/src/prompts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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."
)
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
///
Expand Down Expand Up @@ -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\"),则覆盖此规则。";
Expand All @@ -819,8 +882,8 @@ const LOCALE_PREAMBLE_JA: &str = "## 言語要件\n\n\
codewhale を実行しています。タスクコンテキスト(コード、エラーログ、\
ファイル名)が英語であっても、システムプロンプトの他の部分が英語で\
あっても、`reasoning_content`(内部思考)と最終的な返信は日本語で\
行ってください。コード、ファイルパス、ツール名(例:`File`、\
`Bash`)、環境変数、コマンドライン引数、URL は元のまま —— \
行ってください。コード、ファイルパス、ツール名(例:`bash`、\
`Web`)、環境変数、コマンドライン引数、URL は元のまま —— \
自然言語の文章のみ日本語に切り替えます。\n\n\
ユーザーがセッション中に別の言語に切り替えた場合は、次のターンから\
それに従ってください。ユーザーが明示的に要求した場合(例:\
Expand All @@ -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. \
Expand Down Expand Up @@ -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. \
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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"),
Expand Down
5 changes: 2 additions & 3 deletions crates/tui/src/prompts/text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<codewhale:subagent.done>`.
a section has no entries. If blocked, name the missing fact. Then stop.
"#;
Loading
Loading