- 1
use std::io::Write as _; - 2
use std::path::PathBuf; - 3
- 4
use clap::Parser; - 5
use tokio_util::sync::CancellationToken; - 6
- 7
use vak_agent::{AgentEvent, TurnOutcome}; - 8
use vak_core::Core; - 9
use vak_llm::stream::StreamEvent; - 10
- 11
mod agents_cli; - 12
mod backup; - 13
mod cli; - 14
mod digest; - 15
mod doctor; - 16
mod entities_cli; - 17
mod format; - 18
mod inbox; - 19
mod install; - 20
mod intent; - 21
mod memory; - 22
mod office; - 23
mod plugins; - 24
mod prompts; - 25
mod setup; - 26
mod tasks; - 27
mod update_check; - 28
- 29
use cli::{CheckpointAction, Cli, Command, FlowAction, SkillsAction, SkillsReviewAction}; - 30
- 31
fn run_export(cwd: PathBuf, session_id: String, html: bool, out: Option<PathBuf>) -> i32 { - 32
let core = match Core::new(cwd) { - 33
Ok(c) => c, - 34
Err(e) => { - 35
eprintln!("error: {e}"); - 36
return 2; - 37
} - 38
}; - 39
if vak_core::trash::is_trashed(&core.shared_data_home(), &session_id) { - 40
eprintln!("error: session '{session_id}' is in the trash"); - 41
return 1; - 42
} - 43
let home = core.sessions_home(); - 44
let path = vak_session::SessionPath::new_session_file(&home, core.cwd(), &session_id); - 45
let log = match vak_session::SessionLog::open_read_only(path) { - 46
Ok(l) => l, - 47
Err(e) => { - 48
eprintln!("error: could not open session '{session_id}': {e}"); - 49
return 1; - 50
} - 51
}; - 52
let msgs = log.derive_conversation(); - 53
let md = vak_core::transcript_md::render_markdown(&msgs); - 54
let content = if html { - 55
vak_presentation::transcode_to_html(&format!("Session {session_id}"), &md) - 56
} else { - 57
md - 58
}; - 59
- 60
if let Some(dest) = out { - 61
if let Err(e) = std::fs::write(&dest, content) { - 62
eprintln!("error writing to {}: {e}", dest.display()); - 63
return 1; - 64
} - 65
println!("Exported session to {}", dest.display()); - 66
} else { - 67
print!("{content}"); - 68
} - 69
0 - 70
} - 71
- 72
fn run_skills_review(cwd: PathBuf, action: SkillsReviewAction) -> i32 { - 73
let Some(core) = Core::new(cwd).ok() else { - 74
return 2; - 75
}; - 76
match action { - 77
SkillsReviewAction::List => { - 78
let proposals = vak_core::learning::list_proposals(&core.sessions_home(), core.cwd()); - 79
if proposals.is_empty() { - 80
println!("no pending skill proposals"); - 81
return 0; - 82
} - 83
for p in &proposals { - 84
println!("{} {} — {}", p.id, p.name, p.description); - 85
} - 86
0 - 87
} - 88
SkillsReviewAction::Promote { id } => { - 89
match vak_core::learning::promote(&core.sessions_home(), core.cwd(), &id) { - 90
Ok(name) => { - 91
println!("promoted skill '{name}'"); - 92
0 - 93
} - 94
Err(e) => { - 95
eprintln!("error: {e}"); - 96
1 - 97
} - 98
} - 99
} - 100
SkillsReviewAction::Reject { id } => { - 101
match vak_core::learning::reject(&core.sessions_home(), core.cwd(), &id) { - 102
Ok(()) => { - 103
println!("rejected proposal {id}"); - 104
0 - 105
} - 106
Err(e) => { - 107
eprintln!("error: {e}"); - 108
1 - 109
} - 110
} - 111
} - 112
} - 113
} - 114
- 115
fn run_skills(cwd: PathBuf, action: SkillsAction) -> i32 { - 116
match action { - 117
SkillsAction::Validate { path, json } => { - 118
let mut files = Vec::new(); - 119
let roots = path.map_or_else( - 120
|| { - 121
vec![ - 122
cwd.join(".vak/skills"), - 123
vak_config::paths::data_home().join("skills"), - 124
] - 125
}, - 126
|path| vec![path], - 127
); - 128
for root in &roots { - 129
collect_skill_files(root, &mut files); - 130
} - 131
files.sort(); - 132
files.dedup(); - 133
if files.is_empty() { - 134
let locations = roots - 135
.iter() - 136
.map(|root| root.display().to_string()) - 137
.collect::<Vec<_>>() - 138
.join(", "); - 139
eprintln!("no SKILL.md files found below {locations}"); - 140
return 1; - 141
} - 142
let mut failed = false; - 143
let mut reports = Vec::new(); - 144
for file in files { - 145
match vak_core::skills::validate(&file) { - 146
Ok((skill, warnings)) => reports.push(serde_json::json!({ - 147
"path": file, - 148
"name": skill.name, - 149
"valid": true, - 150
"warnings": warnings, - 151
})), - 152
Err(error) => { - 153
failed = true; - 154
reports.push(serde_json::json!({ - 155
"path": file, - 156
"valid": false, - 157
"error": error, - 158
})); - 159
} - 160
} - 161
} - 162
if json { - 163
println!("{}", serde_json::Value::Array(reports)); - 164
} else { - 165
for report in &reports { - 166
let path = report["path"].as_str().unwrap_or("unknown"); - 167
if report["valid"] == true { - 168
println!("ok {} ({})", report["name"], path); - 169
for warning in report["warnings"].as_array().into_iter().flatten() { - 170
println!("warning: {}", warning.as_str().unwrap_or("unknown")); - 171
} - 172
} else { - 173
println!("invalid {}: {}", path, report["error"]); - 174
} - 175
} - 176
} - 177
i32::from(failed) - 178
} - 179
} - 180
} - 181
- 182
fn collect_skill_files(root: &std::path::Path, files: &mut Vec<std::path::PathBuf>) { - 183
if root.is_file() { - 184
if root.file_name().is_some_and(|name| name == "SKILL.md") { - 185
files.push(root.to_path_buf()); - 186
} - 187
return; - 188
} - 189
let Ok(entries) = std::fs::read_dir(root) else { - 190
return; - 191
}; - 192
for entry in entries.flatten() { - 193
collect_skill_files(&entry.path(), files); - 194
} - 195
} - 196
- 197
async fn run_checkpoints(cwd: PathBuf, action: CheckpointAction) -> i32 { - 198
let core = match Core::new(cwd) { - 199
Ok(c) => c, - 200
Err(e) => { - 201
eprintln!("error: {e}"); - 202
return 2; - 203
} - 204
}; - 205
match action { - 206
CheckpointAction::List { session } => { - 207
let sid = match session { - 208
Some(s) => s, - 209
None => match latest_session_id(&core) { - 210
Some(s) => s, - 211
None => { - 212
println!("no sessions yet"); - 213
return 0; - 214
} - 215
}, - 216
}; - 217
match vak_core::checkpoints::list(&core.sessions_home(), &sid) { - 218
Ok(list) if list.is_empty() => { - 219
println!("no checkpoints for {sid}"); - 220
0 - 221
} - 222
Ok(list) => { - 223
for cp in list { - 224
println!( - 225
"{:04} {} files {} {}", - 226
cp.seq, - 227
cp.files.len(), - 228
cp.created_at.format("%H:%M:%S"), - 229
cp.label.chars().take(60).collect::<String>() - 230
); - 231
} - 232
0 - 233
} - 234
Err(e) => { - 235
eprintln!("error: {e}"); - 236
2 - 237
} - 238
} - 239
} - 240
CheckpointAction::Restore { session, seq } => { - 241
match vak_core::checkpoints::load(&core.sessions_home(), &session, seq) { - 242
Ok(cp) => { - 243
match vak_core::checkpoints::restore(core.cwd(), &core.sessions_home(), &cp) { - 244
Ok((restored, deleted)) => { - 245
println!( - 246
"restored {restored} files, removed {deleted} (checkpoint {seq})" - 247
); - 248
0 - 249
} - 250
Err(e) => { - 251
eprintln!("error: restore failed: {e}"); - 252
1 - 253
} - 254
} - 255
} - 256
Err(e) => { - 257
eprintln!("error: checkpoint not found: {e}"); - 258
2 - 259
} - 260
} - 261
} - 262
} - 263
} - 264
- 265
fn latest_session_id(core: &Core) -> Option<String> { - 266
let mut session_dirs = Vec::new(); - 267
let direct = vak_session::SessionPath::sessions_dir(&core.sessions_home(), core.cwd()); - 268
if direct.exists() { - 269
session_dirs.push(direct); - 270
} - 271
let shared = core.shared_data_home(); - 272
if let Ok(agents) = std::fs::read_dir(shared.join("agents")) { - 273
for agent in agents.flatten() { - 274
let s = vak_session::SessionPath::sessions_dir(&agent.path(), core.cwd()); - 275
if s.exists() && !session_dirs.contains(&s) { - 276
session_dirs.push(s); - 277
} - 278
} - 279
} - 280
let trashed = vak_core::trash::trashed(&shared); - 281
let mut rows: Vec<_> = Vec::new(); - 282
for dir in session_dirs { - 283
if let Ok(entries) = std::fs::read_dir(&dir) { - 284
for e in entries.flatten() { - 285
let name = e.file_name().to_string_lossy().into_owned(); - 286
let maybe_m = (name.ends_with(".jsonl") - 287
&& !trashed.contains(name.trim_end_matches(".jsonl"))) - 288
.then(|| e.metadata().ok().and_then(|meta| meta.modified().ok())) - 289
.flatten(); - 290
if let Some(m) = maybe_m { - 291
rows.push((m, name)); - 292
} - 293
} - 294
} - 295
} - 296
rows.sort_by_key(|(m, _)| std::cmp::Reverse(*m)); - 297
rows.first() - 298
.map(|(_, name)| name.trim_end_matches(".jsonl").to_string()) - 299
} - 300
/// Every turn this binary runs is read in a terminal, so the system prompt - 301
/// says so (docs/design/07-prompt.md). `run_serve` is the exception and - 302
/// stamps its own surface. - 303
/// Stamp the CLI surface and, with it, whether this invocation's approver - 304
/// can answer a gate. These one-shot paths install `AutoApprove` under - 305
/// `--yes` and `AutoDeny` otherwise, and the prompt is composed by - 306
/// `start_session()` further down — so the flag has to be on the `Core` - 307
/// before that, or the prompt advertises capabilities the run will refuse. - 308
fn with_cli_surface(core: Core, approver_answerable: bool) -> Core { - 309
core.with_surface(vak_core::Surface::Cli) - 310
.with_approver_answerable(approver_answerable) - 311
} - 312
- 313
#[tokio::main] - 314
async fn main() { - 315
let internal = std::env::args_os().nth(1); - 316
#[cfg(target_os = "linux")] - 317
{ - 318
if internal.as_deref() - 319
== Some(std::ffi::OsStr::new( - 320
vak_tools::landlock::SANDBOX_SUBCOMMAND, - 321
)) - 322
{ - 323
std::process::exit(vak_tools::landlock::runner_main( - 324
std::env::args_os().skip(2), - 325
)); - 326
} - 327
} - 328
if internal.as_deref() == Some(std::ffi::OsStr::new(vak_tools::broker::WORKER_SUBCOMMAND)) { - 329
std::process::exit(vak_tools::broker::worker_main().await); - 330
} - 331
if internal.as_deref() - 332
== Some(std::ffi::OsStr::new( - 333
vak_tools::broker::PERSISTENT_WORKER_SUBCOMMAND, - 334
)) - 335
{ - 336
std::process::exit(vak_tools::broker::persistent_worker_main().await); - 337
} - 338
if internal.as_deref() - 339
== Some(std::ffi::OsStr::new( - 340
vak_delivery::worker::WORKER_SUBCOMMAND, - 341
)) - 342
{ - 343
std::process::exit(vak_delivery::worker::run_stdio()); - 344
} - 345
let cli = Cli::parse(); - 346
let current_dir = std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")); - 347
let cwd = cli - 348
.workspace - 349
.as_ref() - 350
.map(|w| { - 351
if w.is_absolute() { - 352
w.clone() - 353
} else { - 354
current_dir.join(w) - 355
} - 356
}) - 357
.unwrap_or(current_dir); - 358
- 359
// User-level secrets always load. The PROJECT secret scope is only - 360
// loaded for trusted workspaces: a cloned repository must not be able - 361
// to inject VAK_*_BASE_URL (credential redirection) or other env on - 362
// first run. - 363
if let Some(env_path) = vak_config::user_env_path() { - 364
vak_config::load_env_file(&env_path); - 365
} - 366
- 367
let code = match cli.command { - 368
None => { - 369
use clap::CommandFactory as _; - 370
cli::Cli::command().print_help().ok(); - 371
println!(); - 372
println!( - 373
"vak is a headless CLI/automation runtime. Try `vak exec \"<prompt>\"` \ - 374
or `vak plan` — for an interactive GUI use the vak-desktop app." - 375
); - 376
0 - 377
} - 378
Some(Command::Term { - 379
server, - 380
token, - 381
session, - 382
}) => { - 383
let opts = vak_terminal::TerminalOptions { - 384
session_id: session, - 385
server_url: server, - 386
token, - 387
workspace_cwd: Some(cwd), - 388
}; - 389
match vak_terminal::run_terminal(opts).await { - 390
Ok(code) => code, - 391
Err(e) => { - 392
eprintln!("terminal error: {e}"); - 393
1 - 394
} - 395
} - 396
} - 397
Some(Command::Exec { - 398
prompt, - 399
agent, - 400
accept_drift, - 401
model, - 402
provider, - 403
max_turns, - 404
json, - 405
yes, - 406
permission_mode, - 407
write_paths, - 408
worktree, - 409
session, - 410
managed, - 411
goal, - 412
criteria, - 413
trust, - 414
}) => { - 415
let trusted = resolve_trust(&cwd, trust, false); - 416
if trusted { - 417
vak_config::load_env_file(std::path::Path::new(".env")); - 418
} - 419
run_exec( - 420
cwd, - 421
prompt, - 422
agent, - 423
model, - 424
provider, - 425
max_turns, - 426
json, - 427
yes, - 428
permission_mode, - 429
write_paths, - 430
worktree, - 431
session, - 432
accept_drift, - 433
managed, - 434
goal, - 435
criteria, - 436
trusted, - 437
) - 438
.await - 439
} - 440
Some(Command::Config { action }) => { - 441
// Each of these resolves workspace trust for itself, because a - 442
// reader must see the same layers a run would: an untrusted - 443
// project's `permission_mode` and allow rules are stripped by - 444
// the loader, so reporting them would describe a policy no run - 445
// uses. - 446
match action { - 447
None | Some(cli::ConfigAction::Dump) => { - 448
run_config_dump(cwd); - 449
0 - 450
} - 451
Some(cli::ConfigAction::Permissions) => run_config_permissions(cwd), - 452
Some(cli::ConfigAction::SetMode { mode, scope }) => { - 453
run_config_set_mode(cwd, &mode, scope) - 454
} - 455
Some(cli::ConfigAction::SetApproval { mode, scope }) => { - 456
run_config_set_approval(cwd, &mode, scope) - 457
} - 458
} - 459
} - 460
Some(Command::Prompts { action }) => { - 461
// Reading and previewing must show what a real run would see, so - 462
// trust is resolved exactly as `exec` resolves it. An untrusted - 463
// workspace's own identity stays hidden here too. - 464
let trusted = resolve_trust(&cwd, false, false); - 465
prompts::run(cwd, action, trusted) - 466
} - 467
Some(Command::Sessions) => { - 468
run_sessions_list(cwd); - 469
0 - 470
} - 471
Some(Command::Self_ { action }) => match action { - 472
cli::SelfAction::State { verify } => setup::run_state(verify), - 473
cli::SelfAction::Install { prefix, force } => install::run_install(prefix, force), - 474
cli::SelfAction::Reinstall { prefix, yes } => install::run_reinstall(prefix, yes), - 475
cli::SelfAction::Verify { prefix } => install::run_verify(prefix), - 476
cli::SelfAction::ServicesSync { prefix, names } => { - 477
install::run_services_sync(prefix, names) - 478
} - 479
cli::SelfAction::Status { prefix } => install::run_status(prefix), - 480
cli::SelfAction::Uninstall { prefix, yes, purge } => { - 481
install::run_uninstall(prefix, yes, purge) - 482
} - 483
cli::SelfAction::Update { - 484
prefix, - 485
url, - 486
yes, - 487
dry_run, - 488
} => match resolve_update_url(url) { - 489
Some(u) => install::run_update(prefix, &u, yes, dry_run), - 490
None => { - 491
eprintln!( - 492
"error: no release feed URL — pass `--url` or set `[update] url` in config" - 493
); - 494
2 - 495
} - 496
}, - 497
}, - 498
Some(Command::Memory { action }) => memory::run_memory(cwd, action), - 499
Some(Command::Entities { action }) => entities_cli::run_entities(cwd, action), - 500
Some(Command::Agents { action }) => agents_cli::run_agents(cwd, action), - 501
Some(Command::Export { - 502
session_id, - 503
html, - 504
out, - 505
}) => run_export(cwd, session_id, html, out), - 506
Some(Command::SkillsReview { action }) => run_skills_review(cwd, action), - 507
Some(Command::Skills { action }) => run_skills(cwd, action), - 508
Some(Command::Plugins { action }) => plugins::run_plugins(cwd, action), - 509
Some(Command::Intent { action }) => intent::run_intent(cwd, action), - 510
Some(Command::Office { action }) => office::run_office(action).await, - 511
Some(Command::Commit { action }) => intent::run_commit(cwd, action), - 512
Some(Command::Grant { - 513
id, - 514
paths, - 515
tools, - 516
spend_usd, - 517
hours, - 518
permission, - 519
on_silence, - 520
after_hours, - 521
}) => intent::run_grant( - 522
cwd, - 523
id, - 524
paths, - 525
tools, - 526
spend_usd, - 527
hours, - 528
permission, - 529
on_silence, - 530
after_hours, - 531
), - 532
Some(Command::Revoke { id }) => intent::run_revoke(cwd, id), - 533
Some(Command::Checkpoints { action }) => run_checkpoints(cwd, action).await, - 534
Some(Command::Telegram { - 535
server, - 536
token, - 537
bot_id, - 538
}) => run_telegram(server, token, bot_id).await, - 539
Some(Command::Discord { - 540
server, - 541
token, - 542
bot_id, - 543
}) => run_discord(server, token, bot_id).await, - 544
Some(Command::Slack { - 545
server, - 546
token, - 547
bot_id, - 548
}) => run_slack(server, token, bot_id).await, - 549
Some(Command::Flow { action }) => run_flow(cwd, action).await, - 550
Some(Command::Setup { - 551
action, - 552
no_browser, - 553
print_url, - 554
terminal, - 555
non_interactive, - 556
}) => match action { - 557
Some(cli::SetupAction::Status { json, prefix }) => setup::run_status(cwd, prefix, json), - 558
Some(cli::SetupAction::Seed) => setup::run_seed(), - 559
None if terminal || non_interactive => setup::run_terminal(cwd, non_interactive).await, - 560
None => setup::run_wizard(cwd, !no_browser && !print_url, print_url).await, - 561
}, - 562
Some(Command::Doctor { trust, repair }) => { - 563
let trusted = resolve_trust(&cwd, trust, false); - 564
if trusted { - 565
vak_config::load_env_file(std::path::Path::new(".env")); - 566
} - 567
doctor::run_doctor(cwd, trusted, repair) - 568
} - 569
Some(Command::Backup { action }) => backup::run_backup(cwd, action), - 570
Some(Command::Digest { days }) => digest::run_digest(cwd, days), - 571
Some(Command::Tasks { action }) => tasks::run_tasks(cwd, action), - 572
Some(Command::Inbox { action }) => inbox::run_inbox(cwd, action), - 573
Some(Command::Plan { - 574
task, - 575
yes, - 576
permission_mode, - 577
write_paths, - 578
worktree, - 579
trust, - 580
}) => { - 581
let trusted = resolve_trust(&cwd, trust, false); - 582
if trusted { - 583
vak_config::load_env_file(std::path::Path::new(".env")); - 584
} - 585
run_plan( - 586
cwd, - 587
task, - 588
yes, - 589
permission_mode, - 590
write_paths, - 591
worktree, - 592
trusted, - 593
) - 594
.await - 595
} - 596
Some(Command::Eval { - 597
report, - 598
live, - 599
provider, - 600
model, - 601
}) => run_eval(report, live, provider, model).await, - 602
Some(Command::Open { - 603
surface, - 604
port, - 605
print, - 606
}) => run_open(surface, port, print), - 607
Some(Command::Serve { - 608
port, - 609
host, - 610
gateway, - 611
trust, - 612
}) => { - 613
let serve_cwd = if gateway { - 614
vak_config::paths::gateway_workspace() - 615
} else { - 616
cwd - 617
}; - 618
let trusted = resolve_trust(&serve_cwd, trust, false); - 619
if trusted { - 620
vak_config::load_env_file(&serve_cwd.join(".env")); - 621
} - 622
run_serve(serve_cwd, port, host, gateway, trusted).await - 623
} - 624
}; - 625
std::process::exit(code); - 626
} - 627
- 628
// --------------------------------------------------------------------------- - 629
// Workspace trust: a project's .vak/config.toml and secret scope can grant - 630
// execution power (permission mode, allow rules, hooks, MCP servers, base - 631
// URL redirection). First use of an untrusted workspace demotes those keys - 632
// until the user confirms — per-directory, remembered under ~/.vak. - 633
// --------------------------------------------------------------------------- - 634
- 635
/// Release feed URL for `self update`: the flag when given, otherwise the - 636
/// `[update] url` already used by the startup update check, so the two - 637
/// paths can never point at different feeds. - 638
/// - 639
/// Loaded untrusted on purpose. The feed names the binary that replaces this - 640
/// one and supplies its own artifact checksums, so a project config must not - 641
/// be able to redirect it; `load_with_trust` drops `[update] url` from an - 642
/// untrusted project layer and the user's global value still applies. - 643
fn resolve_update_url(flag: Option<String>) -> Option<String> { - 644
if let Some(u) = flag.filter(|u| !u.trim().is_empty()) { - 645
return Some(u); - 646
} - 647
let cwd = std::env::current_dir().ok()?; - 648
let trusted = trust_marker_path(&cwd).is_some_and(|marker| marker.exists()); - 649
vak_config::load_with_trust(&cwd, trusted).ok()?.update.url - 650
} - 651
- 652
fn fnv1a(bytes: &[u8]) -> u64 { - 653
let mut h: u64 = 0xcbf2_9ce4_8422_2325; - 654
for b in bytes { - 655
h ^= u64::from(*b); - 656
h = h.wrapping_mul(0x0000_0100_0000_01b3); - 657
} - 658
h - 659
} - 660
- 661
fn trust_marker_path(cwd: &std::path::Path) -> Option<PathBuf> { - 662
Some( - 663
vak_config::paths::data_home() - 664
.join("trusted") - 665
.join(format!("{:016x}", fnv1a(cwd.to_string_lossy().as_bytes()))), - 666
) - 667
} - 668
- 669
fn resolve_trust(cwd: &std::path::Path, flag: bool, interactive: bool) -> bool { - 670
if !vak_config::project_path(cwd).is_file() - 671
&& !vak_config::credentials::scope_has_any(&cwd.join(".env")) - 672
{ - 673
return true; - 674
} - 675
if flag { - 676
return true; - 677
} - 678
if let Some(marker) = trust_marker_path(cwd) - 679
&& marker.is_file() - 680
{ - 681
return true; - 682
} - 683
if interactive && std::io::IsTerminal::is_terminal(&std::io::stdin()) { - 684
eprintln!(); - 685
eprintln!( - 686
"This directory ({}) contains a project-level Vakyartha", - 687
cwd.display() - 688
); - 689
eprintln!("config (.vak/config.toml) and/or secrets that can run commands,"); - 690
eprintln!("auto-approve tools, or redirect API traffic."); - 691
eprint!("Trust this workspace? [y/N] "); - 692
let _ = std::io::stderr().flush(); - 693
let mut answer = String::new(); - 694
if std::io::stdin().read_line(&mut answer).is_ok() - 695
&& matches!(answer.trim().to_ascii_lowercase().as_str(), "y" | "yes") - 696
&& let Some(marker) = trust_marker_path(cwd) - 697
&& let Some(parent) = marker.parent() - 698
&& std::fs::create_dir_all(parent).is_ok() - 699
&& std::fs::write(&marker, cwd.to_string_lossy().as_bytes()).is_ok() - 700
{ - 701
return true; - 702
} - 703
} else if !flag { - 704
eprintln!( - 705
"note: workspace {} is untrusted; project permission/allow/hooks/mcp/base-url settings are ignored (pass --trust to apply)", - 706
cwd.display() - 707
); - 708
} - 709
false - 710
} - 711
- 712
pub(crate) fn print_config_warnings(core: &Core) { - 713
for w in &core.config().warnings { - 714
eprintln!("warning: {w}"); - 715
} - 716
} - 717
- 718
fn flow_dirs(cwd: &std::path::Path) -> Vec<PathBuf> { - 719
vec![ - 720
cwd.join(".vak/flows"), - 721
vak_config::paths::data_home().join("flows"), - 722
] - 723
} - 724
- 725
fn discover_flows(cwd: &std::path::Path) -> Vec<(String, PathBuf)> { - 726
let mut out = Vec::new(); - 727
for dir in flow_dirs(cwd) { - 728
let Ok(entries) = std::fs::read_dir(&dir) else { - 729
continue; - 730
}; - 731
for e in entries.flatten() { - 732
let p = e.path(); - 733
if p.extension().and_then(|x| x.to_str()) == Some("toml") { - 734
out.push(( - 735
p.file_stem() - 736
.map(|s| s.to_string_lossy().into_owned()) - 737
.unwrap_or_default(), - 738
p, - 739
)); - 740
} - 741
} - 742
} - 743
out.sort(); - 744
out.dedup_by(|a, b| a.0 == b.0); - 745
out - 746
} - 747
- 748
async fn run_flow(cwd: PathBuf, action: FlowAction) -> i32 { - 749
match action { - 750
FlowAction::List => { - 751
let flows = discover_flows(&cwd); - 752
if flows.is_empty() { - 753
println!("no flows found (.vak/flows/*.toml)"); - 754
return 0; - 755
} - 756
for (name, path) in flows { - 757
println!("{name} {}", path.display()); - 758
} - 759
0 - 760
} - 761
FlowAction::Adopt { from, name, force } => { - 762
let flows_dir = cwd.join(".vak/flows"); - 763
let out_path = flows_dir.join(format!("{name}.toml")); - 764
if out_path.exists() && !force { - 765
eprintln!( - 766
"error: {} exists (use --force to overwrite)", - 767
out_path.display() - 768
); - 769
return 2; - 770
} - 771
let adopted = if std::path::Path::new(&from).is_file() { - 772
// Ledger JSON path. - 773
let body = std::fs::read_to_string(&from).unwrap_or_default(); - 774
vak_flow::adopt::from_flow_state(&body, &name, &from) - 775
} else { - 776
// Session id: extract settled bash commands. - 777
let core = match Core::new(cwd.clone()) { - 778
Ok(c) => c, - 779
Err(e) => { - 780
eprintln!("error: {e}"); - 781
return 2; - 782
} - 783
}; - 784
let path = vak_session::SessionPath::new_session_file( - 785
&core.sessions_home(), - 786
core.cwd(), - 787
&from, - 788
); - 789
let Ok(log) = vak_session::SessionLog::open(path) else { - 790
eprintln!("error: session '{from}' not found in this workspace"); - 791
return 2; - 792
}; - 793
let cmds = log.settled_bash_commands(); - 794
vak_flow::adopt::from_green_commands( - 795
&name, - 796
&format!("adopted from session {from}"), - 797
&cmds, - 798
) - 799
}; - 800
match adopted { - 801
Ok(a) => { - 802
if std::fs::create_dir_all(&flows_dir).is_err() { - 803
eprintln!("error: cannot create {}", flows_dir.display()); - 804
return 2; - 805
} - 806
if let Err(e) = std::fs::write(&out_path, a.toml) { - 807
eprintln!("error: write failed: {e}"); - 808
return 2; - 809
} - 810
println!("✓ adopted → {}", out_path.display()); - 811
for w in a.warnings { - 812
println!(" warning: {w}"); - 813
} - 814
println!(" next: vak flow check {name} && vak flow run {name}"); - 815
0 - 816
} - 817
Err(e) => { - 818
eprintln!("error: adopt failed: {e}"); - 819
1 - 820
} - 821
} - 822
} - 823
FlowAction::Diff { a, b } => { - 824
let (ra, rb) = ( - 825
std::fs::read_to_string(&a).unwrap_or_default(), - 826
std::fs::read_to_string(&b).unwrap_or_default(), - 827
); - 828
match vak_flow::adopt::diff_flow_states(&ra, &rb) { - 829
Ok(report) => { - 830
print!("{report}"); - 831
if report.contains("identical") { 0 } else { 1 } - 832
} - 833
Err(e) => { - 834
eprintln!("error: {e}"); - 835
2 - 836
} - 837
} - 838
} - 839
FlowAction::Check { name } => { - 840
let Some((_, path)) = discover_flows(&cwd).into_iter().find(|(n, _)| *n == name) else { - 841
eprintln!("error: flow '{name}' not found"); - 842
return 2; - 843
}; - 844
let toml_str = std::fs::read_to_string(&path).unwrap_or_default(); - 845
match vak_flow::parse_flow(&toml_str) { - 846
Ok(flow) => { - 847
let layers = vak_flow::parse::layers(&flow).unwrap_or_default(); - 848
println!( - 849
"✓ {} valid — {} nodes, {} layers", - 850
flow.name, - 851
flow.nodes.len(), - 852
layers.len() - 853
); - 854
for (i, layer) in layers.iter().enumerate() { - 855
println!(" layer {}: {}", i + 1, layer.join(", ")); - 856
} - 857
0 - 858
} - 859
Err(e) => { - 860
eprintln!("✗ invalid: {e}"); - 861
1 - 862
} - 863
} - 864
} - 865
FlowAction::Run { - 866
name, - 867
resume, - 868
yes, - 869
provider, - 870
model, - 871
accept_drift, - 872
trust, - 873
} => { - 874
let trusted = resolve_trust(&cwd, trust, false); - 875
if trusted { - 876
vak_config::load_env_file(std::path::Path::new(".env")); - 877
} - 878
run_flow_exec( - 879
cwd, - 880
name, - 881
resume, - 882
accept_drift, - 883
yes, - 884
provider, - 885
model, - 886
trusted, - 887
) - 888
.await - 889
} - 890
} - 891
} - 892
- 893
#[allow(clippy::too_many_arguments)] - 894
async fn run_flow_exec( - 895
cwd: PathBuf, - 896
name: String, - 897
resume: bool, - 898
accept_drift: bool, - 899
yes: bool, - 900
provider_flag: Option<String>, - 901
model_flag: Option<String>, - 902
trusted: bool, - 903
) -> i32 { - 904
let core = match Core::new_with_trust(cwd.clone(), trusted).map(|c| with_cli_surface(c, yes)) { - 905
Ok(c) => c, - 906
Err(e) => { - 907
eprintln!("error: {e}"); - 908
return 2; - 909
} - 910
}; - 911
let Some((_, path)) = discover_flows(&core.cwd().clone()) - 912
.into_iter() - 913
.find(|(n, _)| *n == name) - 914
else { - 915
eprintln!("error: flow '{name}' not found"); - 916
return 2; - 917
}; - 918
let toml_str = std::fs::read_to_string(&path).unwrap_or_default(); - 919
let flow = match vak_flow::parse_flow(&toml_str) { - 920
Ok(f) => f, - 921
Err(e) => { - 922
eprintln!("✗ invalid: {e}"); - 923
return 1; - 924
} - 925
}; - 926
- 927
// Plan preview (docs/design/10-flows.md): show the shape before any effect. - 928
if let Ok(layers) = vak_flow::parse::layers(&flow) { - 929
let rendered: Vec<String> = layers.iter().map(|l| l.join(", ")).collect(); - 930
eprintln!("plan: {}", rendered.join(" | ")); - 931
} - 932
- 933
if provider_flag.is_some() || model_flag.is_some() { - 934
let route = core.effective_route(); - 935
core.set_route( - 936
provider_flag.unwrap_or(route.provider), - 937
model_flag.unwrap_or(route.model), - 938
); - 939
} - 940
let provider = match core.provider() { - 941
Ok(p) => p, - 942
Err(e) => { - 943
eprintln!("error: {e}"); - 944
return 2; - 945
} - 946
}; - 947
- 948
let session = match core.start_session().await { - 949
Ok(s) => s, - 950
Err(e) => { - 951
eprintln!("error: {e}"); - 952
return 2; - 953
} - 954
}; - 955
let parent_session_id = session - 956
.header() - 957
.map(|h| h.session_id.clone()) - 958
.unwrap_or_default(); - 959
- 960
let approver: Option<std::sync::Arc<dyn vak_agent::Approver>> = Some(if yes { - 961
std::sync::Arc::new(vak_agent::AutoApprove) - 962
} else { - 963
std::sync::Arc::new(vak_agent::AutoDeny) - 964
}); - 965
- 966
let engine = match core.build_permission_engine(&core.extra_allow_snapshot()) { - 967
Ok(e) => e, - 968
Err(e) => { - 969
eprintln!("error: {e}"); - 970
return 2; - 971
} - 972
}; - 973
- 974
let runs_dir = core.sessions_home().join("flow-runs").join(&name); - 975
let state_path = if resume { - 976
let mut latest: Option<PathBuf> = None; - 977
if let Ok(entries) = std::fs::read_dir(&runs_dir) { - 978
let mut files: Vec<_> = entries.flatten().map(|e| e.path()).collect(); - 979
files.sort(); - 980
latest = files.pop(); - 981
} - 982
match latest { - 983
Some(p) => p, - 984
None => { - 985
eprintln!("error: no previous run to resume"); - 986
return 2; - 987
} - 988
} - 989
} else { - 990
let run_id = format!( - 991
"{}", - 992
std::time::SystemTime::now() - 993
.duration_since(std::time::UNIX_EPOCH) - 994
.unwrap_or_default() - 995
.as_nanos() - 996
); - 997
runs_dir.join(format!("{run_id}.json")) - 998
}; - 999
- 1000
// Recovery audit (docs/design/10-flows.md): classify the snapshot vs
Indexing the workspace…
Vakyartha documentation is discovering safe artifacts, anchors, and source references.