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)
This commit is contained in:
Bryan Helmkamp 2026-05-09 07:18:20 -07:00 committed by GitHub
parent f07bb4aaba
commit 5fc9157017
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
115 changed files with 189 additions and 2595 deletions

View file

@ -109,7 +109,7 @@ Fabro is an AI-powered workflow orchestration platform. Workflows are defined as
### Rust crates (`lib/crates/`)
- **fabro-cli** — CLI entry point. Commands: `run`, `exec`, `serve`, `validate`, `parse`, `cp`, `model`, `doctor`, `install`, `ps`, `system prune`
- **fabro-workflow** — Core workflow engine. Parses Graphviz graphs, runs stages, manages checkpoints/resume, hooks, retros, and human-in-the-loop interactions
- **fabro-workflow** — Core workflow engine. Parses Graphviz graphs, runs stages, manages checkpoints/resume, hooks, and human-in-the-loop interactions
- **fabro-agent** — AI coding agent with tool use (Bash, Read, Write, Edit, Glob, Grep, WebFetch). `Sandbox` trait abstracts execution environments
- **fabro-sandbox** — Local, Docker, and Daytona sandbox providers. Docker is the default runtime provider and creates clone-based `/workspace` containers through the operator's Docker daemon; Daytona uses the same GitHub-only clone-source contract. Docker daemon access is host-root-equivalent and assumes trusted callers/payloads.
- **fabro-server** — Axum HTTP server. Routes for runs, sessions, models, completions, usage. SSE event streaming. Demo mode via header
@ -180,7 +180,6 @@ Never run `cargo insta accept` without first checking what's pending — it acce
## Testing workflows
- `fabro run <name>` — run a workflow by name (resolves `.fabro/workflows/<name>/workflow.toml`), e.g. `fabro run repl`
- Use `--no-retro` to skip the retro step and finish faster
- `#[e2e_test(twin, live("VAR"))]` — dual-mode test that runs against twin-openai or real API. `#[e2e_test(twin)]` for twin-only tests (e.g., scripted failures). `#[e2e_test(live("VAR"))]` for live-only tests requiring secrets. `#[e2e_test()]` for sandbox tests with no API deps. Behavior is controlled by `FABRO_TEST_MODE` (`live`, `strict`; default is `twin`), and `cargo nextest run --profile e2e ...` implies `strict`. Use `fabro_test::e2e_openai!()` in twin/dual-mode tests to get `(base_url, api_key)`.
- Local test HTTP clients must use `.no_proxy()`. Prefer shared helpers like `fabro_test::test_http_client()` or crate-local equivalents instead of `reqwest::Client::new()`, bare `Client::builder().build()`, or `reqwest::get(...)`.
- This is not cosmetic: macOS proxy discovery adds hidden startup overhead to repeated localhost reqwest clients and can surface as misleading nextest timeouts.

22
Cargo.lock generated
View file

