fabro/lib/crates/fabro-cli/src/commands/run/overrides.rs
Bryan Helmkamp 5fc9157017
refactor(workflow): remove retro stage (#230)
## Summary

Removes Fabro's automatic retro generation stage so workflow runs go
directly from execution to finalization and optional PR creation. This
drops the retro-specific crate, events, projection fields, config/API
knobs, and user-facing docs in favor of the existing durable run
observability surfaces.

## What Changed

- Deleted the `fabro-retro` crate and the workflow `retro` pipeline
phase, with finalization now consuming `Executed` state directly.
- Removed retro configuration and API surface area, including
`--no-retro`, `[run.execution].retros`, manifest `no_retro`,
`features.retros`, and run projection `retro*` fields.
- Retired typed `retro.*` events while keeping historical event logs
readable by deserializing retired retro event names as `Unknown`.
- Stopped appending retro sections to generated PR bodies and updated
docs, marketing copy, screenshots, and navigation to point users toward
observability/event-stream inspection.

## Testing

Not run during PR creation; this branch already contained the
implementation commit.

---

[![Compound
Engineering](https://img.shields.io/badge/Compound_Engineering-6366f1)](https://github.com/EveryInc/compound-engineering-plugin)
🤖 Generated with GPT-5 (unknown context, reasoning unspecified) via
[Codex](https://openai.com/codex)
2026-05-09 10:18:20 -04:00

226 lines
7 KiB
Rust

use std::collections::HashMap;
use std::path::{Path, PathBuf};
use anyhow::{Result, anyhow};
use fabro_config::{
CliLayer, CliOutputLayer, ReplaceMap, RunExecutionLayer, RunGoalLayer, RunLayer, RunModelLayer,
RunSandboxLayer, parse_input_overrides,
};
use fabro_sandbox::SandboxProvider;
use fabro_types::settings::cli::OutputVerbosity;
use fabro_types::settings::interp::InterpString;
use fabro_types::settings::run::{ApprovalMode, RunMode};
use crate::args::{PreflightArgs, RunArgs};
#[derive(Clone, Debug, Default)]
pub(crate) struct ManifestSettingsOverrides {
pub(crate) run: Option<RunLayer>,
pub(crate) cli: Option<CliLayer>,
pub(crate) input_overrides: HashMap<String, toml::Value>,
}
fn sparse_flag(value: bool) -> Option<bool> {
value.then_some(true)
}
pub(crate) fn parse_labels(labels: &[String]) -> HashMap<String, String> {
labels
.iter()
.filter_map(|label| label.split_once('='))
.map(|(key, value)| (key.to_string(), value.to_string()))
.collect()
}
fn model_from_args(model: Option<&str>, provider: Option<&str>) -> Option<RunModelLayer> {
if model.is_none() && provider.is_none() {
return None;
}
Some(RunModelLayer {
provider: provider.map(InterpString::parse),
name: model.map(InterpString::parse),
fallbacks: Vec::new(),
})
}
fn sandbox_layer(
sandbox: Option<SandboxProvider>,
preserve: Option<bool>,
) -> Option<RunSandboxLayer> {
if sandbox.is_none() && preserve.is_none() {
return None;
}
Some(RunSandboxLayer {
provider: sandbox.map(|p| p.to_string()),
preserve,
..RunSandboxLayer::default()
})
}
fn execution_layer(dry_run: Option<bool>, auto_approve: Option<bool>) -> Option<RunExecutionLayer> {
if dry_run.is_none() && auto_approve.is_none() {
return None;
}
Some(RunExecutionLayer {
mode: dry_run.map(|d| if d { RunMode::DryRun } else { RunMode::Normal }),
approval: auto_approve.map(|a| {
if a {
ApprovalMode::Auto
} else {
ApprovalMode::Prompt
}
}),
})
}
fn cli_layer_for_verbose(verbose: bool) -> Option<CliLayer> {
verbose.then(|| CliLayer {
output: Some(CliOutputLayer {
verbosity: Some(OutputVerbosity::Verbose),
..CliOutputLayer::default()
}),
..CliLayer::default()
})
}
/// Build the `run.goal` override from the `--goal` / `--goal-file` args.
///
/// The two are mutually exclusive at the clap level; this helper assumes
/// at most one is set and returns an error if that invariant is violated.
///
/// CLI-supplied file paths are anchored at `cwd` (where the user invoked
/// the command), matching standard Unix CLI-flag conventions.
fn goal_layer_from_args(
goal: Option<&str>,
goal_file: Option<&Path>,
cwd: &Path,
) -> Result<Option<RunGoalLayer>> {
match (goal, goal_file) {
(Some(_), Some(_)) => Err(anyhow!(
"--goal and --goal-file are mutually exclusive; use exactly one"
)),
(Some(text), None) => Ok(Some(RunGoalLayer::Inline(InterpString::parse(text)))),
(None, Some(path)) => {
let absolute = if path.is_absolute() {
path.to_path_buf()
} else {
cwd.join(path)
};
Ok(Some(RunGoalLayer::File {
file: InterpString::parse(&absolute.to_string_lossy()),
}))
}
(None, None) => Ok(None),
}
}
fn current_dir_or_dot() -> PathBuf {
std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))
}
pub(crate) fn run_args_overrides(args: &RunArgs) -> Result<ManifestSettingsOverrides> {
let model = model_from_args(args.model.as_deref(), args.provider.as_deref());
let sandbox = sandbox_layer(
args.sandbox.map(Into::into),
sparse_flag(args.preserve_sandbox),
);
let execution = execution_layer(sparse_flag(args.dry_run), sparse_flag(args.auto_approve));
let cwd = current_dir_or_dot();
let goal = goal_layer_from_args(args.goal.as_deref(), args.goal_file.as_deref(), &cwd)?;
let run = RunLayer {
goal,
metadata: ReplaceMap::from(parse_labels(&args.label)),
model,
sandbox,
execution,
..RunLayer::default()
};
Ok(ManifestSettingsOverrides {
run: Some(run),
cli: cli_layer_for_verbose(args.verbose),
input_overrides: parse_input_overrides(&args.inputs.values)?,
})
}
pub(crate) fn preflight_args_overrides(args: &PreflightArgs) -> Result<ManifestSettingsOverrides> {
let model = model_from_args(args.model.as_deref(), args.provider.as_deref());
let sandbox = args.sandbox.map(|s| RunSandboxLayer {
provider: Some(SandboxProvider::from(s).to_string()),
..RunSandboxLayer::default()
});
let cwd = current_dir_or_dot();
let goal = goal_layer_from_args(args.goal.as_deref(), args.goal_file.as_deref(), &cwd)?;
let run = RunLayer {
goal,
model,
sandbox,
..RunLayer::default()
};
Ok(ManifestSettingsOverrides {
run: Some(run),
cli: cli_layer_for_verbose(args.verbose),
input_overrides: parse_input_overrides(&args.inputs.values)?,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn goal_and_goal_file_together_is_rejected() {
let err = goal_layer_from_args(
Some("inline text"),
Some(Path::new("goal.md")),
Path::new("/tmp"),
)
.unwrap_err();
assert!(err.to_string().contains("mutually exclusive"));
}
#[test]
fn goal_file_is_anchored_at_cwd_when_relative() {
let layer =
goal_layer_from_args(None, Some(Path::new("prompts/goal.md")), Path::new("/cwd"))
.unwrap()
.expect("should build a goal layer");
let RunGoalLayer::File { file } = layer else {
panic!("expected file variant");
};
assert_eq!(file.as_source(), "/cwd/prompts/goal.md");
}
#[test]
fn absolute_goal_file_is_preserved() {
let layer = goal_layer_from_args(None, Some(Path::new("/abs/goal.md")), Path::new("/cwd"))
.unwrap()
.expect("should build a goal layer");
let RunGoalLayer::File { file } = layer else {
panic!("expected file variant");
};
assert_eq!(file.as_source(), "/abs/goal.md");
}
#[test]
fn inline_goal_builds_inline_variant() {
let layer = goal_layer_from_args(Some("inline goal"), None, Path::new("/cwd"))
.unwrap()
.expect("should build a goal layer");
assert!(matches!(layer, RunGoalLayer::Inline(_)));
}
#[test]
fn empty_args_produce_no_goal_layer() {
assert!(
goal_layer_from_args(None, None, Path::new("/cwd"))
.unwrap()
.is_none()
);
}
}