From a1f6a6bad55ff27d3a8f8c00aac43f5dd227e039 Mon Sep 17 00:00:00 2001 From: Hare Date: Fri, 14 Aug 2026 13:58:11 +0900 Subject: [PATCH] prompt: close whitespace-control include validation gap --- Cargo.lock | 2 +- crates/config-source/src/lib.rs | 2 ++ crates/tui/Cargo.toml | 2 +- crates/tui/src/dashboard/mod.rs | 15 +++----- crates/worker/src/prompt/catalog.rs | 12 +++++-- .../workspace-server/src/prompt_settings.rs | 4 +-- crates/workspace-server/src/server.rs | 34 ++++++++++++++----- 7 files changed, 46 insertions(+), 25 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 94dde183..b4a6768f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4795,7 +4795,6 @@ dependencies = [ "fs4", "llm-engine", "manifest", - "minijinja", "protocol", "pulldown-cmark", "ratatui", @@ -4809,6 +4808,7 @@ dependencies = [ "toml", "unicode-width", "uuid", + "worker", ] [[package]] diff --git a/crates/config-source/src/lib.rs b/crates/config-source/src/lib.rs index 2923b2a1..1dd92cc5 100644 --- a/crates/config-source/src/lib.rs +++ b/crates/config-source/src/lib.rs @@ -1372,6 +1372,8 @@ fn parse_static_template_includes(template: &str, source: &str) -> Result Result { - let mut env = minijinja::Environment::new(); - env.set_undefined_behavior(minijinja::UndefinedBehavior::Strict); - env.add_template( - "orchestrator_idle_queue_notice", - ORCHESTRATOR_IDLE_QUEUE_NOTICE_TEMPLATE, - )?; - env.get_template("orchestrator_idle_queue_notice")? - .render(context) +) -> Result { + worker::PromptCatalog::builtins_only()? + .render_serializable(ORCHESTRATOR_IDLE_QUEUE_NOTICE_PROMPT, context) } fn orchestrator_work_set_detail( diff --git a/crates/worker/src/prompt/catalog.rs b/crates/worker/src/prompt/catalog.rs index 4b3e5001..f3480cbc 100644 --- a/crates/worker/src/prompt/catalog.rs +++ b/crates/worker/src/prompt/catalog.rs @@ -230,6 +230,14 @@ impl PromptCatalog { self.projection.templates.contains_key(key) } + pub fn render_serializable( + &self, + key: &str, + context: &T, + ) -> Result { + self.render_name(key, Value::from_serialize(context)) + } + pub fn render_name(&self, key: &str, ctx: Value) -> Result { let template = self .env @@ -538,7 +546,7 @@ mod tests { #[test] fn graph_rejects_dynamic_legacy_missing_and_cycles() { let invalid = BTreeMap::from([ - ("a".into(), "{% include target %}".into()), + ("a".into(), "{%- include target -%}".into()), ("target".into(), "ok".into()), ]); assert!(validate_prompt_templates(&invalid).is_err()); @@ -546,7 +554,7 @@ mod tests { let legacy = BTreeMap::from([("a".into(), "{% include \"legacy/default\" %}".into())]); assert!(validate_prompt_templates(&legacy).is_err()); - let missing = BTreeMap::from([("a".into(), "{% include \"missing\" %}".into())]); + let missing = BTreeMap::from([("a".into(), "{%- include \"missing\" -%}".into())]); assert!(validate_prompt_templates(&missing).is_err()); let cycle = BTreeMap::from([ diff --git a/crates/workspace-server/src/prompt_settings.rs b/crates/workspace-server/src/prompt_settings.rs index fa32f5c5..f7c15617 100644 --- a/crates/workspace-server/src/prompt_settings.rs +++ b/crates/workspace-server/src/prompt_settings.rs @@ -130,8 +130,8 @@ mod tests { .unwrap()]) .unwrap(); for source in [ - r#"{ prompts = { common = { language = "{% include target %}"; }; }; }"#, - r#"{ prompts = { common = { language = "{% include \"missing\" %}"; }; }; }"#, + r#"{ prompts = { common = { language = "{%- include target -%}"; }; }; }"#, + r#"{ prompts = { common = { language = "{%- include \"missing\" -%}"; }; }; }"#, r#"{ prompts = { common = { language = "{% include \"common.workspace\" %}"; workspace = "{% include \"common.language\" %}"; }; }; }"#, ] { let snapshot = ConfigTreeSnapshot::from_entries( diff --git a/crates/workspace-server/src/server.rs b/crates/workspace-server/src/server.rs index d1e95588..095d88fb 100644 --- a/crates/workspace-server/src/server.rs +++ b/crates/workspace-server/src/server.rs @@ -240,10 +240,7 @@ impl ServerConfig { } const ORCHESTRATOR_ATTENTION_TICKET_LIMIT: usize = 20; -const ORCHESTRATOR_ATTENTION_PROMPT: &str = include_str!(concat!( - env!("CARGO_MANIFEST_DIR"), - "/../../resources/prompts/internal/workspace_orchestrator_queue_attention.md" -)); +const ORCHESTRATOR_ATTENTION_PROMPT_NAME: &str = "internal.workspace_orchestrator_queue_attention"; #[derive(Clone)] pub struct WorkspaceApi { @@ -4811,10 +4808,31 @@ fn dispatch_orchestrator_queue_attention(api: &WorkspaceApi) { } else { format!("Additional queued Tickets omitted from this notice: {omitted}\n") }; - let content = ORCHESTRATOR_ATTENTION_PROMPT - .replace("{{omitted_line}}", &omitted_line) - .replace("{{workspace_id}}", &api.config.workspace_id) - .replace("{{ticket_lines}}", &shown); + let Ok(Some(config_state)) = api + .config_store + .load_workspace_config(&api.config.workspace_id) + else { + return; + }; + let Ok(projection) = + crate::prompt_settings::project_prompts_from_workspace_config(&config_state) + else { + return; + }; + let Ok(catalog) = worker::PromptCatalog::from_projection(projection) else { + return; + }; + let content = match catalog.render_serializable( + ORCHESTRATOR_ATTENTION_PROMPT_NAME, + &BTreeMap::from([ + ("omitted_line", omitted_line.as_str()), + ("workspace_id", api.config.workspace_id.as_str()), + ("ticket_lines", shown.as_str()), + ]), + ) { + Ok(content) => content, + Err(_) => return, + }; let accepted = api .runtime .send_input(