refactor(tui): delete the dead prompt compatibility layer
Dead-code audit 2026-08-03 (P1 row "Prompt compatibility layer"),
verified against the v0.9.4 train with rg across the workspace — every
deleted item had zero production callers:
- locale preamble/closer override setters (8) and the authority-recap
override setter + validator: never called; the OnceLock cells and
effective_* readers stay, so composition falls back to the bundled
constants exactly as before
- set_static_prompt_composer_override + helper: never called; the
composer cell/read path (apply_static_prompt_composer,
effective_base_prompt_source) is unchanged
- SHELL_POLICY_DISABLED: unreferenced constant
- Personality::Playful + PLAYFUL_PERSONALITY overlay, and the dead
Personality::{from_settings, prompt} helpers: tone is folded into the
constitution preamble; only Calm ever shipped
- compose_prompt wrapper: test-only callers rewritten to the live
compose_prompt_with_approval_model_and_shell(.., "codewhale") path
- system_prompt_with_world_state / build_system_prompt: zero callers
- unused mode/approval constants YOLO_MODE, AUTO_APPROVAL,
SUGGEST_APPROVAL, NEVER_APPROVAL and legacy AGENT_PROMPT (plus their
content-guard tests); Yolo maps to AGENT_MODE in mode_doctrine and
approval policy no longer inlines prompt overlays
Preserved: BASE_PROMPT and the constitution (incl. today's
verify-then-stop amendment), mode/session/approval assembly,
environment/skills blocks, WorldState types, the config-dir
constitution override path (#3638), and CALM_PERSONALITY for the #2953
regression guard.
Tests: prompts:: 108 passed; execpolicy:: 11 passed; engine prompt
fixture/baseline tests pass; clippy --all-targets clean; cargo check
-p codewhale-tui clean.
This commit is contained in:
+44
-290
@@ -11,7 +11,7 @@
|
||||
//! single-file operation.
|
||||
|
||||
use crate::models::{SystemBlock, SystemPrompt};
|
||||
use crate::project_context::{ProjectContext, load_project_context_with_parents};
|
||||
use crate::project_context::load_project_context_with_parents;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{LazyLock, Mutex};
|
||||
|
||||
@@ -387,13 +387,13 @@ fn user_constitution_disabled_by_setup_state() -> bool {
|
||||
// prompt text in `text.rs` directly; the test suite below guards content
|
||||
// and ordering invariants (constitution structure and binding gates #4032,
|
||||
// byte-stable prefix ordering, prefix privacy #4632).
|
||||
pub use text::{
|
||||
AGENT_MODE, BASE_PROMPT, CALM_PERSONALITY, COMPACT_TEMPLATE, CORE_EXECUTION_PROFILE_PROMPT,
|
||||
GOAL_CONTINUATION_PROMPT, LANGUAGE_PROMPT, MEMORY_GUIDANCE, OPERATE_MODE, OUTPUT_PROMPT,
|
||||
PLAN_MODE, PLAYFUL_PERSONALITY,
|
||||
};
|
||||
#[cfg(test)]
|
||||
use text::{AGENT_PROMPT, AUTO_APPROVAL, NEVER_APPROVAL, SUGGEST_APPROVAL, YOLO_MODE};
|
||||
use text::CALM_PERSONALITY;
|
||||
pub use text::{
|
||||
AGENT_MODE, BASE_PROMPT, COMPACT_TEMPLATE, CORE_EXECUTION_PROFILE_PROMPT,
|
||||
GOAL_CONTINUATION_PROMPT, LANGUAGE_PROMPT, MEMORY_GUIDANCE, OPERATE_MODE, OUTPUT_PROMPT,
|
||||
PLAN_MODE,
|
||||
};
|
||||
|
||||
// ── Embedder prompt overrides ──
|
||||
// Let an embedder replace these compile-time prompt constants at startup,
|
||||
@@ -444,106 +444,6 @@ pub fn set_base_prompt_override(s: String) -> Result<(), String> {
|
||||
set_prompt_override(&BASE_PROMPT_OVERRIDE, s)
|
||||
}
|
||||
|
||||
/// Replace the Simplified-Chinese locale preamble (`## 语言要求`).
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
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 (`## 语言再次提醒`).
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
pub fn set_locale_closer_vi_override(s: String) -> Result<(), String> {
|
||||
set_prompt_override(&LOCALE_CLOSER_VI_OVERRIDE, s)
|
||||
}
|
||||
|
||||
/// Replace the trailing `## Authority Recap` block.
|
||||
///
|
||||
/// The recap must not restate or reorder ranks — precedence lives only in
|
||||
/// `BASE_PROMPT` § Whose word wins (#4777). Reject overrides that introduce
|
||||
/// numbered ranks or claim a different ordering.
|
||||
pub fn set_authority_recap_override(s: String) -> Result<(), String> {
|
||||
validate_authority_recap_override(&s)?;
|
||||
set_prompt_override(&AUTHORITY_RECAP_OVERRIDE, s)
|
||||
}
|
||||
|
||||
fn validate_authority_recap_override(s: &str) -> Result<(), String> {
|
||||
// Retired rank vocabulary and any restated ordering both re-introduce a
|
||||
// second authority ladder; refuse them. Precedence lives only in
|
||||
// BASE_PROMPT § Whose word wins (#4777).
|
||||
let lower = s.to_ascii_lowercase();
|
||||
for forbidden in [
|
||||
"tier ",
|
||||
"statute",
|
||||
"regulation",
|
||||
"local law",
|
||||
"article ",
|
||||
"outrank",
|
||||
"whose word wins", // override may *point* at the section only via the default; custom text that re-embeds the ladder is refused below
|
||||
] {
|
||||
// Allow the default pointer phrase "consult ### Whose word wins".
|
||||
if forbidden == "whose word wins" {
|
||||
continue;
|
||||
}
|
||||
if lower.contains(forbidden) {
|
||||
return Err(format!(
|
||||
"authority recap override must not restate ranks (found {forbidden:?}); \
|
||||
precedence is only in BASE_PROMPT § Whose word wins"
|
||||
));
|
||||
}
|
||||
}
|
||||
// A custom numbered 1..5 ladder is the classic reorder footgun.
|
||||
let has_numbered_ladder = (1..=5).all(|n| {
|
||||
lower.contains(&format!("\n{n}."))
|
||||
|| lower.contains(&format!("\n{n})"))
|
||||
|| lower.contains(&format!(" {n}. "))
|
||||
});
|
||||
if has_numbered_ladder {
|
||||
return Err(
|
||||
"authority recap override must not restate a numbered authority ladder; \
|
||||
precedence is only in BASE_PROMPT § Whose word wins"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Replace the byte-stable base/personality prompt segment for subsequent
|
||||
/// prompt composition. First call wins; later calls return the rejected
|
||||
/// composer so embedders can preserve ownership.
|
||||
pub fn set_static_prompt_composer_override(
|
||||
f: Box<StaticPromptComposer>,
|
||||
) -> Result<(), Box<StaticPromptComposer>> {
|
||||
set_static_prompt_composer(&STATIC_PROMPT_COMPOSER, f)
|
||||
}
|
||||
|
||||
// ── 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
|
||||
@@ -677,13 +577,6 @@ fn set_prompt_override(cell: &std::sync::OnceLock<String>, s: String) -> Result<
|
||||
cell.set(s)
|
||||
}
|
||||
|
||||
fn set_static_prompt_composer(
|
||||
cell: &std::sync::OnceLock<Box<StaticPromptComposer>>,
|
||||
f: Box<StaticPromptComposer>,
|
||||
) -> Result<(), Box<StaticPromptComposer>> {
|
||||
cell.set(f)
|
||||
}
|
||||
|
||||
fn effective_prompt_override<'a>(
|
||||
cell: &'a std::sync::OnceLock<String>,
|
||||
fallback: &'static str,
|
||||
@@ -983,46 +876,15 @@ dự án có là tiếng Anh, quá trình suy nghĩ của bạn cũng không đ
|
||||
tích lũy trong ngữ cảnh. Trừ khi người dùng yêu cầu rõ ràng việc chuyển đổi (ví dụ \"think in English\"), \
|
||||
hãy tiếp tục suy nghĩ và trả lời bằng tiếng Việt.";
|
||||
|
||||
/// Shell policy guidance for `allow_shell=false`. Referenced from the
|
||||
/// Runtime Policy Reference so the model can adapt without mutating the
|
||||
/// static system-prompt prefix (preserves DeepSeek prefix cache across
|
||||
/// shell-access toggles).
|
||||
pub const SHELL_POLICY_DISABLED: &str = "Shell tools unavailable. For mandatory-use items referencing \
|
||||
`exec_shell`, use `code_execution` (Python sandbox). For GitHub triage, use \
|
||||
`github_issue_context` / `github_pr_context` as primary route.";
|
||||
|
||||
// ── Personality selection ─────────────────────────────────────────────
|
||||
|
||||
/// Which personality overlay to apply.
|
||||
/// Which personality overlay to apply. Tone is folded into the constitutional
|
||||
/// preamble, so this is a compile-time marker carried through the static-prompt
|
||||
/// composer context rather than a separate overlay.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Personality {
|
||||
/// Cool, spatial, reserved — the default.
|
||||
/// Cool, spatial, reserved — the default and only shipped personality.
|
||||
Calm,
|
||||
/// Warm, energetic, playful — alternative for fun mode.
|
||||
Playful,
|
||||
}
|
||||
|
||||
impl Personality {
|
||||
/// Resolve from the `calm_mode` settings flag.
|
||||
/// When `calm_mode` is true → Calm; when false → Playful (future).
|
||||
/// For now, always returns Calm — Playful is wired but opt-in.
|
||||
#[must_use]
|
||||
pub fn from_settings(calm_mode: bool) -> Self {
|
||||
if calm_mode {
|
||||
Self::Calm
|
||||
} else {
|
||||
// Future: when playful mode is exposed in settings, return Playful here.
|
||||
// For now, calm is the only default.
|
||||
Self::Calm
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt(self) -> &'static str {
|
||||
match self {
|
||||
Self::Calm => CALM_PERSONALITY,
|
||||
Self::Playful => PLAYFUL_PERSONALITY,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Composition ───────────────────────────────────────────────────────
|
||||
@@ -1050,10 +912,6 @@ whole list: the user may override a fact, but no one may invent one. When
|
||||
guidance conflicts, consult ### Whose word wins — that is the only place
|
||||
precedence is stated.";
|
||||
|
||||
pub fn compose_prompt(personality: Personality) -> String {
|
||||
compose_prompt_with_approval_model_and_shell(personality, "codewhale")
|
||||
}
|
||||
|
||||
pub(crate) fn compose_prompt_with_approval_model_and_shell(
|
||||
personality: Personality,
|
||||
model_id: &str,
|
||||
@@ -1418,23 +1276,6 @@ fn render_route_fragment(session_context: &PromptSessionContext<'_>) -> String {
|
||||
)
|
||||
}
|
||||
|
||||
/// Assemble a cache-stable constitution prefix with a typed WorldState layer.
|
||||
///
|
||||
/// This is the Codex-parity assembly point: constitution stays byte-stable for
|
||||
/// prefix caching; volatile concerns live in `WorldState` fragments with
|
||||
/// markers, caps, and `render_diff` retain-unchanged behavior. Callers that
|
||||
/// still need a flat string can use `WorldStateSnapshot::render_text`.
|
||||
pub fn system_prompt_with_world_state(
|
||||
constitution: impl Into<String>,
|
||||
world_state: crate::model_context::WorldState,
|
||||
) -> SystemPrompt {
|
||||
let snapshot = crate::model_context::WorldStateSnapshot {
|
||||
constitution: constitution.into(),
|
||||
world_state,
|
||||
};
|
||||
SystemPrompt::Blocks(snapshot.to_system_blocks())
|
||||
}
|
||||
|
||||
/// Build a WorldState from the common volatile session facts.
|
||||
///
|
||||
/// Does not load constitution — callers keep that as the stable base.
|
||||
@@ -1468,16 +1309,6 @@ pub fn world_state_from_session_facts(
|
||||
state
|
||||
}
|
||||
|
||||
/// Build a system prompt with explicit project context
|
||||
pub fn build_system_prompt(base: &str, project_context: Option<&ProjectContext>) -> SystemPrompt {
|
||||
let full_prompt =
|
||||
match project_context.and_then(super::project_context::ProjectContext::as_system_block) {
|
||||
Some(project_block) => format!("{}\n\n{}", base.trim(), project_block),
|
||||
None => base.trim().to_string(),
|
||||
};
|
||||
SystemPrompt::Text(full_prompt)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
// Don't assert on prose. If you wouldn't fail a code review for
|
||||
@@ -1591,43 +1422,17 @@ mod tests {
|
||||
assert_eq!(effective_prompt_override(&cell, "fallback"), "first");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn static_prompt_composer_storage_returns_rejected_composer() {
|
||||
let cell = std::sync::OnceLock::new();
|
||||
let first: Box<StaticPromptComposer> =
|
||||
Box::new(|ctx| format!("first:{}", ctx.default_layers.len()));
|
||||
let second: Box<StaticPromptComposer> =
|
||||
Box::new(|ctx| format!("second:{}", ctx.default_layers.len()));
|
||||
|
||||
assert!(set_static_prompt_composer(&cell, first).is_ok());
|
||||
let rejected = set_static_prompt_composer(&cell, second)
|
||||
.expect_err("second composer should be rejected");
|
||||
let ctx = StaticPromptCtx {
|
||||
model_id: "deepseek-v4-pro",
|
||||
personality: Personality::Calm,
|
||||
default_layers: "fallback",
|
||||
};
|
||||
|
||||
assert_eq!(rejected(&ctx), "second:8");
|
||||
assert_eq!(
|
||||
cell.get().expect("first composer retained")(&ctx),
|
||||
"first:8"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn static_prompt_composer_unset_keeps_default_layers_byte_identical() {
|
||||
for personality in [Personality::Calm, Personality::Playful] {
|
||||
let default_layers = compose_default_static_layers(personality, "deepseek-v4-flash");
|
||||
let composed = apply_static_prompt_composer(
|
||||
None,
|
||||
personality,
|
||||
"deepseek-v4-flash",
|
||||
&default_layers,
|
||||
);
|
||||
let default_layers = compose_default_static_layers(Personality::Calm, "deepseek-v4-flash");
|
||||
let composed = apply_static_prompt_composer(
|
||||
None,
|
||||
Personality::Calm,
|
||||
"deepseek-v4-flash",
|
||||
&default_layers,
|
||||
);
|
||||
|
||||
assert_byte_identical("unset static prompt composer", &default_layers, &composed);
|
||||
}
|
||||
assert_byte_identical("unset static prompt composer", &default_layers, &composed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -2036,7 +1841,7 @@ mod tests {
|
||||
// 0.9.0 has no personality tier. Voice and tone live in the
|
||||
// compact constitution rather than a separate section, so
|
||||
// personality remains folded in by omission.
|
||||
let prompt = compose_prompt(Personality::Calm);
|
||||
let prompt = compose_prompt_with_approval_model_and_shell(Personality::Calm, "codewhale");
|
||||
assert!(
|
||||
!prompt.contains("Personality: Calm — Tier 8"),
|
||||
"Personality tier should not appear as a separate section"
|
||||
@@ -2677,14 +2482,9 @@ mod tests {
|
||||
// Composed overlays must describe behavior, never their own rank.
|
||||
let overlays = [
|
||||
("CALM_PERSONALITY", CALM_PERSONALITY),
|
||||
("PLAYFUL_PERSONALITY", PLAYFUL_PERSONALITY),
|
||||
("AGENT_MODE", AGENT_MODE),
|
||||
("PLAN_MODE", PLAN_MODE),
|
||||
("YOLO_MODE", YOLO_MODE),
|
||||
("OPERATE_MODE", OPERATE_MODE),
|
||||
("AUTO_APPROVAL", AUTO_APPROVAL),
|
||||
("SUGGEST_APPROVAL", SUGGEST_APPROVAL),
|
||||
("NEVER_APPROVAL", NEVER_APPROVAL),
|
||||
("COMPACT_TEMPLATE", COMPACT_TEMPLATE),
|
||||
("MEMORY_GUIDANCE", MEMORY_GUIDANCE),
|
||||
("LANGUAGE_PROMPT", LANGUAGE_PROMPT),
|
||||
@@ -2828,7 +2628,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn compose_prompt_includes_all_layers() {
|
||||
let prompt = compose_prompt(Personality::Calm);
|
||||
let prompt = compose_prompt_with_approval_model_and_shell(Personality::Calm, "codewhale");
|
||||
// Base layer — balanced Constitution; procedural recipes stay out.
|
||||
assert!(prompt.contains("## Codewhale"));
|
||||
assert!(prompt.contains("### Whose word wins"));
|
||||
@@ -2922,7 +2722,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn compose_prompt_deterministic_order() {
|
||||
let prompt = compose_prompt(Personality::Calm);
|
||||
let prompt = compose_prompt_with_approval_model_and_shell(Personality::Calm, "codewhale");
|
||||
let base_pos = prompt.find("## Codewhale").unwrap();
|
||||
let article_pos = prompt.find("### Ground truth").unwrap();
|
||||
|
||||
@@ -2933,7 +2733,7 @@ mod tests {
|
||||
fn base_prompt_is_mode_agnostic() {
|
||||
// Mode and approval text are no longer inlined into compose_prompt —
|
||||
// they travel as request-time runtime metadata.
|
||||
let prompt = compose_prompt(Personality::Calm);
|
||||
let prompt = compose_prompt_with_approval_model_and_shell(Personality::Calm, "codewhale");
|
||||
assert!(!prompt.contains("Mode: Agent"));
|
||||
assert!(!prompt.contains("Mode: YOLO"));
|
||||
assert!(!prompt.contains("Mode: Plan"));
|
||||
@@ -2972,11 +2772,11 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn mode_prompts_remain_small_deltas_not_base_policy_copies() {
|
||||
for (name, prompt) in [
|
||||
("agent", AGENT_MODE),
|
||||
("plan", PLAN_MODE),
|
||||
("yolo", YOLO_MODE),
|
||||
] {
|
||||
assert!(
|
||||
PLAN_MODE.contains("All writes, patches, shell commands"),
|
||||
"Plan may summarize the user-facing mode delta"
|
||||
);
|
||||
for (name, prompt) in [("agent", AGENT_MODE), ("plan", PLAN_MODE)] {
|
||||
// Measure semantic size on LF so Windows autocrlf checkouts do not
|
||||
// inflate char/3 token estimates via extra `\r` bytes.
|
||||
let normalized = prompt.replace("\r\n", "\n").replace('\r', "\n");
|
||||
@@ -3012,38 +2812,9 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mode_prompts_do_not_inline_full_approval_policy_overlays() {
|
||||
for (name, mode_prompt) in [
|
||||
("agent", AGENT_MODE),
|
||||
("plan", PLAN_MODE),
|
||||
("yolo", YOLO_MODE),
|
||||
] {
|
||||
for (approval_name, approval_prompt) in [
|
||||
("auto", AUTO_APPROVAL),
|
||||
("suggest", SUGGEST_APPROVAL),
|
||||
("never", NEVER_APPROVAL),
|
||||
] {
|
||||
assert!(
|
||||
!mode_prompt.contains(approval_prompt.trim()),
|
||||
"{name} mode prompt must not inline the full {approval_name} approval overlay"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
assert!(
|
||||
PLAN_MODE.contains("All writes, patches, shell commands"),
|
||||
"Plan may summarize the user-facing mode delta"
|
||||
);
|
||||
assert!(
|
||||
NEVER_APPROVAL.contains("The write-block is a runtime setting"),
|
||||
"the approval overlay keeps the policy authority explanation without rank vocabulary"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn approval_policy_no_longer_inlined_in_base_prompt() {
|
||||
let prompt = compose_prompt(Personality::Calm);
|
||||
let prompt = compose_prompt_with_approval_model_and_shell(Personality::Calm, "codewhale");
|
||||
assert!(!prompt.contains("Mode: Agent"));
|
||||
assert!(!prompt.contains("Approval Policy:"));
|
||||
// The compact Constitutional preamble is still present.
|
||||
@@ -3053,14 +2824,9 @@ mod tests {
|
||||
#[test]
|
||||
fn personality_is_folded_into_constitution() {
|
||||
// v4 has no separate personality tier. Voice and tone live in
|
||||
// the preamble, so both Calm and Playful compose_prompt calls
|
||||
// produce identical output (no personality overlay is appended).
|
||||
let calm = compose_prompt(Personality::Calm);
|
||||
let playful = compose_prompt(Personality::Playful);
|
||||
assert_eq!(
|
||||
calm, playful,
|
||||
"personality enum is a no-op — both produce identical output"
|
||||
);
|
||||
// the preamble, so composition appends no personality overlay.
|
||||
let calm = compose_prompt_with_approval_model_and_shell(Personality::Calm, "codewhale");
|
||||
assert!(!calm.contains("## Personality:"));
|
||||
assert!(calm.contains("Take the work seriously. Don't take"));
|
||||
assert!(calm.contains("You are Codewhale"));
|
||||
}
|
||||
@@ -3149,7 +2915,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn agent_mode_tool_guidance_avoids_defensive_tool_suppression() {
|
||||
let prompt = compose_prompt(Personality::Calm);
|
||||
let prompt = compose_prompt_with_approval_model_and_shell(Personality::Calm, "codewhale");
|
||||
assert!(!prompt.contains("Tool Selection Guide"));
|
||||
for tool in ["`File`", "`Git`", "`Run`", "`Bash`"] {
|
||||
assert!(!AGENT_MODE.contains(tool));
|
||||
@@ -3172,7 +2938,7 @@ mod tests {
|
||||
/// reinforcement lives in its own static segment plus locale bookends.
|
||||
#[test]
|
||||
fn language_segment_present_outside_reduced_constitution() {
|
||||
let prompt = compose_prompt(Personality::Calm);
|
||||
let prompt = compose_prompt_with_approval_model_and_shell(Personality::Calm, "codewhale");
|
||||
assert!(
|
||||
!BASE_PROMPT.contains("## Language"),
|
||||
"0.9.0 constitution.md should stay reduced; language belongs in its own segment"
|
||||
@@ -3200,7 +2966,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn output_formatting_segment_present_outside_reduced_constitution() {
|
||||
let prompt = compose_prompt(Personality::Calm);
|
||||
let prompt = compose_prompt_with_approval_model_and_shell(Personality::Calm, "codewhale");
|
||||
assert!(
|
||||
!BASE_PROMPT.contains("## Output Formatting"),
|
||||
"0.9.0 constitution.md should stay reduced; output formatting belongs in its own segment"
|
||||
@@ -3261,7 +3027,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn english_base_prompt_avoids_native_script_language_priming() {
|
||||
let prompt = compose_prompt(Personality::Calm);
|
||||
let prompt = compose_prompt_with_approval_model_and_shell(Personality::Calm, "codewhale");
|
||||
assert!(
|
||||
!contains_cjk(&prompt),
|
||||
"English base prompt should keep native-script reinforcement in locale bookends only"
|
||||
@@ -3308,7 +3074,7 @@ mod tests {
|
||||
/// by project law/instructions sitting above memory/handoffs.
|
||||
#[test]
|
||||
fn project_instructions_outrank_memory_in_whose_word_wins() {
|
||||
let prompt = compose_prompt(Personality::Calm);
|
||||
let prompt = compose_prompt_with_approval_model_and_shell(Personality::Calm, "codewhale");
|
||||
let project_at = prompt
|
||||
.find("3. Project law and instructions")
|
||||
.expect("Whose word wins must rank project instructions");
|
||||
@@ -3324,7 +3090,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn workspace_orientation_guidance_present() {
|
||||
let prompt = compose_prompt(Personality::Calm);
|
||||
let prompt = compose_prompt_with_approval_model_and_shell(Personality::Calm, "codewhale");
|
||||
assert!(prompt.contains("Project law and instructions"));
|
||||
assert!(
|
||||
prompt.contains("the nearest in\nscope winning over the broader")
|
||||
@@ -3433,19 +3199,13 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn preamble_carries_tone_and_ownership_guidance() {
|
||||
let prompt = compose_prompt(Personality::Calm);
|
||||
let prompt = compose_prompt_with_approval_model_and_shell(Personality::Calm, "codewhale");
|
||||
assert!(prompt.contains("The A is already yours"));
|
||||
assert!(prompt.contains("Your competence is a settled fact"));
|
||||
assert!(prompt.contains("Take the work seriously. Don't take"));
|
||||
assert!(prompt.contains("Let the work speak"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_constants_still_available() {
|
||||
// Verify the legacy .txt constant still compiles and contains expected content
|
||||
assert!(AGENT_PROMPT.lines().next().is_some());
|
||||
}
|
||||
|
||||
// ── Cache-prefix stability harness (#263 step 2) ───────────────────────
|
||||
//
|
||||
// These tests pin the byte-stability invariant required for DeepSeek's
|
||||
@@ -3460,15 +3220,9 @@ mod tests {
|
||||
// Suspect #4 from #263: mode prompt churn within a single mode.
|
||||
// Two calls with identical (mode, personality) inputs must produce
|
||||
// identical bytes — anything else is a cache buster.
|
||||
for personality in [Personality::Calm, Personality::Playful] {
|
||||
let a = compose_prompt(personality);
|
||||
let b = compose_prompt(personality);
|
||||
assert_byte_identical(
|
||||
&format!("compose_prompt(personality={personality:?})"),
|
||||
&a,
|
||||
&b,
|
||||
);
|
||||
}
|
||||
let a = compose_prompt_with_approval_model_and_shell(Personality::Calm, "codewhale");
|
||||
let b = compose_prompt_with_approval_model_and_shell(Personality::Calm, "codewhale");
|
||||
assert_byte_identical("compose_prompt(Personality::Calm)", &a, &b);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -3818,7 +3572,7 @@ mod tests {
|
||||
/// guidance travels via the constitution preamble instead.
|
||||
#[test]
|
||||
fn default_prompt_does_not_include_calm_personality_overlay() {
|
||||
let prompt = compose_prompt(Personality::Calm);
|
||||
let prompt = compose_prompt_with_approval_model_and_shell(Personality::Calm, "codewhale");
|
||||
let calm_text = CALM_PERSONALITY;
|
||||
let first_calm_line = calm_text.lines().find(|l| !l.is_empty()).unwrap_or("");
|
||||
assert!(
|
||||
@@ -3960,7 +3714,7 @@ mod tests {
|
||||
#[test]
|
||||
fn default_prompt_stays_under_2953_static_baseline() {
|
||||
const ISSUE_2953_BASELINE_CHARS: usize = 30_461;
|
||||
let prompt = compose_prompt(Personality::Calm);
|
||||
let prompt = compose_prompt_with_approval_model_and_shell(Personality::Calm, "codewhale");
|
||||
|
||||
assert!(
|
||||
prompt.chars().count() < ISSUE_2953_BASELINE_CHARS,
|
||||
|
||||
@@ -230,19 +230,6 @@ This personality may never:
|
||||
- Contradict a clear user directive.
|
||||
- Supersede the constitution or the user's current request.
|
||||
"#;
|
||||
/// Playful personality overlay.
|
||||
pub const PLAYFUL_PERSONALITY: &str = r#"## Personality: Playful
|
||||
|
||||
Your voice is warm, energetic, and playful. You're still precise — you just have more fun doing it.
|
||||
|
||||
- Open with personality: "Alright, let's dig into this." or "Ooh, interesting problem."
|
||||
- Occasional light humor is welcome. Puns, metaphors, and analogies that illuminate the work.
|
||||
- Use em dashes, parenthetical asides, and a conversational cadence.
|
||||
- Celebrate wins briefly: "Nice — that compiled on the first try."
|
||||
- When things go sideways, keep it light: "Well, that didn't go as planned. Let me try another angle."
|
||||
- Match the user's energy. If they're casual, be casual. If they get technical, tighten up.
|
||||
- Avoid corporate cheerfulness. Be genuinely warm, not performatively positive.
|
||||
"#;
|
||||
|
||||
// ── Mode deltas — permissions, workflow expectations, mode rules ───
|
||||
/// Agent mode (Act) delta.
|
||||
@@ -273,13 +260,6 @@ delegation, it may support parallel investigation. After presenting the plan,
|
||||
ask the user to reply with revisions or switch to Act (`/mode act`) to
|
||||
implement, then wait. Do not announce the mode.
|
||||
"#;
|
||||
/// Full-access mode delta.
|
||||
pub const YOLO_MODE: &str = r#"##### Mode: YOLO
|
||||
|
||||
All actions are auto-approved within the user's scope. Verify destructive
|
||||
targets and preserve unrelated work. When `work_update` is present, use it only
|
||||
for genuinely multi-step work. Do not announce the mode.
|
||||
"#;
|
||||
/// Operate mode delta.
|
||||
///
|
||||
/// Hard doctrine (not soft preferences): the parent session is the conductor,
|
||||
@@ -315,48 +295,6 @@ Operate doctrine (must):
|
||||
unless asked.
|
||||
"#;
|
||||
|
||||
// ── Approval-policy overlays ───────────────────────────────────────
|
||||
/// Tool calls are auto-approved.
|
||||
pub const AUTO_APPROVAL: &str = r#"##### Approval Policy: Auto
|
||||
|
||||
All tool calls are pre-approved. You will not see approval prompts — your actions execute immediately.
|
||||
|
||||
This means you carry more responsibility:
|
||||
- Pause before destructive operations (deletes, force-pushes, `rm -rf`).
|
||||
- When `work_update` is present, use it for multi-step work so progress stays visible.
|
||||
- If you're uncertain about a course of action, state your reasoning before proceeding.
|
||||
- The user can interrupt you at any time.
|
||||
|
||||
Execute rather than narrate. Verification still applies — check your work even when no one prompts you to.
|
||||
"#;
|
||||
/// Tool calls require confirmation.
|
||||
pub const SUGGEST_APPROVAL: &str = r#"##### Approval Policy: Suggest
|
||||
|
||||
Read-only operations run silently. File edits and patches whose targets all stay inside this workspace also run without approval, because version control keeps them reviewable — but only when they steer clear of `.git` internals, runtime state (`.codewhale`), and sensitive files (`.env`, credentials, key material). Spawning a read-only sub-agent role (scout, planner, reviewer, verifier, consultant) runs without approval too; the child's own gates keep it read-only. Every other write requires user approval before executing: paths outside the workspace, those excluded paths, shell execution, write-capable sub-agent spawns, CSV batches.
|
||||
|
||||
When you need approval:
|
||||
1. For multi-step changes, use `work_update` when it is present; otherwise state the approach briefly.
|
||||
2. The user will see your proposed action and can approve or deny it.
|
||||
|
||||
Decomposition is your best tool for earning approvals. A clear plan with verifiable steps gets approved faster than an opaque request.
|
||||
|
||||
This policy only controls which tool calls are gated. The user may change it at any time, including by approving or denying a specific prompt.
|
||||
"#;
|
||||
/// Tool calls are blocked.
|
||||
pub const NEVER_APPROVAL: &str = r#"##### Approval Policy: Never
|
||||
|
||||
All write operations are blocked. You can read, search, and investigate, but you cannot modify the workspace.
|
||||
|
||||
This is a read-only mode. Build thorough plans, investigate codebases, trace
|
||||
logic, and gather context. When `work_update` is present, use it as the one
|
||||
canonical list. When read-only delegation is present, it may support parallel
|
||||
exploration.
|
||||
|
||||
If the user asks you to edit files, run shell commands, apply patches, or otherwise change the workspace while this policy is active, do not draft a large implementation first. Stop early, say that the current approval policy blocks writes, and give the exact escape hatch: run `/config approval_mode suggest` for prompted writes, or select Full Access only in a trusted workspace.
|
||||
|
||||
The write-block is a runtime setting the user may change at any time — not a prohibition in the constitution itself.
|
||||
"#;
|
||||
|
||||
// ── Runtime templates ──────────────────────────────────────────────
|
||||
/// Compaction relay template — written into the system prompt so the
|
||||
/// model knows the format to use when writing `.codewhale/handoff.md`.
|
||||
@@ -469,64 +407,3 @@ tool errors, and distinguish child reports from evidence you verified. Write
|
||||
`None.` where a section has no entries. If blocked, name the missing fact or
|
||||
capability. Then stop.
|
||||
"#;
|
||||
|
||||
// ── Legacy prompt constants (kept for backwards compatibility) ─────
|
||||
/// Legacy base prompt (the retired `agent.txt` — now decomposed into the
|
||||
/// constitution + overlays above). Still available for callers that haven't
|
||||
/// migrated to the layered API.
|
||||
pub const AGENT_PROMPT: &str = r#"## Mode: agent
|
||||
|
||||
Read-only tools (reads, searches, persistent RLM session tools, git inspection) run silently.
|
||||
Any write, patch, shell execution, sub-agent start, or CSV batch operation will ask for approval first.
|
||||
|
||||
Before requesting approval for multi-step writes, lay out your work with `work_update` so the user
|
||||
can see what you intend to do and approve with context. Do not create a second
|
||||
strategy checklist. For simple writes, state the direct edit and proceed through the normal approval
|
||||
flow.
|
||||
|
||||
## Sub-agent completion sentinel
|
||||
|
||||
When you open a sub-agent via `agent`, the child runs independently.
|
||||
You will receive a `<codewhale:subagent.done>` element in the transcript when it finishes.
|
||||
Read its `summary` field and integrate the work — do not re-do what the child already did.
|
||||
Use the returned transcript handle with `handle_read` only when the completion summary is insufficient.
|
||||
|
||||
Write child prompts as a compact Subagent Brief:
|
||||
|
||||
QUESTION: exact question or task.
|
||||
SCOPE: files, PRs, issue IDs, commands, or behavior areas to inspect.
|
||||
ALREADY_KNOWN: facts you already checked; do not repeat unless contradicted.
|
||||
EFFORT: quick | medium | thorough.
|
||||
STOP_CONDITION: evidence enough to return.
|
||||
OUTPUT: VERDICT, EVIDENCE, GAPS, NEXT.
|
||||
|
||||
Child model choice is explicit. Use `model_strength: "same"` when the child needs your current
|
||||
capability level. Use `model_strength: "faster"` for read-only lookup/search, status, or other
|
||||
low-risk tasks that should run on a smaller/faster same-family model — `type: "scout"` already
|
||||
defaults to `model_strength: "faster"` for exactly this kind of bounded read-only work, so you only
|
||||
need to set it for non-scout children. Use an exact `model` only when you know the
|
||||
provider-specific id; it overrides `model_strength`.
|
||||
Child thinking is explicit too. Use `thinking: "off"` for fast scout/lookups, `thinking: "high"`
|
||||
for ordinary reasoning, `thinking: "max"` for hard design/debug/release/security work, and
|
||||
`thinking: "auto"` when you want Codewhale to choose from the child prompt. Omit it to inherit the
|
||||
parent thinking mode; explicit `thinking` overrides the default off used with `model_strength:
|
||||
"faster"`.
|
||||
|
||||
Prefer parallel exploration for broad investigations. For repo, version, branch, benchmark,
|
||||
API-surface, bug, PR, issue, or multi-module investigations, start by splitting independent
|
||||
read-only exploration across 2-4 `type: "scout"` Fleet workers when that will reduce uncertainty
|
||||
faster than reading sequentially. Each child runs concurrently in one turn and returns findings you
|
||||
synthesize; keep architecture decisions, integration, verification, and the final response in the
|
||||
parent. Do not open sub-agents for tiny one-step tasks — the spawn overhead is not worth it for a
|
||||
single read or search.
|
||||
|
||||
For `type: "scout"`, default to `EFFORT: quick`: stay read-only, aim for about 3-5 tool calls,
|
||||
do not broaden once QUESTION is answered, and return partial findings if the next step would be
|
||||
speculative or duplicative. Review/verifier children can spend more calls but should stop after
|
||||
decisive evidence. Builder/repair children are not subject to the 3-5 call cap; ask them to
|
||||
checkpoint before expanding scope or after repeated failures.
|
||||
|
||||
Sub-agent outputs are self-reports, not verified facts. Re-check material claims before relying on
|
||||
them: read changed files directly, run the relevant tests, and inspect unexpected results. Keep
|
||||
final verification in the parent.
|
||||
"#;
|
||||
|
||||
Reference in New Issue
Block a user