- 23
/// message authorizes completion. - 24
UserCompletionRequired, - 25
/// The request requires execution or tool receipts, but none were produced. - 26
ExecutionReceiptMissing { act: String, hint: String }, - 27
/// A tool failed with an error and the model neither repaired it nor reported the blocker. - 28
UnresolvedToolFailure { tool: String, error: String }, - 29
} - 30
- 31
impl BlockReason { - 32
pub fn message(&self) -> String { - 33
match self { - 34
BlockReason::TruncatedPlan(tail) => format!( - 35
"your last message appears cut off mid-plan (ends with {tail:?}). \ - 36
Finish the work now; if you are actually done, say so plainly." - 37
), - 38
BlockReason::VerificationMissing => String::from( - 39
"the task asked for verification or sandbox execution, but no substantive commands were \ - 40
executed this run. Call the `bash` tool to actually execute, build, or verify the work now; do not print commands or dummy echo statements.", - 41
), - 42
BlockReason::VerificationStale => String::from( - 43
"the task changed files after its last verification command. \ - 44
Call the `bash` tool to run the verification again before finishing; do not describe it in text.", - 45
), - 46
BlockReason::UserCompletionRequired => String::from( - 47
"the user asked you to keep working until they say done. Continue making \ - 48
useful progress; do not declare completion yet.", - 49
), - 50
BlockReason::ExecutionReceiptMissing { act, hint } => format!( - 51
"the request requires {act} ({hint}), but no execution or modification receipts were produced. \ - 52
Execute the necessary commands or file edits now using the available tools; do not just describe the work in prose.", - 53
), - 54
BlockReason::UnresolvedToolFailure { tool, error } => format!( - 55
"the `{tool}` tool failed with an error: {error}. \ - 56
Repair the failure using the appropriate tools, or clearly report the concrete blocker to the user.", - 57
), - 58
} - 59
} - 60
} - 61
- 62
/// Summary of tool execution receipts produced during a run. - 63
#[derive(Debug, Clone, Default, PartialEq, Eq)] - 64
pub struct ReceiptSummary { - 65
/// Proven saved file from the same intent thread before a step-limit - 66
/// continuation. Only counts with a successful inspection in this turn. - 67
pub continued_saved_file: bool, - 68
/// Total tool invocations attempted. - 69
pub total_tool_calls: u32, - 70
/// Number of tool calls that completed with ToolRunOutput::Ok. - 71
pub successful_tool_calls: u32, - 72
/// Number of tool calls that completed with ToolRunOutput::Err. - 73
pub failed_tool_calls: u32, - 74
/// Number of substantive bash invocations. - 75
pub substantive_bash_calls: u32, - 76
/// Number of files modified/written (via edit, write, etc.). - 77
pub files_modified: u32, - 78
/// Number of code files modified/written. - 79
pub code_files_modified: u32, - 80
/// Number of documentation/content/non-code files modified/written. - 81
pub doc_files_modified: u32, - 82
/// Number of inspection/read tool calls (read_file, glob, grep, etc.). - 83
pub read_or_inspected: u32, - 84
/// Inspections that actually returned successfully. A prior saved file - 85
/// only counts after one of these in the continuation turn. - 86
pub successful_inspections: u32, - 87
/// Work that succeeded outside the built-in tools: an integration's tool - 88
/// invoked through `mcp` (`action = "call"`) or a delegated `task`, - 89
/// whose worker keeps its own receipts. Counted on success only. - 90
pub external_effects: u32, - 91
/// Most recent unresolved tool failure, if any. - 92
pub unresolved_error: Option<(String, String)>, - 93
} - 94
- 95
impl ReceiptSummary { - 96
pub fn has_execution_receipt(&self) -> bool { - 97
self.substantive_bash_calls > 0 - 98
|| self.files_modified > 0 - 99
|| self.external_effects > 0 - 100
|| (self.continued_saved_file && self.successful_inspections > 0) - 101
} - 102
- 103
pub fn has_inspection_receipt(&self) -> bool { - 104
self.read_or_inspected > 0 || self.has_execution_receipt() - 105
} - 106
- 107
pub fn has_any_receipt(&self) -> bool { - 108
self.successful_tool_calls > 0 - 109
} - 110
} - 111
- 112
fn reports_blocker(text: &str, tool: &str, error: &str) -> bool { - 113
let lower = text.to_ascii_lowercase(); - 114
let err_first_line = error - 115
.lines() - 116
.next() - 117
.unwrap_or("") - 118
.trim() - 119
.to_ascii_lowercase(); - 120
let keywords = [ - 121
"error", - 122
"failed", - 123
"failure", - 124
"failing", - 125
"blocked", - 126
"blocker", - 127
"could not", - 128
"cannot", - 129
"can't", - 130
"unable to", - 131
"unavailable", - 132
"issue", - 133
"problem", - 134
"exit code", - 135
"exception", - 136
"recover", - 137
"repaired", - 138
"unsupported", - 139
"guard", - 140
"denied", - 141
"denial", - 142
"rejected", - 143
"rejection", - 144
"repeated", - 145
"skip", - 146
"skipping", - 147
]; - 148
let mentions_keyword = keywords.iter().any(|k| lower.contains(k)); - 149
let mentions_tool = lower.contains(&tool.to_ascii_lowercase()); - 150
let mentions_snippet = !err_first_line.is_empty() && lower.contains(&err_first_line); - 151
mentions_keyword || mentions_tool || mentions_snippet - 152
} - 153
- 154
#[derive(Debug, Clone)] - 155
pub struct StopPolicy { - 156
/// Gate on truncated-looking final messages. - 157
pub marker_gate: bool, - 158
/// Gate on promised-but-never-run verification. - 159
pub verify_gate: bool, - 160
/// Hard cap of guard continuations per run. - 161
pub max_blocks: u32, - 162
} - 163
- 164
impl Default for StopPolicy { - 165
fn default() -> Self { - 166
StopPolicy { - 167
marker_gate: true, - 168
verify_gate: true, - 169
max_blocks: 2, - 170
} - 171
} - 172
} - 173
- 174
impl StopPolicy { - 175
pub fn requires_user_completion(prompt: &str) -> bool { - 176
let p = prompt.to_ascii_lowercase(); - 177
[ - 178
"until i say done", - 179
"until i tell you to stop", - 180
"until i tell you you're done", - 181
"keep working until", - 182
"keep improving until", - 183
"don't stop until", - 184
"do not stop until", - 185
] - 186
.iter() - 187
.any(|marker| p.contains(marker)) - 188
} - 189
- 190
pub fn is_done_message(message: &str) -> bool { - 191
let normalized = message.trim().to_ascii_lowercase(); - 192
[ - 193
"done", - 194
"stop", - 195
"you can stop", - 196
"that's enough", - 197
"that’s enough", - 198
] - 199
.iter() - 200
.any(|marker| normalized == *marker) - 201
} - 202
- 203
/// Conservative trailing-intent patterns: only fire on line-final - 204
/// markers so normal prose summaries never match. - 205
fn truncated_plan(final_text: &str) -> Option<BlockReason> { - 206
let trimmed = final_text.trim_end(); - 207
if trimmed.is_empty() { - 208
return None; - 209
} - 210
// Unclosed fenced code block: strong truncation signal. - 211
if trimmed.matches("```").count() % 2 == 1 { - 212
return Some(BlockReason::TruncatedPlan("unclosed code fence".into())); - 213
} - 214
let last_line = trimmed.lines().next_back()?.trim_end(); - 215
if last_line.ends_with(':') && !last_line.starts_with('#') && last_line.len() < 200 { - 216
return Some(BlockReason::TruncatedPlan(format!( - 217
"'{}'", - 218
last_line - 219
.chars() - 220
.rev() - 221
.take(40) - 222
.collect::<Vec<_>>() - 223
.into_iter() - 224
.rev() - 225
.collect::<String>() - 226
))); - 227
} - 228
const MARKERS: [&str; 8] = [ - 229
"now i'll", - 230
"now let me", - 231
"let me ", - 232
"i will ", - 233
"i'll ", - 234
"going to ", - 235
"next,", - 236
"then,", - 237
]; - 238
let lower = last_line.to_ascii_lowercase(); - 239
if lower.len() < 300 - 240
&& MARKERS - 241
.iter() - 242
.any(|m| lower.starts_with(m) || lower.contains(m)) - 243
&& !lower.ends_with('.') - 244
&& !lower.ends_with('!') - 245
&& !lower.ends_with('?') - 246
{ - 247
return Some(BlockReason::TruncatedPlan("a plan marker".into())); - 248
} - 249
None - 250
} - 251
- 252
/// True when the prompt itself explicitly asks for executed verification, testing, or sandbox commands. - 253
pub fn demands_code_execution(prompt: &str) -> bool { - 254
let stripped = if let Some(idx) = prompt.find("[Scheduled-run context:") { - 255
&prompt[..idx] - 256
} else { - 257
prompt - 258
}; - 259
const DEMANDS: [&str; 11] = [ - 260
"must pass", - 261
"tests pass", - 262
"test pass", - 263
"run it", - 264
"run them", - 265
"run the test", - 266
"run tests", - 267
"verify by running", - 268
"prove by running", - 269
"run in sandbox", - 270
"execute in sandbox", - 271
]; - 272
let p = stripped.to_ascii_lowercase(); - 273
if DEMANDS.iter().any(|d| p.contains(d)) { - 274
return true; - 275
} - 276
if p.contains("sandbox") - 277
&& ["run", "show", "test", "build", "execute", "serve", "start"] - 278
.iter() - 279
.any(|action| p.contains(action)) - 280
{ - 281
return true; - 282
} - 283
if p.contains("cargo test") - 284
|| p.contains("pytest") - 285
|| p.contains("npm test") - 286
|| p.contains("go test") - 287
|| p.contains("python -m unittest") - 288
{ - 289
return true; - 290
} - 291
p.contains("run ") - 292
&& [ - 293
"test", "tests", "command", "script", "check", "app", "code", "python", "cargo", - 294
"binary", - 295
] - 296
.iter() - 297
.any(|word| p.contains(word)) - 298
} - 299
- 300
/// True when the prompt asks for any verification (code or universal/content). - 301
pub fn demands_verification(prompt: &str) -> bool { - 302
if Self::demands_code_execution(prompt) { - 303
return true; - 304
} - 305
let stripped = if let Some(idx) = prompt.find("[Scheduled-run context:") { - 306
&prompt[..idx] - 307
} else { - 308
prompt - 309
}; - 310
let p = stripped.to_ascii_lowercase(); - 311
const VERIFY_MARKERS: [&str; 7] = [ - 312
"verify that", - 313
"verify the", - 314
"double check", - 315
"double-check", - 316
"make sure that", - 317
"check that", - 318
"verify whether", - 319
]; - 320
VERIFY_MARKERS.iter().any(|m| p.contains(m)) - 321
} - 322
- 323
/// True when the assistant response claims execution or emits shell scripts without tool calls having run. - 324
pub fn claims_execution_unexecuted(final_text: &str) -> bool { - 325
let lower = final_text.to_ascii_lowercase(); - 326
let markers = [ - 327
"use the bash tool", - 328
"use the `bash` tool", - 329
"using the bash tool", - 330
"using the `bash` tool", - 331
"run the python script", - 332
"running the python script", - 333
"execute the python script", - 334
"executing the python script", - 335
"execute in the sandbox", - 336
"running in the sandbox", - 337
"run in the sandbox", - 338
"execute in sandbox", - 339
"running in sandbox", - 340
]; - 341
if markers.iter().any(|m| lower.contains(m)) { - 342
return true; - 343
} - 344
if (lower.contains("```bash") || lower.contains("```sh")) - 345
&& (lower.contains(".vak/scratch") - 346
|| lower.contains("python3 ") - 347
|| lower.contains("node ") - 348
|| lower.contains("cargo ")) - 349
{ - 350
return true; - 351
} - 352
false - 353
} - 354
- 355
/// Intent- and receipt-driven evaluation of completion validity. - 356
pub fn evaluate_receipts( - 357
&self, - 358
prompt: &str, - 359
final_text: &str, - 360
outcome: Option<&vak_intent::OutcomeSpec>, - 361
receipts: &ReceiptSummary, - 362
verification_stale: bool, - 363
) -> Option<BlockReason> { - 364
if final_text.trim().is_empty() { - 365
return None; - 366
} - 367
if Self::requires_user_completion(prompt) { - 368
return Some(BlockReason::UserCompletionRequired); - 369
} - 370
if self.marker_gate - 371
&& let Some(r) = Self::truncated_plan(final_text) - 372
{ - 373
return Some(r); - 374
} - 375
- 376
// If a tool failed and hasn't been repaired or reported in text, block. - 377
if let Some((tool, err)) = &receipts.unresolved_error - 378
&& !reports_blocker(final_text, tool, err) - 379
{ - 380
return Some(BlockReason::UnresolvedToolFailure { - 381
tool: tool.clone(), - 382
error: err.clone(), - 383
}); - 384
} - 385
- 386
// Intent-driven gate: when outcome specification is available. The - 387
// engagement's stop profile (docs/design/47-commitment-kernel.md) - 388
// decides first; the typed acts refine it. - 389
if let Some(spec) = outcome { - 390
use vak_intent::StopProfile; - 391
// `Verification`: a checkable result was demanded. An execution - 392
// receipt is required, and it must not be stale. - 393
if spec.stop == StopProfile::Verification { - 394
if !receipts.has_execution_receipt() { - 395
return Some(BlockReason::ExecutionReceiptMissing { - 396
act: spec.deliverable_act().unwrap_or("verification").to_string(), - 397
hint: "run the check that proves this is done".into(), - 398
}); - 399
} - 400
if verification_stale && receipts.code_files_modified > 0 { - 401
return Some(BlockReason::VerificationStale); - 402
} - 403
} - 404
if spec.stop == StopProfile::Effect || spec.requires_execution() { - 405
if !receipts.has_execution_receipt() { - 406
let act = spec.deliverable_act().unwrap_or("execution").to_string(); - 407
return Some(BlockReason::ExecutionReceiptMissing { - 408
act, - 409
hint: "run code, build, test, or modify files".into(), - 410
}); - 411
} - 412
if self.verify_gate - 413
&& verification_stale - 414
&& (spec.requires_execution() || Self::demands_verification(prompt)) - 415
&& (receipts.code_files_modified > 0 || Self::demands_code_execution(prompt)) - 416
{ - 417
return Some(BlockReason::VerificationStale); - 418
} - 419
// `Inspection` on its own gates nothing: "something was looked - 420
// at" includes the material the request carried (an attachment, - 421
// pasted text), which leaves no receipt. Only a `locate` act - 422
// demands an inspection receipt. - 423
} else if spec.requires_inspection() { - 424
let direct_substantive = final_text.trim().len() >= 80 - 425
&& !Self::demands_code_execution(prompt) - 426
&& !Self::claims_execution_unexecuted(final_text); - 427
if !receipts.has_inspection_receipt() && !direct_substantive { - 428
let act = spec.deliverable_act().unwrap_or("inspection").to_string(); - 429
return Some(BlockReason::ExecutionReceiptMissing { - 430
act, - 431
hint: "read, search, inspect files or data".into(), - 432
}); - 433
} - 434
} else if spec.requires_tool() && !receipts.has_any_receipt() { - 435
let direct_substantive = final_text.trim().len() >= 80 - 436
&& !Self::demands_code_execution(prompt) - 437
&& !Self::claims_execution_unexecuted(final_text); - 438
if !direct_substantive { - 439
return Some(BlockReason::ExecutionReceiptMissing { - 440
act: "tool execution".into(), - 441
hint: "execute relevant tools".into(), - 442
}); - 443
} - 444
} - 445
} - 446
- 447
// Verification and execution gate: runs whenever verify_gate is enabled. - 448
if self.verify_gate { - 449
let demands_code = Self::demands_code_execution(prompt); - 450
let claims_exec = Self::claims_execution_unexecuted(final_text); - 451
- 452
if claims_exec { - 453
return Some(BlockReason::VerificationMissing); - 454
} - 455
- 456
if demands_code && receipts.substantive_bash_calls == 0 { - 457
return Some(BlockReason::VerificationMissing); - 458
} - 459
- 460
if Self::demands_verification(prompt) && receipts.substantive_bash_calls == 0 { - 461
// If code files were touched, verification commands are required. - 462
if receipts.code_files_modified > 0 { - 463
return Some(BlockReason::VerificationMissing); - 464
} - 465
// If no tools were called and the text is not a substantive direct answer: - 466
let direct_substantive = final_text.trim().len() >= 80; - 467
if !receipts.has_any_receipt() && !direct_substantive { - 468
return Some(BlockReason::VerificationMissing); - 469
} - 470
// If an execution deliverable was required or a specific file target was requested, - 471
// but no files were modified or inspected: - 472
let lower_p = prompt.to_ascii_lowercase(); - 473
let mentions_file_target = lower_p.contains(".md") - 474
|| lower_p.contains(".txt") - 475
|| lower_p.contains(".json") - 476
|| lower_p.contains(".csv") - 477
|| lower_p.contains("into ") - 478
|| lower_p.contains("in file") - 479
|| lower_p.contains("in the file"); - 480
if (outcome.map(|s| s.requires_execution()).unwrap_or(false) - 481
|| mentions_file_target) - 482
&& receipts.files_modified == 0 - 483
&& receipts.read_or_inspected == 0 - 484
{ - 485
return Some(BlockReason::VerificationMissing); - 486
} - 487
// Universal tasks (documentation, research synthesis, lifestyle, notes, recipes, - 488
// explanations) where content was inspected, written, or substantively answered - 489
// are NOT falsely blocked on non-existent bash commands. - 490
} - 491
- 492
// A re-run is owed only when a check was asked for: by the - 493
// reading, which decides what completion requires, or by the - 494
// request's own words. An edit alone does not owe one — "add a - 495
// subtract function" asked for the function — and demanding a - 496
// check the surface could not run (a shell needing an approver - 497
// nobody could be) looped a live turn until the block cap. - 498
let check_owed = outcome.is_none_or(|spec| { - 499
spec.stop == vak_intent::StopProfile::Verification || spec.requires_execution() - 500
}) || demands_code - 501
|| Self::demands_verification(prompt); - 502
if verification_stale - 503
&& check_owed - 504
&& (receipts.code_files_modified > 0 || demands_code) - 505
{ - 506
return Some(BlockReason::VerificationStale); - 507
} - 508
} - 509
None - 510
} - 511
- 512
/// Returns Some(reason) when completion should be blocked. - 513
pub fn evaluate( - 514
&self, - 515
prompt: &str, - 516
final_text: &str, - 517
bash_calls_this_run: u32, - 518
) -> Option<BlockReason> { - 519
let receipts = ReceiptSummary { - 520
substantive_bash_calls: bash_calls_this_run, - 521
successful_tool_calls: if bash_calls_this_run > 0 { - 522
bash_calls_this_run - 523
} else { - 524
0 - 525
}, - 526
..Default::default() - 527
}; - 528
self.evaluate_receipts(prompt, final_text, None, &receipts, false) - 529
} - 530
- 531
pub fn evaluate_with_state( - 532
&self, - 533
prompt: &str, - 534
final_text: &str, - 535
bash_calls_this_run: u32, - 536
verification_stale: bool, - 537
) -> Option<BlockReason> { - 538
let receipts = ReceiptSummary { - 539
substantive_bash_calls: bash_calls_this_run, - 540
code_files_modified: if verification_stale { 1 } else { 0 }, - 541
files_modified: if verification_stale { 1 } else { 0 }, - 542
successful_tool_calls: if bash_calls_this_run > 0 { - 543
bash_calls_this_run - 544
} else { - 545
0 - 546
}, - 547
..Default::default() - 548
}; - 549
self.evaluate_receipts(prompt, final_text, None, &receipts, verification_stale) - 550
} - 551
} - 552
- 553
/// Checks whether a file path points to an executable, compilable, or script source file - 554
/// (as opposed to documentation, notes, recipes, data, or content assets). - 555
pub fn is_code_path(path: &str) -> bool { - 556
let p = std::path::Path::new(path); - 557
match p - 558
.extension() - 559
.and_then(|ext| ext.to_str()) - 560
.map(|ext| ext.to_ascii_lowercase()) - 561
{ - 562
Some(ext) => matches!( - 563
ext.as_str(), - 564
"rs" | "py" - 565
| "js" - 566
| "mjs" - 567
| "cjs" - 568
| "ts" - 569
| "tsx" - 570
| "jsx" - 571
| "c" - 572
| "cpp" - 573
| "cc" - 574
| "cxx" - 575
| "h" - 576
| "hpp" - 577
| "go" - 578
| "java" - 579
| "kt" - 580
| "kts" - 581
| "rb" - 582
| "php" - 583
| "swift" - 584
| "scala" - 585
| "sh" - 586
| "bash" - 587
| "zsh" - 588
| "fish" - 589
| "ps1" - 590
| "bat" - 591
| "cmd" - 592
| "lua" - 593
| "pl" - 594
| "pm" - 595
| "r" - 596
| "jl" - 597
| "dart" - 598
| "zig" - 599
| "nim" - 600
| "sql" - 601
), - 602
None => false, - 603
} - 604
} - 605
- 606
/// Checks if a shell command is substantive rather than a dummy echo/no-op evasion. - 607
pub fn is_substantive_command(command: &str) -> bool { - 608
let trimmed = command.trim(); - 609
if trimmed.is_empty() { - 610
return false; - 611
} - 612
// If it writes to a file or pipes to another command, it has side effects or processing - 613
if trimmed.contains('>') || trimmed.contains('|') { - 614
return true; - 615
} - 616
// Check if the command line is an explicit evasion claiming verification without doing work - 617
let lower = trimmed.to_ascii_lowercase(); - 618
if (lower.starts_with("echo ") || lower.starts_with("printf ")) - 619
&& (lower.contains("verification") - 620
|| lower.contains("shell execution path is functional") - 621
|| lower.contains("verified") - 622
|| lower.contains("dummy")) - 623
{ - 624
return false; - 625
} - 626
let first_word = trimmed - 627
.split_whitespace() - 628
.next() - 629
.unwrap_or("") - 630
.trim_start_matches("./"); - 631
let is_noop = matches!(first_word, ":" | "true" | "false" | "exit"); - 632
!is_noop - 633
} - 634
- 635
#[cfg(test)] - 636
mod tests { - 637
use super::*; - 638
- 639
#[test] - 640
fn truncated_shapes_block_normal_prose_passes() { - 641
let p = StopPolicy::default(); - 642
assert!( - 643
p.evaluate("", "Working on it:\n- fix parser\n- then", 1) - 644
.is_none() - 645
|| true - 646
); // sanity no-op to keep structure - 647
- 648
// trailing colon line - 649
assert!(matches!( - 650
p.evaluate("", "Let me check the config:", 3), - 651
Some(BlockReason::TruncatedPlan(_)) - 652
)); - 653
// unclosed fence - 654
assert!(matches!( - 655
p.evaluate("", "here is the patch:\n```rust\nfn a() {}", 3), - 656
Some(BlockReason::TruncatedPlan(_)) - 657
)); - 658
// plan-marker final line without terminal punctuation - 659
assert!(matches!( - 660
p.evaluate( - 661
"", - 662
"done with part one.\nNow I'll write the store module", - 663
3 - 664
), - 665
Some(BlockReason::TruncatedPlan(_)) - 666
)); - 667
// normal summary passes - 668
assert_eq!( - 669
p.evaluate("", "All done. Tests pass.\nSummary:\n- added x", 3), - 670
None - 671
); - 672
assert_eq!( - 673
p.evaluate("", "Fixed both issues. cargo test green.", 3), - 674
None - 675
); - 676
// headings ending in colon are fine (e.g. 'Summary:') - 677
assert_eq!(p.evaluate("", "Results\n# Summary:", 3), None); - 678
} - 679
- 680
#[test] - 681
fn verify_gate_needs_demand_and_zero_bash() { - 682
let p = StopPolicy::default(); - 683
let prompt = "Create fizzbuzz.py and run it to prove that it works."; - 684
assert!(matches!( - 685
p.evaluate(prompt, "Created the file.", 0), - 686
Some(BlockReason::VerificationMissing) - 687
)); - 688
assert_eq!(p.evaluate(prompt, "Created the file.", 2), None); - 689
// no demand -> never blocks - 690
assert_eq!(p.evaluate("Write a haiku about sand.", "Done.", 0), None); - 691
} - 692
- 693
#[test] - 694
fn verify_gate_recognizes_explicit_run_commands() { - 695
let p = StopPolicy::default(); - 696
assert!(matches!( - 697
p.evaluate( - 698
"Read README.md, implement the change, then run python3 test_app.py.", - 699
"I need more details.", - 700
0 - 701
), - 702
Some(BlockReason::VerificationMissing) - 703
)); - 704
} - 705
- 706
#[test] - 707
fn explicit_until_done_request_requires_user_release() { - 708
let p = StopPolicy::default(); - 709
assert!(matches!( - 710
p.evaluate( - 711
"Keep improving the project until I say done.", - 712
"Improved it.", - 713
1 - 714
), - 715
Some(BlockReason::UserCompletionRequired) - 716
)); - 717
assert!(StopPolicy::is_done_message("done")); - 718
assert!(!StopPolicy::is_done_message( - 719
"done, and here is the summary" - 720
)); - 721
} - 722
- 723
/// An edit the reading did not hold to a check owes none: authoring a - 724
/// function into a code file is done when the function is there. - 725
#[test] - 726
fn an_edit_owes_no_check_the_reading_did_not_ask_for() { - 727
let p = StopPolicy::default(); - 728
let authoring = vak_intent::OutcomeSpec::from_reading( - 729
"explain what calc.py does, then add a subtract function to it", - 730
&vak_intent::Reading { - 731
act: vak_intent::Act::Author, - 732
..vak_intent::Reading::general() - 733
}, - 734
4, - 735
); - 736
let edited = ReceiptSummary { - 737
total_tool_calls: 1, - 738
successful_tool_calls: 1, - 739
files_modified: 1, - 740
code_files_modified: 1, - 741
..Default::default() - 742
}; - 743
assert_eq!( - 744
p.evaluate_receipts( - 745
"explain what calc.py does, then add a subtract function to it", - 746
"calc.py now defines add and subtract.", - 747
Some(&authoring), - 748
&edited, - 749
true, - 750
), - 751
None - 752
); - 753
// A fix is held to a check: it modifies code the reading expects to - 754
// be proven. - 755
let fixing = vak_intent::OutcomeSpec::from_reading( - 756
"fix the off-by-one in calc.py", - 757
&vak_intent::Reading { - 758
act: vak_intent::Act::Modify, - 759
..vak_intent::Reading::general() - 760
}, - 761
4, - 762
); - 763
let fixed = ReceiptSummary { - 764
substantive_bash_calls: 1, - 765
..edited - 766
}; - 767
assert_eq!( - 768
p.evaluate_receipts( - 769
"fix the off-by-one in calc.py", - 770
"Fixed.", - 771
Some(&fixing), - 772
&fixed, - 773
true, - 774
), - 775
Some(BlockReason::VerificationStale) - 776
); - 777
} - 778
- 779
#[test] - 780
fn stale_verification_blocks_after_a_file_change() { - 781
let p = StopPolicy::default(); - 782
assert_eq!( - 783
p.evaluate_with_state("implement it and run the tests", "Done.", 1, true), - 784
Some(BlockReason::VerificationStale) - 785
); - 786
assert_eq!( - 787
p.evaluate_with_state("implement it and run the tests", "Done.", 1, false), - 788
None - 789
); - 790
} - 791
- 792
#[test] - 793
fn empty_final_text_is_left_alone() { - 794
let p = StopPolicy::default(); - 795
assert_eq!(p.evaluate("run the tests", "", 0), None); - 796
} - 797
- 798
#[test] - 799
fn test_substantive_command_detection() { - 800
assert!(!is_substantive_command( - 801
"echo \"Verification successful: Shell execution path is functional.\"" - 802
)); - 803
assert!(!is_substantive_command("echo 'verification passed'")); - 804
assert!(!is_substantive_command("printf 'verified\\n'")); - 805
assert!(!is_substantive_command("true")); - 806
assert!(!is_substantive_command(":")); - 807
assert!(!is_substantive_command("exit 0")); - 808
assert!(!is_substantive_command("")); - 809
- 810
assert!(is_substantive_command("echo ran")); - 811
assert!(is_substantive_command("echo 'hello'")); - 812
assert!(is_substantive_command("echo 'hello' > index.html")); - 813
assert!(is_substantive_command("echo 'hi' | wc -l")); - 814
assert!(is_substantive_command("python3 -m unittest")); - 815
assert!(is_substantive_command("cargo test")); - 816
assert!(is_substantive_command("npm start")); - 817
assert!(is_substantive_command("node server.js")); - 818
} - 819
- 820
#[test] - 821
fn test_sandbox_demands_verification() { - 822
let p = StopPolicy::default(); - 823
let prompt = - 824
"make in using react with beautifull design and run them in sandbox and show me"; - 825
assert!(matches!( - 826
p.evaluate(prompt, "Here is the code in a block.", 0), - 827
Some(BlockReason::VerificationMissing) - 828
)); - 829
} - 830
- 831
#[test] - 832
fn test_outcome_intent_requires_execution_receipt() { - 833
let p = StopPolicy::default(); - 834
let mut reading = vak_intent::Reading::general(); - 835
reading.act = vak_intent::Act::Modify; - 836
let spec = vak_intent::OutcomeSpec::from_reading("create an svg animation", &reading, 1); - 837
assert!(spec.requires_execution()); - 838
- 839
// 0 receipts -> blocked - 840
let empty_receipts = ReceiptSummary::default(); - 841
let blocked = p.evaluate_receipts( - 842
"create an svg animation", - 843
"Here is your svg:\n```xml\n<svg/>\n```", - 844
Some(&spec), - 845
&empty_receipts, - 846
false, - 847
); - 848
assert!(matches!( - 849
blocked, - 850
Some(BlockReason::ExecutionReceiptMissing { .. }) - 851
)); - 852
- 853
// with substantive bash receipt -> allowed - 854
let with_bash = ReceiptSummary { - 855
substantive_bash_calls: 1, - 856
successful_tool_calls: 1, - 857
..Default::default() - 858
}; - 859
assert_eq!( - 860
p.evaluate_receipts( - 861
"create an svg animation", - 862
"Created and verified.", - 863
Some(&spec), - 864
&with_bash, - 865
false - 866
), - 867
None - 868
); - 869
- 870
// Work done through an integration or a delegated worker is - 871
// execution too: the stop gate must not demand a shell receipt for - 872
// an email an MCP server sent. - 873
let external = ReceiptSummary { - 874
external_effects: 1, - 875
successful_tool_calls: 1, - 876
..Default::default() - 877
}; - 878
assert!(external.has_execution_receipt()); - 879
assert_eq!( - 880
p.evaluate_receipts( - 881
"create an svg animation", - 882
"Created and verified.", - 883
Some(&spec), - 884
&external, - 885
false - 886
), - 887
None - 888
); - 889
- 890
// with file write receipt -> allowed - 891
let with_file = ReceiptSummary { - 892
files_modified: 1, - 893
successful_tool_calls: 1, - 894
..Default::default() - 895
}; - 896
assert_eq!( - 897
p.evaluate_receipts( - 898
"create an svg animation", - 899
"Created file.", - 900
Some(&spec), - 901
&with_file, - 902
false - 903
), - 904
None - 905
); - 906
} - 907
- 908
#[test] - 909
fn capped_continuation_counts_prior_saved_file_only_after_successful_inspection() { - 910
let policy = StopPolicy::default(); - 911
let mut reading = vak_intent::Reading::general(); - 912
reading.act = vak_intent::Act::Modify; - 913
let spec = vak_intent::OutcomeSpec::from_reading("create report.csv", &reading, 1); - 914
let prompt = "Continue the most recent unfinished task"; - 915
let answer = "The saved report contains 60 minutes."; - 916
let prior_only = ReceiptSummary { - 917
continued_saved_file: true, - 918
read_or_inspected: 1, - 919
..Default::default() - 920
}; - 921
assert!(matches!( - 922
policy.evaluate_receipts(prompt, answer, Some(&spec), &prior_only, false), - 923
Some(BlockReason::ExecutionReceiptMissing { .. }) - 924
)); - 925
let inspected = ReceiptSummary { - 926
successful_inspections: 1, - 927
..prior_only - 928
}; - 929
assert_eq!( - 930
policy.evaluate_receipts(prompt, answer, Some(&spec), &inspected, false), - 931
None - 932
); - 933
} - 934
- 935
#[test] - 936
fn test_outcome_conversational_allows_prose_completion() { - 937
let p = StopPolicy::default(); - 938
let mut reading = vak_intent::Reading::general(); - 939
reading.act = vak_intent::Act::Answer; - 940
let spec = vak_intent::OutcomeSpec::from_reading("what is rust?", &reading, 1); - 941
assert!(!spec.requires_execution()); - 942
assert!(!spec.requires_tool()); - 943
- 944
let receipts = ReceiptSummary::default(); - 945
assert_eq!( - 946
p.evaluate_receipts( - 947
"what is rust?", - 948
"Rust is a systems programming language.", - 949
Some(&spec), - 950
&receipts, - 951
false - 952
), - 953
None - 954
); - 955
} - 956
- 957
#[test] - 958
fn test_claims_execution_unexecuted_blocks_even_with_outcome_spec() { - 959
let p = StopPolicy::default(); - 960
let reading = vak_intent::Reading::general(); - 961
let spec = vak_intent::OutcomeSpec::from_reading("can you run it and show", &reading, 1); - 962
let receipts = ReceiptSummary::default(); - 963
- 964
let blocked = p.evaluate_receipts( - 965
"can you run it and show", - 966
"I will use the bash tool to execute a Python script:\n```bash\npython3 script.py\n```", - 967
Some(&spec), - 968
&receipts, - 969
false, - 970
); - 971
assert_eq!(blocked, Some(BlockReason::VerificationMissing)); - 972
} - 973
- 974
#[test] - 975
fn test_unresolved_tool_failure_blocks_unless_reported() { - 976
let p = StopPolicy::default(); - 977
let receipts = ReceiptSummary { - 978
total_tool_calls: 1, - 979
failed_tool_calls: 1, - 980
unresolved_error: Some(("bash".into(), "exit code 1: compile error".into())), - 981
..Default::default() - 982
}; - 983
- 984
// Model hallucinates success without reporting error -> blocked - 985
let blocked = p.evaluate_receipts( - 986
"build it", - 987
"All done! Everything succeeded.", - 988
None, - 989
&receipts, - 990
false, - 991
); - 992
assert!(matches!( - 993
blocked, - 994
Some(BlockReason::UnresolvedToolFailure { .. }) - 995
)); - 996
- 997
// Model reports the error/blocker -> allowed - 998
let reported = p.evaluate_receipts( - 999
"build it", - 1000
"The build failed with exit code 1: compile error. Cannot proceed without missing dependency.", - 1001
None, - 1002
&receipts, - 1003
false, - 1004
); - 1005
assert_eq!(reported, None); - 1006
} - 1007
- 1008
#[test] - 1009
fn test_direct_substantive_answer_not_blocked_for_inspection_spec() { - 1010
let p = StopPolicy::default(); - 1011
let mut reading = vak_intent::Reading::general(); - 1012
reading.act = vak_intent::Act::Locate; - 1013
let spec = vak_intent::OutcomeSpec::from_reading( - 1014
"explain the architectural differences", - 1015
&reading, - 1016
1, - 1017
); - 1018
let receipts = ReceiptSummary::default(); - 1019
- 1020
// Substantive direct analysis without false tool claims or execution demands -> allowed - 1021
let substantive_answer = "Optimistic locking assumes multiple transactions can complete without affecting each other. It verifies no other transaction has modified the data before committing. In contrast, pessimistic locking acquires locks immediately upon reading."; - 1022
let blocked = p.evaluate_receipts(
Indexing the workspace…
Vakyartha documentation is discovering safe artifacts, anchors, and source references.