feat(TASK-050): bracket --macro runs like --execute and close the isolated-macro leak

The --macro dispatch arm now opens the top-level agent bracket before
macro_execute and closes it signal-aware afterwards, draining pending
hooks like the --execute arm; both bracket calls no-op without a loaded
agent, so agent-less macro runs are unaffected. The isolated arm of
macro_execute closes the bracket an in-macro .agent step opens on its
fresh context, on the step-failure path too. Binary-level tests pin
agent.started plus exactly one terminal event for the --agent flag and
for in-macro .agent steps in both isolation modes, on success and
failure.
This commit is contained in:
Dark-Alex-17 committed 2026-09-16 21:23:04 -06:00
1 parent 90f7631b35
commit dee89d4ca3
3 files changed
+249 -12

No files matched your search

+15 -5
View File
@@ -83,12 +83,22 @@ pub async fn macro_execute(
.bootstrap_tools(app.as_ref(), true, abort_signal.clone())
.await?;
for step in &macro_value.steps {
let command = Macro::interpolate_command(step, &variables);
println!(">> {}", multiline_text(&command));
run_repl_command(&mut macro_ctx, abort_signal.clone(), &command).await?;
// An in-macro `.agent` step opens the top-level agent bracket on this
// fresh context, so it must also close here — on the step-failure path
// too, or the run would leak `agent.started` with no terminal event.
// Signal-aware like the headless dispatch arms: a ctrl-c'd macro
// classifies as interrupted, not failed.
let result = async {
for step in &macro_value.steps {
let command = Macro::interpolate_command(step, &variables);
println!(">> {}", multiline_text(&command));
run_repl_command(&mut macro_ctx, abort_signal.clone(), &command).await?;
}
Ok(())
}
Ok(())
.await;
macro_ctx.top_level_agent_finished(result.as_ref().err(), Some(&abort_signal));
result
}
struct MacroModeGuard<'a> {
+13 -7
View File
@@ -438,11 +438,12 @@ async fn run(
ctx.use_agent(app.as_ref(), agent, session, abort_signal.clone())
.await?;
// The top-level agent bracket does NOT open here: the inspection
// flags below (--list-sessions, --info, --macro, --rebuild-rag,
// --init-*) return early without running the agent, and an open
// bracket on those paths would leak `agent.started` with no
// terminal event. It opens at the dispatch sites further down,
// where an agent run is actually committed.
// flags below (--list-sessions, --info, --rebuild-rag, --init-*)
// return early without running the agent, and an open bracket on
// those paths would leak `agent.started` with no terminal event.
// It opens at the dispatch sites further down, where a run is
// actually committed — including the --macro arm, which executes
// rather than inspects.
} else {
let app: Arc<AppConfig> = Arc::clone(&ctx.app.config);
if let Some(prompt) = &cli.temp_role {
@@ -575,8 +576,13 @@ async fn run(
}
}
if let Some(name) = &cli.macro_name {
macro_execute(&mut ctx, name, text.as_deref(), abort_signal.clone()).await?;
return Ok(());
// Dispatch committed: open the top-level agent bracket only now,
// past every inspection-flag early return above.
ctx.top_level_agent_started();
let result = macro_execute(&mut ctx, name, text.as_deref(), abort_signal.clone()).await;
ctx.top_level_agent_finished(result.as_ref().err(), Some(&abort_signal));
hooks::drain_pending(EXIT_HOOK_DRAIN_TIMEOUT).await;
return result;
}
if cli.execute && !is_repl {
// Dispatch committed: open the top-level agent bracket only now,