mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-09-05 08:10:39 +00:00
## Summary - **Unify foreground and detach code paths**: Both `fabro run` modes now go through the same `create_run() + start_run()` pipeline, with foreground adding `attach_run()`. Only `--preflight` remains as a special case. - **Fix three bugs in create→start→attach path**: (1) `_run_engine` crashed for `.fabro` workflows by hardcoding `run.toml` — now falls back to `graph.fabro`; (2) `attach_run` couldn't detect crashed engines due to zombie processes — `start_run` now returns the `Child` handle; (3) `create_run` ignored `--run-id`. - **Configure nextest slow-timeout profiles**: Tighten unit test timeout to 2s slow / 4s kill, add `e2e` profile with 10s/30s. Switch CI and docs to `cargo nextest run`. ## Test plan - [ ] `cargo nextest run --workspace` passes with new timeout profiles - [ ] `fabro run <workflow>` works in foreground mode (create + start + attach) - [ ] `fabro run --detach <workflow>` prints run ID and exits - [ ] `fabro attach <run>` works standalone (without child handle) - [ ] `fabro resume <run>` works for both `.toml` and `.fabro` workflows 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Fabro <noreply@fabro.sh> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
124 lines
5.9 KiB
Text
124 lines
5.9 KiB
Text
---
|
|
title: "Interviews"
|
|
description: "How Fabro collects human input during workflow execution"
|
|
---
|
|
|
|
When a workflow reaches a [human gate](/workflows/human-in-the-loop), it needs to pause and wait for a person to respond. The **interviewer** is the abstraction that makes this work — it presents a question, collects an answer, and returns it to the engine so execution can continue.
|
|
|
|
Fabro ships with several interviewer implementations for different environments: an interactive terminal prompt for the CLI, a web-based queue for the API server and web UI, an auto-approve mode for CI, and record/replay support for testing.
|
|
|
|
## Question types
|
|
|
|
Every human interaction is modeled as a `Question` with a type that determines how it's presented:
|
|
|
|
| Type | Description | CLI presentation |
|
|
|---|---|---|
|
|
| `YesNo` | Binary yes/no decision | `[Y/N]` prompt |
|
|
| `Confirmation` | Confirm an action (like YesNo) | `[Y/N]` prompt |
|
|
| `MultipleChoice` | Pick one option from a list | Arrow-key selector or numbered list |
|
|
| `MultiSelect` | Pick one or more from a list | Checkbox selector |
|
|
| `Freeform` | Open-ended text input | `>` prompt |
|
|
|
|
### Question structure
|
|
|
|
Each question carries metadata beyond the prompt text:
|
|
|
|
| Field | Description |
|
|
|---|---|
|
|
| `text` | The question displayed to the user |
|
|
| `question_type` | One of the types above |
|
|
| `options` | List of `{key, label}` pairs for choice questions |
|
|
| `allow_freeform` | Whether free-text input is accepted in addition to fixed options |
|
|
| `default` | Default answer used on timeout |
|
|
| `timeout_seconds` | How long to wait before using the default or timing out |
|
|
| `stage` | The node ID that generated this question |
|
|
| `metadata` | Arbitrary key-value metadata for integrations |
|
|
|
|
## Answer values
|
|
|
|
Answers are one of seven variants:
|
|
|
|
| Value | Meaning |
|
|
|---|---|
|
|
| `Yes` | Affirmative response to a yes/no or confirmation question |
|
|
| `No` | Negative response |
|
|
| `Selected(key)` | A specific option was chosen (carries the option key) |
|
|
| `Text(string)` | Free-text input |
|
|
| `Aborted` | No answer was obtained because the prompt ended early (EOF, cancel, disconnected session, exhausted replay/queue) |
|
|
| `Skipped` | Legacy skip-style answer; not treated as approval by human gates |
|
|
| `Timeout` | The question's timeout elapsed without a response |
|
|
|
|
An answer can also carry a `selected_option` (the full `{key, label}` pair) and a `text` field for freeform input.
|
|
|
|
## How human gates build questions
|
|
|
|
When the engine reaches a human gate node (`shape=hexagon`), the [human handler](/workflows/human-in-the-loop) builds a question from the node's outgoing edges:
|
|
|
|
1. Each edge becomes an option, with the accelerator key parsed from the label (e.g. `[A] Approve` → key `A`, label `[A] Approve`)
|
|
2. Edges with `freeform=true` are excluded from the option list and enable free-text fallback
|
|
3. The question text comes from the node's `label` attribute
|
|
|
|
The handler then passes the question to the interviewer, waits for an answer, and maps it back to an edge for [transition](/workflows/transitions#human-gate-transitions).
|
|
|
|
## Channels
|
|
|
|
The `Interviewer` trait has a simple interface — `ask(question) → answer` — and Fabro provides implementations for each delivery channel:
|
|
|
|
### Console
|
|
|
|
The default for CLI runs. On a TTY, the console interviewer uses interactive widgets (arrow-key selection, checkbox multi-select, confirm prompts) via `dialoguer`. When stdin is piped (non-TTY), it falls back to a line-based reader with numbered options.
|
|
|
|
```bash
|
|
fabro run workflow.fabro
|
|
# At a human gate:
|
|
# ? Approve Plan
|
|
# [1] A - [A] Approve
|
|
# [2] R - [R] Revise
|
|
# Select:
|
|
```
|
|
|
|
If the console prompt is canceled or stdin is already closed, the interviewer returns `Aborted`. That does not count as approval.
|
|
|
|
### Web
|
|
|
|
The default for API server runs. The web interviewer holds questions in a queue until answers are submitted externally — typically by the web UI or a REST API call. Each question gets a unique ID (e.g. `q-1`), and the `ask()` call blocks on a oneshot channel until `submit_answer(id, answer)` is called.
|
|
|
|
This decoupling means the workflow engine and the user interface can run in different processes. The web UI polls for pending questions and posts answers back to the API.
|
|
|
|
If the pending session disappears before an answer is submitted, the waiting question resolves as `Aborted`.
|
|
|
|
### Slack
|
|
|
|
Fabro's Slack integration uses the web interviewer under the hood. When a human gate fires, the pending question is rendered as a Slack message with interactive buttons. When a user clicks a button, the Slack event handler calls `submit_answer()` on the web interviewer, unblocking the workflow.
|
|
|
|
### Auto-approve
|
|
|
|
For fully automated runs or CI pipelines, the auto-approve interviewer answers every question without human input:
|
|
|
|
- `YesNo` / `Confirmation` → `Yes`
|
|
- `MultipleChoice` / `MultiSelect` → first option
|
|
- `Freeform` → `"auto-approved"`
|
|
|
|
Enable it with the `--auto-approve` flag:
|
|
|
|
```bash
|
|
fabro run workflow.fabro --auto-approve
|
|
```
|
|
|
|
`--auto-approve` is intentionally distinct from an unanswered prompt. Auto-approve is an explicit operator choice to advance human gates automatically.
|
|
|
|
## Timeouts
|
|
|
|
Questions can have a `timeout_seconds` field. When set, Fabro wraps the interviewer call with a timeout:
|
|
|
|
- If the user answers before the deadline, their answer is used normally
|
|
- If the timeout elapses and a `default` answer is set on the question, the default is used
|
|
- If the timeout elapses with no default, the answer is `Timeout`
|
|
|
|
The human handler then checks the node's `human.default_choice` attribute. If set, execution continues to the default target. Otherwise, the stage retries.
|
|
|
|
```dot
|
|
approve [shape=hexagon, label="Approve?", human.default_choice="deploy"]
|
|
```
|
|
|
|
Outside of timeout defaults, human gates fail closed: `Aborted` and `Skipped` answers do not fall through to ordinary approval edges. To model an explicit unanswered path, add an edge such as `condition="outcome=fail"` or configure a `retry_target`.
|