mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-08-28 05:27:41 +00:00
## 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. --- [](https://github.com/EveryInc/compound-engineering-plugin) 🤖 Generated with GPT-5 (unknown context, reasoning unspecified) via [Codex](https://openai.com/codex)
226 lines
7 KiB
Rust
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()
|
|
);
|
|
}
|
|
}
|