@ -1675,7 +1675,6 @@ dependencies = [
"fabro-oauth",
"fabro-proc",
"fabro-redact",
"fabro-retro",
"fabro-sandbox",
"fabro-server",
"fabro-static",
@ -2078,25 +2077,6 @@ dependencies = [
"url",
]
[[package]]
name = "fabro-retro"
version = "0.228.0-nightly.0"
dependencies = [
"anyhow",
"chrono",
"fabro-agent",
"fabro-dump",
"fabro-llm",
"fabro-store",
"fabro-types",
"fabro-util",
"serde",
"serde_json",
"tempfile",
"tokio",
"tracing",
]
[[package]]
name = "fabro-sandbox"
version = "0.228.0-nightly.0"
@ -2166,7 +2146,6 @@ dependencies = [
"fabro-model",
"fabro-proc",
"fabro-redact",
"fabro-retro",
"fabro-sandbox",
"fabro-slack",
"fabro-spa",
@ -2443,7 +2422,6 @@ dependencies = [
"fabro-mcp",
"fabro-model",
"fabro-redact",
"fabro-retro",
"fabro-sandbox",
"fabro-static",
"fabro-store",

View file

@ -41,7 +41,7 @@ Then run `fabro server start` to finish setup in your browser. The server opens
- **Run agents 24/7** — Fabro's API server queues and executes runs continuously. Close your laptop — workflows keep running and results are waiting when you return.
- **Scale infinitely** — Move execution off your laptop and into cloud sandboxes. Run as many concurrent workflows as your infrastructure allows.
- **Guarantee code quality** — Layer deterministic verifications — test suites, linters, type checkers, LLM-as-judge — into your workflow graph. Failures trigger fix loops automatically.
- **Achieve compounding engineering** — Automatic retrospectives after every run feed a continuous improvement loop. Your workflows get better over time, not just your code.
- **Inspect every run** — Query durable event streams, checkpoints, conclusions, and stage outputs to understand what happened and improve the workflow.
- **Specify in natural language** — Define requirements as natural-language specs and let Fabro generate — and regenerate — implementations that conform to them.
---
@ -56,7 +56,7 @@ Then run `fabro server start` to finish setup in your browser. The server opens
| ☁️ | Cloud sandboxes | Run agents in isolated Daytona cloud VMs with snapshot-based setup, network controls, and automatic cleanup |
| 🔌 | SSH access and preview links | Shell into running sandboxes with `fabro sandbox ssh` and expose ports with `fabro sandbox preview` for live debugging |
| 🌲 | Git checkpointing | Every stage commits code changes and execution metadata to Git branches. Resume, revert, or trace any change |
| 📊 | Automatic retros | Each run generates a retrospective with cost, duration, files touched, and an LLM-written narrative |
| 📊 | Run observability | Durable events, checkpoints, conclusions, and stage outputs make every run inspectable and exportable |
| ⚡ | Comprehensive API | REST API with SSE event streaming and a React web UI. Run workflows programmatically or as a service |
| 🦀 | Single binary, no runtime | One compiled Rust executable with zero dependencies. No Python, no Node, no Docker required |
| ⚖️ | Open source (MIT) | Full source code, no vendor lock-in. Self-host, fork, or extend to fit your workflow |
@ -104,7 +104,7 @@ Fabro ships with [comprehensive documentation](https://docs.fabro.sh) covering e
- [**Getting Started**](https://docs.fabro.sh/getting-started/introduction) -- Installation, first workflow, and why Fabro exists
- [**Defining Workflows**](https://docs.fabro.sh/workflows/stages-and-nodes) -- Node types, transitions, variables, stylesheets, and human gates
- [**Executing Workflows**](https://docs.fabro.sh/execution/run-configuration) -- Run configuration, sandboxes, checkpoints, retros, and failure handling
- [**Executing Workflows**](https://docs.fabro.sh/execution/run-configuration) -- Run configuration, sandboxes, checkpoints, observability, and failure handling
- [**Tutorials**](https://docs.fabro.sh/tutorials/hello-world) -- Step-by-step guides from hello world to parallel multi-model ensembles
- [**API Reference**](https://docs.fabro.sh/api-reference/overview) -- Full OpenAPI spec with authentication, SSE events, and client SDKs

View file

@ -48,7 +48,6 @@ export default function Start() {
const systemInfo = useSystemInfo();
const features = systemInfo.data?.features ?? {
session_sandboxes: false,
retros: false,
};
const [prompt, setPrompt] = useState("");
const [project, setProject] = useState(projects[0]);

View file

@ -61,7 +61,7 @@ function sampleSettings({
model: { provider: null, name: "claude-sonnet", fallbacks: [] },
git: { author: null },
prepare: { commands: prepareCommands, timeout_ms: 120_000 },
execution: { mode: "normal", approval: "prompt", retros: true },
execution: { mode: "normal", approval: "prompt" },
checkpoint: { exclude_globs: [] },
sandbox: {
provider: "daytona",

View file

@ -453,7 +453,7 @@
<div class="sample-prose">
<h4 class="display">Verification is a first-class concept</h4>
<p class="body">Every workflow can include build gates, test assertions, and human approval stages. When a stage fails, Fabro routes back to the appropriate fix loop — no manual re-runs, no <code class="mono">git stash</code> gymnastics.</p>
<p class="body">The <a href="#">retrospective engine</a> runs after every workflow, surfacing what went wrong and why. Over time, your workflows get smarter.</p>
<p class="body">The <a href="#">event stream</a> captures every workflow, surfacing what happened and why. Over time, your workflows get easier to inspect.</p>
</div>
</div>
</div>
@ -498,7 +498,7 @@
<div class="sample-prose">
<h4 class="display">Verification is a first-class concept</h4>
<p class="body">Every workflow can include build gates, test assertions, and human approval stages. When a stage fails, Fabro routes back to the appropriate fix loop — no manual re-runs, no <code class="mono">git stash</code> gymnastics.</p>
<p class="body">The <a href="#">retrospective engine</a> runs after every workflow, surfacing what went wrong and why. Over time, your workflows get smarter.</p>
<p class="body">The <a href="#">event stream</a> captures every workflow, surfacing what happened and why. Over time, your workflows get easier to inspect.</p>
</div>
</div>
</div>
@ -543,7 +543,7 @@
<div class="sample-prose">
<h4 class="display">Verification is a first-class concept</h4>
<p class="body">Every workflow can include build gates, test assertions, and human approval stages. When a stage fails, Fabro routes back to the appropriate fix loop — no manual re-runs, no <code class="mono">git stash</code> gymnastics.</p>
<p class="body">The <a href="#">retrospective engine</a> runs after every workflow, surfacing what went wrong and why. Over time, your workflows get smarter.</p>
<p class="body">The <a href="#">event stream</a> captures every workflow, surfacing what happened and why. Over time, your workflows get easier to inspect.</p>
</div>
</div>
</div>
@ -588,7 +588,7 @@
<div class="sample-prose">
<h4 class="display">Verification is a first-class concept</h4>
<p class="body">Every workflow can include build gates, test assertions, and human approval stages. When a stage fails, Fabro routes back to the appropriate fix loop — no manual re-runs, no <code class="mono">git stash</code> gymnastics.</p>
<p class="body">The <a href="#">retrospective engine</a> runs after every workflow, surfacing what went wrong and why. Over time, your workflows get smarter.</p>
<p class="body">The <a href="#">event stream</a> captures every workflow, surfacing what happened and why. Over time, your workflows get easier to inspect.</p>
</div>
</div>
</div>

View file

@ -57,7 +57,6 @@ Fabro replaces the prompt-wait-review loop with version-controlled workflow grap
- [Environments](https://docs.fabro.sh/execution/environments): Sandbox providers for workflow execution
- [Run Configuration](https://docs.fabro.sh/execution/run-configuration): Configure runs with TOML files
- [Checkpoints](https://docs.fabro.sh/execution/checkpoints): Git-based checkpoint and resume
- [Retros](https://docs.fabro.sh/execution/retros): Automatic retrospectives for every run
- [Observability](https://docs.fabro.sh/execution/observability): Monitor, inspect, and analyze runs
## Integrations

View file

@ -90,4 +90,4 @@ curl -fsSL https://fabro.sh/install.sh | bash
fabro run implement
```
Check the [roadmap](/roadmap) to see what we're building — including automatic retrospectives, a REST API server, and analytics — and join us on [Discord](/discord) to shape what comes next.
Check the [roadmap](/roadmap) to see what we're building — including the REST API server, analytics, and web workflows — and join us on [Discord](/discord) to shape what comes next.

View file

@ -1,4 +0,0 @@
title: Compounding with auto-retrospectives
description: Structured post-run analysis with cost, duration, smoothness ratings, friction points, and LLM-generated narratives.
status: building
date: 2026-04-03

View file

@ -1,4 +1,4 @@
title: Web app
description: React dashboard for managing workflows, viewing runs, approving human gates, and browsing retrospectives.
description: React dashboard for managing workflows, viewing runs, approving human gates, and inspecting execution traces.
status: next
date: 2026-06-03

View file

@ -283,7 +283,7 @@ const cssExample = `<span class="text-ice-300">/* All nodes default to fast + ch
</p>
</div>
<!-- Retrospectives -->
<!-- Observability -->
<div class="glow-card reveal reveal-d5 rounded-xl border border-navy-800 bg-navy-800/25 p-8 relative overflow-hidden">
<div class="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-coral/25 to-transparent"></div>
<div class="mb-4 flex h-10 w-10 items-center justify-center rounded-lg border border-teal-700/30 bg-teal-700/10 text-teal-300">
@ -293,9 +293,9 @@ const cssExample = `<span class="text-ice-300">/* All nodes default to fast + ch
</svg>
</div>
<p class="font-mono uppercase text-xs tracking-widest text-teal-500 mb-2">Analytics</p>
<h3 class="font-display text-lg font-semibold text-ice-50">Automatic retrospectives</h3>
<h3 class="font-display text-lg font-semibold text-ice-50">Run observability</h3>
<p class="mt-2 text-sm leading-relaxed text-ice-300">
Each run generates a retro with cost, duration, files touched, and an LLM-written narrative. Your workflows improve over time.
Durable event streams, checkpoints, conclusions, and stage outputs make every run inspectable and exportable.
</p>
</div>
</div>
@ -524,7 +524,7 @@ const cssExample = `<span class="text-ice-300">/* All nodes default to fast + ch
</h2>
<p class="reveal reveal-d1 mt-5 text-lg leading-relaxed text-ice-300">
Full traceability across runs — every model call, every tool invocation, every decision point.
Query run data with SQL. Automatic retrospectives rate each run and surface friction points.
Query run data with SQL and inspect the full execution trail.
</p>
</div>
@ -568,7 +568,7 @@ const cssExample = `<span class="text-ice-300">/* All nodes default to fast + ch
</div>
</div>
<div class="flex items-center gap-3">
<span class="shrink-0 w-20 text-right text-xs font-mono text-ice-300/50">retro</span>
<span class="shrink-0 w-20 text-right text-xs font-mono text-ice-300/50">verify</span>
<div class="relative h-7 flex-1">
<div class="trace-bar absolute left-[82%] h-full rounded-md bg-coral/12 border border-coral/20 flex items-center px-3" style="width: 14%; transition-delay: 600ms">
<span class="text-[11px] font-mono text-coral truncate">3.3s</span>

View file

@ -1,6 +1,6 @@
# Fabro Events Strategy
Fabro emits structured **workflow run events** during execution for observability. Events are the durable audit trail for a run: they drive the run store, SSE streaming, CLI progress rendering, retro analysis, and optional JSONL sinks.
Fabro emits structured **workflow run events** during execution for observability. Events are the durable audit trail for a run: they drive the run store, SSE streaming, CLI progress rendering, and optional JSONL sinks.
Events are distinct from tracing logs. Tracing is developer diagnostics; events are product-facing state transitions and activity records that other systems consume.
@ -159,7 +159,6 @@ Check:
- CLI progress parsing
- `fabro events`
- retro duration extraction
- store validation
- tests or fixtures that inspect event names or fields

View file

@ -2263,65 +2263,3 @@ Emitted when the stall watchdog detects no progress.
| Property | Type | Description |
|----------|------|-------------|
| `idle_seconds` | number | Seconds since last activity |
---
## Retro events
### `retro.started`
```json
{
"id": "...", "ts": "...", "run_id": "...",
"event": "retro.started",
"properties": {
"prompt": "Analyze the workflow run data at `/tmp/retro_data/` ...",
"provider": "anthropic",
"model": "claude-sonnet-4-20250514"
}
}
```
| Property | Type | Description |
|----------|------|-------------|
| `prompt` | string? | Prompt sent to the retro agent |
| `provider` | string? | LLM provider for the retro agent |
| `model` | string? | Model used for the retro agent |
### `retro.completed`
```json
{
"id": "...", "ts": "...", "run_id": "...",
"event": "retro.completed",
"properties": {
"duration_ms": 5000,
"response": "The run was mostly smooth...",
"retro": {"smoothness": "smooth"}
}
}
```
| Property | Type | Description |
|----------|------|-------------|
| `duration_ms` | number | Retro duration |
| `response` | string? | Raw assistant response from the retro agent |
| `retro` | object? | Parsed `Retro` payload |
### `retro.failed`
```json
{
"id": "...", "ts": "...", "run_id": "...",
"event": "retro.failed",
"properties": {
"error": "LLM request failed",
"duration_ms": 3000
}
}
```
| Property | Type | Description |
|----------|------|-------------|
| `error` | string | Error message |
| `duration_ms` | number | Duration before failure |

View file

@ -18,7 +18,7 @@ Fabro treats the process itself as code:
- Workflow graphs define stages, branching, loops, parallelism, and human gates.
- Model stylesheets route different stages to different models and providers.
- Sandboxes and Git checkpoints make runs isolated, resumable, and inspectable.
- Event logs, verifications, and retros create a feedback loop after execution.
- Event logs, verifications, and run state create a feedback loop after execution.
## Product direction

View file

@ -19,7 +19,7 @@ Fabro currently presents as:
- Git checkpointing for resume, rewind, fork, and auditability
- structured run artifacts and event streams for observability
- verifications and insights in the broader product surface
- retrospectives, documented as experimental
- observability surfaces for event streams, run state, and verification data
## Current positioning

View file

@ -18,7 +18,7 @@ A workflow can combine:
- model stylesheets choose models and providers per stage
- sandboxes isolate execution from the host
- Git checkpoints make runs resumable and auditable
- event logs, verifications, and optional retros help teams inspect outcomes
- event logs, verifications, and run state help teams inspect outcomes
## Interfaces

View file

@ -7,7 +7,6 @@ For product work, measure trust and useful outcomes, not just command execution.
- CLI telemetry for command success and failure
- per-run event streams in `progress.jsonl`
- run artifacts such as `checkpoint.json`, `conclusion.json`, and verification data
- `retro.json` when retros are enabled
- API and web data for runs, workflows, usage, verifications, and insights
## Metrics that matter most

View file

@ -10,7 +10,7 @@ This note captures stable constraints that product changes should respect.
- Model routing is per-stage and provider-agnostic through stylesheets and config.
- Execution happens through sandbox providers rather than assuming direct host access.
- Git checkpointing is central to resume, rewind, fork, and auditability.
- Runs produce structured artifacts such as `progress.jsonl`, `live.json`, `checkpoint.json`, and optional `retro.json`.
- Runs produce structured artifacts such as `progress.jsonl`, `live.json`, `checkpoint.json`, and `conclusion.json`.
- The HTTP API is OpenAPI-based, and the web app depends on that contract.
## Operational constraints

View file

@ -32,9 +32,8 @@ These paths are local runtime state, not canonical event projections.
These names are still real, but they are no longer live scratch files by default:
- Metadata branch files such as `run.json`, `start.json`, `checkpoint.json`, and `retro.json`
- `fabro dump` exports such as `run.json`, `start.json`, `status.json`, `checkpoint.json`, `conclusion.json`, `retro.json`, `events.jsonl`, and per-node prompt/response/status/stdout/stderr files
- Retro-agent temp uploads named `events.jsonl`, `run.json`, `graph.fabro`, `checkpoints/{seq:04}.json`, `run.log` when available, and per-stage files under `stages/{rank:03}-{node_id}@{visit}/...` inside the retro sandbox
- Metadata branch files such as `run.json`, `start.json`, and `checkpoint.json`
- `fabro dump` exports such as `run.json`, `start.json`, `status.json`, `checkpoint.json`, `conclusion.json`, `events.jsonl`, and per-node prompt/response/status/stdout/stderr files
## Notes

View file

@ -57,9 +57,7 @@ Screenshots live in `docs/public/images/web/`. Each screenshot maps to a specifi
| `run-overview.png` | `/runs/run-1` |
| `run-stages.png` | `/runs/run-1/stages/detect-drift` |
| `run-files-changed.png` | `/runs/run-1/compare` |
| `run-retro.png` | `/runs/run-1/retro` |
| `run-usage.png` | `/runs/run-1/usage` |
| `retros-list.png` | `/retros` |
### Verification checklist
@ -96,8 +94,6 @@ Current placements:
| `workflow-runs.png` | `core-concepts/workflows.mdx` |
| `run-stages.png` | `execution/observability.mdx` |
| `run-usage.png` | `execution/observability.mdx` |
| `retros-list.png` | `execution/retros.mdx` |
| `run-retro.png` | `execution/retros.mdx` |
| `run-files-changed.png` | `human-tools/steering.mdx` |
## Cleanup

View file

@ -121,7 +121,6 @@ The tracked paths are stored as `files_touched` on the stage outcome:
|---|---|
| `StageCompleted` event | Emitted with `files_touched` in the event stream and surfaced by `fabro events` / exported event streams |
| Preambles | Listed under each completed stage so downstream agents know what changed |
| Retros | Included per-stage and aggregated across the full run |
| `status.json` | Written to the stage's logs directory after each node completes |
### How tracking works
@ -240,6 +239,5 @@ Outputs and artifacts appear in several observability surfaces:
| `StageCompleted` event | `files_touched` list for the stage |
| `WorkflowRunCompleted` event | `artifact_count` -- total number of offloaded artifacts across the run |
| Web UI | Run stage output, stage artifacts, and downloadable artifact files |
| [Retros](/execution/retros) | Per-stage `files_touched` and aggregate `files_touched` across all stages |
| [Preambles](/execution/context#preamble-construction) | File list and artifact pointer references for completed stages |
| Stage logs | `status.json` in each stage's run directory contains the full outcome including `files_touched` |

View file

@ -47,7 +47,6 @@ Demo mode implements every API endpoint. Read endpoints return static data repre
| Run Internals | Stages, turns, checkpoints, context, and configuration all return static data |
| Verifications | Categories and controls with pass/fail examples |
| Insights | Saved queries and history; execute returns a canned result |
| Retros | Retrospective data for completed runs |
| Projects | Sample projects and branches |
| Settings | Platform configuration |

View file

@ -4781,8 +4781,6 @@ components:
type: boolean
auto_approve:
type: boolean
no_retro:
type: boolean
preserve_sandbox:
type: boolean
worktree_mode:
@ -6102,13 +6100,6 @@ components:
conclusion:
type: ["object", "null"]
additionalProperties: true
retro:
type: ["object", "null"]
additionalProperties: true
retro_prompt:
type: ["string", "null"]
retro_response:
type: ["string", "null"]
sandbox:
type: ["object", "null"]
additionalProperties: true
@ -8087,14 +8078,12 @@ components:
RunExecutionSettings:
type: object
required: [mode, approval, retros]
required: [mode, approval]
properties:
mode:
$ref: "#/components/schemas/RunMode"
approval:
$ref: "#/components/schemas/ApprovalMode"
retros:
type: boolean
RunMode:
type: string
@ -8549,9 +8538,6 @@ components:
session_sandboxes:
type: boolean
description: Whether session sandboxes are enabled.
retros:
type: boolean
description: Whether workflow retros are enabled.
SystemRunCounts:
description: Counts of known runs in the active server process.

View file

@ -82,7 +82,7 @@ The sandbox is configured per-run via CLI flags (`--sandbox docker`) or the run
Every significant action — stage starts, LLM calls, tool invocations, edge selections, stage completions — is emitted as a structured event. These events power:
- The **web UI** for real-time run monitoring
- **Retrospectives** generated automatically after each run
- **Run summaries** built from durable events, checkpoints, conclusions, and stage outputs
- **DuckDB queries** via `fabro insights` for SQL-based analysis across runs
See [Observability](/execution/observability) for more on querying run data.

View file

@ -28,7 +28,7 @@ digraph MyWorkflow {
}
```
The `goal` attribute describes what the workflow accomplishes. Fabro uses it to guide agent behavior and generate retrospectives.
The `goal` attribute describes what the workflow accomplishes. Fabro uses it to guide agent behavior.
## Key node types

View file

@ -55,7 +55,6 @@
"execution/checkpoints",
"execution/outcomes",
"execution/failures",
"execution/retros",
"execution/observability",
"execution/devcontainers"
]

View file

@ -45,14 +45,13 @@ Fabro disables Git commit and tag signing for checkpoint commits created inside
The metadata branch (`fabro/meta/{run_id}`) is an orphan branch that stores structured run data using Git's object storage directly. Fabro writes it from inside the sandbox with Git plumbing commands, without checking out a metadata worktree. It is initialized at run start with:
- **`run.json`** — Current projection snapshot: run spec, start/status records, current checkpoint, conclusion, sandbox, retro state, and other run-level metadata
- **`run.json`** — Current projection snapshot: run spec, start/status records, current checkpoint, conclusion, sandbox, and other run-level metadata
- **`graph.fabro`** — Workflow source for the run
After each node, the metadata branch is updated with:
- **`run.json`** — Refreshed projection snapshot with the new current checkpoint
- **`stages/{rank:03}-{node_id}@{visit}/...`** — Execution-order-prefixed per-stage trace files (prompts, responses, status, diffs, command output, and tool metadata)
- **`stages/retro/*.md`** — Retro prompt/response text when present
## What's in a checkpoint

View file

@ -3,7 +3,7 @@ title: "Observability"
description: "How to monitor, inspect, and analyze workflow runs"
---
Fabro captures a structured event for every significant action during a workflow run. These events cover stage execution, agent tool calls, retries, routing, sandbox lifecycle, git checkpoints, retro generation, and more.
Fabro captures a structured event for every significant action during a workflow run. These events cover stage execution, agent tool calls, retries, routing, sandbox lifecycle, git checkpoints, and more.
## Event stream
@ -11,7 +11,7 @@ Every workflow run emits a sequence of canonical **run event envelopes** that ar
- Stored durably in the run store
- Broadcast over SSE to connected API clients
- Stored for later analysis and retro generation
- Stored for later analysis
- Rendered by CLI progress and log tooling
- Optionally materialized into JSONL by export/debug paths
@ -95,7 +95,6 @@ Common categories include:
| Routing | `edge.selected`, `loop.restart`, `parallel.started` |
| Git and checkpoints | `checkpoint.completed`, `git.commit`, `git.push` |
| Setup and sandbox | `sandbox.initializing`, `sandbox.ready`, `setup.started` |
| Retro | `retro.started`, `retro.completed`, `retro.failed` |
## Sub-agent visibility
@ -135,4 +134,4 @@ Post-run analysis surfaces include:
| `fabro inspect <RUN>` | Current durable run state, including run/start/checkpoint/conclusion records |
| `fabro dump --output <DIR> <RUN>` | Exported `events.jsonl` plus reconstructed JSON and node files |
See [retros](/execution/retros), [stages](/api-reference/run-internals/list-run-stages), and [turns](/api-reference/run-internals/list-stage-turns) for higher-level analysis views built on top of this event stream.
See [stages](/api-reference/run-internals/list-run-stages) and [turns](/api-reference/run-internals/list-stage-turns) for higher-level analysis views built on top of this event stream.

View file

@ -1,146 +0,0 @@
---
title: "Retros"
description: "Automatic retrospectives that analyze every workflow run"
---
<Warning>
**Experimental feature.** Retros are disabled by default. Enable them by setting `retros = true` under `[run.execution]` in your project or workflow config.
</Warning>
After every workflow run, Fabro can generate a **retro** — a structured retrospective that captures what happened, what went well, and what didn't. Retros combine deterministic metrics extracted from the run's checkpoint with a qualitative narrative produced by an LLM agent that analyzes the full event stream.
The goal is continuous improvement. Retros give you a searchable history of how your workflows perform over time, surface friction patterns that would otherwise go unnoticed, and identify follow-up work before it falls through the cracks.
<Frame caption="The Retros page shows all retrospectives with smoothness ratings, duration, and friction counts.">
<img src="/images/web/retros-list.png" alt="Fabro web UI Retros list showing runs with Smooth, Bumpy, Effortless, and Struggled ratings" />
</Frame>
## What's in a retro
A retro has two layers: **quantitative stats** derived cheaply from checkpoint data, and an **agent-generated narrative** that interprets the run holistically.
### Quantitative layer
The quantitative layer is extracted directly from the [checkpoint](/execution/checkpoints) and event stream — no LLM calls required:
| Field | Description |
|---|---|
| **Per-stage breakdown** | Duration, retry count, cost, files touched, status, and failure reason for each stage |
| **Aggregate stats** | Total duration, total cost, total retries, all files touched, stages completed vs. failed |
### Narrative layer
An LLM agent reads the run's full event stream and produces a structured analysis:
| Field | Description |
|---|---|
| **Smoothness** | Overall rating on a 5-point scale (see below) |
| **Intent** | What the run was trying to accomplish |
| **Outcome** | What actually happened |
| **Learnings** | What was discovered about the repo, code, workflow, or tools |
| **Friction points** | Where things got stuck and why |
| **Open items** | Follow-up work, tech debt, test gaps, or investigations identified |
The agent has tool access to grep and read the event stream, so it can inspect actual tool call patterns, error messages, and approach pivots — not just pass/fail signals.
## Smoothness ratings
Every retro includes a smoothness rating that grades the overall quality of the run's execution:
| Rating | Meaning |
|---|---|
| **Effortless** | Goal achieved on the first try. No retries, no wrong approaches. Agent moved efficiently from start to finish. |
| **Smooth** | Goal achieved with minor hiccups — 12 retries or a brief wrong approach quickly corrected. No human intervention needed. |
| **Bumpy** | Goal achieved but with notable friction: multiple retries, at least one significant wrong approach, or substantial time on dead ends. |
| **Struggled** | Goal achieved only with difficulty: many retries, major approach changes, human intervention, or partial failures requiring recovery. |
| **Failed** | Run did not achieve its stated goal. Some stages may have completed, but the overall intent was not fulfilled. |
The rating considers the full context visible in agent events — tool call patterns, error recovery sequences, approach pivots — not just stage pass/fail counts.
## Learnings, friction points, and open items
### Learnings
Learnings capture what was discovered during the run, categorized by type:
| Category | Examples |
|---|---|
| `repo` | Repository structure, build system quirks, CI configuration |
| `code` | Bug root causes, module boundaries, API contracts |
| `workflow` | Node ordering issues, missing stages, prompt improvements |
| `tool` | Tool limitations, MCP server behavior, command output parsing |
### Friction points
Friction points identify where the run got stuck and what caused the slowdown:
| Kind | Description |
|---|---|
| `retry` | A stage needed multiple attempts |
| `timeout` | A stage or tool call hit a time limit |
| `wrong_approach` | The agent pursued a dead end before pivoting |
| `tool_failure` | A tool or command failed unexpectedly |
| `ambiguity` | Unclear requirements or conflicting signals caused confusion |
Each friction point can optionally reference the `stage_id` where it occurred.
### Open items
Open items capture follow-up work identified during the run:
| Kind | Description |
|---|---|
| `tech_debt` | Code quality issues worth addressing later |
| `follow_up` | Work that's related but out of scope for this run |
| `investigation` | Unknowns that need further research |
| `test_gap` | Missing test coverage discovered during the run |
## How retros are generated
Retro generation happens in two phases after a run completes:
1. **Derive** — Fabro extracts stage durations from durable run events and builds a retro from the checkpoint data. This is deterministic, fast, and produces the quantitative layer.
2. **Narrate** — An LLM agent session analyzes the run data. The agent receives `events.jsonl`, `run.json`, `graph.fabro`, checkpoint snapshots under `checkpoints/{seq:04}.json`, `run.log` when available, and per-stage files under `stages/{rank:03}-{node_id}@{visit}/...` inside its sandbox so it can grep and read the event stream, run snapshot, workflow source, checkpoints, logs, and full stage payloads. The narrative fields are merged back into durable retro state.
Both phases run automatically at the end of every CLI run. The API server derives the quantitative layer but does not currently run the narrative agent.
<Frame caption="A run's Retro tab shows the smoothness rating, stats, learnings, and open items.">
<img src="/images/web/run-retro.png" alt="Fabro web UI run retro showing Smooth rating, duration, cost, learnings, and follow-up items" />
</Frame>
## Accessing retros
### CLI
To enable retros for your project, set `retros = true` under `[run.execution]` in your `.fabro/project.toml`:
```toml title=".fabro/project.toml"
_version = 1
[run.execution]
retros = true
```
To skip retro generation for a single run when retros are enabled, pass `--no-retro`:
```bash
fabro run workflow.fabro --no-retro
```
Retros can also be enabled server-wide in `settings.toml`:
```toml title="settings.toml"
_version = 1
[run.execution]
retros = true
```
### API
Retros are also available via the REST API. See the [list retros](/api-reference/retros/list-retros) and [retrieve retro](/api-reference/retros/retrieve-retro) API reference pages.
## Storage
Retros are stored in durable run state. If you need files on disk, `fabro dump` materializes retro text under `stages/retro/` alongside `run.json`, stage files, and the rest of the exported run data.

View file

@ -27,7 +27,7 @@ goal = "Implement the login feature"
|---|---|---|
| `_version` | No (defaults to `1`) | Schema version. Must be `1` in the first pass. |
| `[workflow].graph` | No | Path to the Graphviz workflow file, relative to the TOML file's directory. Defaults to `workflow.fabro`. |
| `[run].goal` | No | What the workflow should accomplish. Passed to agents and used in retrospectives. Can also be provided via `--goal` CLI flag or Graphviz graph `goal` attribute. |
| `[run].goal` | No | What the workflow should accomplish. Passed to agents and available via `--goal` CLI flag or Graphviz graph `goal` attribute. |
Goal precedence: CLI `--goal` > `[run].goal` > Graphviz graph attribute.

View file

@ -25,7 +25,7 @@ The dark factory isn't a single tool or practice. It's a set of capabilities tha
**Checkpointed execution over black-box runs.** Git commits after every stage create an audit trail. When something goes wrong, you can inspect, revert, or fork from any point — without having watched the run live.
**Continuous improvement over static processes.** Automatic retrospectives after every run feed a learning loop. Workflows get better over time, not just the code they produce.
**Observability over black-box automation.** Durable event streams, checkpoints, conclusions, and stage outputs make each run inspectable after the fact. Workflows get better when teams can see what happened and adjust the graph.
## The human role in a dark factory
@ -36,7 +36,7 @@ The dark factory doesn't eliminate engineering judgment. It redirects it:
| Writing code | Defining workflows and prompts |
| Reviewing diffs | Defining verification criteria |
| Debugging test failures | Designing fix loops |
| Watching agent sessions | Reviewing retrospectives |
| Watching agent sessions | Inspecting run traces |
| Manual quality checks | Tuning goal gates and evals |
The goal is to spend your time on the parts that require human judgment — what to build, how to verify it, and when something doesn't look right — while the factory handles the rest.
@ -53,7 +53,7 @@ The goal is to spend your time on the parts that require human judgment — what
<Card title="Quality Verification" icon="shield-check">
Build verification into your workflows.
</Card>
<Card title="Retros" icon="magnifying-glass-chart" href="/execution/retros">
Automatic retrospectives for continuous improvement.
<Card title="Observability" icon="magnifying-glass-chart" href="/execution/observability">
Inspect event streams, logs, and exported run state.
</Card>
</Columns>

View file

@ -16,7 +16,7 @@ Fabro replaces the prompt-wait-review loop with version-controlled workflow grap
- **Improve agent security** — Run agents in cloud sandboxes with full network and filesystem isolation. Keep untrusted code off your laptop and out of your production environment.
- **Run agents 24/7 at scale** — Fabro's API server queues and executes runs continuously in cloud sandboxes. Close your laptop — workflows keep running across as many concurrent runs as your infrastructure allows.
- **Guarantee code quality** — Layer deterministic verifications — test suites, linters, type checkers, LLM-as-judge — into your workflow graph. Failures trigger fix loops automatically.
- **Achieve compounding engineering** — Automatic retrospectives after every run feed a continuous improvement loop. Your workflows get better over time, not just your code.
- **Inspect every run** — Query durable event streams, checkpoints, conclusions, and stage outputs to understand what happened and improve the workflow.
- **Specify in natural language** — Define requirements as natural-language specs and let Fabro generate — and regenerate — implementations that conform to them.
<Columns cols={2}>

View file

@ -35,7 +35,7 @@ Fabro gives you a deterministic harness around non-deterministic AI. You define
Combine LLM-as-judge, test suites, third-party tools, and human review. Verifications act as an eval suite tailored to your organization, building confidence over time.
</Card>
<Card title="Observability" icon="magnifying-glass-chart">
Every tool call, agent turn, and shell command is captured in a unified event stream. Query run data with SQL via DuckDB and generate automatic retrospectives.
Every tool call, agent turn, and shell command is captured in a unified event stream. Query run data with SQL via DuckDB and inspect the full execution trail.
</Card>
<Card title="Open source" icon="code-branch">
Licensed under MIT. Written in Rust with minimal dependencies. Runs on a single node with no databases to set up.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 MiB

View file

@ -306,7 +306,6 @@ fabro create [OPTIONS] <WORKFLOW>
| `--in-place` | Run directly in the source checkout without git checkpoints |
| `--label <key=value>` | Attach a label to this run (repeatable, format: KEY=VALUE) |
| `--model <model>` | Override default LLM model |
| `--no-retro` | Skip retro generation after the run |
| `--preserve-sandbox` | Keep the sandbox alive after the run finishes (for debugging) |
| `--provider <provider>` | Override default LLM provider |
| `--sandbox <sandbox>` | Sandbox for agent tools<br />Values: `local`, `docker`, `daytona` |
@ -845,7 +844,6 @@ fabro run [OPTIONS] <WORKFLOW>
| `--in-place` | Run directly in the source checkout without git checkpoints |
| `--label <key=value>` | Attach a label to this run (repeatable, format: KEY=VALUE) |
| `--model <model>` | Override default LLM model |
| `--no-retro` | Skip retro generation after the run |
| `--preserve-sandbox` | Keep the sandbox alive after the run finishes (for debugging) |
| `--provider <provider>` | Override default LLM provider |
| `--sandbox <sandbox>` | Sandbox for agent tools<br />Values: `local`, `docker`, `daytona` |

View file

@ -73,7 +73,7 @@ rankdir=LR
| Attribute | Type | Description |
|---|---|---|
| `goal` | String | Workflow objective — guides agent behavior and retrospectives |
| `goal` | String | Workflow objective — guides agent behavior |
| `rankdir` | Identifier | Layout direction: `LR` (left-to-right) or `TB` (top-to-bottom) |
| `model_stylesheet` | String | CSS-like rules for model assignment (see [Model Stylesheets](/workflows/stylesheets)) |
| `default_max_retries` | Integer | Default retry count for all nodes (default: 0) |

View file

@ -30,7 +30,7 @@ These paths are local runtime state and caches, not the canonical run state.
- **`runtime/`** — Local runtime files. Today this includes `runtime/server.log` for the raw per-run worker tracing log and materialized blob payloads under `runtime/blobs/`.
- **`nodes/{manager_node}_{visit}/child/`** — Nested scratch directories for manager-loop child workflows.
Large durable values, event streams, checkpoints, diffs, conclusions, and retros are no longer projected into live scratch by default. Use `fabro events`, `fabro inspect`, the API, or `fabro dump` for those surfaces. Use `fabro logs` for the raw per-run worker tracing log when it is available.
Large durable values, event streams, checkpoints, diffs, and conclusions are no longer projected into live scratch by default. Use `fabro events`, `fabro inspect`, the API, or `fabro dump` for those surfaces. Use `fabro logs` for the raw per-run worker tracing log when it is available.
## Reconstructed and export-only layouts
@ -38,7 +38,6 @@ Metadata branch snapshots and `fabro dump` exports now use the same core layout:
- `run.json` for the current projection snapshot, including the current checkpoint
- `graph.fabro` for workflow source
- `stages/retro/*.md` for retro prompt/response text
- `stages/{rank:03}-{node_id}@{visit}/...` for execution-order-prefixed per-stage prompt, response, status, diff, and command output files
`fabro dump` adds export-only history surfaces on top of that shared layout:

View file

@ -41,7 +41,7 @@ fabro run docs/internal/demo/01-hello.fabro
- `shape=tab` makes this a **prompt node** — a single LLM call with no tool access. Good for generation, summarization, and classification.
- `reasoning_effort="low"` tells the model to think less. This is a simple task that doesn't need deep reasoning.
- `graph [goal="..."]` describes the workflow's purpose. Fabro uses it in preambles and retrospectives.
- `graph [goal="..."]` describes the workflow's purpose. Fabro uses it in preambles and agent context.
Every workflow needs exactly one `start` node (`shape=Mdiamond`) and one `exit` node (`shape=Msquare`).

View file

@ -350,7 +350,6 @@ def evaluate_instance(
cmd = [
"fabro", "run", str(toml_file),
"--auto-approve",
"--no-retro",
"--label", f"swe-eval={instance_id}",
]

View file

@ -247,7 +247,6 @@ def run_instance(
"--model", model,
"--provider", provider,
"--goal-file", str(goal_file),
"--no-retro",
"--label", f"swe-bench={instance_id}",
]

View file

@ -32,9 +32,6 @@ fn run_projection_round_trips_populated_projection() {
}
]],
"conclusion": null,
"retro": null,
"retro_prompt": null,
"retro_response": null,
"sandbox": null,
"final_patch": null,
"pull_request": null,
@ -90,9 +87,6 @@ fn run_projection_round_trips_with_pending_control_unset() {
"checkpoint": null,
"checkpoints": [],
"conclusion": null,
"retro": null,
"retro_prompt": null,
"retro_response": null,
"sandbox": null,
"final_patch": null,
"pull_request": null,

View file

@ -32,7 +32,6 @@ fabro-install = { path = "../fabro-install" }
fabro-interview = { path = "../fabro-interview" }
fabro-mcp = { path = "../fabro-mcp" }
fabro-proc = { path = "../fabro-proc" }
fabro-retro = { path = "../fabro-retro" }
fabro-sandbox = { path = "../fabro-sandbox", features = ["daytona"] }
fabro-checkpoint = { path = "../fabro-checkpoint" }
fabro-graphviz = { path = "../fabro-graphviz" }

View file

@ -254,10 +254,6 @@ pub(crate) struct RunArgs {
#[arg(long = "label", value_name = "KEY=VALUE")]
pub(crate) label: Vec<String>,
/// Skip retro generation after the run
#[arg(long)]
pub(crate) no_retro: bool,
/// Keep the sandbox alive after the run finishes (for debugging)
#[arg(long)]
pub(crate) preserve_sandbox: bool,

View file

@ -849,9 +849,6 @@ mod tests {
"checkpoint": null,
"checkpoints": [],
"conclusion": null,
"retro": null,
"retro_prompt": null,
"retro_response": null,
"sandbox": null,
"final_patch": null,
"pull_request": null,

View file

@ -775,31 +775,6 @@ fn format_event_pretty_value(envelope: &serde_json::Value, styles: &Styles) -> O
styles.red.apply_to(error),
))
}
"retro.completed" => {
let duration = format_duration_ms(prop_field(envelope, "duration_ms"));
Some(format!(
"{} {} Retro {}",
styles.dim.apply_to(&ts),
styles.green.apply_to("\u{2713}"),
duration,
))
}
"retro.failed" => {
let error = prop_str_field(envelope, "error").unwrap_or("unknown error");
let duration = format_duration_ms(prop_field(envelope, "duration_ms"));
Some(format!(
"{} {} Retro {} {}",
styles.dim.apply_to(&ts),
styles.bold_red.apply_to("\u{2717}"),
duration,
styles.red.apply_to(error),
))
}
"retro.started" => Some(format!(
"{} {} Retro",
styles.dim.apply_to(&ts),
styles.bold_cyan.apply_to("\u{25b6}"),
)),
_ => None,
}
}

View file

@ -57,12 +57,8 @@ fn sandbox_layer(
})
}
fn execution_layer(
dry_run: Option<bool>,
auto_approve: Option<bool>,
no_retro: Option<bool>,
) -> Option<RunExecutionLayer> {
if dry_run.is_none() && auto_approve.is_none() && no_retro.is_none() {
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 {
@ -74,7 +70,6 @@ fn execution_layer(
ApprovalMode::Prompt
}
}),
retros: no_retro.map(|nr| !nr),
})
}
@ -129,11 +124,7 @@ pub(crate) fn run_args_overrides(args: &RunArgs) -> Result<ManifestSettingsOverr
args.sandbox.map(Into::into),
sparse_flag(args.preserve_sandbox),
);
let execution = execution_layer(
sparse_flag(args.dry_run),
sparse_flag(args.auto_approve),
sparse_flag(args.no_retro),
);
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)?;

View file

@ -217,13 +217,6 @@ pub(super) enum ProgressEvent {
from_node: String,
to_node: String,
},
RetroStarted,
RetroCompleted {
duration_ms: u64,
},
RetroFailed {
duration_ms: u64,
},
MetadataSnapshotFailed {
phase: String,
failure_kind: String,
@ -462,13 +455,6 @@ pub(super) fn from_run_event(stored: &RunEvent) -> Option<ProgressEvent> {
from_node: props.from_node.clone(),
to_node: props.to_node.clone(),
}),
EventBody::RetroStarted(_) => Some(ProgressEvent::RetroStarted),
EventBody::RetroCompleted(props) => Some(ProgressEvent::RetroCompleted {
duration_ms: props.duration_ms,
}),
EventBody::RetroFailed(props) => Some(ProgressEvent::RetroFailed {
duration_ms: props.duration_ms,
}),
EventBody::MetadataSnapshotFailed(props) => Some(ProgressEvent::MetadataSnapshotFailed {
phase: props.phase.to_string(),
failure_kind: props.failure_kind.to_string(),

View file

@ -420,15 +420,6 @@ impl ProgressUI {
ProgressEvent::LoopRestart { from_node, to_node } => {
self.info.on_loop_restart(renderer, &from_node, &to_node);
}
ProgressEvent::RetroStarted => {
self.stage.on_retro_started(renderer);
}
ProgressEvent::RetroCompleted { duration_ms } => {
self.stage.on_retro_completed(renderer, duration_ms);
}
ProgressEvent::RetroFailed { duration_ms } => {
self.stage.on_retro_failed(renderer, duration_ms);
}
ProgressEvent::MetadataSnapshotFailed {
phase,
failure_kind,

View file

@ -547,30 +547,6 @@ impl StageDisplay {
);
}
pub(super) fn on_retro_started(&mut self, renderer: &ProgressRenderer) {
self.on_stage_started(renderer, "retro", "Retro", None);
}
pub(super) fn on_retro_completed(&mut self, renderer: &ProgressRenderer, duration_ms: u64) {
self.finish_stage(
renderer,
"retro",
"Retro",
&styles::green_check(renderer.styles()),
&format_duration_ms(duration_ms),
);
}
pub(super) fn on_retro_failed(&mut self, renderer: &ProgressRenderer, duration_ms: u64) {
self.finish_stage(
renderer,
"retro",
"Retro",
&styles::red_cross(renderer.styles()),
&format_duration_ms(duration_ms),
);
}
fn finish_stage(
&mut self,
renderer: &ProgressRenderer,

View file

@ -177,7 +177,6 @@ pub(crate) fn run_manifest_args(args: &RunArgs) -> Option<types::ManifestArgs> {
dry_run: args.dry_run.then_some(true),
label: args.label.clone(),
model: args.model.clone(),
no_retro: args.no_retro.then_some(true),
preserve_sandbox: args.preserve_sandbox.then_some(true),
provider: args.provider.clone(),
sandbox: args
@ -201,7 +200,6 @@ pub(crate) fn preflight_manifest_args(args: &PreflightArgs) -> Option<types::Man
dry_run: None,
label: Vec::new(),
model: args.model.clone(),
no_retro: None,
preserve_sandbox: None,
provider: args.provider.clone(),
sandbox: args
@ -662,7 +660,6 @@ fn manifest_args_is_empty(args: &types::ManifestArgs) -> bool {
&& args.dry_run.is_none()
&& args.label.is_empty()
&& args.model.is_none()
&& args.no_retro.is_none()
&& args.preserve_sandbox.is_none()
&& args.provider.is_none()
&& args.sandbox.is_none()

View file

@ -161,7 +161,6 @@ fn start_detached_human_run(
.args([
"run",
"--detach",
"--no-retro",
"--sandbox",
"local",
"--provider",
@ -330,7 +329,6 @@ fn attach_replays_completed_detached_run() {
"run",
"--dry-run",
"--auto-approve",
"--no-retro",
"--detach",
"--run-id",
run_id.as_str(),
@ -394,7 +392,6 @@ fn attach_advances_when_pending_question_is_answered_elsewhere() {
.args([
"run",
"--detach",
"--no-retro",
"--sandbox",
"local",
"--provider",
@ -522,7 +519,6 @@ fn attach_before_completion_streams_to_finished_state() {
"openai",
"--sandbox",
"local",
"--no-retro",
"slow.fabro",
]);
let run_output = run_cmd.output().expect("command should execute");
@ -643,7 +639,6 @@ fn attach_json_errors_without_prompting_for_human_input() {
.args([
"run",
"--detach",
"--no-retro",
"--sandbox",
"local",
"--provider",
@ -937,8 +932,7 @@ fn attach_json_errors_without_prompting_for_human_input() {
},
"execution": {
"approval": "prompt",
"mode": "normal",
"retros": false
"mode": "normal"
},
"git": {
"author": null

View file

@ -55,7 +55,6 @@ fn help() {
--sandbox <SANDBOX> Sandbox for agent tools [possible values: local, docker, daytona]
--in-place Run directly in the source checkout without git checkpoints
--label <KEY=VALUE> Attach a label to this run (repeatable, format: KEY=VALUE)
--no-retro Skip retro generation after the run
--preserve-sandbox Keep the sandbox alive after the run finishes (for debugging)
-d, --detach Run the workflow in the background and print the run ID
-h, --help Print help
@ -325,7 +324,6 @@ fn create_persists_requested_overrides_into_store() {
"--label",
"team=cli",
"--verbose",
"--no-retro",
"--preserve-sandbox",
workflow.to_str().unwrap(),
]);
@ -362,7 +360,6 @@ fn create_persists_requested_overrides_into_store() {
},
"dry_run": resolved_run.execution.mode == fabro_types::settings::run::RunMode::DryRun,
"auto_approve": resolved_run.execution.approval == fabro_types::settings::run::ApprovalMode::Auto,
"no_retro": !resolved_run.execution.retros,
"llm": {
"model": resolved_run.model.name.as_ref().map(fabro_types::settings::InterpString::as_source),
"provider": resolved_run.model.provider.as_ref().map(fabro_types::settings::InterpString::as_source),
@ -382,7 +379,6 @@ fn create_persists_requested_overrides_into_store() {
"goal": "Ship the release",
"dry_run": true,
"auto_approve": true,
"no_retro": true,
"llm": {
"model": "gpt-5",
"provider": "openai"

View file

@ -103,13 +103,7 @@ fn dump_exports_large_command_output_backed_by_blob_refs() {
let mut run_cmd = context.run_cmd();
run_cmd.current_dir(&context.temp_dir);
run_cmd.timeout(Duration::from_secs(30));
run_cmd.args([
"--run-id",
run_id.as_str(),
"--no-retro",
"--sandbox",
"local",
]);
run_cmd.args(["--run-id", run_id.as_str(), "--sandbox", "local"]);
run_cmd.arg(&workflow);
let run_output = run_cmd.output().expect("command should execute");
assert!(
@ -203,7 +197,6 @@ include = ["assets/**"]
run_cmd.args([
"--run-id",
run_id.as_str(),
"--no-retro",
"--sandbox",
"local",
"run.toml",

View file

@ -282,25 +282,13 @@ fn ps_filters_by_workflow_and_label() {
context
.run_cmd()
.args([
"--dry-run",
"--auto-approve",
"--no-retro",
"--label",
"suite=alpha",
])
.args(["--dry-run", "--auto-approve", "--label", "suite=alpha"])
.arg(&simple)
.assert()
.success();
context
.create_cmd()
.args([
"--dry-run",
"--auto-approve",
"--no-retro",
"--label",
"suite=beta",
])
.args(["--dry-run", "--auto-approve", "--label", "suite=beta"])
.arg(&branching)
.assert()
.success();

View file

@ -57,9 +57,6 @@ fn remote_run_state_response() -> serde_json::Value {
"billing": null,
"total_retries": 0,
},
"retro": null,
"retro_prompt": null,
"retro_response": null,
"sandbox": null,
"final_patch": null,
"pull_request": null,
@ -154,7 +151,6 @@ fn help() {
--sandbox <SANDBOX> Sandbox for agent tools [possible values: local, docker, daytona]
--in-place Run directly in the source checkout without git checkpoints
--label <KEY=VALUE> Attach a label to this run (repeatable, format: KEY=VALUE)
--no-retro Skip retro generation after the run
--preserve-sandbox Keep the sandbox alive after the run finishes (for debugging)
-d, --detach Run the workflow in the background and print the run ID
-h, --help Print help
@ -373,7 +369,6 @@ digraph VaultWorkerLlm {
"--run-id",
run_id.as_str(),
"--auto-approve",
"--no-retro",
"--sandbox",
"local",
"--provider",
@ -412,7 +407,6 @@ fn detach_rejects_storage_dir_flag() {
"--detach",
"--dry-run",
"--auto-approve",
"--no-retro",
workflow.to_str().unwrap(),
])
.output()
@ -691,7 +685,6 @@ include = ["assets/**"]
"--run-id",
run_id.as_str(),
"--auto-approve",
"--no-retro",
"--sandbox",
"local",
"--provider",
@ -966,7 +959,6 @@ fn json_run_requires_manual_input_for_human_gates_without_auto_approve() {
"run",
"--sandbox",
"local",
"--no-retro",
workflow.to_str().unwrap(),
])
.output()

View file

@ -481,7 +481,6 @@ methods = ["dev-token"]
run_id.as_str(),
"--detach",
"--auto-approve",
"--no-retro",
"--sandbox",
"local",
workflow_path
@ -566,7 +565,6 @@ digraph Test {
"run",
"--dry-run",
"--auto-approve",
"--no-retro",
"--detach",
context.temp_dir.join("workflow.fabro").to_str().unwrap(),
])
@ -667,9 +665,6 @@ fn runner_reports_missing_run_spec_without_prefetching_events() {
"checkpoint": null,
"checkpoints": [],
"conclusion": null,
"retro": null,
"retro_prompt": null,
"retro_response": null,
"sandbox": null,
"final_patch": null,
"pull_request": null,
@ -749,7 +744,6 @@ fn detached_run_answers_pending_question_without_interview_scratch_files() {
"--detach",
"--run-id",
run_id.as_str(),
"--no-retro",
"--sandbox",
"local",
workflow_path.to_str().unwrap(),
@ -810,77 +804,6 @@ fn detached_run_answers_pending_question_without_interview_scratch_files() {
)));
}
#[test]
fn worker_exits_with_retro_enabled_even_when_stdin_stays_open() {
let context = auth_context();
let run_id = unique_run_id();
let workflow_path = context.temp_dir.join("retro-success.fabro");
context.write_temp(
".fabro/project.toml",
r"_version = 1
[run.execution]
retros = true
",
);
context.write_temp(
"retro-success.fabro",
r#"digraph RetroSuccess {
graph [goal="Finish successfully with retro enabled"]
start [shape=Mdiamond, label="Start"]
exit [shape=Msquare, label="Exit"]
work [shape=parallelogram, label="Work", script="true"]
start -> work -> exit
}
"#,
);
context
.command()
.args([
"create",
"--dry-run",
"--auto-approve",
"--run-id",
run_id.as_str(),
workflow_path.to_str().unwrap(),
])
.assert()
.success();
let run_dir = context.find_run_dir(&run_id);
let server = server_target(&context.storage_dir);
let mut child = spawn_worker_process(&context, &server, &run_dir, &run_id, "start");
let stdin = child.stdin.take().expect("worker stdin should be piped");
wait_for_event_names(&run_dir, &["run.completed", "retro.completed"]);
let status = wait_for_child_exit(&mut child, SHARED_DAEMON_TIMEOUT);
drop(stdin);
let output = child_output(child, status);
assert!(
output.status.success(),
"worker should exit successfully after retro even with stdin open:\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
let events = stored_worker_events(&run_dir);
let run_completed_index = events
.iter()
.position(|event| matches!(&event.body, EventBody::RunCompleted(_)))
.expect("run.completed should be present");
let retro_completed_index = events
.iter()
.position(|event| matches!(&event.body, EventBody::RetroCompleted(_)))
.expect("retro.completed should be present");
assert!(
retro_completed_index < run_completed_index,
"retro.completed must precede run.completed"
);
}
#[cfg(unix)]
#[test]
fn worker_exits_after_sigterm_cancel_even_when_stdin_stays_open() {
@ -894,7 +817,6 @@ fn worker_exits_after_sigterm_cancel_even_when_stdin_stays_open() {
.args([
"create",
"--auto-approve",
"--no-retro",
"--sandbox",
"local",
"--run-id",

View file

@ -145,7 +145,6 @@ fn start_rejects_already_active_or_completed_run() {
"openai",
"--sandbox",
"local",
"--no-retro",
"slow.fabro",
]);
let create_output = create_cmd.output().expect("command should execute");
@ -207,7 +206,6 @@ fn start_runs_under_server_ownership_without_launcher_record() {
"openai",
"--sandbox",
"local",
"--no-retro",
"owned-by-server.fabro",
])
.env("OPENAI_API_KEY", "test")

View file

@ -200,7 +200,6 @@ fn run_completed_dry_run(context: &TestContext, workflow: &Path) -> RunSetup {
run_id.as_str(),
"--dry-run",
"--auto-approve",
"--no-retro",
"--sandbox",
"local",
]);
@ -208,7 +207,7 @@ fn run_completed_dry_run(context: &TestContext, workflow: &Path) -> RunSetup {
let output = cmd.output().expect("command should execute");
if !output.status.success() {
panic!(
"command failed: fabro run --dry-run --auto-approve --no-retro --sandbox local {}\nstdout:\n{}\nstderr:\n{}",
"command failed: fabro run --dry-run --auto-approve --sandbox local {}\nstdout:\n{}\nstderr:\n{}",
workflow.display(),
stdout(&output),
stderr(&output)
@ -264,7 +263,6 @@ pub(crate) fn setup_detached_dry_run(context: &TestContext) -> RunSetup {
"--detach",
"--dry-run",
"--auto-approve",
"--no-retro",
"--sandbox",
"local",
]);
@ -272,7 +270,7 @@ pub(crate) fn setup_detached_dry_run(context: &TestContext) -> RunSetup {
let output = cmd.output().expect("command should execute");
if !output.status.success() {
panic!(
"command failed: fabro run --detach --dry-run --auto-approve --no-retro --sandbox local {}\nstdout:\n{}\nstderr:\n{}",
"command failed: fabro run --detach --dry-run --auto-approve --sandbox local {}\nstdout:\n{}\nstderr:\n{}",
fixture("simple.fabro").display(),
stdout(&output),
stderr(&output)
@ -372,7 +370,6 @@ fn run_local_workflow(context: &TestContext, workspace_dir: &Path, workflow: &st
"--run-id",
run_id.as_str(),
"--auto-approve",
"--no-retro",
"--sandbox",
"local",
"--provider",
@ -382,7 +379,7 @@ fn run_local_workflow(context: &TestContext, workspace_dir: &Path, workflow: &st
let output = cmd.output().expect("command should execute");
if !output.status.success() {
panic!(
"command failed: fabro run --auto-approve --no-retro --sandbox local --provider openai {workflow}\nstdout:\n{}\nstderr:\n{}",
"command failed: fabro run --auto-approve --sandbox local --provider openai {workflow}\nstdout:\n{}\nstderr:\n{}",
stdout(&output),
stderr(&output)
);
@ -752,7 +749,6 @@ async fn seed_dry_run(context: &TestContext, state: SeededRunState) -> RunSetup
serde_json::json!({
"dry_run": true,
"auto_approve": true,
"no_retro": true,
"sandbox": "local",
"label": test_labels(context),
}),
@ -780,7 +776,6 @@ async fn seed_git_backed_changed_run(context: &TestContext) -> SeededGitRunSetup
serde_json::json!({
"provider": "openai",
"sandbox": "local",
"no_retro": true,
"label": test_labels(context),
}),
Some(serde_json::json!({
@ -825,7 +820,6 @@ async fn seed_git_backed_noop_run(context: &TestContext) -> RunSetup {
serde_json::json!({
"provider": "openai",
"sandbox": "local",
"no_retro": true,
"label": test_labels(context),
}),
Some(serde_json::json!({
@ -855,7 +849,6 @@ async fn seed_artifact_run(context: &TestContext) -> RunSetup {
artifact_workflow_source(),
serde_json::json!({
"sandbox": "local",
"no_retro": true,
"label": test_labels(context),
}),
None,

View file

@ -302,7 +302,6 @@ async fn github_only_server_dispatched_worker_succeeds_without_worker_auth_store
"--detach",
"--dry-run",
"--auto-approve",
"--no-retro",
"--sandbox",
"local",
workflow.to_str().unwrap(),
@ -359,7 +358,6 @@ fn runner_rejects_bogus_worker_token_against_github_only_server() {
&run_id,
"--dry-run",
"--auto-approve",
"--no-retro",
"--sandbox",
"local",
workflow.to_str().unwrap(),

View file

@ -29,7 +29,6 @@ fn local_run_lifecycle() {
cmd(&[
"run",
"--auto-approve",
"--no-retro",
"--sandbox",
"local",
fixture("command_pipeline.fabro").to_str().unwrap(),

View file

@ -15,9 +15,6 @@ fn live_run_state_response() -> serde_json::Value {
"checkpoint": null,
"checkpoints": [],
"conclusion": null,
"retro": null,
"retro_prompt": null,
"retro_response": null,
"sandbox": null,
"final_patch": null,
"pull_request": null,

View file

@ -13,7 +13,6 @@ fn scenario_agent_linear(sandbox: &str) {
.run_cmd()
.args([
"--auto-approve",
"--no-retro",
"--sandbox",
sandbox,
"--model",

View file

@ -19,7 +19,6 @@ fn scenario_command_agent_mixed(sandbox: &str) {
.run_cmd()
.args([
"--auto-approve",
"--no-retro",
"--sandbox",
sandbox,
"--model",

View file

@ -23,7 +23,7 @@ fn scenario_command_pipeline(sandbox: &str) {
context
.run_cmd()
.args(["--auto-approve", "--no-retro", "--sandbox", sandbox])
.args(["--auto-approve", "--sandbox", sandbox])
.arg(fixture("command_pipeline.fabro"))
.timeout(timeout_for(sandbox))
.assert()

View file

@ -9,7 +9,7 @@ fn scenario_conditional_branching(sandbox: &str) {
context
.run_cmd()
.args(["--auto-approve", "--no-retro", "--sandbox", sandbox])
.args(["--auto-approve", "--sandbox", sandbox])
.arg(fixture("conditional_branching.fabro"))
.timeout(timeout_for(sandbox))
.assert()

View file

@ -19,7 +19,6 @@ fn scenario_full_stack(sandbox: &str) {
.run_cmd()
.args([
"--auto-approve",
"--no-retro",
"--sandbox",
sandbox,
"--model",

View file

@ -64,7 +64,6 @@ fn configure_hook_env(cmd: &mut assert_cmd::Command, hook_model: &str) {
cmd.env_remove("ANTHROPIC_API_KEY");
}
cmd.arg("--sandbox").arg("local");
cmd.arg("--no-retro");
cmd.arg("--auto-approve");
cmd.arg("--provider").arg(stage_provider());
cmd.arg("--model").arg(hook_model);

View file

@ -11,7 +11,6 @@ fn scenario_human_gate(sandbox: &str) {
.run_cmd()
.args([
"--auto-approve",
"--no-retro",
"--sandbox",
sandbox,
"--model",

View file

@ -501,7 +501,6 @@ command = ["demo-mcp"]
execution: Some(RunExecutionLayer {
mode: Some(RunMode::DryRun),
approval: Some(ApprovalMode::Auto),
retros: Some(false),
}),
..RunLayer::default()
})
@ -539,6 +538,5 @@ command = ["demo-mcp"]
);
assert_eq!(settings.run.execution.mode, RunMode::DryRun);
assert_eq!(settings.run.execution.approval, ApprovalMode::Auto);
assert!(!settings.run.execution.retros);
}
}

View file

@ -11,7 +11,6 @@ graph = "workflow.fabro"
[run.execution]
mode = "normal"
approval = "prompt"
retros = true
[run.prepare]
timeout = "5m"

View file

@ -238,9 +238,6 @@ pub struct RunExecutionLayer {
pub mode: Option<RunMode>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub approval: Option<ApprovalMode>,
/// Positive-form: `true` runs retros, `false` skips them.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub retros: Option<bool>,
}
/// `[run.checkpoint]` — checkpoint policy.

View file

@ -126,7 +126,6 @@ fn rename_hint(key: &str) -> Option<String> {
"upgrade_check" => "rename to `[cli.updates] check`",
"dry_run" => "rename to `[run.execution] mode = \"dry_run\"`",
"auto_approve" => "rename to `[run.execution] approval = \"auto\"`",
"no_retro" => "rename to `[run.execution] retros = false`",
_ => return None,
};
Some(target.to_owned())

View file

@ -334,25 +334,6 @@ pub fn resolve_workflow(arg: &Path) -> Result<PathBuf> {
Ok(resolution.dot_path)
}
/// Check whether retros are enabled in the project config.
/// Retros are now expressed as `[run.execution] retros = true` in v2.
pub fn is_retro_enabled() -> bool {
let start = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
match discover_project_config(&start) {
Ok(Some(path)) => load_project_config(&path)
.ok()
.and_then(|config| {
config
.run
.as_ref()
.and_then(|r| r.execution.as_ref())
.and_then(|e| e.retros)
})
.unwrap_or(false),
_ => false,
}
}
fn normalize_joined_path(base_dir: &Path, reference: &Path) -> PathBuf {
if reference.is_absolute() {
return reference.to_path_buf();
@ -420,26 +401,6 @@ directory = "custom/"
);
}
#[test]
fn parse_with_run_execution_retros() {
let config = "
_version = 1
[run.execution]
retros = true
"
.parse::<crate::SettingsLayer>()
.unwrap();
assert_eq!(
config
.run
.as_ref()
.and_then(|r| r.execution.as_ref())
.and_then(|e| e.retros),
Some(true)
);
}
#[test]
fn parse_rejects_legacy_llm_section() {
let err = "_version = 1\n[llm]\nprovider = \"openai\"\n"

View file

@ -142,9 +142,6 @@ fn resolve_execution(execution: Option<&RunExecutionLayer>) -> RunExecutionSetti
approval: execution
.approval
.expect("defaults.toml should provide run.execution.approval"),
retros: execution
.retros
.expect("defaults.toml should provide run.execution.retros"),
}
}

View file

@ -11,7 +11,6 @@ fn resolves_run_defaults_from_empty_settings() {
assert_eq!(settings.execution.mode, RunMode::Normal);
assert_eq!(settings.execution.approval, ApprovalMode::Prompt);
assert!(settings.execution.retros);
assert_eq!(settings.prepare.timeout_ms, 300_000);
assert_eq!(settings.sandbox.provider, "docker");
assert!(settings.sandbox.stop_on_terminal);

View file

@ -137,16 +137,6 @@ impl RunDump {
}
}
if let Some(prompt) = state.retro_prompt.as_ref() {
entries.push(RunDumpEntry::text("stages/retro/prompt.md", prompt.clone()));
}
if let Some(response) = state.retro_response.as_ref() {
entries.push(RunDumpEntry::text(
"stages/retro/response.md",
response.clone(),
));
}
Ok(Self {
entries,
stage_ranks,
@ -571,8 +561,6 @@ mod tests {
clone_origin_url: None,
clone_branch: None,
});
projection.retro_prompt = Some("retro prompt".to_string());
projection.retro_response = Some("retro response".to_string());
let stage =
projection.stage_entry(stage_id.node_id(), stage_id.visit(), first_event_seq(2));
stage.prompt = Some("plan".to_string());
@ -602,8 +590,6 @@ mod tests {
assert!(paths.contains(&"run.json"));
assert!(paths.contains(&"graph.fabro"));
assert!(paths.contains(&"stages/retro/prompt.md"));
assert!(paths.contains(&"stages/retro/response.md"));
assert!(paths.contains(&"stages/001-build@2/prompt.md"));
assert!(paths.contains(&"stages/001-build@2/response.md"));
assert!(paths.contains(&"stages/001-build@2/status.json"));
@ -617,7 +603,6 @@ mod tests {
assert!(!paths.contains(&"status.json"));
assert!(!paths.contains(&"checkpoint.json"));
assert!(!paths.contains(&"sandbox.json"));
assert!(!paths.contains(&"retro.json"));
assert!(!paths.contains(&"conclusion.json"));
let run_json = dump

View file

@ -1,31 +0,0 @@
[package]
name = "fabro-retro"
edition.workspace = true
version.workspace = true
publish = false
license.workspace = true
description = "Retrospective analysis for Fabro workflow runs"
[lib]
doctest = false
[lints]
workspace = true
[dependencies]
anyhow = "1"
chrono = { workspace = true, features = ["serde"] }
fabro-agent = { path = "../fabro-agent" }
fabro-dump = { path = "../fabro-dump" }
fabro-llm = { path = "../fabro-llm" }
fabro-store = { path = "../fabro-store" }
fabro-types = { path = "../fabro-types" }
fabro-util = { path = "../fabro-util" }
serde.workspace = true
serde_json.workspace = true
tokio.workspace = true
tracing.workspace = true
[dev-dependencies]
tokio = { workspace = true, features = ["test-util", "macros"] }
tempfile = "3"

View file

@ -1,2 +0,0 @@
pub mod retro;
pub mod retro_agent;

View file

@ -1,101 +0,0 @@
use std::collections::HashMap;
use fabro_types::RunId;
pub use fabro_types::retro::{
AggregateStats, FrictionKind, FrictionPoint, Learning, LearningCategory, OpenItem,
OpenItemKind, Retro, RetroNarrative, SmoothnessRating, StageRetro,
};
#[derive(Debug, Clone)]
pub struct CompletedStage {
pub node_id: String,
pub status: String,
pub succeeded: bool,
pub failed: bool,
pub retries: u32,
pub billing_usd_micros: Option<i64>,
pub notes: Option<String>,
pub failure_reason: Option<String>,
pub files_touched: Vec<String>,
}
pub fn derive_retro(
run_id: RunId,
workflow_name: &str,
goal: &str,
completed_stages: Vec<CompletedStage>,
duration_ms: u64,
stage_durations: &HashMap<String, u64>,
) -> Retro {
let mut stages = Vec::new();
let mut all_files: Vec<String> = Vec::new();
let mut total_billing_usd_micros: Option<i64> = None;
let mut total_retries: u32 = 0;
let mut stages_completed: usize = 0;
let mut stages_failed: usize = 0;
for cs in completed_stages {
total_retries += cs.retries;
if cs.succeeded {
stages_completed += 1;
}
if cs.failed {
stages_failed += 1;
}
if let Some(cost) = cs.billing_usd_micros {
*total_billing_usd_micros.get_or_insert(0) += cost;
}
let dur = stage_durations.get(&cs.node_id).copied().unwrap_or(0);
stages.push(StageRetro {
stage_label: cs.node_id.clone(),
duration_ms: dur,
retries: cs.retries,
billing_usd_micros: cs.billing_usd_micros,
stage_id: cs.node_id,
status: cs.status,
notes: cs.notes,
failure_reason: cs.failure_reason,
files_touched: cs.files_touched,
});
all_files.extend(
stages
.last()
.expect("stage just pushed")
.files_touched
.iter()
.cloned(),
);
}
all_files.sort();
all_files.dedup();
let stats = AggregateStats {
total_duration_ms: duration_ms,
total_billing_usd_micros,
total_retries,
files_touched: all_files,
stages_completed,
stages_failed,
};
Retro {
run_id,
workflow_name: workflow_name.to_string(),
goal: goal.to_string(),
timestamp: chrono::Utc::now(),
smoothness: None,
stages,
stats,
intent: None,
outcome: None,
learnings: None,
friction_points: None,
open_items: None,
}
}

View file

@ -1,582 +0,0 @@
use std::sync::{Arc, Mutex};
use std::time::Duration;
use anyhow::Context as _;
use fabro_agent::tool_registry::RegisteredTool;
use fabro_agent::{
AgentProfile, AnthropicProfile, GeminiProfile, OpenAiProfile, Sandbox, Session, SessionEvent,
SessionOptions, Turn,
};
use fabro_dump::{BlobReader, RunDump};
use fabro_llm::client::Client;
use fabro_llm::provider::Provider;
use fabro_llm::types::ToolDefinition;
use fabro_store::{EventEnvelope, RunProjection};
use tokio::task::JoinHandle;
use crate::retro::{RetroNarrative, SmoothnessRating};
const RETRO_SYSTEM_PROMPT: &str = r"You are a workflow run retrospective analyst. Your job is to analyze a completed workflow run and generate a structured retrospective.
You have access to the run's data files:
- `events.jsonl` the full event stream (stage starts/completions, agent tool calls, errors, retries)
- `run.json` serialized run projection with the run spec, checkpoint state, conclusion, retro data, and other metadata
- `graph.fabro` the workflow source for the run
- `checkpoints/{seq:04}.json` zero-padded checkpoint snapshots captured during the run
- `run.log` server/worker log output for the run when available
- `stages/{rank:03}-{node_id}@{visit}/...` execution-order-prefixed per-stage prompt, response, status, diff, output, and tool metadata files
## Your task
1. **Explore the data** using grep and read tools to understand what happened:
- Look for failures, retries, and error messages
- Check agent tool call patterns for wrong approaches or pivots
- Note which stages took longest or had issues
- Look for patterns indicating friction (repeated similar tool calls, error recovery)
- Use `run.json` for the run-level snapshot, `graph.fabro` for workflow intent, and `stages/` for full per-stage payloads
2. **Call the `submit_retro` tool** with your structured analysis.
## Smoothness grading guidelines
Grade the run on a 5-point scale:
- **effortless** Run achieved its goal on the first try with no retries, no wrong approaches. Agent moved efficiently from start to finish.
- **smooth** Goal achieved with minor hiccups (1-2 retries or a brief wrong approach quickly corrected). No human intervention needed. Overall clean execution.
- **bumpy** Goal achieved but with notable friction: multiple retries, at least one significant wrong approach, or substantial time spent on dead ends.
- **struggled** Goal achieved only with difficulty: many retries, major approach changes, human intervention, or partial failures requiring recovery.
- **failed** Run did not achieve its stated goal. May have completed some stages but the overall intent was not fulfilled.
Consider the full context: not just stage pass/fail, but the quality of the journey visible in the agent events (tool call patterns, error recovery, approach pivots).
## Guidelines for qualitative fields
- **intent**: What was the workflow run trying to accomplish? Summarize the goal in a sentence.
- **outcome**: What actually happened? Did it succeed? What was produced?
- **learnings**: What was discovered about the repo, code, workflow, or tools?
- **friction_points**: Where did things get stuck? What caused slowdowns?
- **open_items**: What follow-up work, tech debt, or gaps were identified?
Be specific and concise. Reference actual stage names, file paths, and error messages where relevant.";
const SUBMIT_RETRO_SCHEMA: &str = r#"{
"type": "object",
"properties": {
"smoothness": {
"type": "string",
"enum": ["effortless", "smooth", "bumpy", "struggled", "failed"],
"description": "Overall smoothness rating for the workflow run"
},
"intent": {
"type": "string",
"description": "What was the workflow run trying to accomplish?"
},
"outcome": {
"type": "string",
"description": "What actually happened? Did it succeed?"
},
"learnings": {
"type": "array",
"items": {
"type": "object",
"properties": {
"category": { "type": "string", "enum": ["repo", "code", "workflow", "tool"] },
"text": { "type": "string" }
},
"required": ["category", "text"]
},
"description": "What was discovered during the run?"
},
"friction_points": {
"type": "array",
"items": {
"type": "object",
"properties": {
"kind": { "type": "string", "enum": ["retry", "timeout", "wrong_approach", "tool_failure", "ambiguity"] },
"description": { "type": "string" },
"stage_id": { "type": "string" }
},
"required": ["kind", "description"]
},
"description": "Where did things get stuck?"
},
"open_items": {
"type": "array",
"items": {
"type": "object",
"properties": {
"kind": { "type": "string", "enum": ["tech_debt", "follow_up", "investigation", "test_gap"] },
"description": { "type": "string" }
},
"required": ["kind", "description"]
},
"description": "Follow-up work or gaps identified"
}
},
"required": ["smoothness", "intent", "outcome"]
}"#;
pub const RETRO_DATA_DIR: &str = "/tmp/retro_data";
pub struct RetroAgentResult {
pub narrative: RetroNarrative,
pub response: String,
}
#[must_use]
pub fn build_retro_prompt(retro_data_dir: &str) -> String {
format!(
"Analyze the workflow run data at `{retro_data_dir}/` and generate a retrospective. \
The key file is `{retro_data_dir}/events.jsonl` which contains the full event stream. \
Use `{retro_data_dir}/run.json` for the run-level snapshot, `{retro_data_dir}/graph.fabro` \
for the workflow source, `{retro_data_dir}/checkpoints/` for checkpoint snapshots, \
`{retro_data_dir}/run.log` for server logs when present, and `{retro_data_dir}/stages/` \
for full per-stage payloads. \
Use grep to search for interesting signals (failures, retries, errors, approach changes) \
rather than reading the entire file. When done, call the `submit_retro` tool with your analysis."
)
}
/// Run a retro agent session that analyzes workflow run data and produces
/// a structured narrative. The agent explores `events.jsonl` and other
/// files via tool access, then calls `submit_retro` with its analysis.
pub async fn run_retro_agent(
sandbox: &Arc<dyn Sandbox>,
state: &RunProjection,
events: &[EventEnvelope],
run_log: Option<Vec<u8>>,
blob_reader: Option<BlobReader>,
llm_client: &Client,
provider: Provider,
model: &str,
event_callback: Option<Arc<dyn Fn(SessionEvent) + Send + Sync>>,
) -> anyhow::Result<RetroAgentResult> {
// Upload data files into sandbox (needed for Daytona; no-op effect for local
// since the agent can also read from the original paths via tools).
upload_data_files(sandbox, state, events, RETRO_DATA_DIR, run_log, blob_reader).await?;
// Build provider profile with the submit_retro tool
let captured: Arc<Mutex<Option<RetroNarrative>>> = Arc::new(Mutex::new(None));
let captured_clone = Arc::clone(&captured);
let mut profile = build_profile(provider, model);
// Register submit_retro tool
let submit_tool = RegisteredTool {
definition: ToolDefinition {
name: "submit_retro".to_string(),
description: "Submit the structured retrospective analysis. Call this once you have analyzed the workflow run data.".to_string(),
parameters: serde_json::from_str(SUBMIT_RETRO_SCHEMA)
.expect("submit_retro schema should be valid JSON"),
},
executor: Arc::new(move |args, _ctx| {
let captured = Arc::clone(&captured_clone);
Box::pin(async move {
let narrative: RetroNarrative = serde_json::from_value(args)
.map_err(|e| format!("Invalid retro submission: {e}"))?;
*captured.lock().unwrap_or_else(std::sync::PoisonError::into_inner) = Some(narrative);
Ok("Retrospective submitted successfully.".to_string())
})
}),
};
profile.tool_registry_mut().register(submit_tool);
let profile: Arc<dyn AgentProfile> = Arc::from(profile);
let config = SessionOptions {
max_tool_rounds_per_input: 20,
wall_clock_timeout: Some(Duration::from_mins(3)),
// Disable features not needed for retro analysis
enable_context_compaction: false,
skill_dirs: Some(vec![]),
user_instructions: Some(RETRO_SYSTEM_PROMPT.to_string()),
..SessionOptions::default()
};
let mut session = Session::new(
llm_client.clone(),
profile,
Arc::clone(sandbox),
config,
None,
);
// Optionally forward agent events via the callback
let event_forwarder_handle = event_callback.map(|cb| spawn_retro_event_forwarder(&session, cb));
session
.initialize()
.await
.context("Retro agent session initialization failed")?;
let prompt = build_retro_prompt(RETRO_DATA_DIR);
let process_result = session
.process_input(&prompt)
.await
.context("Retro agent session failed");
// Extract response from session history
let response_text = session
.history()
.turns()
.iter()
.rev()
.find_map(|t| match t {
Turn::Assistant { content, .. } => Some(content.as_str()),
_ => None,
})
.unwrap_or_default()
.to_string();
// Extract result / determine outcome
let (_outcome, _failure_reason, narrative_result) = match process_result {
Ok(()) => {
let maybe_narrative = captured
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.take();
match maybe_narrative {
Some(narrative) => ("success", None, Ok(narrative)),
None => (
"error",
Some("Retro agent did not call submit_retro".to_string()),
Err(anyhow::anyhow!("Retro agent did not call submit_retro")),
),
}
}
Err(e) => {
let reason = e.to_string();
("error", Some(reason), Err(e))
}
};
// Drop session to close the broadcast channel, then wait for event forwarder
drop(session);
if let Some(handle) = event_forwarder_handle {
let _ = handle.await;
}
narrative_result.map(|narrative| RetroAgentResult {
narrative,
response: response_text,
})
}
/// Return a placeholder narrative for dry-run mode. Exercises the full
/// derive → apply_narrative → save path without making LLM calls.
pub fn dry_run_narrative() -> RetroNarrative {
RetroNarrative {
smoothness: SmoothnessRating::Smooth,
intent: "[dry-run] No LLM analysis performed".to_string(),
outcome: "[dry-run] Run completed in simulated mode".to_string(),
learnings: vec![],
friction_points: vec![],
open_items: vec![],
}
}
/// Spawn a background task that forwards session events via the provided
/// callback.
fn spawn_retro_event_forwarder(
session: &Session,
callback: Arc<dyn Fn(SessionEvent) + Send + Sync>,
) -> JoinHandle<()> {
let mut rx = session.subscribe();
tokio::spawn(async move {
while let Ok(event) = rx.recv().await {
callback(event);
}
})
}
fn build_profile(provider: Provider, model: &str) -> Box<dyn AgentProfile> {
match provider {
Provider::OpenAi => Box::new(OpenAiProfile::new(model)),
Provider::Kimi
| Provider::Zai
| Provider::Minimax
| Provider::Inception
| Provider::OpenAiCompatible => Box::new(OpenAiProfile::new(model).with_provider(provider)),
Provider::Gemini => Box::new(GeminiProfile::new(model)),
Provider::Anthropic => Box::new(AnthropicProfile::new(model)),
}
}
async fn upload_data_files(
sandbox: &Arc<dyn Sandbox>,
state: &RunProjection,
events: &[EventEnvelope],
target_dir: &str,
run_log: Option<Vec<u8>>,
blob_reader: Option<BlobReader>,
) -> anyhow::Result<()> {
let mut dump = RunDump::from_store_state_and_events(state, events)?;
if let Some(log) = run_log {
dump.add_file_bytes("run.log", log);
}
if let Some(reader) = blob_reader {
dump.hydrate_referenced_blobs_with_reader(reader).await?;
}
for entry in dump.entries() {
let remote_path = format!("{target_dir}/{}", entry.path());
let bytes = entry.to_bytes()?;
let text = String::from_utf8(bytes)
.with_context(|| format!("non-UTF8 retro entry {}", entry.path()))?;
sandbox
.write_file(&remote_path, &text)
.await
.with_context(|| format!("Failed to upload {}", entry.path()))?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use chrono::{TimeZone, Utc};
use fabro_agent::LocalSandbox;
use fabro_store::StageId;
use fabro_types::{StageCompletion, StageOutcome, first_event_seq};
use tokio::fs;
use super::*;
#[test]
fn submit_retro_schema_is_valid_json() {
let schema: serde_json::Value = serde_json::from_str(SUBMIT_RETRO_SCHEMA).unwrap();
assert_eq!(schema["type"], "object");
assert!(schema["properties"]["smoothness"].is_object());
assert!(schema["properties"]["intent"].is_object());
assert!(schema["properties"]["outcome"].is_object());
}
#[test]
fn retro_narrative_parses_from_submit_retro_args() {
let args = serde_json::json!({
"smoothness": "smooth",
"intent": "Fix the login bug",
"outcome": "Successfully fixed the authentication flow",
"learnings": [
{ "category": "code", "text": "Token refresh was in wrong module" }
],
"friction_points": [
{ "kind": "retry", "description": "First attempt had wrong import", "stage_id": "code" }
],
"open_items": [
{ "kind": "test_gap", "description": "No integration test for token refresh" }
]
});
let narrative: RetroNarrative = serde_json::from_value(args).unwrap();
assert_eq!(narrative.smoothness, SmoothnessRating::Smooth);
assert_eq!(narrative.intent, "Fix the login bug");
assert_eq!(narrative.learnings.len(), 1);
assert_eq!(narrative.friction_points.len(), 1);
assert_eq!(narrative.open_items.len(), 1);
}
#[test]
fn retro_narrative_parses_minimal_args() {
let args = serde_json::json!({
"smoothness": "effortless",
"intent": "Deploy feature",
"outcome": "Deployed successfully"
});
let narrative: RetroNarrative = serde_json::from_value(args).unwrap();
assert_eq!(narrative.smoothness, SmoothnessRating::Effortless);
assert!(narrative.learnings.is_empty());
assert!(narrative.friction_points.is_empty());
assert!(narrative.open_items.is_empty());
}
#[test]
fn retro_prompt_mentions_graph_and_stage_files() {
let prompt = build_retro_prompt(RETRO_DATA_DIR);
assert!(prompt.contains("run.json"));
assert!(prompt.contains("graph.fabro"));
assert!(prompt.contains("stages/"));
}
#[tokio::test]
async fn upload_data_files_writes_projection_graph_and_stage_files() {
let sandbox_root = tempfile::tempdir().expect("sandbox tempdir should exist");
let sandbox: Arc<dyn Sandbox> =
Arc::new(LocalSandbox::new(sandbox_root.path().to_path_buf()));
let output_dir = tempfile::tempdir().expect("retro tempdir should exist");
let target_dir = output_dir.path().join("retro");
let target_dir_str = target_dir.to_string_lossy().to_string();
let stage_id = StageId::new("build", 2);
let mut state = RunProjection::default();
state.graph_source = Some("digraph Ship {}".to_string());
let stage = state.stage_entry(stage_id.node_id(), stage_id.visit(), first_event_seq(2));
stage.prompt = Some("plan".to_string());
stage.response = Some("done".to_string());
stage.completion = Some(StageCompletion {
outcome: StageOutcome::Succeeded,
notes: Some("ok".to_string()),
failure_reason: None,
timestamp: Utc
.with_ymd_and_hms(2026, 4, 20, 12, 1, 0)
.single()
.unwrap(),
});
stage.provider_used = Some(serde_json::json!({ "provider": "openai" }));
stage.diff = Some("diff --git a/a b/a".to_string());
stage.script_invocation = Some(serde_json::json!({ "command": "cargo test" }));
stage.script_timing = Some(serde_json::json!({ "duration_ms": 10 }));
stage.parallel_results = Some(serde_json::json!([{ "stage": "fanout@1" }]));
stage.output = Some("output".to_string());
upload_data_files(
&sandbox,
&state,
&[],
&target_dir_str,
Some(b"server log\n".to_vec()),
None,
)
.await
.expect("retro files should upload");
let run_json: serde_json::Value = serde_json::from_str(
&fs::read_to_string(target_dir.join("run.json"))
.await
.expect("run.json should exist"),
)
.expect("run.json should parse");
assert!(run_json.get("spec").is_some());
assert!(run_json.get("run").is_none());
assert!(run_json["stages"]["build@2"]["prompt"].is_null());
assert!(run_json["stages"]["build@2"]["diff"].is_null());
assert_eq!(
fs::read_to_string(target_dir.join("graph.fabro"))
.await
.expect("graph.fabro should exist"),
"digraph Ship {}"
);
assert_eq!(
fs::read_to_string(target_dir.join("stages/001-build@2/prompt.md"))
.await
.expect("prompt file should exist"),
"plan"
);
assert_eq!(
fs::read_to_string(target_dir.join("stages/001-build@2/response.md"))
.await
.expect("response file should exist"),
"done"
);
assert_eq!(
fs::read_to_string(target_dir.join("stages/001-build@2/output.log"))
.await
.expect("output file should exist"),
"output"
);
assert_eq!(
fs::read_to_string(target_dir.join("events.jsonl"))
.await
.expect("events file should exist"),
""
);
assert_eq!(
fs::read_to_string(target_dir.join("run.log"))
.await
.expect("run.log should exist"),
"server log\n"
);
assert!(
target_dir.join("stages/001-build@2/status.json").exists(),
"status file should exist"
);
assert!(
!target_dir.join("progress.jsonl").exists(),
"legacy progress file should not be emitted"
);
}
#[tokio::test]
async fn upload_data_files_resolves_command_output_blob_refs() {
let sandbox_root = tempfile::tempdir().expect("sandbox tempdir should exist");
let sandbox: Arc<dyn Sandbox> =
Arc::new(LocalSandbox::new(sandbox_root.path().to_path_buf()));
let output_dir = tempfile::tempdir().expect("retro tempdir should exist");
let target_dir = output_dir.path().join("retro");
let target_dir_str = target_dir.to_string_lossy().to_string();
let output_blob = serde_json::to_vec("resolved output").unwrap();
let output_id = fabro_types::RunBlobId::new(&output_blob);
let stage_id = StageId::new("build", 1);
let mut state = RunProjection::default();
let output_ref = fabro_types::format_blob_ref(&output_id);
let stage = state.stage_entry(stage_id.node_id(), stage_id.visit(), first_event_seq(1));
stage.script_invocation = Some(serde_json::json!({
"command": "cargo test",
"output": output_ref,
}));
stage.script_timing = Some(serde_json::json!({
"exit_code": 0,
"output": output_ref,
}));
stage.output = Some(output_ref);
let reader: BlobReader = Box::new(move |blob_id| {
let output_blob = output_blob.clone();
Box::pin(async move {
if blob_id == output_id {
Ok(Some(output_blob.into()))
} else {
Ok(None)
}
})
});
upload_data_files(&sandbox, &state, &[], &target_dir_str, None, Some(reader))
.await
.expect("retro files should upload");
assert_eq!(
fs::read_to_string(target_dir.join("stages/001-build@1/output.log"))
.await
.expect("output file should exist"),
"resolved output"
);
let script_timing: serde_json::Value = serde_json::from_str(
&fs::read_to_string(target_dir.join("stages/001-build@1/script_timing.json"))
.await
.expect("script timing should exist"),
)
.expect("script timing should parse");
assert_eq!(script_timing["output"], "resolved output");
let script_invocation: serde_json::Value = serde_json::from_str(
&fs::read_to_string(target_dir.join("stages/001-build@1/script_invocation.json"))
.await
.expect("script invocation should exist"),
)
.expect("script invocation should parse");
assert_eq!(script_invocation["output"], "resolved output");
let run_json: serde_json::Value = serde_json::from_str(
&fs::read_to_string(target_dir.join("run.json"))
.await
.expect("run.json should exist"),
)
.expect("run.json should parse");
assert_eq!(
run_json["stages"]["build@1"]["script_timing"]["output"],
"resolved output"
);
assert_eq!(
run_json["stages"]["build@1"]["script_invocation"]["output"],
"resolved output"
);
assert!(run_json["stages"]["build@1"]["output"].is_null());
}
}

View file

@ -37,7 +37,6 @@ fabro-agent = { path = "../fabro-agent" }
fabro-llm = { path = "../fabro-llm" }
fabro-model = { path = "../fabro-model" }
fabro-proc = { path = "../fabro-proc" }
fabro-retro = { path = "../fabro-retro" }
fabro-types = { path = "../fabro-types" }
fabro-util = { path = "../fabro-util" }
fabro-api = { path = "../fabro-api" }
@ -109,4 +108,4 @@ tokio-util.workspace = true
fabro-macros = { path = "../fabro-macros" }
fabro-sandbox = { path = "../fabro-sandbox", features = ["test-support"] }
fabro-test = { workspace = true }
fabro-types = { path = "../fabro-types", features = ["test-support"] }
fabro-types = { path = "../fabro-types", features = ["test-support"] }

View file

@ -762,7 +762,7 @@ pub(crate) async fn get_system_info(
"uptime_secs": 42,
"runs": { "total": 3, "active": 1 },
"sandbox_provider": "local",
"features": { "session_sandboxes": false, "retros": false }
"features": { "session_sandboxes": false }
})),
)
.into_response()

View file

@ -341,8 +341,7 @@ fn manifest_args_overrides(
..RunSandboxLayer::default()
});
let execution_has_any =
args.dry_run.is_some() || args.auto_approve.is_some() || args.no_retro.is_some();
let execution_has_any = args.dry_run.is_some() || args.auto_approve.is_some();
let execution = execution_has_any.then(|| RunExecutionLayer {
mode: args
.dry_run
@ -354,7 +353,6 @@ fn manifest_args_overrides(
ApprovalMode::Prompt
}
}),
retros: args.no_retro.map(|nr| !nr),
});
let run_has_any =
@ -1711,7 +1709,6 @@ root = "/srv/fabro"
dry_run: Some(true),
label: Vec::new(),
model: None,
no_retro: None,
preserve_sandbox: None,
provider: None,
sandbox: None,
@ -1746,7 +1743,6 @@ override = "server"
dry_run: None,
label: Vec::new(),
model: None,
no_retro: None,
preserve_sandbox: None,
provider: None,
sandbox: None,

View file

@ -1281,15 +1281,11 @@ async fn github_webhook(
fn system_features(
server_settings: &ServerSettings,
manifest_run_settings: &std::result::Result<RunNamespace, SharedError>,
_manifest_run_settings: &std::result::Result<RunNamespace, SharedError>,
) -> SystemFeatures {
let session_sandboxes = server_settings.features.session_sandboxes;
let retros = manifest_run_settings
.as_ref()
.is_ok_and(|settings| settings.execution.retros);
SystemFeatures {
session_sandboxes: Some(session_sandboxes),
retros: Some(retros),
}
}

View file

@ -819,9 +819,6 @@ methods = ["dev-token"]
[features]
session_sandboxes = true
[run.execution]
retros = false
"#;
let server_settings = server_settings_from_toml(source);
let manifest_run_settings = resolve_manifest_run_settings(
@ -830,11 +827,10 @@ retros = false
let features = system_features(&server_settings, &manifest_run_settings);
assert_eq!(features.session_sandboxes, Some(true));
assert_eq!(features.retros, Some(false));
}
#[test]
fn system_features_default_retros_when_manifest_run_settings_do_not_resolve() {
fn system_features_ignore_manifest_run_settings_resolution() {
let source = r#"
_version = 1
@ -854,7 +850,6 @@ provider = "invalid-provider"
let features = system_features(&server_settings, &manifest_run_settings);
assert_eq!(features.session_sandboxes, Some(true));
assert_eq!(features.retros, Some(false));
}
#[test]

View file

@ -138,8 +138,10 @@ async fn get_system_info_returns_runtime_fields() {
assert_eq!(body["runs"]["total"], 0);
assert_eq!(body["runs"]["active"], 0);
assert!(body["uptime_secs"].as_i64().is_some());
assert_eq!(body["features"]["session_sandboxes"], false);
assert_eq!(body["features"]["retros"], true);
assert_eq!(
body["features"],
serde_json::json!({ "session_sandboxes": false })
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]

View file

@ -241,18 +241,6 @@ impl RunProjectionReducer for RunProjection {
clone_branch: props.clone_branch.clone(),
});
}
EventBody::RetroStarted(props) => {
self.retro_prompt.clone_from(&props.prompt);
}
EventBody::RetroCompleted(props) => {
self.retro_response.clone_from(&props.response);
self.retro = props
.retro
.clone()
.map(serde_json::from_value)
.transpose()
.map_err(|err| Error::InvalidEvent(format!("invalid retro payload: {err}")))?;
}
EventBody::PullRequestCreated(props) => {
self.pull_request = Some(PullRequestRecord {
html_url: props.pr_url.clone(),

View file

@ -81,10 +81,10 @@ mod tests {
#[test]
fn simple_command_with_flag() {
let result = sanitize_command(
&args(&["fabro", "run", "my-workflow.toml", "--no-retro"]),
&args(&["fabro", "run", "my-workflow.toml", "--preserve-sandbox"]),
"run",
);
insta::assert_snapshot!(result, @"fabro run VALUE --no-retro");
insta::assert_snapshot!(result, @"fabro run VALUE --preserve-sandbox");
}
#[test]

View file

@ -17,7 +17,6 @@ pub mod outcome;
pub mod principal;
pub mod pull_request;
pub mod repository;
pub mod retro;
pub mod run;
pub mod run_blob_id;
pub mod run_event;
@ -62,10 +61,6 @@ pub use pull_request::{
PullRequestDetail, PullRequestGithubDetail, PullRequestRecord, PullRequestRef, PullRequestUser,
};
pub use repository::RepositoryReference;
pub use retro::{
AggregateStats, FrictionKind, FrictionPoint, Learning, LearningCategory, OpenItem,
OpenItemKind, Retro, RetroNarrative, SmoothnessRating, StageRetro,
};
pub use run::{
DirtyStatus, ForkSourceRef, GitContext, PreRunPushOutcome, RunClientProvenance, RunProvenance,
RunServerProvenance, RunSpec,

View file

@ -1,164 +0,0 @@
use std::fmt;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::run_id::RunId;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SmoothnessRating {
Effortless,
Smooth,
Bumpy,
Struggled,
Failed,
}
impl fmt::Display for SmoothnessRating {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
Self::Effortless => "effortless",
Self::Smooth => "smooth",
Self::Bumpy => "bumpy",
Self::Struggled => "struggled",
Self::Failed => "failed",
};
f.write_str(s)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum LearningCategory {
Repo,
Code,
Workflow,
Tool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Learning {
pub category: LearningCategory,
pub text: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum FrictionKind {
Retry,
Timeout,
WrongApproach,
ToolFailure,
Ambiguity,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FrictionPoint {
pub kind: FrictionKind,
pub description: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub stage_id: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum OpenItemKind {
TechDebt,
FollowUp,
Investigation,
TestGap,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OpenItem {
pub kind: OpenItemKind,
pub description: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StageRetro {
pub stage_id: String,
pub stage_label: String,
pub status: String,
pub duration_ms: u64,
pub retries: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub billing_usd_micros: Option<i64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub notes: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub failure_reason: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub files_touched: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AggregateStats {
pub total_duration_ms: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub total_billing_usd_micros: Option<i64>,
pub total_retries: u32,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub files_touched: Vec<String>,
pub stages_completed: usize,
pub stages_failed: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RetroNarrative {
pub smoothness: SmoothnessRating,
pub intent: String,
pub outcome: String,
#[serde(default)]
pub learnings: Vec<Learning>,
#[serde(default)]
pub friction_points: Vec<FrictionPoint>,
#[serde(default)]
pub open_items: Vec<OpenItem>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Retro {
pub run_id: RunId,
pub workflow_name: String,
pub goal: String,
pub timestamp: DateTime<Utc>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub smoothness: Option<SmoothnessRating>,
pub stages: Vec<StageRetro>,
pub stats: AggregateStats,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub intent: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub outcome: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub learnings: Option<Vec<Learning>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub friction_points: Option<Vec<FrictionPoint>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub open_items: Option<Vec<OpenItem>>,
}
impl Retro {
pub fn apply_narrative(&mut self, narrative: RetroNarrative) {
self.smoothness = Some(narrative.smoothness);
self.intent = Some(narrative.intent);
self.outcome = Some(narrative.outcome);
self.learnings = if narrative.learnings.is_empty() {
None
} else {
Some(narrative.learnings)
};
self.friction_points = if narrative.friction_points.is_empty() {
None
} else {
Some(narrative.friction_points)
};
self.open_items = if narrative.open_items.is_empty() {
None
} else {
Some(narrative.open_items)
};
}
}

View file

@ -351,30 +351,3 @@ pub struct PullRequestCreatedProps {
pub struct PullRequestFailedProps {
pub error: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RetroStartedProps {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub prompt: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub provider: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub model: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RetroCompletedProps {
pub duration_ms: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub response: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub retro: Option<Value>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RetroFailedProps {
pub error: String,
pub duration_ms: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub exec_output_tail: Option<ExecOutputTail>,
}

View file

@ -304,12 +304,6 @@ pub enum EventBody {
DevcontainerLifecycleCompleted(DevcontainerLifecycleCompletedProps),
#[serde(rename = "devcontainer.lifecycle.failed")]
DevcontainerLifecycleFailed(DevcontainerLifecycleFailedProps),
#[serde(rename = "retro.started")]
RetroStarted(RetroStartedProps),
#[serde(rename = "retro.completed")]
RetroCompleted(RetroCompletedProps),
#[serde(rename = "retro.failed")]
RetroFailed(RetroFailedProps),
Unknown {
name: String,
properties: Value,
@ -499,9 +493,6 @@ impl EventBody {
}
Self::DevcontainerLifecycleCompleted(_) => "devcontainer.lifecycle.completed",
Self::DevcontainerLifecycleFailed(_) => "devcontainer.lifecycle.failed",
Self::RetroStarted(_) => "retro.started",
Self::RetroCompleted(_) => "retro.completed",
Self::RetroFailed(_) => "retro.failed",
Self::Unknown { name, .. } => name.as_str(),
}
}
@ -643,9 +634,6 @@ fn is_known_event_name(event: &str) -> bool {
| "devcontainer.lifecycle.command.completed"
| "devcontainer.lifecycle.completed"
| "devcontainer.lifecycle.failed"
| "retro.started"
| "retro.completed"
| "retro.failed"
)
}
@ -1504,6 +1492,41 @@ mod tests {
}
}
#[test]
fn retired_retro_events_deserialize_as_unknown() {
for (event_name, expected_properties) in [
(
"retro.started",
json!({"prompt": "Analyze the run", "provider": "openai", "model": "gpt-5"}),
),
(
"retro.completed",
json!({"duration_ms": 1200, "response": "done", "retro": {"smoothness": "smooth"}}),
),
(
"retro.failed",
json!({"duration_ms": 1200, "error": "state unavailable"}),
),
] {
let value = json!({
"id": "evt_retired_retro",
"ts": "2026-05-08T12:00:00.000Z",
"run_id": fixtures::RUN_1,
"event": event_name,
"properties": expected_properties
});
let parsed = RunEvent::from_value(value).unwrap();
match parsed.body {
EventBody::Unknown { name, properties } => {
assert_eq!(name, event_name);
assert_eq!(properties, expected_properties);
}
other => panic!("expected Unknown body, got {other:?}"),
}
}
}
#[test]
fn metadata_snapshot_failed_omits_empty_optional_fields() {
let body = EventBody::MetadataSnapshotFailed(MetadataSnapshotFailedProps {
@ -1621,11 +1644,6 @@ mod tests {
success: false,
exec_output_tail: Some(tail.clone()),
}),
EventBody::RetroFailed(RetroFailedProps {
error: "state unavailable".to_string(),
duration_ms: 10,
exec_output_tail: Some(tail.clone()),
}),
] {
let value = serde_json::to_value(&body).unwrap();
assert_eq!(
@ -1657,11 +1675,6 @@ mod tests {
success: false,
exec_output_tail: None,
}),
EventBody::RetroFailed(RetroFailedProps {
error: "state unavailable".to_string(),
duration_ms: 10,
exec_output_tail: None,
}),
] {
let value = serde_json::to_value(&body).unwrap();
assert!(

View file

@ -5,7 +5,7 @@ use chrono::{DateTime, Utc};
use crate::{
BilledModelUsage, Checkpoint, Conclusion, DiffSummary, InterviewQuestionRecord,
InvalidTransition, PullRequestRecord, Retro, RunControlAction, RunId, RunSpec, RunStatus,
InvalidTransition, PullRequestRecord, RunControlAction, RunId, RunSpec, RunStatus,
SandboxRecord, StageCompletion, StageHandler, StageId, StageState, StartRecord,
};
@ -22,9 +22,6 @@ pub struct RunProjection {
pub checkpoint: Option<Checkpoint>,
pub checkpoints: Vec<(u32, Checkpoint)>,
pub conclusion: Option<Conclusion>,
pub retro: Option<Retro>,
pub retro_prompt: Option<String>,
pub retro_response: Option<String>,
pub sandbox: Option<SandboxRecord>,
pub final_patch: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]

View file

@ -177,7 +177,6 @@ impl Default for RunPrepareSettings {
pub struct RunExecutionSettings {
pub mode: RunMode,
pub approval: ApprovalMode,
pub retros: bool,
}
impl Default for RunExecutionSettings {
@ -185,7 +184,6 @@ impl Default for RunExecutionSettings {
Self {
mode: RunMode::Normal,
approval: ApprovalMode::Prompt,
retros: true,
}
}
}

View file

@ -36,7 +36,6 @@ fabro-redact.workspace = true
fabro-checkpoint = { path = "../fabro-checkpoint" }
fabro-llm = { path = "../fabro-llm" }
fabro-model = { path = "../fabro-model" }
fabro-retro = { path = "../fabro-retro" }
fabro-core = { path = "../fabro-core" }
fabro-store = { path = "../fabro-store" }
fabro-static.workspace = true

View file

@ -67,7 +67,7 @@ assert_eq!(graph.goal(), "Run tests");
use fabro_workflow::operations::start;
use fabro_workflow::pipeline;
// Use `operations::start(...)` for the full initialize -> execute -> retro -> finalize flow.
// Use `operations::start(...)` for the full initialize -> execute -> finalize flow.
// Use `pipeline::initialize(...)` + `pipeline::execute(...)` when you need partial lifecycle control.
```

View file

@ -1226,33 +1226,6 @@ fn event_body_from_event(event: &Event) -> EventBody {
exec_output_tail: exec_output_tail.clone(),
})
}
Event::RetroStarted {
prompt,
provider,
model,
} => EventBody::RetroStarted(fabro_types::RetroStartedProps {
prompt: prompt.clone(),
provider: provider.clone(),
model: model.clone(),
}),
Event::RetroCompleted {
duration_ms,
response,
retro,
} => EventBody::RetroCompleted(fabro_types::RetroCompletedProps {
duration_ms: *duration_ms,
response: response.clone(),
retro: retro.clone(),
}),
Event::RetroFailed {
error,
duration_ms,
exec_output_tail,
} => EventBody::RetroFailed(fabro_types::RetroFailedProps {
error: error.clone(),
duration_ms: *duration_ms,
exec_output_tail: exec_output_tail.clone(),
}),
}
}
@ -1862,24 +1835,6 @@ mod tests {
}
}
#[test]
fn retro_failed_maps_exec_output_tail_to_props() {
let stored = to_run_event(&fixtures::RUN_1, &Event::RetroFailed {
error: "state load failed".to_string(),
duration_ms: 12,
exec_output_tail: Some(exec_tail()),
});
match stored.body {
EventBody::RetroFailed(props) => {
assert_eq!(props.duration_ms, 12);
let tail = props.exec_output_tail.expect("exec output tail");
assert_eq!(tail.stdout.as_deref(), Some("last stdout line"));
}
other => panic!("expected RetroFailed body, got {other:?}"),
}
}
#[test]
fn metadata_snapshot_events_map_to_typed_bodies() {
let started = to_run_event(&fixtures::RUN_1, &Event::MetadataSnapshotStarted {

View file

@ -665,27 +665,6 @@ pub enum Event {
#[serde(default, skip_serializing_if = "Option::is_none")]
exec_output_tail: Option<fabro_types::ExecOutputTail>,
},
RetroStarted {
#[serde(default, skip_serializing_if = "Option::is_none")]
prompt: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
provider: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
model: Option<String>,
},
RetroCompleted {
duration_ms: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
response: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
retro: Option<serde_json::Value>,
},
RetroFailed {
error: String,
duration_ms: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
exec_output_tail: Option<fabro_types::ExecOutputTail>,
},
}
impl Event {
@ -1476,37 +1455,6 @@ impl Event {
"Devcontainer lifecycle command failed"
);
}
Self::RetroStarted {
prompt: _,
provider,
model,
} => {
info!(
provider = provider.as_deref().unwrap_or(""),
model = model.as_deref().unwrap_or(""),
"Retro started"
);
}
Self::RetroCompleted { duration_ms, .. } => {
info!(duration_ms, "Retro completed");
}
Self::RetroFailed {
error,
duration_ms,
exec_output_tail,
} => {
let tail = fabro_types::ExecOutputTail::trace_summary(exec_output_tail.as_ref());
error!(
error = %error,
duration_ms,
exec_output_tail_present = tail.present,
exec_stdout_tail_bytes = tail.stdout_bytes,
exec_stderr_tail_bytes = tail.stderr_bytes,
exec_stdout_truncated = tail.stdout_truncated,
exec_stderr_truncated = tail.stderr_truncated,
"Retro failed"
);
}
}
}
}

View file

@ -148,9 +148,6 @@ pub fn event_name(event: &Event) -> &'static str {
}
Event::DevcontainerLifecycleCompleted { .. } => "devcontainer.lifecycle.completed",
Event::DevcontainerLifecycleFailed { .. } => "devcontainer.lifecycle.failed",
Event::RetroStarted { .. } => "retro.started",
Event::RetroCompleted { .. } => "retro.completed",
Event::RetroFailed { .. } => "retro.failed",
}
}
@ -164,14 +161,6 @@ mod tests {
#[test]
fn event_name_matches_new_dot_notation() {
assert_eq!(
event_name(&Event::RetroStarted {
prompt: None,
provider: None,
model: None,
}),
"retro.started"
);
assert_eq!(
event_name(&Event::ParallelBranchStarted {
parallel_group_id: StageId::new("plan", 1),

Some files were not shown because too many files have changed in this diff Show more