Skip to main content

vtcode_core/prompts/
system.rs

1//! System instructions and prompt management.
2//!
3//! Prompt variants share one canonical base contract plus thin mode deltas and
4//! compact runtime addenda. Richer behavior comes from AGENTS.md, dynamic tool
5//! guidance, skill metadata, and runtime notices.
6
7use crate::config::constants::prompt_budget as prompt_budget_constants;
8use crate::config::types::SystemPromptMode;
9use crate::llm::providers::gemini::wire::Content;
10use crate::project_doc::read_project_doc;
11use crate::prompts::context::PromptContext;
12use crate::prompts::guidelines::generate_tool_guidelines;
13use crate::prompts::output_styles::OutputStyleApplier;
14use crate::prompts::resources::{apply_system_prompt_layers, resolve_system_prompt_layers};
15use crate::prompts::system_prompt_cache::PROMPT_CACHE;
16use crate::prompts::temporal::generate_temporal_context;
17use crate::skills::render::render_prompt_skills_section;
18use std::env;
19use std::path::Path;
20use std::sync::OnceLock;
21use tracing::warn;
22
23/// Shared Planning workflow header used by both static and incremental prompt builders.
24pub const PLANNING_WORKFLOW_READ_ONLY_HEADER: &str = "# PLANNING WORKFLOW (READ-ONLY)";
25/// Shared Planning workflow notice line describing strict read-only enforcement.
26pub const PLANNING_WORKFLOW_READ_ONLY_NOTICE_LINE: &str = "Planning workflow is active. Mutating tools are blocked except for optional plan artifact writes under `.vtcode/plans/` (or an explicit custom plan path).";
27/// Shared Planning workflow instruction line for transitioning to implementation.
28pub const PLANNING_WORKFLOW_EXIT_INSTRUCTION_LINE: &str =
29    "Call `finish_planning` when ready to transition to implementation.";
30/// Shared Planning workflow instruction line for decision-complete planning output.
31pub const PLANNING_WORKFLOW_PLAN_QUALITY_LINE: &str = "Explore repository facts first, ask only material blocking questions, keep planning read-only, and emit exactly one decision-complete `<proposed_plan>` block with a summary, implementation steps, test cases, and assumptions/defaults. If something is still unresolved, end with `Next open decision: ...`.";
32/// Shared Planning workflow policy line requiring context-aware interview closure before final plans.
33pub const PLANNING_WORKFLOW_INTERVIEW_POLICY_LINE: &str = "In Planning workflow, prefer model-generated `request_user_input` interview questions informed by discovered repository context, keep custom notes/free-form responses available as first-class input, and continue interviewing until material scope/decomposition/verification decisions are closed before finalizing `<proposed_plan>`.";
34/// Shared Planning workflow policy line for runtimes where `request_user_input` is unavailable.
35pub const PLANNING_WORKFLOW_NO_REQUEST_USER_INPUT_POLICY_LINE: &str = "In this runtime, `request_user_input` is unavailable. In Planning workflow, continue exploring repository facts with read-only permissions, finish any unblocked planning work, and surface material blockers explicitly in plain text instead of emitting interview tool calls.";
36/// Shared Planning workflow guard line requiring explicit transition from planning to execution.
37pub const PLANNING_WORKFLOW_NO_AUTO_EXIT_LINE: &str = "Do not auto-exit Planning workflow just because a plan exists; wait for explicit implementation intent.";
38/// Shared Planning workflow task-tracking line clarifying availability and aliasing.
39pub const PLANNING_WORKFLOW_TASK_TRACKER_LINE: &str =
40    "`task_tracker` remains available while planning.";
41/// Shared reminder appended when presenting plans while still in Planning workflow.
42pub const PLANNING_WORKFLOW_IMPLEMENT_REMINDER: &str = "• Planning workflow is active with read-only permissions. Say “implement” to execute, or “stay in planning workflow” to revise. If automatic planning handoff fails, call `finish_planning` to present the plan again.";
43
44const PROMPT_TITLE: &str = "# VT Code";
45const PROMPT_INTRO: &str = "VT Code. Be concise and safe.";
46const CONTRACT_HEADER: &str = "## Contract";
47
48const OPENAI_GPT55_CONTRACT_HEADER: &str = "## GPT-5.5 OpenAI Addendum";
49const OPENAI_GPT55_CONTRACT_LINES: &[&str] = &[
50    "State the outcome, constraints, evidence, and output shape up front; avoid over-prescribing the path unless the exact steps matter.",
51    "If context is missing, say so plainly; use the smallest missing detail that would change the result, and finish any unblocked portion first.",
52    "Verify changes yourself with the smallest relevant check; never claim a check passed unless you ran it.",
53    "Before multi-step tool work, send a brief progress update that names the first step.",
54    "Use retrieved evidence for citation-sensitive work; use the minimum evidence sufficient to answer correctly, then stop.",
55];
56
57const FABLE_5_CONTRACT_HEADER: &str = "## Fable 5 Behavioral Addendum";
58const FABLE_5_CONTRACT_LINES: &[&str] = &[
59    "When you have enough information to act, act. Do not re-derive facts already established in the conversation, re-litigate a decision already made, or narrate options you will not pursue. If you are weighing a choice, give a recommendation, not an exhaustive survey.",
60    "Don't add features, refactor, or introduce abstractions beyond what the task requires. Do the simplest thing that works well. Don't design for hypothetical future requirements.",
61    "Before reporting progress, audit each claim against a tool result from this session. Only report work you can point to evidence for; if something is not yet verified, say so explicitly.",
62    "When the user is describing a problem, asking a question, or thinking out loud rather than requesting a change, the deliverable is your assessment. Don't apply a fix until they ask for one.",
63    "Pause for the user only when the work genuinely requires them: a destructive or irreversible action, a real scope change, or input that only they can provide.",
64    "You have ample context remaining. Do not stop, summarize, or suggest a new session on account of context limits. Continue the work.",
65    "Delegate independent subtasks to subagents and keep working while they run. Intervene if a subagent goes off track or is missing relevant context.",
66    "Lead with the outcome. Your first sentence after finishing should answer what happened or what you found. Supporting detail comes after. Being readable and being concise are different things; readability matters more.",
67];
68
69const DEFAULT_CONTRACT_LINES: &[&str] = &[
70    "Start with `AGENTS.md`; inspect code first, match local patterns, use `@file`.",
71    "If context is missing, say so, do not guess, finish unblocked slices first.",
72    "Take safe, reversible steps; ask only for material behavior, API, UX, or credential changes.",
73    "Keep control on the main thread. Delegate bounded, independent work only.",
74    "Verify changes yourself; never claim a check passed unless you ran it.",
75    "Keep outputs concise. Keep user updates brief and high-signal.",
76    "Use retrieved evidence for citation-sensitive work.",
77    "Preserve task goal, tracker state, touched files, verification status, and decisions across compaction.",
78    "Read files before answering. Never speculate about code you have not opened.",
79    "Prefer `ast-grep` for code-shape queries; keep text grep for prose, logs, and config strings.",
80    "Make only requested changes. Implement by default; suggest only when intent is unclear.",
81];
82
83const MINIMAL_CONTRACT_LINES: &[&str] = &[
84    "Use `AGENTS.md`; inspect code first.",
85    "If context is missing, say so, do not guess, finish unblocked slices.",
86    "Take safe, reversible steps; verify changes yourself.",
87    "Keep delegation bounded and explicit.",
88    "Preserve tracker state, touched files, and verification status across compaction.",
89    "Use retrieved evidence when citation-sensitive.",
90    "Prefer `ast-grep` for code-shape queries; keep text grep for prose and config.",
91    "Keep outputs concise.",
92];
93
94const DEFAULT_OPERATING_PROFILE_DELTA: &str = r#"## Operating Profile
95
96- Use `task_tracker` for non-trivial work.
97- Treat completion language as a checkpoint, not proof; only stop when the tracker is current and verification is resolved.
98- Use Planning workflow for research/spec work; stay read-only until implementation intent is explicit."#;
99
100const MINIMAL_OPERATING_PROFILE_DELTA: &str = r#"## Operating Profile
101
102- Stay precise; use `task_tracker` once work stops being trivial.
103- Treat completion language as a checkpoint, not proof.
104- Use `AGENTS.md` as the map; open repo docs only when structural rules matter."#;
105
106const LIGHTWEIGHT_OPERATING_PROFILE_DELTA: &str = r#"## Operating Profile
107
108- Act and verify in one thread.
109- Completion language is a checkpoint.
110- Use `task_tracker` for nontrivial work."#;
111
112const SPECIALIZED_OPERATING_PROFILE_DELTA: &str = r#"## Operating Profile
113
114- Explore, plan, then execute.
115- Use `task_tracker` for multi-step work and Planning workflow when scope or verification is still open.
116- Treat completion language as a checkpoint, not proof; only stop when tracker state, verification, and resumable state agree.
117- End plan work with one `<proposed_plan>` block; if a path stalls, re-plan into smaller verified slices.
118- Use `AGENTS.md` and `docs/harness/ARCHITECTURAL_INVARIANTS.md` when repo-wide invariants matter."#;
119
120static DEFAULT_SYSTEM_PROMPT: OnceLock<String> = OnceLock::new();
121static MINIMAL_SYSTEM_PROMPT: OnceLock<String> = OnceLock::new();
122static DEFAULT_LIGHTWEIGHT_PROMPT: OnceLock<String> = OnceLock::new();
123static DEFAULT_SPECIALIZED_PROMPT: OnceLock<String> = OnceLock::new();
124
125pub fn default_system_prompt() -> &'static str {
126    static_profile_prompt(SystemPromptMode::Default)
127}
128
129pub fn minimal_system_prompt() -> &'static str {
130    static_profile_prompt(SystemPromptMode::Minimal)
131}
132
133pub fn default_lightweight_prompt() -> &'static str {
134    static_profile_prompt(SystemPromptMode::Lightweight)
135}
136
137pub fn specialized_system_prompt() -> &'static str {
138    static_profile_prompt(SystemPromptMode::Specialized)
139}
140
141pub fn minimal_instruction_text() -> String {
142    minimal_system_prompt().to_string()
143}
144
145pub fn lightweight_instruction_text() -> String {
146    default_lightweight_prompt().to_string()
147}
148
149pub fn specialized_instruction_text() -> String {
150    specialized_system_prompt().to_string()
151}
152
153pub fn openai_gpt55_contract_addendum() -> String {
154    let lines_len = OPENAI_GPT55_CONTRACT_LINES
155        .iter()
156        .map(|line| line.len())
157        .sum::<usize>();
158    let mut prompt = String::with_capacity(
159        OPENAI_GPT55_CONTRACT_HEADER.len() + lines_len + OPENAI_GPT55_CONTRACT_LINES.len() * 3 + 8,
160    );
161    prompt.push_str(OPENAI_GPT55_CONTRACT_HEADER);
162    prompt.push_str("\n\n");
163    for line in OPENAI_GPT55_CONTRACT_LINES {
164        prompt.push_str("- ");
165        prompt.push_str(line);
166        prompt.push('\n');
167    }
168    prompt.pop();
169    prompt
170}
171
172pub fn fable_5_contract_addendum() -> String {
173    let lines_len = FABLE_5_CONTRACT_LINES
174        .iter()
175        .map(|line| line.len())
176        .sum::<usize>();
177    let mut prompt = String::with_capacity(
178        FABLE_5_CONTRACT_HEADER.len() + lines_len + FABLE_5_CONTRACT_LINES.len() * 3 + 8,
179    );
180    prompt.push_str(FABLE_5_CONTRACT_HEADER);
181    prompt.push_str("\n\n");
182    for line in FABLE_5_CONTRACT_LINES {
183        prompt.push_str("- ");
184        prompt.push_str(line);
185        prompt.push('\n');
186    }
187    prompt.pop();
188    prompt
189}
190
191const STRUCTURED_REASONING_INSTRUCTIONS: &str = r#"
192## Structured Reasoning
193
194Use tags when helpful: `<analysis>` facts/options, `<plan>` steps, `<uncertainty>` blockers, `<verification>` checks.
195"#;
196
197/// System instruction configuration
198#[derive(Debug, Clone, Default)]
199pub struct SystemPromptConfig;
200
201/// Generate system instruction
202pub async fn generate_system_instruction(_config: &SystemPromptConfig) -> Content {
203    // OPTIMIZATION: default_system_prompt() is &'static str, use directly
204    let instruction = default_system_prompt().to_string();
205
206    // Apply output style if possible (using current directory as project root)
207    if let Ok(current_dir) = env::current_dir() {
208        let styled_instruction = apply_output_style(instruction, None, &current_dir).await;
209        Content::system_text(styled_instruction)
210    } else {
211        Content::system_text(instruction)
212    }
213}
214
215/// Read AGENTS.md file if present and extract agent guidelines
216pub async fn read_agent_guidelines(project_root: &Path) -> Option<String> {
217    let max_bytes = prompt_budget_constants::DEFAULT_MAX_BYTES;
218    match read_project_doc(project_root, max_bytes).await {
219        Ok(Some(bundle)) => Some(bundle.contents),
220        Ok(None) => None,
221        Err(err) => {
222            warn!("failed to load project documentation: {err:#}");
223            None
224        }
225    }
226}
227
228/// Compose the base system instruction plus compact tool/skill/environment addenda.
229pub async fn compose_system_instruction_text(
230    _project_root: &Path,
231    vtcode_config: Option<&crate::config::VTCodeConfig>,
232    prompt_context: Option<&PromptContext>,
233) -> String {
234    let prompt_mode = vtcode_config
235        .map(|c| c.agent.system_prompt_mode)
236        .unwrap_or(SystemPromptMode::Default);
237    let static_base_prompt = static_profile_prompt(prompt_mode);
238    let resolved_layers = resolve_system_prompt_layers(_project_root).await;
239    let base_prompt = apply_system_prompt_layers(static_base_prompt, &resolved_layers);
240
241    tracing::trace!(
242        mode = ?prompt_mode,
243        base_tokens_approx = base_prompt.len() / 4, // rough token estimate
244        "Selected system prompt mode"
245    );
246
247    let base_len = base_prompt.len();
248    let config_overhead = vtcode_config.map_or(0, |_| 1024);
249    let estimated_capacity = base_len + config_overhead + 1024;
250    let mut instruction = String::with_capacity(estimated_capacity);
251    instruction.push_str(&base_prompt);
252    if should_include_structured_reasoning(vtcode_config, prompt_mode) {
253        append_prompt_section(&mut instruction, STRUCTURED_REASONING_INSTRUCTIONS);
254    }
255
256    if let Some(ctx) = prompt_context {
257        let guidelines = generate_tool_guidelines(&ctx.available_tools, ctx.capability_level);
258        if !guidelines.is_empty() {
259            append_prompt_section(&mut instruction, guidelines.trim_start_matches('\n'));
260        }
261        if let Some(skills_section) = render_prompt_skills_section(&ctx.available_skill_metadata) {
262            append_prompt_section(&mut instruction, &skills_section);
263        }
264    }
265
266    if let Some(environment_section) = render_environment_addenda(vtcode_config, prompt_context) {
267        append_prompt_section(&mut instruction, &environment_section);
268    }
269
270    instruction
271}
272
273fn append_prompt_section(prompt: &mut String, section: &str) {
274    prompt.push_str("\n\n");
275    prompt.push_str(section);
276}
277
278fn static_profile_prompt(prompt_mode: SystemPromptMode) -> &'static str {
279    match prompt_mode {
280        SystemPromptMode::Default => DEFAULT_SYSTEM_PROMPT.get_or_init(|| {
281            build_profile_prompt(
282                &build_contract_prompt(DEFAULT_CONTRACT_LINES),
283                DEFAULT_OPERATING_PROFILE_DELTA,
284            )
285        }),
286        SystemPromptMode::Minimal => MINIMAL_SYSTEM_PROMPT.get_or_init(|| {
287            build_profile_prompt(
288                &build_contract_prompt(MINIMAL_CONTRACT_LINES),
289                MINIMAL_OPERATING_PROFILE_DELTA,
290            )
291        }),
292        SystemPromptMode::Lightweight => DEFAULT_LIGHTWEIGHT_PROMPT.get_or_init(|| {
293            build_profile_prompt(
294                &build_contract_prompt(DEFAULT_CONTRACT_LINES),
295                LIGHTWEIGHT_OPERATING_PROFILE_DELTA,
296            )
297        }),
298        SystemPromptMode::Specialized => DEFAULT_SPECIALIZED_PROMPT.get_or_init(|| {
299            build_profile_prompt(
300                &build_contract_prompt(DEFAULT_CONTRACT_LINES),
301                SPECIALIZED_OPERATING_PROFILE_DELTA,
302            )
303        }),
304    }
305}
306
307fn build_contract_prompt(contract_lines: &[&str]) -> String {
308    let lines_len = contract_lines.iter().map(|line| line.len()).sum::<usize>();
309    let mut prompt = String::with_capacity(
310        PROMPT_TITLE.len()
311            + PROMPT_INTRO.len()
312            + CONTRACT_HEADER.len()
313            + lines_len
314            + contract_lines.len() * 3
315            + 8,
316    );
317    prompt.push_str(PROMPT_TITLE);
318    prompt.push_str("\n\n");
319    prompt.push_str(PROMPT_INTRO);
320    prompt.push_str("\n\n");
321    prompt.push_str(CONTRACT_HEADER);
322    prompt.push_str("\n\n");
323
324    for line in contract_lines {
325        prompt.push_str("- ");
326        prompt.push_str(line);
327        prompt.push('\n');
328    }
329
330    if !contract_lines.is_empty() {
331        prompt.pop();
332    }
333    prompt
334}
335
336fn build_profile_prompt(base_prompt: &str, profile_delta: &str) -> String {
337    let mut prompt = String::with_capacity(base_prompt.len() + profile_delta.len() + 2);
338    prompt.push_str(base_prompt);
339    prompt.push_str("\n\n");
340    prompt.push_str(profile_delta);
341    prompt
342}
343
344fn render_environment_addenda(
345    vtcode_config: Option<&crate::config::VTCodeConfig>,
346    prompt_context: Option<&PromptContext>,
347) -> Option<String> {
348    let mut lines = Vec::new();
349
350    if let Some(ctx) = prompt_context
351        && !ctx.languages.is_empty()
352    {
353        lines.push(format!(
354            "- Languages: {}. Match structural-search `lang` when needed.",
355            ctx.languages.join(", ")
356        ));
357    }
358
359    if let Some(cfg) = vtcode_config {
360        if let Some(interaction_line) = render_interaction_addendum(cfg) {
361            lines.push(interaction_line);
362        }
363
364        if cfg.mcp.enabled {
365            lines.push("- Sources: prefer MCP before external fetches when available.".to_string());
366        }
367
368        if cfg.agent.include_temporal_context && !cfg.prompt_cache.cache_friendly_prompt_shaping {
369            lines.push(
370                generate_temporal_context(cfg.agent.temporal_context_use_utc)
371                    .trim()
372                    .replacen("Current date and time", "- Time", 1)
373                    .to_string(),
374            );
375        }
376
377        if cfg.agent.include_working_directory
378            && let Some(ctx) = prompt_context
379            && let Some(cwd) = &ctx.current_directory
380        {
381            lines.push(format!("- Working directory: {}", cwd.display()));
382        }
383    }
384
385    if lines.is_empty() {
386        None
387    } else {
388        Some(format!("## Environment\n{}", lines.join("\n")))
389    }
390}
391
392fn render_interaction_addendum(cfg: &crate::config::VTCodeConfig) -> Option<String> {
393    match (cfg.security.human_in_the_loop, cfg.chat.ask_questions.enabled) {
394        (true, true) => None,
395        (true, false) => Some(
396            "- Interaction: approval may gate sensitive actions; no `request_user_input`, so make reasonable assumptions unless Planning workflow needs follow-up.".to_string(),
397        ),
398        (false, true) => Some(
399            "- Interaction: approval reduced by config; use `request_user_input` for material blockers.".to_string(),
400        ),
401        (false, false) => Some(
402            "- Interaction: approval reduced by config; no `request_user_input`, so make reasonable assumptions unless Planning workflow needs follow-up.".to_string(),
403        ),
404    }
405}
406
407fn should_include_structured_reasoning(
408    vtcode_config: Option<&crate::config::VTCodeConfig>,
409    mode: SystemPromptMode,
410) -> bool {
411    if let Some(cfg) = vtcode_config {
412        return cfg.agent.should_include_structured_reasoning_tags();
413    }
414
415    // Backward-compatible fallback when no config is available.
416    matches!(mode, SystemPromptMode::Specialized)
417}
418
419/// Generate the stable base system instruction with configuration-aware sections.
420///
421/// Note: This function maintains backward compatibility by not accepting prompt_context.
422/// For enhanced prompts with dynamic guidelines, call `compose_system_instruction_text` directly.
423pub async fn generate_system_instruction_with_config(
424    _config: &SystemPromptConfig,
425    project_root: &Path,
426    vtcode_config: Option<&crate::config::VTCodeConfig>,
427) -> Content {
428    let cache_key = cache_key(project_root, vtcode_config, None);
429    let instruction = match PROMPT_CACHE.get(&cache_key) {
430        Some(cached) => cached,
431        None => {
432            let built = compose_system_instruction_text(project_root, vtcode_config, None).await;
433            PROMPT_CACHE.insert(cache_key, built.clone());
434            built
435        }
436    };
437
438    // Apply output style if configured
439    let styled_instruction = apply_output_style(instruction, vtcode_config, project_root).await;
440    Content::system_text(styled_instruction)
441}
442
443/// Generate the stable base system instruction without workspace configuration.
444pub async fn generate_system_instruction_with_guidelines(
445    _config: &SystemPromptConfig,
446    project_root: &Path,
447) -> Content {
448    let cache_key = cache_key(project_root, None, None);
449    let instruction = match PROMPT_CACHE.get(&cache_key) {
450        Some(cached) => cached,
451        None => {
452            let built = compose_system_instruction_text(project_root, None, None).await;
453            PROMPT_CACHE.insert(cache_key, built.clone());
454            built
455        }
456    };
457    // Apply output style if configured
458    let styled_instruction = apply_output_style(instruction, None, project_root).await;
459    Content::system_text(styled_instruction)
460}
461
462/// Apply output style to a generated system instruction
463pub async fn apply_output_style(
464    instruction: String,
465    vtcode_config: Option<&crate::config::VTCodeConfig>,
466    project_root: &Path,
467) -> String {
468    if let Some(config) = vtcode_config {
469        let output_style_applier = OutputStyleApplier::new();
470        if let Err(e) = output_style_applier
471            .load_styles_from_config(config, project_root)
472            .await
473        {
474            tracing::warn!("Failed to load output styles: {}", e);
475            instruction // Return original if loading fails
476        } else {
477            output_style_applier
478                .apply_style(&config.output_style.active_style, &instruction, config)
479                .await
480        }
481    } else {
482        instruction // Return original if no config
483    }
484}
485
486/// Build a cache key for the system prompt.
487///
488/// `catalog_epoch` is the tool-catalog version at the time of the request. When
489/// the tool set changes (e.g. planning workflow is toggled, MCP tools are refreshed), the
490/// epoch advances and the old cached prompt is superseded rather than served stale.
491/// Pass `None` to get the same behaviour as before epoch tracking was introduced.
492fn cache_key(
493    project_root: &Path,
494    vtcode_config: Option<&crate::config::VTCodeConfig>,
495    catalog_epoch: Option<u64>,
496) -> String {
497    use std::collections::hash_map::DefaultHasher;
498    use std::hash::{Hash, Hasher};
499
500    let mut hasher = DefaultHasher::new();
501
502    // Core key: project root
503    project_root.hash(&mut hasher);
504
505    if let Some(cfg) = vtcode_config {
506        // Config fields that affect prompt generation
507        cfg.agent.include_working_directory.hash(&mut hasher);
508        cfg.agent.include_temporal_context.hash(&mut hasher);
509        cfg.prompt_cache
510            .cache_friendly_prompt_shaping
511            .hash(&mut hasher);
512        cfg.agent
513            .include_structured_reasoning_tags
514            .hash(&mut hasher);
515        // Use discriminant since SystemPromptMode doesn't derive Hash
516        std::mem::discriminant(&cfg.agent.system_prompt_mode).hash(&mut hasher);
517    } else {
518        "default".hash(&mut hasher);
519    }
520
521    // Invalidate the cached prompt when the tool catalog changes (planning workflow toggle,
522    // MCP refresh, permission grant/revoke).
523    catalog_epoch.unwrap_or(0).hash(&mut hasher);
524
525    format!("sys_prompt:{:016x}", hasher.finish())
526}
527
528/// Generate a minimal system instruction (pi-inspired, <1K tokens)
529pub fn generate_minimal_instruction() -> Content {
530    Content::system_text(minimal_instruction_text())
531}
532
533/// Generate a lightweight system instruction for simple operations
534pub fn generate_lightweight_instruction() -> Content {
535    Content::system_text(lightweight_instruction_text())
536}
537
538/// Generate a specialized system instruction for advanced operations
539pub fn generate_specialized_instruction() -> Content {
540    Content::system_text(specialized_instruction_text())
541}
542
543#[cfg(test)]
544mod tests {
545    use super::*;
546    use crate::config::VTCodeConfig;
547    use crate::config::types::SystemPromptMode;
548    use std::path::PathBuf;
549
550    #[tokio::test]
551    async fn test_minimal_mode_selection() {
552        let mut config = VTCodeConfig::default();
553        config.agent.system_prompt_mode = SystemPromptMode::Minimal;
554        // Disable enhancements for base prompt size testing
555        config.agent.include_temporal_context = false;
556        config.agent.include_working_directory = false;
557        config.agent.instruction_max_bytes = 0;
558
559        let result =
560            compose_system_instruction_text(&PathBuf::from("."), Some(&config), None).await;
561
562        // Minimal prompt should remain compact and deterministic without AGENTS.md injection
563        assert!(
564            result.len() < 2200,
565            "Minimal mode should produce <2.2K chars (was {} chars)",
566            result.len()
567        );
568        assert!(
569            result.contains("VT Code") || result.contains("VT Code"),
570            "Should contain VT Code identifier"
571        );
572    }
573
574    #[tokio::test]
575    async fn test_default_prompt_selection() {
576        let mut config = VTCodeConfig::default();
577        config.agent.system_prompt_mode = SystemPromptMode::Default;
578        // Disable enhancements for base prompt size testing
579        config.agent.include_temporal_context = false;
580        config.agent.include_working_directory = false;
581        config.agent.instruction_max_bytes = 0;
582
583        let result =
584            compose_system_instruction_text(&PathBuf::from("."), Some(&config), None).await;
585
586        assert!(
587            result.len() <= 1700,
588            "Default mode should stay sparse (<=1.7K chars, was {} chars)",
589            result.len()
590        );
591        assert!(result.contains("task_tracker"));
592        assert!(result.contains("@file"));
593        assert!(result.contains("Planning workflow"));
594    }
595
596    #[tokio::test]
597    async fn test_lightweight_mode_selection() {
598        let mut config = VTCodeConfig::default();
599        config.agent.system_prompt_mode = SystemPromptMode::Lightweight;
600        // Disable enhancements for base prompt size testing
601        config.agent.include_temporal_context = false;
602        config.agent.include_working_directory = false;
603        config.agent.instruction_max_bytes = 0;
604
605        let result =
606            compose_system_instruction_text(&PathBuf::from("."), Some(&config), None).await;
607
608        assert!(result.len() > 100, "Lightweight should be >100 chars");
609        assert!(
610            result.len() < 1550,
611            "Lightweight should be compact (<1.55K chars, was {} chars)",
612            result.len()
613        );
614        assert!(result.contains("task_tracker"));
615        assert!(result.contains("@file"));
616        assert!(result.contains("Act and verify in one thread"));
617    }
618
619    #[tokio::test]
620    async fn test_lightweight_mode_skips_structured_reasoning_by_default() {
621        let mut config = VTCodeConfig::default();
622        config.agent.system_prompt_mode = SystemPromptMode::Lightweight;
623        config.agent.include_temporal_context = false;
624        config.agent.include_working_directory = false;
625        config.agent.instruction_max_bytes = 0;
626        config.agent.include_structured_reasoning_tags = None;
627
628        let result =
629            compose_system_instruction_text(&PathBuf::from("."), Some(&config), None).await;
630
631        assert!(
632            !result.contains("## Structured Reasoning"),
633            "Lightweight mode should omit structured reasoning by default"
634        );
635    }
636
637    #[tokio::test]
638    async fn test_lightweight_mode_allows_explicit_structured_reasoning() {
639        let mut config = VTCodeConfig::default();
640        config.agent.system_prompt_mode = SystemPromptMode::Lightweight;
641        config.agent.include_temporal_context = false;
642        config.agent.include_working_directory = false;
643        config.agent.instruction_max_bytes = 0;
644        config.agent.include_structured_reasoning_tags = Some(true);
645
646        let result =
647            compose_system_instruction_text(&PathBuf::from("."), Some(&config), None).await;
648
649        assert!(
650            result.contains("## Structured Reasoning"),
651            "Lightweight mode should include structured reasoning when explicitly enabled"
652        );
653    }
654
655    #[tokio::test]
656    async fn test_default_prompt_omits_structured_reasoning_by_default() {
657        let mut config = VTCodeConfig::default();
658        config.agent.system_prompt_mode = SystemPromptMode::Default;
659        config.agent.include_temporal_context = false;
660        config.agent.include_working_directory = false;
661        config.agent.instruction_max_bytes = 0;
662        config.agent.include_structured_reasoning_tags = None;
663
664        let result =
665            compose_system_instruction_text(&PathBuf::from("."), Some(&config), None).await;
666
667        assert!(
668            !result.contains("## Structured Reasoning"),
669            "Default mode should omit structured reasoning by default"
670        );
671    }
672
673    #[tokio::test]
674    async fn test_specialized_mode_selection() {
675        let mut config = VTCodeConfig::default();
676        config.agent.system_prompt_mode = SystemPromptMode::Specialized;
677        // Disable enhancements for base prompt size testing
678        config.agent.include_temporal_context = false;
679        config.agent.include_working_directory = false;
680        config.agent.instruction_max_bytes = 0;
681
682        let result =
683            compose_system_instruction_text(&PathBuf::from("."), Some(&config), None).await;
684
685        assert!(
686            result.len() <= 2050,
687            "Specialized should stay sparse (<=2.05K chars, was {} chars)",
688            result.len()
689        );
690        assert!(result.contains("task_tracker"));
691        assert!(result.contains("<proposed_plan>"));
692        assert!(result.contains("ARCHITECTURAL_INVARIANTS"));
693    }
694
695    #[test]
696    fn test_prompt_mode_enum_parsing() {
697        assert_eq!(
698            SystemPromptMode::parse("minimal"),
699            Some(SystemPromptMode::Minimal)
700        );
701        assert_eq!(
702            SystemPromptMode::parse("LIGHTWEIGHT"),
703            Some(SystemPromptMode::Lightweight)
704        );
705        assert_eq!(
706            SystemPromptMode::parse("Default"),
707            Some(SystemPromptMode::Default)
708        );
709        assert_eq!(
710            SystemPromptMode::parse("specialized"),
711            Some(SystemPromptMode::Specialized)
712        );
713        assert_eq!(SystemPromptMode::parse("invalid"), None);
714    }
715
716    #[test]
717    fn test_minimal_prompt_token_count() {
718        // Rough estimate: 1 token ≈ 4 characters
719        let approx_tokens = minimal_system_prompt().len() / 4;
720        assert!(
721            approx_tokens < 220,
722            "Minimal prompt should stay compact, got ~{}",
723            approx_tokens
724        );
725    }
726
727    #[test]
728    fn test_default_prompt_token_count() {
729        let approx_tokens = default_system_prompt().len() / 4;
730        assert!(
731            approx_tokens < 420,
732            "Default prompt should stay compact, got ~{}",
733            approx_tokens
734        );
735    }
736
737    #[tokio::test]
738    async fn test_default_live_prompt_budget_with_instruction_summary() {
739        use crate::project_doc::build_instruction_appendix_with_context;
740
741        let workspace = tempfile::TempDir::new().expect("workspace");
742        std::fs::write(workspace.path().join(".git"), "gitdir: /tmp/git").expect("git marker");
743        std::fs::write(
744            workspace.path().join("AGENTS.md"),
745            "- run ./scripts/check.sh\n- avoid adding to vtcode-core\n- use Conventional Commits\n- start with docs/ARCHITECTURE.md\n",
746        )
747        .expect("write agents");
748        std::fs::create_dir_all(workspace.path().join(".vtcode/rules")).expect("rules dir");
749        std::fs::write(
750            workspace.path().join(".vtcode/rules/rust.md"),
751            "---\npaths:\n  - \"**/*.rs\"\n---\n# Rust\n- keep changes surgical\n",
752        )
753        .expect("write rust rule");
754
755        let mut config = VTCodeConfig::default();
756        config.agent.include_temporal_context = false;
757        config.agent.include_working_directory = false;
758        let base = compose_system_instruction_text(workspace.path(), Some(&config), None).await;
759        let appendix = build_instruction_appendix_with_context(
760            &config.agent,
761            workspace.path(),
762            &[workspace.path().join("src/lib.rs")],
763        )
764        .await
765        .expect("instruction appendix");
766        let prompt = format!("{base}\n\n# INSTRUCTIONS\n{appendix}");
767        let approx_tokens = prompt.len() / 4;
768
769        assert!(prompt.contains("### Instruction map"));
770        assert!(prompt.contains("### On-demand loading"));
771        assert!(approx_tokens <= 1100, "got ~{} tokens", approx_tokens);
772    }
773
774    #[tokio::test]
775    async fn test_generated_prompts_use_task_tracker_not_update_plan() {
776        let project_root = PathBuf::from(".");
777
778        for (mode_name, mode) in [
779            ("default", SystemPromptMode::Default),
780            ("minimal", SystemPromptMode::Minimal),
781            ("specialized", SystemPromptMode::Specialized),
782        ] {
783            let mut config = VTCodeConfig::default();
784            config.agent.system_prompt_mode = mode;
785            config.agent.include_temporal_context = false;
786            config.agent.include_working_directory = false;
787            config.agent.instruction_max_bytes = 0;
788
789            let result = compose_system_instruction_text(&project_root, Some(&config), None).await;
790
791            assert!(
792                result.contains("task_tracker"),
793                "{mode_name} prompt should reference task_tracker"
794            );
795            assert!(
796                !result.contains("update_plan"),
797                "{mode_name} prompt should not reference deprecated update_plan"
798            );
799        }
800    }
801
802    #[tokio::test]
803    async fn test_default_and_specialized_prompts_drop_rigid_summary_template() {
804        let project_root = PathBuf::from(".");
805
806        for (mode_name, mode) in [
807            ("default", SystemPromptMode::Default),
808            ("specialized", SystemPromptMode::Specialized),
809        ] {
810            let mut config = VTCodeConfig::default();
811            config.agent.system_prompt_mode = mode;
812            config.agent.include_temporal_context = false;
813            config.agent.include_working_directory = false;
814            config.agent.instruction_max_bytes = 0;
815
816            let result = compose_system_instruction_text(&project_root, Some(&config), None).await;
817
818            assert!(
819                !result.contains("References\n"),
820                "{mode_name} prompt should not force a References section"
821            );
822            assert!(
823                !result.contains("Next action"),
824                "{mode_name} prompt should not force a Next action section"
825            );
826            assert!(
827                !result.contains("Scope checkpoint"),
828                "{mode_name} prompt should not require the old plan blueprint bullets"
829            );
830        }
831    }
832
833    #[tokio::test]
834    async fn test_generated_prompts_keep_sparse_execution_contract() {
835        let project_root = PathBuf::from(".");
836
837        for (mode_name, mode) in [
838            ("default", SystemPromptMode::Default),
839            ("minimal", SystemPromptMode::Minimal),
840            ("lightweight", SystemPromptMode::Lightweight),
841            ("specialized", SystemPromptMode::Specialized),
842        ] {
843            let mut config = VTCodeConfig::default();
844            config.agent.system_prompt_mode = mode;
845            config.agent.include_temporal_context = false;
846            config.agent.include_working_directory = false;
847            config.agent.instruction_max_bytes = 0;
848
849            let result = compose_system_instruction_text(&project_root, Some(&config), None).await;
850            let normalized = result.to_ascii_lowercase();
851
852            assert!(
853                normalized.contains("compact") || normalized.contains("concise"),
854                "{mode_name} prompt should keep output guidance compact"
855            );
856            assert!(
857                normalized.contains("low-risk") || normalized.contains("reversible"),
858                "{mode_name} prompt should include follow-through guidance"
859            );
860            assert!(
861                normalized.contains("verify") || normalized.contains("validation"),
862                "{mode_name} prompt should include verification guidance"
863            );
864            assert!(
865                normalized.contains("do not guess"),
866                "{mode_name} prompt should gate missing context"
867            );
868            assert!(
869                normalized.contains("unblocked portion")
870                    || normalized.contains("unblocked slices")
871                    || normalized.contains("answerable without a missing detail"),
872                "{mode_name} prompt should require partial progress before clarification"
873            );
874            assert!(
875                normalized.contains("retrieved sources")
876                    || normalized.contains("retrieved evidence"),
877                "{mode_name} prompt should include grounding/citation guidance"
878            );
879            assert!(
880                !result.contains('ƒ'),
881                "{mode_name} prompt should not contain stray prompt characters"
882            );
883        }
884    }
885
886    #[test]
887    fn test_prompt_text_avoids_hardcoded_loop_thresholds() {
888        let specialized_prompt = specialized_instruction_text();
889        assert!(!default_system_prompt().contains("stuck twice"));
890        assert!(!minimal_system_prompt().contains("stuck twice"));
891        assert!(!specialized_prompt.contains("stuck twice"));
892        assert!(!specialized_prompt.contains("10+ calls without progress"));
893        assert!(!specialized_prompt.contains("Same tool+params twice"));
894    }
895
896    #[test]
897    fn test_harness_awareness_in_prompts() {
898        assert!(
899            default_system_prompt().contains("AGENTS.md"),
900            "Default prompt should reference AGENTS.md as map"
901        );
902        assert!(
903            specialized_instruction_text().contains("ARCHITECTURAL_INVARIANTS"),
904            "Specialized prompt should reference architectural invariants"
905        );
906        assert!(
907            minimal_system_prompt().contains("AGENTS.md"),
908            "Minimal prompt should still reference AGENTS.md"
909        );
910    }
911
912    #[test]
913    fn test_prompts_reject_guessing_when_context_is_missing() {
914        assert!(
915            default_system_prompt().contains("do not guess"),
916            "Default prompt should reject guessing"
917        );
918        assert!(
919            specialized_instruction_text().contains("do not guess"),
920            "Specialized prompt should reject guessing"
921        );
922        assert!(
923            minimal_system_prompt().contains("do not guess"),
924            "Minimal prompt should still reject guessing"
925        );
926    }
927
928    #[test]
929    fn test_prompts_include_compaction_preservation_contract() {
930        assert!(
931            default_system_prompt().contains("touched files"),
932            "Default prompt should preserve touched files across compaction"
933        );
934        assert!(
935            default_system_prompt().contains("decisions across compaction"),
936            "Default prompt should preserve decision rationale across compaction"
937        );
938        assert!(
939            default_system_prompt().contains("tracker state"),
940            "Default prompt should preserve tracker state across compaction"
941        );
942        assert!(
943            default_system_prompt().contains("verification status"),
944            "Default prompt should preserve verification status across compaction"
945        );
946        assert!(
947            minimal_system_prompt().contains("touched files"),
948            "Minimal prompt should preserve touched files across compaction"
949        );
950    }
951
952    #[test]
953    fn test_default_prompt_stays_lean_but_complete() {
954        let prompt = default_system_prompt();
955
956        assert!(
957            prompt.contains("## Contract"),
958            "Default prompt should include the lean contract section"
959        );
960        assert!(
961            prompt.contains("Keep outputs concise"),
962            "Default prompt should clamp output shape"
963        );
964        assert!(
965            prompt.contains("Verify changes yourself"),
966            "Default prompt should require verification before finalizing"
967        );
968        assert!(
969            prompt.contains("Keep user updates brief and high-signal"),
970            "Default prompt should constrain progress updates"
971        );
972    }
973
974    #[test]
975    fn test_all_prompt_modes_treat_completion_as_checkpoint_not_proof() {
976        for (mode_name, prompt) in [
977            ("default", default_system_prompt()),
978            ("minimal", minimal_system_prompt()),
979            ("lightweight", default_lightweight_prompt()),
980            ("specialized", specialized_instruction_text().as_str()),
981        ] {
982            assert!(
983                prompt.contains("completion language as a checkpoint")
984                    || prompt.contains("Verify changes yourself")
985                    || prompt.contains("verification"),
986                "{mode_name} prompt should include verification guidance"
987            );
988        }
989    }
990
991    #[test]
992    fn test_prompts_encode_explicit_delegation_contract() {
993        let prompt = default_system_prompt();
994
995        assert!(
996            prompt.contains("Keep control on the main thread"),
997            "Default prompt should keep control on the main thread"
998        );
999        assert!(
1000            prompt.contains("Delegate bounded, independent work"),
1001            "Default prompt should restrict delegation to bounded independent work"
1002        );
1003        assert!(
1004            minimal_system_prompt().contains("Keep delegation bounded and explicit"),
1005            "Minimal prompt should preserve the delegation contract"
1006        );
1007    }
1008
1009    #[test]
1010    fn test_default_prompt_includes_grounding_and_action_bias() {
1011        let prompt = default_system_prompt();
1012        assert!(
1013            prompt.contains("Never speculate about code you have not opened"),
1014            "Default prompt should include grounding guidance"
1015        );
1016        assert!(
1017            prompt.contains("Make only requested changes"),
1018            "Default prompt should include anti-overengineering guidance"
1019        );
1020        assert!(
1021            prompt.contains("Implement by default"),
1022            "Default prompt should include action bias"
1023        );
1024    }
1025
1026    #[test]
1027    fn test_default_prompt_omits_accuracy_addendum() {
1028        let runtime = tokio::runtime::Runtime::new().expect("runtime");
1029        let config = VTCodeConfig::default();
1030        let prompt = runtime.block_on(compose_system_instruction_text(
1031            &PathBuf::from("."),
1032            Some(&config),
1033            None,
1034        ));
1035
1036        assert!(
1037            !prompt.contains("## Accuracy Optimization"),
1038            "Runtime prompt should omit the accuracy optimization section"
1039        );
1040        assert!(
1041            prompt.contains("do not guess"),
1042            "Prompt should still preserve the uncertainty guardrail"
1043        );
1044    }
1045
1046    #[test]
1047    fn test_openai_gpt55_contract_addendum_is_specific() {
1048        let addendum = openai_gpt55_contract_addendum();
1049
1050        assert!(addendum.contains(OPENAI_GPT55_CONTRACT_HEADER));
1051        assert!(addendum.contains("outcome, constraints, evidence, and output shape"));
1052        assert!(addendum.contains("smallest missing detail"));
1053        assert!(addendum.contains("brief progress update"));
1054        assert!(addendum.contains("minimum evidence sufficient"));
1055        assert!(!default_system_prompt().contains(OPENAI_GPT55_CONTRACT_HEADER));
1056    }
1057
1058    #[test]
1059    fn test_fable5_contract_addendum_is_specific() {
1060        let addendum = fable_5_contract_addendum();
1061
1062        assert!(addendum.contains(FABLE_5_CONTRACT_HEADER));
1063        assert!(addendum.contains("enough information to act, act"));
1064        assert!(addendum.contains("audit each claim against a tool result"));
1065        assert!(addendum.contains("deliverable is your assessment"));
1066        assert!(addendum.contains("ample context remaining"));
1067        assert!(addendum.contains("Delegate independent subtasks"));
1068        assert!(addendum.contains("Lead with the outcome"));
1069        assert!(!default_system_prompt().contains(FABLE_5_CONTRACT_HEADER));
1070    }
1071
1072    #[tokio::test]
1073    async fn test_generated_prompts_keep_operating_profiles_bounded() {
1074        let project_root = PathBuf::from(".");
1075
1076        for (mode_name, mode) in [
1077            ("default", SystemPromptMode::Default),
1078            ("minimal", SystemPromptMode::Minimal),
1079            ("lightweight", SystemPromptMode::Lightweight),
1080            ("specialized", SystemPromptMode::Specialized),
1081        ] {
1082            let mut config = VTCodeConfig::default();
1083            config.agent.system_prompt_mode = mode;
1084            config.agent.include_temporal_context = false;
1085            config.agent.include_working_directory = false;
1086            config.agent.instruction_max_bytes = 0;
1087
1088            let result = compose_system_instruction_text(&project_root, Some(&config), None).await;
1089
1090            assert!(
1091                result.contains("## Contract"),
1092                "{mode_name} prompt should reuse the canonical base prompt"
1093            );
1094            assert!(
1095                result.matches("## Operating Profile").count() == 1,
1096                "{mode_name} prompt should add only one operating profile"
1097            );
1098        }
1099    }
1100
1101    #[test]
1102    fn test_search_guidance_prefers_structural_and_rg() {
1103        let guidelines = generate_tool_guidelines(
1104            &["unified_search".to_string(), "unified_exec".to_string()],
1105            None,
1106        );
1107        assert!(
1108            guidelines.contains("Prefer search over shell"),
1109            "Tool guidance should prefer search over shell exploration"
1110        );
1111        assert!(
1112            guidelines.contains("git diff -- <path>"),
1113            "Tool guidance should keep diff guidance explicit"
1114        );
1115    }
1116
1117    // ENHANCEMENT TESTS
1118
1119    #[tokio::test]
1120    async fn test_dynamic_guidelines_read_only() {
1121        use crate::config::types::CapabilityLevel;
1122
1123        let mut config = VTCodeConfig::default();
1124        config.agent.system_prompt_mode = SystemPromptMode::Default;
1125
1126        let mut ctx = PromptContext::default();
1127        ctx.add_tool("unified_search".to_string());
1128        ctx.capability_level = Some(CapabilityLevel::FileReading);
1129
1130        let result =
1131            compose_system_instruction_text(&PathBuf::from("."), Some(&config), Some(&ctx)).await;
1132
1133        assert!(
1134            result.contains("Capabilities: read-only"),
1135            "Should detect read-only capabilities when no edit/write/exec tools available"
1136        );
1137        assert!(
1138            result.contains("do not modify files"),
1139            "Should explain read-only constraints"
1140        );
1141    }
1142
1143    #[tokio::test]
1144    async fn test_dynamic_guidelines_tool_preferences() {
1145        let config = VTCodeConfig::default();
1146
1147        let mut ctx = PromptContext::default();
1148        ctx.add_tool("unified_exec".to_string());
1149        ctx.add_tool("unified_search".to_string());
1150        ctx.add_tool("unified_file".to_string());
1151
1152        let result =
1153            compose_system_instruction_text(&PathBuf::from("."), Some(&config), Some(&ctx)).await;
1154
1155        assert!(
1156            result.contains("unified_search") || result.contains("unified_file"),
1157            "Should suggest canonical search/file tools"
1158        );
1159    }
1160
1161    #[tokio::test]
1162    async fn test_live_prompt_renders_workspace_language_hints() {
1163        let workspace = tempfile::TempDir::new().expect("workspace tempdir");
1164        std::fs::create_dir_all(workspace.path().join("src")).expect("create src");
1165        std::fs::create_dir_all(workspace.path().join("web")).expect("create web");
1166        std::fs::write(workspace.path().join("src/lib.rs"), "fn alpha() {}\n").expect("write rust");
1167        std::fs::write(workspace.path().join("web/app.ts"), "const app = 1;\n").expect("write ts");
1168
1169        let config = VTCodeConfig::default();
1170        let ctx = PromptContext::from_workspace_tools(workspace.path(), ["unified_search"]);
1171        let result =
1172            compose_system_instruction_text(workspace.path(), Some(&config), Some(&ctx)).await;
1173
1174        assert!(result.contains("## Environment"));
1175        assert!(result.contains("Rust, TypeScript"));
1176        assert!(result.contains("structural-search `lang`"));
1177    }
1178
1179    #[tokio::test]
1180    async fn test_live_prompt_omits_workspace_language_hints_without_languages() {
1181        let workspace = tempfile::TempDir::new().expect("workspace tempdir");
1182        let config = VTCodeConfig::default();
1183        let ctx = PromptContext::from_workspace_tools(workspace.path(), ["unified_search"]);
1184        let result =
1185            compose_system_instruction_text(workspace.path(), Some(&config), Some(&ctx)).await;
1186
1187        assert!(!result.contains("Languages:"));
1188    }
1189
1190    #[tokio::test]
1191    async fn test_live_prompt_omits_project_docs_and_user_instructions_from_base_prompt() {
1192        let workspace = tempfile::TempDir::new().expect("workspace tempdir");
1193        std::fs::write(
1194            workspace.path().join("AGENTS.md"),
1195            "- Root summary\n\nFollow the root guidance.\n",
1196        )
1197        .expect("write agents");
1198
1199        let mut config = VTCodeConfig::default();
1200        config.agent.user_instructions = Some("keep responses terse".to_string());
1201        config.agent.include_temporal_context = false;
1202        config.agent.include_working_directory = false;
1203        config.agent.instruction_max_bytes = 4096;
1204
1205        let result = compose_system_instruction_text(workspace.path(), Some(&config), None).await;
1206
1207        assert!(!result.contains("## AGENTS.MD INSTRUCTION HIERARCHY"));
1208        assert!(!result.contains("### Instruction map"));
1209        assert!(!result.contains("### Key points"));
1210        assert!(!result.contains("keep responses terse"));
1211        assert!(!result.contains("Root summary"));
1212        assert!(!result.contains("Follow the root guidance."));
1213    }
1214
1215    #[tokio::test]
1216    async fn test_workspace_prompt_resources_override_base_and_keep_dynamic_sections() {
1217        use crate::skills::model::{SkillMetadata, SkillScope};
1218
1219        let workspace = tempfile::TempDir::new().expect("workspace tempdir");
1220        let prompts_dir = workspace.path().join(".vtcode/prompts");
1221        std::fs::create_dir_all(&prompts_dir).expect("create prompts dir");
1222        std::fs::write(prompts_dir.join("system.md"), "# Workspace system base").expect("system");
1223        std::fs::write(
1224            prompts_dir.join("append-system.md"),
1225            "Workspace prompt appendix",
1226        )
1227        .expect("append");
1228
1229        let mut config = VTCodeConfig::default();
1230        config.agent.include_temporal_context = false;
1231        config.agent.include_working_directory = true;
1232
1233        let mut ctx = PromptContext::default();
1234        ctx.add_tool("unified_search".to_string());
1235        ctx.add_skill_metadata(SkillMetadata {
1236            name: "skill-creator".to_string(),
1237            description: "Create skills".to_string(),
1238            short_description: None,
1239            path: PathBuf::from("/tmp/skill-creator/SKILL.md"),
1240            scope: SkillScope::System,
1241            manifest: None,
1242        });
1243        ctx.set_current_directory(workspace.path().to_path_buf());
1244
1245        let result =
1246            compose_system_instruction_text(workspace.path(), Some(&config), Some(&ctx)).await;
1247
1248        assert!(result.starts_with("# Workspace system base"));
1249        assert!(result.contains("Workspace prompt appendix"));
1250        assert!(result.contains("## Active Tools"));
1251        assert!(result.contains("## Skills"));
1252        assert!(result.contains("## Environment"));
1253
1254        let appendix_pos = result
1255            .find("Workspace prompt appendix")
1256            .expect("append text");
1257        let tools_pos = result.find("## Active Tools").expect("tools section");
1258        let skills_pos = result.find("## Skills").expect("skills section");
1259        let env_pos = result.find("## Environment").expect("environment section");
1260
1261        assert!(appendix_pos < tools_pos);
1262        assert!(tools_pos < skills_pos);
1263        assert!(skills_pos < env_pos);
1264    }
1265
1266    #[tokio::test]
1267    async fn test_temporal_context_inclusion() {
1268        let mut config = VTCodeConfig::default();
1269        config.agent.include_temporal_context = true;
1270        config.prompt_cache.cache_friendly_prompt_shaping = false;
1271        config.agent.temporal_context_use_utc = false; // Local time
1272
1273        let result =
1274            compose_system_instruction_text(&PathBuf::from("."), Some(&config), None).await;
1275
1276        assert!(
1277            result.contains("Time:"),
1278            "Should include temporal context when enabled"
1279        );
1280        let env_pos = result.find("## Environment");
1281        let temporal_pos = result.find("Time:");
1282        if let (Some(t), Some(e)) = (temporal_pos, env_pos) {
1283            assert!(
1284                t > e,
1285                "Temporal context should appear inside the environment section"
1286            );
1287        }
1288    }
1289
1290    #[tokio::test]
1291    async fn test_temporal_context_utc_format() {
1292        let mut config = VTCodeConfig::default();
1293        config.agent.include_temporal_context = true;
1294        config.prompt_cache.cache_friendly_prompt_shaping = false;
1295        config.agent.temporal_context_use_utc = true; // UTC format
1296
1297        let result =
1298            compose_system_instruction_text(&PathBuf::from("."), Some(&config), None).await;
1299
1300        assert!(
1301            result.contains("UTC"),
1302            "Should indicate UTC when temporal_context_use_utc is true"
1303        );
1304        assert!(
1305            result.contains("T") && result.contains("Z"),
1306            "Should use RFC3339 format for UTC (contains T and Z)"
1307        );
1308    }
1309
1310    #[tokio::test]
1311    async fn test_temporal_context_disabled() {
1312        let mut config = VTCodeConfig::default();
1313        config.agent.include_temporal_context = false;
1314
1315        let result =
1316            compose_system_instruction_text(&PathBuf::from("."), Some(&config), None).await;
1317
1318        assert!(
1319            !result.contains("Time:"),
1320            "Should not include temporal context when disabled"
1321        );
1322    }
1323
1324    #[tokio::test]
1325    async fn test_cache_friendly_temporal_context_stays_out_of_base_prompt() {
1326        let mut config = VTCodeConfig::default();
1327        config.agent.include_temporal_context = true;
1328        config.prompt_cache.cache_friendly_prompt_shaping = true;
1329
1330        let result =
1331            compose_system_instruction_text(&PathBuf::from("."), Some(&config), None).await;
1332
1333        assert!(
1334            !result.contains("Time:"),
1335            "Stable system prompt should omit temporal context when cache-friendly shaping is enabled"
1336        );
1337    }
1338
1339    #[tokio::test]
1340    async fn test_configuration_awareness_stays_behavior_focused() {
1341        let mut config = VTCodeConfig::default();
1342        config.security.human_in_the_loop = true;
1343        config.chat.ask_questions.enabled = false;
1344        config.mcp.enabled = true;
1345        config.ide_context.enabled = true;
1346        config.ide_context.inject_into_prompt = true;
1347
1348        let result =
1349            compose_system_instruction_text(&PathBuf::from("."), Some(&config), None).await;
1350
1351        assert!(result.contains("## Environment"));
1352        assert!(result.contains("Interaction: approval may gate sensitive actions"));
1353        assert!(result.contains("request_user_input"));
1354        assert!(result.contains("Sources: prefer MCP"));
1355        assert!(!result.contains("PTY functionality"));
1356        assert!(!result.contains("Loop guards"));
1357        assert!(!result.contains(".vtcode/context/tool_outputs/"));
1358        assert!(!result.contains("IDE context:"));
1359    }
1360
1361    #[tokio::test]
1362    async fn test_configuration_awareness_mentions_reduced_approval_when_disabled() {
1363        let mut config = VTCodeConfig::default();
1364        config.security.human_in_the_loop = false;
1365
1366        let result =
1367            compose_system_instruction_text(&PathBuf::from("."), Some(&config), None).await;
1368
1369        assert!(result.contains("Interaction: approval reduced by config"));
1370    }
1371
1372    #[tokio::test]
1373    async fn test_default_environment_omits_default_interaction_guidance() {
1374        let config = VTCodeConfig::default();
1375
1376        let result =
1377            compose_system_instruction_text(&PathBuf::from("."), Some(&config), None).await;
1378
1379        assert!(
1380            !result.contains("Interaction:"),
1381            "Default-on interaction guidance should stay out of the prompt"
1382        );
1383    }
1384
1385    #[tokio::test]
1386    async fn test_working_directory_inclusion() {
1387        let mut config = VTCodeConfig::default();
1388        config.agent.include_working_directory = true;
1389
1390        let mut ctx = PromptContext::default();
1391        ctx.set_current_directory(PathBuf::from("/tmp/test"));
1392
1393        let result =
1394            compose_system_instruction_text(&PathBuf::from("."), Some(&config), Some(&ctx)).await;
1395
1396        assert!(
1397            result.contains("Working directory"),
1398            "Should include working directory label"
1399        );
1400        assert!(
1401            result.contains("/tmp/test"),
1402            "Should show actual directory path"
1403        );
1404        let wd_pos = result.find("Working directory");
1405        let env_pos = result.find("## Environment");
1406        if let (Some(w), Some(e)) = (wd_pos, env_pos) {
1407            assert!(
1408                w > e,
1409                "Working directory should appear inside the environment section"
1410            );
1411        }
1412    }
1413
1414    #[tokio::test]
1415    async fn test_working_directory_disabled() {
1416        let mut config = VTCodeConfig::default();
1417        config.agent.include_working_directory = false;
1418
1419        let mut ctx = PromptContext::default();
1420        ctx.set_current_directory(PathBuf::from("/tmp/test"));
1421
1422        let result =
1423            compose_system_instruction_text(&PathBuf::from("."), Some(&config), Some(&ctx)).await;
1424
1425        assert!(
1426            !result.contains("Working directory"),
1427            "Should not include working directory when disabled"
1428        );
1429    }
1430
1431    #[tokio::test]
1432    async fn test_backward_compatibility() {
1433        let config = VTCodeConfig::default();
1434
1435        // Old signature: no prompt context
1436        let result = compose_system_instruction_text(
1437            &PathBuf::from("."),
1438            Some(&config),
1439            None, // No context - backward compatible
1440        )
1441        .await;
1442
1443        // Should still work without new features
1444        assert!(result.len() > 600, "Should generate substantial prompt");
1445        assert!(
1446            result.contains("VT Code"),
1447            "Should contain base prompt content"
1448        );
1449        // Should not have dynamic guidelines without context
1450        assert!(
1451            !result.contains("## Active Tools"),
1452            "Should not have tool guidelines without prompt context"
1453        );
1454    }
1455
1456    #[tokio::test]
1457    async fn test_all_enhancements_combined() {
1458        use crate::skills::model::{SkillMetadata, SkillScope};
1459
1460        let mut config = VTCodeConfig::default();
1461        config.agent.include_temporal_context = true;
1462        config.agent.include_working_directory = true;
1463        config.prompt_cache.cache_friendly_prompt_shaping = false;
1464
1465        let mut ctx = PromptContext::default();
1466        ctx.add_tool("unified_file".to_string());
1467        ctx.add_tool("unified_search".to_string());
1468        ctx.infer_capability_level();
1469        ctx.set_current_directory(PathBuf::from("/workspace"));
1470        ctx.add_skill_metadata(SkillMetadata {
1471            name: "rust-skills".to_string(),
1472            description: "Rust coding guidance".to_string(),
1473            short_description: None,
1474            path: PathBuf::from("/tmp/rust-skills/SKILL.md"),
1475            scope: SkillScope::System,
1476            manifest: None,
1477        });
1478
1479        let result =
1480            compose_system_instruction_text(&PathBuf::from("."), Some(&config), Some(&ctx)).await;
1481
1482        // Verify all enhancements present
1483        assert!(
1484            result.contains("## Active Tools"),
1485            "Should have dynamic guidelines"
1486        );
1487        assert!(
1488            result.contains("## Skills"),
1489            "Should have lean skills routing"
1490        );
1491        assert!(
1492            result.contains("## Environment"),
1493            "Should have environment addenda"
1494        );
1495        assert!(result.contains("Time:"), "Should have temporal context");
1496        assert!(
1497            result.contains("Working directory"),
1498            "Should have working directory"
1499        );
1500        assert!(result.contains("/workspace"), "Should show workspace path");
1501
1502        // Verify specific guideline for this tool set
1503        assert!(
1504            result.contains("Read before edit"),
1505            "Should have read-before-edit guideline"
1506        );
1507    }
1508
1509    #[tokio::test]
1510    async fn test_prompt_layers_render_in_stable_order() {
1511        use crate::skills::model::{SkillMetadata, SkillScope};
1512
1513        let mut config = VTCodeConfig::default();
1514        config.agent.include_temporal_context = true;
1515        config.agent.include_working_directory = true;
1516
1517        let mut ctx = PromptContext::default();
1518        ctx.add_tool("unified_search".to_string());
1519        ctx.add_tool("unified_exec".to_string());
1520        ctx.add_skill_metadata(SkillMetadata {
1521            name: "skill-creator".to_string(),
1522            description: "Create skills".to_string(),
1523            short_description: None,
1524            path: PathBuf::from("/tmp/skill-creator/SKILL.md"),
1525            scope: SkillScope::System,
1526            manifest: None,
1527        });
1528        ctx.add_language("Rust".to_string());
1529        ctx.set_current_directory(PathBuf::from("/workspace"));
1530
1531        let result =
1532            compose_system_instruction_text(&PathBuf::from("."), Some(&config), Some(&ctx)).await;
1533
1534        let mode_pos = result
1535            .find("## Operating Profile")
1536            .expect("operating profile section");
1537        let tools_pos = result.find("## Active Tools").expect("tools section");
1538        let skills_pos = result.find("## Skills").expect("skills section");
1539        let env_pos = result.find("## Environment").expect("environment section");
1540
1541        assert!(
1542            mode_pos < tools_pos,
1543            "operating profile should precede tools"
1544        );
1545        assert!(tools_pos < skills_pos, "tools should precede skills");
1546        assert!(skills_pos < env_pos, "skills should precede environment");
1547    }
1548
1549    #[tokio::test]
1550    async fn test_skills_section_stays_lean_and_routing_focused() {
1551        use crate::skills::model::SkillScope;
1552        use crate::skills::types::SkillManifest;
1553
1554        let config = VTCodeConfig::default();
1555        let mut ctx = PromptContext::default();
1556        ctx.available_skill_metadata
1557            .push(crate::skills::model::SkillMetadata {
1558                name: "skill-creator".to_string(),
1559                description: "Create or update skills".to_string(),
1560                short_description: None,
1561                path: PathBuf::from("/tmp/skill-creator/SKILL.md"),
1562                scope: SkillScope::System,
1563                manifest: Some(
1564                    SkillManifest {
1565                        when_to_use: Some("Use when creating or updating a skill.".to_string()),
1566                        when_not_to_use: Some(
1567                            "Avoid for unrelated implementation work.".to_string(),
1568                        ),
1569                        ..SkillManifest::default()
1570                    }
1571                    .into(),
1572                ),
1573            });
1574
1575        let result =
1576            compose_system_instruction_text(&PathBuf::from("."), Some(&config), Some(&ctx)).await;
1577
1578        assert!(result.contains("## Skills"));
1579        assert!(result.contains("skill-creator: Create or update skills"));
1580        assert!(result.contains("Use a skill only when the user names it"));
1581        assert!(!result.contains("Discovery: Available skills are listed"));
1582        assert!(!result.contains("/tmp/skill-creator/SKILL.md"));
1583        assert!(!result.contains("use: Use when creating or updating a skill."));
1584        assert!(!result.contains("avoid: Avoid for unrelated implementation work."));
1585    }
1586
1587    #[test]
1588    fn test_static_prompts_have_no_placeholders() {
1589        let _minimal = generate_minimal_instruction();
1590        let _lightweight = generate_lightweight_instruction();
1591        let _specialized = generate_specialized_instruction();
1592
1593        let minimal_text = minimal_instruction_text();
1594        let lightweight_text = lightweight_instruction_text();
1595        let specialized_text = specialized_instruction_text();
1596
1597        assert!(
1598            !minimal_text.contains("__UNIFIED_TOOL_GUIDANCE__"),
1599            "Minimal prompt has uninterpolated placeholder"
1600        );
1601        assert!(
1602            !lightweight_text.contains("__UNIFIED_TOOL_GUIDANCE__"),
1603            "Lightweight prompt has uninterpolated placeholder"
1604        );
1605        assert!(
1606            !specialized_text.contains("__UNIFIED_TOOL_GUIDANCE__"),
1607            "Specialized prompt has uninterpolated placeholder"
1608        );
1609        assert!(
1610            !default_system_prompt().contains("__UNIFIED_TOOL_GUIDANCE__"),
1611            "Default prompt has uninterpolated placeholder"
1612        );
1613    }
1614}