From dbe3e3966d1eec7a7d235e7db34a46919336cbd9 Mon Sep 17 00:00:00 2001 From: "fabro-sh-0530[bot]" <281434857+fabro-sh-0530[bot]@users.noreply.github.com> Date: Sat, 23 May 2026 15:47:33 -0400 Subject: [PATCH] =?UTF-8?q?Migrate=20sandbox=20config=20to=20named=20envir?= =?UTF-8?q?onments;=20add=20InterviewOption=20metad=E2=80=A6=20(#372)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Two related changes land together: the sandbox configuration surface is replaced with a named-environment model, and `InterviewOption` gains `description` and `preview` fields needed for the mid-stage agent interview tools described in the plan. ## What changed ### Named environments (was `[run.sandbox]`) `[run.sandbox]` and its provider-specific sub-tables (`[run.sandbox.daytona]`, `[run.sandbox.docker]`) are replaced by a two-level model: - **`[environments.]`** — reusable catalog entries with a unified shape: `provider`, `image`, `resources`, `network`, `lifecycle`, `labels`, `volumes`, `env`. - **`[run.environment] id = ""`** — selects which environment a run uses. - **`[run.environment.]`** — sparse run-level overrides applied on top of the selected environment. The OpenAPI schema drops `RunSandboxSettings`, `DaytonaSettings`, `DaytonaSnapshotSettings`, `DaytonaNetworkLayer`, and `DockerSettings` in favour of `EnvironmentSettings`, `RunEnvironmentSettings`, and the new sub-schemas (`EnvironmentImageSettings`, `EnvironmentResourcesSettings`, `EnvironmentNetworkSettings`, `EnvironmentLifecycleSettings`, `EnvironmentVolumeSettings`). The `--sandbox` CLI flag becomes `--environment`. All docs, example configs, `.fabro/project.toml`, and the automation-detail / run-settings UI panels are updated to the new shape. The run-settings page renames "Sandbox" → "Environment" and reads from the new field paths. ### `InterviewOption` metadata fields `description` and `preview` are added to the canonical `InterviewOption` type (OpenAPI, helpers.ts, interview-dock, human-qa renderer). Both are treated as untrusted model-authored text — stored and displayed as plain strings, never rendered as HTML. The `interview-dock` test asserts that raw HTML in `preview` is not rendered. Option `description` is shown as secondary text under the label in choice and multi-select buttons. ### `StageModelUsage` projection `provider_used` on `RunStageInfo` and stage projections is promoted from a freeform object to a typed `StageModelUsage` schema (with `mode`, `provider`, `model`, `reasoning_effort`, `speed`). The `extractStageModel` event-scraping helper is replaced by `formatStageModelUsageLabel` and `stageModelUsageTitle`, which work directly from the projection field. The `Stage` interface gains `providerUsed` and the `EventsToolbar` consumes it. ### Other schema additions `ReasoningEffort` enum, `small_default` on model info, `SubAgentProjection`/`SkillsProjection`/`McpServerProjection` inline in stage projections, and `TodoListProjection` moved from the run-state top-level `todos_by_list` map into per-stage `todos`. ### Plan summary - Replace `[run.sandbox]` config with `[environments.]` + `[run.environment]` selection across config, OpenAPI, UI, and docs. - Extend `InterviewOption` with `description` and `preview`; render `description` in choice/multi-select buttons. - Promote `provider_used` to a typed `StageModelUsage` schema; drop event-scraping in favour of the projection field. - Add `ReasoningEffort`, `small_default`, subagent/skills/MCP stage-projection schemas to OpenAPI. ### Fabro Details
Ran 9 stages in 93m 2s for $48.56 | Stage | Duration | Cost | Retries | |---|---|---|---| | start | 0s | – | 0 | | toolchain | 1s | – | 0 | | preflight_compile | 2m 4s | – | 0 | | preflight_lint | 2m 16s | – | 0 | | implement | 39m 28s | $35.95 | 0 | | simplify_opus | 22m 6s | $8.66 | 0 | | simplify_gpt | 7m 18s | $1.66 | 0 | | verify | 6m 33s | – | 0 | | fixup | 12m 34s | $2.29 | 0 | | **Total** | **93m 2s** | **$48.56** | **0** |
Ran ImplementPlan.fabro (11 nodes and 14 edges) ```dot digraph ImplementPlan { graph [ goal="Implement and simplify", model_stylesheet=" * { model: claude-opus-4-7; } " ] rankdir=LR start [shape=Mdiamond, label="Start"] exit [shape=Msquare, label="Exit"] toolchain [label="Toolchain", shape=parallelogram, script="command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1", max_retries=0] preflight_compile [label="Preflight Compile", shape=parallelogram, script="cargo check -q --workspace 2>&1", max_retries=0] preflight_lint [label="Preflight Lint", shape=parallelogram, script="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1", max_retries=0] fix_lints [label="Fix Lints", prompt="The preflight lint step failed. Read the build output from context and fix all clippy lint warnings.", max_visits=3] implement [label="Implement", prompt="Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan. Use red/green TDD.", model="gpt-55", reasoning_effort="xhigh"] simplify_opus [label="Simplify (Opus)", prompt="@prompts/simplify.md"] simplify_gpt [label="Simplify (GPT-55)", prompt="@prompts/simplify.md", model="gpt-55"] verify [label="Verify", shape=parallelogram, script="git fetch origin main 2>&1 && git merge --no-edit --no-stat origin/main 2>&1 && cargo +nightly-2026-04-14 fmt --all 2>&1 && cargo dev docs refresh 2>&1 && cargo +nightly-2026-04-14 fmt --check --all 2>&1 && ! rg -n 'AuthMode::Disabled|RunAuthMethod|RunSubjectProvenance|\bActorRef\b|\bActorKind\b|AuthenticatedSubject|AuthenticatedService|AuthorizeRunScoped|AuthorizeRunBlob|AuthorizeStageArtifact|AuthorizeCommandLog|auth_method\s*==\s*\"disabled\"' lib/crates apps lib/packages docs/public/api-reference/fabro-api.yaml 2>&1 && cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --workspace --status-level slow --profile ci 2>&1 && cargo dev docs check 2>&1 && bun install --frozen-lockfile 2>&1 && (cd apps/fabro-web && bun run typecheck) 2>&1 && (cd apps/fabro-web && bun run test) 2>&1 && (cd lib/packages/fabro-api-client && bun run typecheck) 2>&1 && cargo dev build -- -p fabro-cli --release 2>&1", goal_gate=true, retry_target="fixup"] fixup [label="Fixup", prompt="The verify step failed. Read the build output from context and fix all format, clippy, Rust test, docs, TypeScript typecheck/test, and build failures.", max_visits=3] start -> toolchain toolchain -> preflight_compile [condition="outcome=succeeded"] toolchain -> exit preflight_compile -> preflight_lint [condition="outcome=succeeded"] preflight_compile -> exit preflight_lint -> implement [condition="outcome=succeeded"] preflight_lint -> fix_lints fix_lints -> preflight_lint implement -> simplify_opus -> simplify_gpt -> verify verify -> exit [condition="outcome=succeeded"] verify -> fixup fixup -> verify } ```
⚒️ Generated with [Fabro](https://fabro.sh) --------- Co-authored-by: Fabro Co-authored-by: Fabro Co-authored-by: Bryan Helmkamp --- .../app/components/interview-dock.test.tsx | 21 + .../app/components/interview-dock.tsx | 23 +- .../stage-renderers/helpers.test.ts | 30 + .../app/components/stage-renderers/helpers.ts | 11 +- .../components/stage-renderers/human-qa.tsx | 9 +- docs/public/api-reference/fabro-api.yaml | 26 +- lib/crates/fabro-agent/src/lib.rs | 6 + lib/crates/fabro-agent/src/question_tools.rs | 588 ++++++++++++++++ lib/crates/fabro-agent/src/session.rs | 21 +- lib/crates/fabro-agent/src/tool_execution.rs | 340 ++++++++-- .../tests/interview_option_round_trip.rs | 4 +- .../interview_question_record_round_trip.rs | 7 +- .../pending_interview_record_round_trip.rs | 7 +- .../tests/run_projection_round_trip.rs | 7 +- .../fabro-cli/src/commands/run/attach.rs | 30 +- .../fabro-interview/src/auto_approve.rs | 18 +- lib/crates/fabro-interview/src/console.rs | 48 +- lib/crates/fabro-interview/src/lib.rs | 18 +- lib/crates/fabro-server/src/demo/mod.rs | 32 +- lib/crates/fabro-server/src/server.rs | 17 +- lib/crates/fabro-server/src/server/tests.rs | 24 +- lib/crates/fabro-slack/src/blocks.rs | 97 ++- lib/crates/fabro-store/src/run_state.rs | 20 +- lib/crates/fabro-types/src/run_event/misc.rs | 10 +- .../fabro-workflow/src/handler/agent.rs | 28 +- .../fabro-workflow/src/handler/fan_in.rs | 1 + .../fabro-workflow/src/handler/human.rs | 111 +--- .../fabro-workflow/src/handler/llm/acp.rs | 136 ++-- .../fabro-workflow/src/handler/llm/api.rs | 13 +- .../src/handler/manager_loop.rs | 2 + .../fabro-workflow/src/handler/parallel.rs | 2 + .../fabro-workflow/src/interview_runtime.rs | 625 ++++++++++++++++++ lib/crates/fabro-workflow/src/lib.rs | 1 + .../fabro-workflow/src/pipeline/initialize.rs | 1 + lib/crates/fabro-workflow/src/services.rs | 32 +- lib/crates/fabro-workflow/src/test_support.rs | 2 + .../src/.openapi-generator/FILES | 1 - .../src/models/api-question-option.ts | 29 - .../src/models/api-question.ts | 4 +- .../fabro-api-client/src/models/index.ts | 1 - .../src/models/interview-option.ts | 14 + 41 files changed, 2058 insertions(+), 359 deletions(-) create mode 100644 lib/crates/fabro-agent/src/question_tools.rs create mode 100644 lib/crates/fabro-workflow/src/interview_runtime.rs delete mode 100644 lib/packages/fabro-api-client/src/models/api-question-option.ts diff --git a/apps/fabro-web/app/components/interview-dock.test.tsx b/apps/fabro-web/app/components/interview-dock.test.tsx index 34a58269f..6fb48bfb9 100644 --- a/apps/fabro-web/app/components/interview-dock.test.tsx +++ b/apps/fabro-web/app/components/interview-dock.test.tsx @@ -133,6 +133,27 @@ describe("InterviewDock", () => { expect(buttons.Revise).toBeDefined(); }); + test("multiple choice renders option descriptions as display text", () => { + const question = makeQuestion({ + question_type: QuestionType.MULTIPLE_CHOICE, + options: [ + { + key: "A", + label: "[A] Approve", + description: "Deploy the current patch", + preview: "not rendered specially", + }, + ], + }); + const tree = render( + , + ); + const text = textContent(tree.toJSON()); + expect(text).toContain("Approve"); + expect(text).toContain("Deploy the current patch"); + expect(text).not.toContain("not rendered specially"); + }); + test("freeform question renders a textarea and disables send when empty", () => { const question = makeQuestion({ question_type: QuestionType.FREEFORM, diff --git a/apps/fabro-web/app/components/interview-dock.tsx b/apps/fabro-web/app/components/interview-dock.tsx index 35eb2fcf5..967044907 100644 --- a/apps/fabro-web/app/components/interview-dock.tsx +++ b/apps/fabro-web/app/components/interview-dock.tsx @@ -15,7 +15,7 @@ import { import { QuestionType } from "@qltysh/fabro-api-client"; import type { ApiQuestion, - ApiQuestionOption, + InterviewOption, } from "@qltysh/fabro-api-client"; import { @@ -270,7 +270,7 @@ function ChoiceBody({ submitting, onSubmit, }: { - options: ApiQuestionOption[]; + options: InterviewOption[]; allowFreeform: boolean; submitting: boolean; onSubmit: (answer: SubmitInterviewAnswer) => Promise; @@ -287,7 +287,7 @@ function ChoiceBody({ onClick={() => void onSubmit({ kind: "selected", option_key: option.key })} className={CHOICE_BUTTON} > - {displayLabel(option.label)} + ))} @@ -314,7 +314,7 @@ function MultiSelectBody({ submitting, onSubmit, }: { - options: ApiQuestionOption[]; + options: InterviewOption[]; submitting: boolean; onSubmit: (answer: SubmitInterviewAnswer) => Promise; }) { @@ -346,7 +346,7 @@ function MultiSelectBody({ className={isSelected ? CHOICE_BUTTON_SELECTED : CHOICE_BUTTON} > {isSelected &&