Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/cli/hook.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ pub enum HookEvent {
#[arg(long, value_enum)]
agent: Option<agent_registry::Agent>,
},
/// Fires after a tool call succeeds; records verification evidence and
/// surfaces the finishing-a-development-branch decision menu.
PostToolUse {
#[arg(long, value_enum)]
agent: Option<agent_registry::Agent>,
},
/// No-op — kept only so an old settings.json entry from a prior
/// agentflare version doesn't start erroring after an upgrade. New
/// installs never wire this (see init.rs).
Expand Down Expand Up @@ -66,6 +72,7 @@ impl HookArgs {
HookEvent::PostToolFailure { agent } => {
crate::hook::post_tool_failure(&resolve_agent(agent))
}
HookEvent::PostToolUse { agent } => crate::hook::post_tool_use(&resolve_agent(agent)),
HookEvent::SessionEnd { agent } => crate::hook::session_end(&resolve_agent(agent)),
HookEvent::PreCompact { agent } => crate::hook::pre_compact(&resolve_agent(agent)),
}
Expand Down
27 changes: 26 additions & 1 deletion src/hook.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ fn read_stdin_timeout(ms: u64) -> Option<String> {
rx.recv_timeout(Duration::from_millis(ms)).ok()
}

fn read_stdin_or_skip(label: &str) -> Option<String> {
pub(crate) fn read_stdin_or_skip(label: &str) -> Option<String> {
match read_stdin_timeout(STDIN_TIMEOUT_MS) {
Some(s) if !s.is_empty() => Some(s),
_ => {
Expand Down Expand Up @@ -339,6 +339,10 @@ pub fn post_tool_failure(_agent: &str) {
println!("{}", build_failure_decision(&message, severity));
}

// PostToolUse (success) command hook lives in `hook_completion_gate` (item
// #169) -- split out to stay under this file's LOC gate.
pub use crate::hook_completion_gate::post_tool_use;

pub fn pre_tool_use(_agent: &str) {
let Some(input) = read_stdin_or_skip("PreToolUse") else {
return;
Expand Down Expand Up @@ -383,8 +387,28 @@ pub fn pre_tool_use(_agent: &str) {
start_ts: now,
turn_count: 0,
recent_tool_calls: vec![],
last_verification: None,
});

// Completion gate (item #169): `item done`/`check_merge` requires fresh,
// passing verification evidence for this session -- see
// hook_redirect::completion_gate_reason's doc comment.
if let Some(reason) = crate::hook_redirect::completion_gate_reason(
&parsed.tool_name,
parsed.tool_input.as_ref(),
crate::optimize::has_fresh_passing_verification(record, now),
) {
let decision = json!({
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": reason,
}
});
println!("{decision}");
return;
}

let mut nudges: Vec<String> = vec![];

// Freshness guard (item #7): on a session's first mutating-tool call,
Expand Down Expand Up @@ -609,6 +633,7 @@ pub fn prompt_submit(agent: &str) {
start_ts: now,
turn_count: 0,
recent_tool_calls: vec![],
last_verification: None,
});
first_turn = record.turn_count == 0;
record.turn_count += 1;
Expand Down
Loading
Loading