- 1
//! `vak prompts` — the terminal surface for layered prompts - 2
//! (docs/design/45-prompt-layers.md). - 3
//! - 4
//! The CLI carries the full verb set rather than a read-only view, because - 5
//! it is the only surface that works on a machine with no UI. - 6
- 7
use std::io::Read as _; - 8
use std::path::PathBuf; - 9
- 10
use vak_core::Core; - 11
use vak_core::prompts::{self, PromptBlock}; - 12
- 13
use crate::cli::{PromptScope, PromptsAction}; - 14
- 15
fn scope_root(scope: PromptScope, cwd: &std::path::Path) -> PathBuf { - 16
match scope { - 17
PromptScope::User => vak_config::paths::default_workspace(), - 18
PromptScope::Project => cwd.to_path_buf(), - 19
} - 20
} - 21
- 22
fn scope_label(scope: PromptScope) -> &'static str { - 23
match scope { - 24
PromptScope::User => "Shared", - 25
PromptScope::Project => "This project", - 26
} - 27
} - 28
- 29
fn parse_block(raw: &str) -> Result<PromptBlock, i32> { - 30
PromptBlock::parse(raw).ok_or_else(|| { - 31
eprintln!( - 32
"error: unknown block '{raw}'. Editable blocks: identity, operating-rules, \ - 33
guardrails, surface-note." - 34
); - 35
eprintln!( - 36
"note: the capability contract, the Surface line, and the skill/MCP lists are \ - 37
code-owned and describe the interface as it actually is. `surface-note` \ - 38
appends to the Surface line; it cannot rewrite it." - 39
); - 40
2 - 41
}) - 42
} - 43
- 44
pub(crate) fn run(cwd: PathBuf, action: PromptsAction, trusted: bool) -> i32 { - 45
match action { - 46
PromptsAction::Show { - 47
block, - 48
scope, - 49
provenance, - 50
} => show(cwd, block, scope, provenance, trusted), - 51
PromptsAction::Edit { block, scope } => edit(cwd, block, scope), - 52
PromptsAction::Set { block, from, scope } => set(cwd, block, from, scope), - 53
PromptsAction::Reset { block, scope } => reset(cwd, block, scope), - 54
PromptsAction::Diff => diff(cwd, trusted), - 55
PromptsAction::Preview { surface, role } => preview(cwd, surface, role, trusted), - 56
PromptsAction::Roles => roles(cwd, trusted), - 57
} - 58
} - 59
- 60
fn core_at(cwd: PathBuf, trusted: bool) -> Result<Core, i32> { - 61
Core::new_with_trust(cwd, trusted).map_err(|e| { - 62
eprintln!("error: {e}"); - 63
2 - 64
}) - 65
} - 66
- 67
fn show( - 68
cwd: PathBuf, - 69
block: Option<String>, - 70
scope: Option<PromptScope>, - 71
provenance: bool, - 72
trusted: bool, - 73
) -> i32 { - 74
// `--scope` means "show me the layer I would be editing", which is a - 75
// different question from "show me what the model sees". - 76
if let Some(scope) = scope { - 77
let dir = prompts::layer_dir(&scope_root(scope, &cwd)); - 78
let content = prompts::read_layer(&dir); - 79
println!("# {} — {}", scope_label(scope), dir.display()); - 80
if content.is_empty() { - 81
println!("\n(nothing set here; every block is inherited)"); - 82
return 0; - 83
} - 84
for b in PromptBlock::ALL { - 85
match content.block(b) { - 86
Some(text) => println!("\n## {}\n\n{}", b.slug(), text.trim()), - 87
None => println!("\n## {} — inherited", b.slug()), - 88
} - 89
} - 90
return 0; - 91
} - 92
- 93
let core = match core_at(cwd, trusted) { - 94
Ok(c) => c, - 95
Err(code) => return code, - 96
}; - 97
let resolution = core.resolve_prompt(&core.capability_descriptors()); - 98
- 99
if let Some(raw) = block { - 100
let block = match parse_block(&raw) { - 101
Ok(b) => b, - 102
Err(code) => return code, - 103
}; - 104
for descriptor in resolution - 105
.descriptors - 106
.iter() - 107
.filter(|d| d.block == block.slug()) - 108
{ - 109
println!( - 110
"# from {} ({})", - 111
prompts::layer_label(&descriptor.layer), - 112
descriptor.source.as_deref().unwrap_or("built in") - 113
); - 114
} - 115
return 0; - 116
} - 117
- 118
if provenance { - 119
println!("# Contributing layers\n"); - 120
for d in &resolution.descriptors { - 121
println!( - 122
"{:<16} {:<16} {:>6}B {}", - 123
d.block, - 124
prompts::layer_label(&d.layer), - 125
d.bytes, - 126
d.source.as_deref().unwrap_or("built in") - 127
); - 128
} - 129
println!( - 130
"\n# fingerprint {}\n# ~{} tokens\n", - 131
&resolution.fingerprint()[..16], - 132
resolution.text.len() / 4 - 133
); - 134
} - 135
println!("{}", resolution.text); - 136
0 - 137
} - 138
- 139
fn edit(cwd: PathBuf, raw: String, scope: PromptScope) -> i32 { - 140
let block = match parse_block(&raw) { - 141
Ok(b) => b, - 142
Err(code) => return code, - 143
}; - 144
let dir = prompts::layer_dir(&scope_root(scope, &cwd)); - 145
let existing = prompts::read_layer(&dir).block(block).unwrap_or_default(); - 146
- 147
let editor = std::env::var("VISUAL") - 148
.or_else(|_| std::env::var("EDITOR")) - 149
.unwrap_or_else(|_| "vi".to_string()); - 150
let tmp = std::env::temp_dir().join(format!("vak-prompt-{}.md", block.slug())); - 151
if let Err(e) = std::fs::write(&tmp, &existing) { - 152
eprintln!("error: cannot stage editor buffer: {e}"); - 153
return 2; - 154
} - 155
let status = std::process::Command::new(&editor).arg(&tmp).status(); - 156
match status { - 157
Ok(s) if s.success() => {} - 158
Ok(s) => { - 159
eprintln!("error: {editor} exited with {s}; nothing saved"); - 160
let _ = std::fs::remove_file(&tmp); - 161
return 2; - 162
} - 163
Err(e) => { - 164
eprintln!("error: cannot run {editor}: {e}"); - 165
let _ = std::fs::remove_file(&tmp); - 166
return 2; - 167
} - 168
} - 169
let edited = std::fs::read_to_string(&tmp).unwrap_or_default(); - 170
let _ = std::fs::remove_file(&tmp); - 171
if edited.trim() == existing.trim() { - 172
println!("No change."); - 173
return 0; - 174
} - 175
save(&dir, block, &edited, scope) - 176
} - 177
- 178
fn set(cwd: PathBuf, raw: String, from: String, scope: PromptScope) -> i32 { - 179
let block = match parse_block(&raw) { - 180
Ok(b) => b, - 181
Err(code) => return code, - 182
}; - 183
let text = if from == "-" { - 184
let mut buf = String::new(); - 185
if let Err(e) = std::io::stdin().read_to_string(&mut buf) { - 186
eprintln!("error: cannot read stdin: {e}"); - 187
return 2; - 188
} - 189
buf - 190
} else { - 191
match std::fs::read_to_string(&from) { - 192
Ok(text) => text, - 193
Err(e) => { - 194
eprintln!("error: cannot read {from}: {e}"); - 195
return 2; - 196
} - 197
} - 198
}; - 199
save( - 200
&prompts::layer_dir(&scope_root(scope, &cwd)), - 201
block, - 202
&text, - 203
scope, - 204
) - 205
} - 206
- 207
fn save(dir: &std::path::Path, block: PromptBlock, text: &str, scope: PromptScope) -> i32 { - 208
if let Err(e) = prompts::write_block(dir, block, Some(text)) { - 209
eprintln!("error: cannot write {}: {e}", block.slug()); - 210
return 2; - 211
} - 212
println!( - 213
"Saved {} to {} ({}).", - 214
block.slug(), - 215
scope_label(scope), - 216
dir.join(block.file_name()).display() - 217
); - 218
if block == PromptBlock::Guardrails { - 219
println!( - 220
"note: guardrail text instructs the model; it does not enforce anything. \ - 221
Permissions and the sandbox are the enforcement boundary." - 222
); - 223
} - 224
if block == PromptBlock::SurfaceNote { - 225
println!( - 226
"note: notes are appended after the generated Surface line and accumulate \ - 227
across layers; nothing narrower can remove one." - 228
); - 229
} - 230
println!("Applies to new sessions; a running turn keeps the prompt it started with."); - 231
0 - 232
} - 233
- 234
fn reset(cwd: PathBuf, raw: String, scope: PromptScope) -> i32 { - 235
let block = match parse_block(&raw) { - 236
Ok(b) => b, - 237
Err(code) => return code, - 238
}; - 239
let dir = prompts::layer_dir(&scope_root(scope, &cwd)); - 240
if let Err(e) = prompts::write_block(&dir, block, None) { - 241
eprintln!("error: cannot reset {}: {e}", block.slug()); - 242
return 2; - 243
} - 244
println!( - 245
"Reset {} in {}; it is inherited again.", - 246
block.slug(), - 247
scope_label(scope) - 248
); - 249
0 - 250
} - 251
- 252
fn diff(cwd: PathBuf, trusted: bool) -> i32 { - 253
let core = match core_at(cwd, trusted) { - 254
Ok(c) => c, - 255
Err(code) => return code, - 256
}; - 257
let seed = prompts::seed(vak_core::APP_VERSION).content; - 258
let layers = core.prompt_layers(seed.clone()); - 259
let mut changed = false; - 260
for block in PromptBlock::ALL { - 261
let winners: Vec<_> = layers - 262
.iter() - 263
.filter(|l| l.content.block(block).is_some()) - 264
.collect(); - 265
let overridden = winners - 266
.iter() - 267
.any(|l| l.layer != prompts::PromptLayer::Seed); - 268
if !overridden { - 269
continue; - 270
} - 271
changed = true; - 272
println!("## {}", block.slug()); - 273
if matches!(block, PromptBlock::Guardrails | PromptBlock::SurfaceNote) { - 274
// These only ever get added to, so the useful diff is the - 275
// additions — there is no such thing as a removal here. - 276
for layer in &winners { - 277
if layer.layer == prompts::PromptLayer::Seed { - 278
continue; - 279
} - 280
let items = if block == PromptBlock::Guardrails { - 281
&layer.content.guardrails - 282
} else { - 283
&layer.content.surface_notes - 284
}; - 285
for rule in items { - 286
println!("+ [{}] {rule}", layer.layer.label()); - 287
} - 288
} - 289
} else { - 290
if let Some(winner) = winners.last() { - 291
let text = winner.content.block(block).unwrap_or_default(); - 292
println!("- [shipped default]"); - 293
println!( - 294
"+ [{}] {}", - 295
winner.layer.label(), - 296
text.lines().next().unwrap_or("") - 297
); - 298
} - 299
} - 300
println!(); - 301
} - 302
if !changed { - 303
println!("No local prompt changes; running the shipped default."); - 304
} - 305
0 - 306
} - 307
- 308
fn parse_surface(raw: &str) -> vak_core::Surface { - 309
match raw.trim().to_ascii_lowercase().as_str() { - 310
"" | "unknown" => vak_core::Surface::Unknown, - 311
"cli" => vak_core::Surface::Cli, - 312
"desktop" => vak_core::Surface::Desktop, - 313
"server" => vak_core::Surface::Server, - 314
"background" => vak_core::Surface::Background, - 315
"worker" => vak_core::Surface::Worker, - 316
channel => vak_core::Surface::Chat { - 317
channel: channel.to_string(), - 318
}, - 319
} - 320
} - 321
- 322
fn preview(cwd: PathBuf, surface: String, role: Option<String>, trusted: bool) -> i32 { - 323
let core = match core_at(cwd, trusted) { - 324
Ok(c) => c, - 325
Err(code) => return code, - 326
}; - 327
if let Some(role) = role.as_deref() - 328
&& !core.prompt_role_names().iter().any(|n| n == role) - 329
{ - 330
eprintln!("error: no role '{role}'. Run `vak prompts roles` to list them."); - 331
return 2; - 332
} - 333
let core = core - 334
.with_surface(parse_surface(&surface)) - 335
.with_prompt_role(role); - 336
println!("{}", core.system_prompt()); - 337
0 - 338
} - 339
- 340
fn roles(cwd: PathBuf, trusted: bool) -> i32 { - 341
let core = match core_at(cwd, trusted) { - 342
Ok(c) => c, - 343
Err(code) => return code, - 344
}; - 345
let names = core.prompt_role_names(); - 346
if names.is_empty() { - 347
println!("No agent roles defined."); - 348
println!( - 349
"Create one at .vak/prompts/agents/<name>/identity.md — a child spawned with \ - 350
task({{role: \"<name>\"}}) runs under it." - 351
); - 352
return 0; - 353
} - 354
println!("Agent roles (usable as task({{role: \"…\"}})):\n"); - 355
for name in names { - 356
println!(" {name}"); - 357
} - 358
println!("\nA role narrows a child; it can never widen guardrails, capabilities, or mode."); - 359
0 - 360
} - 361
Indexing the workspace…
Vakyartha documentation is discovering safe artifacts, anchors, and source references.