From 13475d7f85ee721e35d1d5dedb19427136b4e905 Mon Sep 17 00:00:00 2001 From: Fabro Date: Sat, 23 May 2026 01:43:25 -0400 Subject: [PATCH] =?UTF-8?q?checkpoint=20=E2=9A=92=EF=B8=8F=20Generated=20w?= =?UTF-8?q?ith=20[Fabro](https://fabro.sh)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- run.json | 338 +++++++++- stages/009-simplify_gpt@1/diff.patch | 740 +++++++++++++++++++++ stages/009-simplify_gpt@1/status.json | 6 + stages/010-verify@1/script_invocation.json | 5 + 4 files changed, 1076 insertions(+), 13 deletions(-) create mode 100644 stages/009-simplify_gpt@1/diff.patch create mode 100644 stages/009-simplify_gpt@1/status.json create mode 100644 stages/010-verify@1/script_invocation.json diff --git a/run.json b/run.json index 1da0fba26..f097e2fe1 100644 --- a/run.json +++ b/run.json @@ -521,7 +521,7 @@ "kind": "running" }, "status_updated_at": "2026-05-23T02:51:52.587571Z", - "last_event_at": "2026-05-23T05:40:53.374510Z", + "last_event_at": "2026-05-23T05:40:58.129452Z", "pending_control": null, "checkpoints": [ { @@ -1429,9 +1429,9 @@ } }, { - "seq": 0, + "seq": 3215, "checkpoint": { - "timestamp": "2026-05-23T05:40:53.443999Z", + "timestamp": "2026-05-23T05:40:58.124666Z", "current_node": "simplify_gpt", "completed_nodes": [ "start", @@ -1445,6 +1445,266 @@ "simplify_gpt" ], "node_retries": {}, + "context_values": { + "internal.retry_count.preflight_lint": 0, + "internal.run_id": "01KS9BXFGAZ32SGNRE4YJV1354", + "thread.simplify_opus.current_node": "simplify_gpt", + "internal.retry_count.simplify_gpt": 0, + "graph.rankdir": "LR", + "failure_signature": "", + "internal.retry_count.fix_lints": 0, + "internal.thread_id": "simplify_opus", + "graph.goal": "# Named Environments Implementation Plan\n\n> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task.\n\n**Goal:** Replace run-scoped sandbox configuration with named, provider-explicit environments that runs can select by slug.\n\n**Architecture:** Add a shared top-level environment catalog, resolve a selected environment into the run's dense settings, validate provider capabilities, and convert the resolved environment into the existing sandbox runtime specs. Keep \"environment\" as reusable desired configuration and \"sandbox\" as the concrete runtime instance created for a run.\n\n**Tech Stack:** Rust config/types crates, TOML settings layers, Fabro workflow sandbox providers, OpenAPI-generated clients, public docs.\n\n---\n\n## Summary\n\nReplace run-scoped sandbox configuration with named, provider-explicit environments. A run selects an environment by slug via `[run.environment] id = \"...\"`; Fabro resolves the environment catalog through normal config precedence, applies run-level environment overrides, validates provider capabilities, freezes the resolved environment into the run settings, and creates a concrete sandbox instance from it.\n\nThis is a greenfield break: no `[run.sandbox]` compatibility layer, no server policy layer, and no required/optional volume semantics.\n\n## Key Interface Changes\n\n- Add top-level `[environments.]` to the shared settings schema. It is valid in `settings.toml`, `.fabro/project.toml`, and `workflow.toml`.\n- Replace sandbox selection with:\n\n```toml\n[run.environment]\nid = \"fabro-dev\"\n```\n\n- Allow sparse run-level overrides under the same table:\n\n```toml\n[run.environment.resources]\nmemory = \"32GB\"\n\n[run.environment.lifecycle]\npreserve = true\n```\n\n- Environment shape:\n\n```toml\n[environments.fabro-dev]\nprovider = \"daytona\" # local | docker | daytona\n\n[environments.fabro-dev.image]\nref = \"fabro-v11\" # Docker image or Daytona snapshot name\ndockerfile = { path = \"Dockerfile\" }\n\n[environments.fabro-dev.resources]\ncpu = 8\nmemory = \"16GB\"\ndisk = \"20GB\"\n\n[environments.fabro-dev.network]\nmode = \"block\" # allow_all | block | cidr_allow_list\nallow = [\"10.0.0.0/8\"]\n\n[environments.fabro-dev.lifecycle]\npreserve = false\nstop_on_terminal = true\nauto_stop = \"30m\"\n\n[environments.fabro-dev.labels]\nrepo = \"fabro-sh/fabro\"\n\n[[environments.fabro-dev.volumes]]\nid = \"vol-agent-state\"\nmount_path = \"/home/daytona/agent-state\"\nsubpath = \"auth\"\n\n[environments.fabro-dev.env]\nNODE_ENV = \"development\"\n```\n\n- Built-in default becomes:\n\n```toml\n[run.environment]\nid = \"default\"\n\n[environments.default]\nprovider = \"docker\"\n\n[environments.default.image]\nref = \"buildpack-deps:noble\"\n\n[environments.default.resources]\ncpu = 2\nmemory = \"4GB\"\n\n[environments.default.lifecycle]\npreserve = false\nstop_on_terminal = true\n```\n\n## Implementation Changes\n\n- Add environment sparse and dense types:\n - Sparse layer in `fabro-config` for `EnvironmentLayer`, `RunEnvironmentLayer`, image/resources/network/lifecycle/volume sublayers, and `[environments]` as a `MergeMap`.\n - Dense types in `fabro-types` for `EnvironmentSettings`, `RunEnvironmentSettings`, `EnvironmentProvider`, `EnvironmentNetworkMode`, and related subsettings.\n - Add `environments` to the top-level `SettingsLayer` and resolved `WorkflowSettings`; add selected `environment` to `RunNamespace`.\n- Resolve environments before run consumers use sandbox data:\n - Merge environment definitions by slug.\n - Resolve `[run.environment].id`; error if the slug is missing.\n - Overlay sparse `[run.environment.*]` fields onto the selected environment.\n - Validate provider is `local`, `docker`, or `daytona`.\n - Validate CIDRs with existing `ipnet`.\n - Store the selected resolved environment in `RunNamespace.environment`.\n- Replace sandbox runtime mapping:\n - Convert `RunNamespace.environment` to `SandboxSpec` in workflow start and server preflight paths.\n - Daytona: `image.ref` maps to snapshot name, `dockerfile` to snapshot Dockerfile, resources to snapshot sizing, network to Daytona policy, labels/volumes/env/lifecycle to existing provider fields.\n - Docker: `image.ref` maps to Docker image, `cpu` maps to `cpu_quota = cpu * 100000`, memory maps to memory limit, `network.mode = block` maps to `network_mode = none`, `allow_all` maps to default/bridge.\n - Local: use resolved working directory; env overlays process env as today.\n- Capability diagnostics:\n - Hard error for explicit security/isolation properties a provider cannot enforce:\n - local with `network.mode = block` or `cidr_allow_list`\n - docker with `network.mode = cidr_allow_list`\n - Warnings only for unsupported resource limits, volumes, labels, `auto_stop`, and Docker `image.dockerfile`.\n - If Daytona has `image.dockerfile` without `image.ref`, error because snapshot creation needs a name.\n- Remove old sandbox config surface:\n - Delete `[run.sandbox]` parsing/resolution/types from user-facing config.\n - Replace CLI/API/tool manifest args named `sandbox` with `environment` where they select execution profile.\n - Keep runtime/public \"sandbox\" terminology only for concrete instances, e.g. `fabro sandbox ssh`, `RunSandbox`, sandbox details.\n- Update docs and generated clients:\n - Update run configuration, environments, Daytona, server configuration, CLI reference, and OpenAPI spec.\n - Regenerate Rust API types/client and TypeScript API client after OpenAPI changes.\n\n## Test Plan\n\n- Config tests:\n - default resolves to `run.environment.id = \"default\"` and Docker environment settings.\n - project/workflow/run layers merge environment catalog by slug.\n - `[run.environment]` overrides selected environment fields.\n - `env` and `labels` merge by key; `volumes` replace wholesale.\n - missing environment slug errors.\n - old `[run.sandbox]` is rejected as an unknown field.\n- Provider mapping tests:\n - Daytona environment maps to snapshot/resources/network/labels/volumes/env.\n - Docker environment maps image, CPU, memory, network block, and env.\n - Local environment ignores non-security unsupported fields with warnings.\n- Validation tests:\n - docker plus CIDR allow-list errors.\n - local plus blocked network errors.\n - resource limits unsupported by provider produce warnings, not errors.\n - volumes unsupported by provider produce warnings, not errors.\n - Daytona dockerfile without image ref errors.\n- Integration/API tests:\n - run manifest with `[environments.]` and `[run.environment]` starts with the selected provider.\n - Dockerfile path bundling works from environment image config.\n - preflight reports capability warnings and security errors.\n - CLI/API `environment` override wins over config selection.\n\n## Assumptions\n\n- No compatibility behavior is required for `[run.sandbox]` or `--sandbox`.\n- No server-side environment policy or quota enforcement is in scope.\n- Volumes are simple provider hints; unsupported volume config warns and continues.\n- Resource limits are best-effort hints; unsupported resource fields warn and continue.\n- Provider names remain explicit for now: `local`, `docker`, and `daytona`.\n", + "internal.retry_count.toolchain": 0, + "response.simplify_gpt": "Implemented cleanup after the review pass.\n\nFixed:\n- Centralized environment env-var interpolation in `RunEnvironmentSettings::resolve_env(...)`.\n - Reused by Docker sandbox mapping and workflow startup.\n - Removed duplicate `resolve_interp` helpers.\n - Added unit tests for interpolation success, fallback, and empty env.\n- Updated stale CLI integration tests from removed `--sandbox` run-selection flag to `--environment`.\n - Also refreshed help snapshots/text for `run`, `create`, `preflight`, and `server start`.\n- Minor comment cleanup: “sandbox selection” → “environment selection”.\n\nValidated with:\n- `cargo +nightly-2026-04-14 fmt --all`\n- `cargo check -q -p fabro-types -p fabro-sandbox -p fabro-workflow -p fabro-cli`\n- `cargo +nightly-2026-04-14 clippy -q -p fabro-types -p fabro-sandbox -p fabro-workflow -p fabro-cli --all-targets -- -D warnings`\n- Targeted CLI help tests for updated snapshots\n- `cargo test -q -p fabro-types run_environment_settings_tests -- --nocapture`\n- `git diff --check`", + "response.simplify_opus": "Done. Here's a summary of what was fixed:\n\n## High-impact refactors\n\n1. **Extracted duplicated `runtime_*_config` helpers** (~180 LOC dedup): moved `runtime_daytona_config`, `runtime_docker_config`, `duration_to_minutes_i32`, `size_to_gb_i32`, `resolve_interp` from both `fabro-workflow/operations/start.rs` and `fabro-server/run_manifest.rs` into a new `fabro-sandbox::from_environment` module. Both callers now share one implementation.\n\n2. **Replaced `apply_*_override` with `Combine`-based merge** (~95 LOC removed): wired up the previously-dead `RunEnvironmentLayer::into_environment_override` so `run_layer.into_environment_override().combine(base.clone())` replaces all five `apply_*_override` functions and their hand-rolled CIDR/extend logic.\n\n3. **Dropped duplicate `validate_daytona_snapshot_name` call**: the snapshot check was running twice for the run-selected environment. Now runs once.\n\n4. **Dropped dead `WorkflowSettings.environments` dense field**: only one test assertion read it; also removed `resolve_environments` and `Combine`-resolution plumbing.\n\n5. **Replaced stringly-typed `clone_sandbox_*_github_credentials(&str)` with `EnvironmentProvider::is_clone_based()`**: the enum already had this method.\n\n6. **Deduplicated demo `EnvironmentSettings` literal**: bound to a local in `demo/mod.rs::runs::settings()`.\n\n## Skipped (after weighing trade-offs)\n\n- **Flatten `RunEnvironmentSettings → { id, environment: EnvironmentSettings }`**: saves ~30 lines but adds an access-path hop on every field read.\n- **Type provider/mode as enums in sparse layers**: would break `workflow_settings_accumulate_multiple_run_errors`, which depends on resolve-time error accumulation across the provider field and `run.prepare`.\n- **Derive `Combine` on `EnvironmentNetworkLayer`**: would require touching the resolver in 3 places for negligible savings.\n\n## Verification\n\n- `cargo check --workspace --tests` — clean.\n- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings` — clean.\n- `cargo nextest run` on affected crates — 2194/2195 pass, 1 fail (pre-existing `[run.sandbox]` test fixture that the original implement stage left broken).\n- Compared full workspace failures vs origin: same 30 distinct test names fail on both branches (all `--sandbox`/old-fixture pre-existing failures); **0 net regressions** from the refactor.\n\nNet diff: **~360 lines removed** across 14 files.", + "internal.retry_count.preflight_compile": 0, + "thread.toolchain.current_node": "preflight_compile", + "failure_class": "", + "thread.implement.current_node": "simplify_opus", + "thread.preflight_compile.current_node": "preflight_lint", + "internal.retry_count.start": 0, + "internal.work_dir": "/home/daytona/workspace/fabro", + "last_stage": "simplify_gpt", + "internal.fidelity": "compact", + "graph.model_stylesheet": "\n * { model: claude-opus-4-7; }\n ", + "response.fix_lints": "Clippy passes cleanly now. Summary of fixes:\n\n1. **`lib/crates/fabro-sandbox/src/docker.rs`** — moved `use std::io::Read as _;` (with its `#[expect(clippy::disallowed_types, ...)]` attribute) to the top of `download_file_bytes`, before any statements, to satisfy `clippy::items_after_statements`.\n\n2. **`lib/crates/fabro-agent/src/apply_patch.rs`** — replaced four `std::fs::{write, read_to_string}` test calls (disallowed in Tokio paths) with `tokio::fs` equivalents, added `use tokio::fs;` to the test module, and rewrote the calls as `fs::…` to also satisfy `clippy::absolute_paths`.", + "internal.retry_count.implement": 0, + "outcome": "succeeded", + "internal.retry_count.simplify_opus": 0, + "last_response": "Implemented cleanup after the review pass.\n\nFixed:\n- Centralized environment env-var interpolation in `RunEnvironmentSettings::resolve_env(...)`.\n - Reused by Docker sandbox mapping and workflow star", + "internal.node_visit_count": 1, + "thread.start.current_node": "toolchain", + "response.implement": "Implemented named environments across the Rust config/types/runtime stack, CLI/API/tool manifest surfaces, OpenAPI, TypeScript client generation, and public docs.\n\n### What changed\n\n- Added top-level `[environments.]` catalog support and `[run.environment] id = \"...\"`.\n- Added sparse config layers and dense settings types for:\n - `EnvironmentSettings`\n - `RunEnvironmentSettings`\n - image/resources/network/lifecycle/labels/volumes/env\n - `EnvironmentProvider`\n - `EnvironmentNetworkMode`\n- Removed user-facing `[run.sandbox]` parsing/resolution/types.\n- Added defaults:\n - selected `run.environment.id = \"default\"`\n - Docker `default` environment\n - convenience built-in `local`, `docker`, and `daytona` named environments.\n- Implemented environment resolution:\n - slug lookup and missing slug errors\n - sparse run-level overrides\n - `env`/`labels` merge by key\n - `volumes` replace wholesale\n - CIDR validation\n - hard security capability errors\n- Replaced runtime mapping:\n - Docker image/CPU/memory/network/env mapping\n - Daytona snapshot/resources/network/labels/volumes/env/lifecycle mapping\n - Local environment env handling\n- Added preflight capability warnings for unsupported best-effort hints.\n- Renamed selection surfaces from `sandbox` to `environment` where they select an execution profile:\n - CLI `--environment`\n - server `--environment`\n - `ManifestArgs.environment`\n - Fabro tool create spec `environment`\n- Preserved runtime “sandbox” terminology for concrete instances and commands.\n- Updated OpenAPI schema and regenerated the TypeScript Axios client.\n- Updated core environment/run/server/Daytona/CLI docs and repo `.fabro` configs.\n\n### Validation run\n\nPassed:\n\n- `cargo check -q --workspace`\n- `cargo test -q --workspace --no-run`\n- `cargo test -q -p fabro-config`\n- `cargo test -q -p fabro-workflow runtime_docker_config_maps_environment_hints`\n- `cargo test -q -p fabro-server prepare_manifest_inlines_project_config_daytona_dockerfile_from_bundle`\n- `cargo test -q -p fabro-server prepare_manifest_errors_when_project_config_dockerfile_bundle_is_missing`\n- `cargo test -q -p fabro-tool`\n- `cargo test -q -p fabro-api`\n- `cargo +nightly-2026-04-14 fmt --check --all`\n- `cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings`\n- `cd lib/packages/fabro-api-client && bun run typecheck`\n\nNote: full `cargo test -q -p fabro-server` still has 3 order-dependent storage-related failures unrelated to this change; each of those failing tests passes when run individually.", + "command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126", + "current_node": "simplify_gpt", + "thread.fix_lints.current_node": "preflight_lint", + "thread.preflight_lint.current_node": "implement" + }, + "node_outcomes": { + "simplify_gpt": { + "status": "succeeded", + "context_updates": { + "last_stage": "simplify_gpt", + "last_response": "Implemented cleanup after the review pass.\n\nFixed:\n- Centralized environment env-var interpolation in `RunEnvironmentSettings::resolve_env(...)`.\n - Reused by Docker sandbox mapping and workflow star", + "response.simplify_gpt": "Implemented cleanup after the review pass.\n\nFixed:\n- Centralized environment env-var interpolation in `RunEnvironmentSettings::resolve_env(...)`.\n - Reused by Docker sandbox mapping and workflow startup.\n - Removed duplicate `resolve_interp` helpers.\n - Added unit tests for interpolation success, fallback, and empty env.\n- Updated stale CLI integration tests from removed `--sandbox` run-selection flag to `--environment`.\n - Also refreshed help snapshots/text for `run`, `create`, `preflight`, and `server start`.\n- Minor comment cleanup: “sandbox selection” → “environment selection”.\n\nValidated with:\n- `cargo +nightly-2026-04-14 fmt --all`\n- `cargo check -q -p fabro-types -p fabro-sandbox -p fabro-workflow -p fabro-cli`\n- `cargo +nightly-2026-04-14 clippy -q -p fabro-types -p fabro-sandbox -p fabro-workflow -p fabro-cli --all-targets -- -D warnings`\n- Targeted CLI help tests for updated snapshots\n- `cargo test -q -p fabro-types run_environment_settings_tests -- --nocapture`\n- `git diff --check`" + }, + "notes": "Stage completed: simplify_gpt", + "usage": { + "input": { + "usage": { + "model": { + "provider": "openai", + "model_id": "gpt-5.5" + }, + "tokens": { + "input_tokens": 93881, + "output_tokens": 9043, + "reasoning_tokens": 3264, + "cache_read_tokens": 2801152, + "cache_write_tokens": 0 + } + }, + "facts": { + "algorithm": "openai" + } + }, + "total_usd_micros": 2239191 + } + }, + "simplify_opus": { + "status": "succeeded", + "context_updates": { + "last_response": "Done. Here's a summary of what was fixed:\n\n## High-impact refactors\n\n1. **Extracted duplicated `runtime_*_config` helpers** (~180 LOC dedup): moved `runtime_daytona_config`, `runtime_docker_config`, `", + "response.simplify_opus": "Done. Here's a summary of what was fixed:\n\n## High-impact refactors\n\n1. **Extracted duplicated `runtime_*_config` helpers** (~180 LOC dedup): moved `runtime_daytona_config`, `runtime_docker_config`, `duration_to_minutes_i32`, `size_to_gb_i32`, `resolve_interp` from both `fabro-workflow/operations/start.rs` and `fabro-server/run_manifest.rs` into a new `fabro-sandbox::from_environment` module. Both callers now share one implementation.\n\n2. **Replaced `apply_*_override` with `Combine`-based merge** (~95 LOC removed): wired up the previously-dead `RunEnvironmentLayer::into_environment_override` so `run_layer.into_environment_override().combine(base.clone())` replaces all five `apply_*_override` functions and their hand-rolled CIDR/extend logic.\n\n3. **Dropped duplicate `validate_daytona_snapshot_name` call**: the snapshot check was running twice for the run-selected environment. Now runs once.\n\n4. **Dropped dead `WorkflowSettings.environments` dense field**: only one test assertion read it; also removed `resolve_environments` and `Combine`-resolution plumbing.\n\n5. **Replaced stringly-typed `clone_sandbox_*_github_credentials(&str)` with `EnvironmentProvider::is_clone_based()`**: the enum already had this method.\n\n6. **Deduplicated demo `EnvironmentSettings` literal**: bound to a local in `demo/mod.rs::runs::settings()`.\n\n## Skipped (after weighing trade-offs)\n\n- **Flatten `RunEnvironmentSettings → { id, environment: EnvironmentSettings }`**: saves ~30 lines but adds an access-path hop on every field read.\n- **Type provider/mode as enums in sparse layers**: would break `workflow_settings_accumulate_multiple_run_errors`, which depends on resolve-time error accumulation across the provider field and `run.prepare`.\n- **Derive `Combine` on `EnvironmentNetworkLayer`**: would require touching the resolver in 3 places for negligible savings.\n\n## Verification\n\n- `cargo check --workspace --tests` — clean.\n- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings` — clean.\n- `cargo nextest run` on affected crates — 2194/2195 pass, 1 fail (pre-existing `[run.sandbox]` test fixture that the original implement stage left broken).\n- Compared full workspace failures vs origin: same 30 distinct test names fail on both branches (all `--sandbox`/old-fixture pre-existing failures); **0 net regressions** from the refactor.\n\nNet diff: **~360 lines removed** across 14 files.", + "last_stage": "simplify_opus" + }, + "notes": "Stage completed: simplify_opus", + "usage": { + "input": { + "usage": { + "model": { + "provider": "anthropic", + "model_id": "claude-opus-4-7" + }, + "tokens": { + "input_tokens": 249877, + "output_tokens": 88107, + "reasoning_tokens": 0, + "cache_read_tokens": 33431455, + "cache_write_tokens": 2881117 + } + }, + "facts": { + "algorithm": "anthropic", + "cache_write_5m_tokens": 2881117, + "cache_write_1h_tokens": 0 + } + }, + "total_usd_micros": 38174768 + }, + "files_touched": [ + "/home/daytona/workspace/fabro/lib/crates/fabro-cli/src/commands/run/runner.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-config/src/builders.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-config/src/resolve/environment.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-config/src/resolve/mod.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-config/src/resolve/run.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-config/src/tests/resolve_root.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-config/src/tests/resolve_run.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-sandbox/src/from_environment.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-sandbox/src/lib.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-server/src/demo/mod.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-server/src/run_manifest.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-server/src/server.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-server/src/server/tests.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-types/src/dense.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-workflow/src/operations/start.rs" + ] + }, + "toolchain": { + "status": "succeeded", + "context_updates": { + "command.output": "blob://sha256/fc14b2ba2d770e5cd3169df7a29525c962adfc4cfa3097b9098c63ebd61a748c" + }, + "notes": "Script completed: 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", + "usage": null + }, + "fix_lints": { + "status": "succeeded", + "context_updates": { + "last_response": "Clippy passes cleanly now. Summary of fixes:\n\n1. **`lib/crates/fabro-sandbox/src/docker.rs`** — moved `use std::io::Read as _;` (with its `#[expect(clippy::disallowed_types, ...)]` attribute) to the", + "response.fix_lints": "Clippy passes cleanly now. Summary of fixes:\n\n1. **`lib/crates/fabro-sandbox/src/docker.rs`** — moved `use std::io::Read as _;` (with its `#[expect(clippy::disallowed_types, ...)]` attribute) to the top of `download_file_bytes`, before any statements, to satisfy `clippy::items_after_statements`.\n\n2. **`lib/crates/fabro-agent/src/apply_patch.rs`** — replaced four `std::fs::{write, read_to_string}` test calls (disallowed in Tokio paths) with `tokio::fs` equivalents, added `use tokio::fs;` to the test module, and rewrote the calls as `fs::…` to also satisfy `clippy::absolute_paths`.", + "last_stage": "fix_lints" + }, + "notes": "Stage completed: fix_lints", + "usage": { + "input": { + "usage": { + "model": { + "provider": "anthropic", + "model_id": "claude-opus-4-7" + }, + "tokens": { + "input_tokens": 19981, + "output_tokens": 7905, + "reasoning_tokens": 0, + "cache_read_tokens": 594685, + "cache_write_tokens": 74870 + } + }, + "facts": { + "algorithm": "anthropic", + "cache_write_5m_tokens": 74870, + "cache_write_1h_tokens": 0 + } + }, + "total_usd_micros": 1062809 + }, + "files_touched": [ + "/home/daytona/workspace/fabro/lib/crates/fabro-agent/src/apply_patch.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-sandbox/src/docker.rs" + ] + }, + "preflight_lint": { + "status": "succeeded", + "context_updates": { + "command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126" + }, + "notes": "Script completed: cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1", + "usage": null + }, + "implement": { + "status": "succeeded", + "context_updates": { + "response.implement": "Implemented named environments across the Rust config/types/runtime stack, CLI/API/tool manifest surfaces, OpenAPI, TypeScript client generation, and public docs.\n\n### What changed\n\n- Added top-level `[environments.]` catalog support and `[run.environment] id = \"...\"`.\n- Added sparse config layers and dense settings types for:\n - `EnvironmentSettings`\n - `RunEnvironmentSettings`\n - image/resources/network/lifecycle/labels/volumes/env\n - `EnvironmentProvider`\n - `EnvironmentNetworkMode`\n- Removed user-facing `[run.sandbox]` parsing/resolution/types.\n- Added defaults:\n - selected `run.environment.id = \"default\"`\n - Docker `default` environment\n - convenience built-in `local`, `docker`, and `daytona` named environments.\n- Implemented environment resolution:\n - slug lookup and missing slug errors\n - sparse run-level overrides\n - `env`/`labels` merge by key\n - `volumes` replace wholesale\n - CIDR validation\n - hard security capability errors\n- Replaced runtime mapping:\n - Docker image/CPU/memory/network/env mapping\n - Daytona snapshot/resources/network/labels/volumes/env/lifecycle mapping\n - Local environment env handling\n- Added preflight capability warnings for unsupported best-effort hints.\n- Renamed selection surfaces from `sandbox` to `environment` where they select an execution profile:\n - CLI `--environment`\n - server `--environment`\n - `ManifestArgs.environment`\n - Fabro tool create spec `environment`\n- Preserved runtime “sandbox” terminology for concrete instances and commands.\n- Updated OpenAPI schema and regenerated the TypeScript Axios client.\n- Updated core environment/run/server/Daytona/CLI docs and repo `.fabro` configs.\n\n### Validation run\n\nPassed:\n\n- `cargo check -q --workspace`\n- `cargo test -q --workspace --no-run`\n- `cargo test -q -p fabro-config`\n- `cargo test -q -p fabro-workflow runtime_docker_config_maps_environment_hints`\n- `cargo test -q -p fabro-server prepare_manifest_inlines_project_config_daytona_dockerfile_from_bundle`\n- `cargo test -q -p fabro-server prepare_manifest_errors_when_project_config_dockerfile_bundle_is_missing`\n- `cargo test -q -p fabro-tool`\n- `cargo test -q -p fabro-api`\n- `cargo +nightly-2026-04-14 fmt --check --all`\n- `cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings`\n- `cd lib/packages/fabro-api-client && bun run typecheck`\n\nNote: full `cargo test -q -p fabro-server` still has 3 order-dependent storage-related failures unrelated to this change; each of those failing tests passes when run individually.", + "last_response": "Implemented named environments across the Rust config/types/runtime stack, CLI/API/tool manifest surfaces, OpenAPI, TypeScript client generation, and public docs.\n\n### What changed\n\n- Added top-level ", + "last_stage": "implement" + }, + "notes": "Stage completed: implement", + "usage": { + "input": { + "usage": { + "model": { + "provider": "openai", + "model_id": "gpt-5.5" + }, + "tokens": { + "input_tokens": 518420, + "output_tokens": 68247, + "reasoning_tokens": 21735, + "cache_read_tokens": 104187392, + "cache_write_tokens": 0 + } + }, + "facts": { + "algorithm": "openai" + } + }, + "total_usd_micros": 57385256 + }, + "files_touched": [ + "/home/daytona/workspace/fabro/docs/public/execution/devcontainers.mdx", + "/home/daytona/workspace/fabro/docs/public/execution/environments.mdx", + "/home/daytona/workspace/fabro/lib/crates/fabro-config/src/layers/environment.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-config/src/resolve/environment.rs" + ] + }, + "preflight_compile": { + "status": "succeeded", + "context_updates": { + "command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126" + }, + "notes": "Script completed: cargo check -q --workspace 2>&1", + "usage": null + }, + "start": { + "status": "succeeded", + "usage": null + } + }, + "next_node_id": "verify", + "git_commit_sha": "ae8a3c43081b34d97401226d98ec4a7e92eb5ec1", + "loop_failure_signatures": { + "preflight_lint|deterministic|script failed with exit code: ## output error: adding items after statements is confusing,since items exist from the start of the scope --> lib/crates/fabro-sandbox/src/docker.rs:: | | use std::io::read as _; | ^^^^^^^^^^^^^^^": 1 + }, + "node_visits": { + "start": 1, + "implement": 1, + "simplify_gpt": 1, + "preflight_lint": 2, + "fix_lints": 1, + "preflight_compile": 1, + "simplify_opus": 1, + "toolchain": 1 + } + }, + "diff": { + "patch": "diff --git a/lib/crates/fabro-cli/tests/it/cmd/attach.rs b/lib/crates/fabro-cli/tests/it/cmd/attach.rs\nindex 3c948c7e6..60e4e0e4b 100644\n--- a/lib/crates/fabro-cli/tests/it/cmd/attach.rs\n+++ b/lib/crates/fabro-cli/tests/it/cmd/attach.rs\n@@ -161,7 +161,7 @@ fn start_detached_human_run(\n .args([\n \"run\",\n \"--detach\",\n- \"--sandbox\",\n+ \"--environment\",\n \"local\",\n \"--provider\",\n \"openai\",\n@@ -391,7 +391,7 @@ fn attach_advances_when_pending_question_is_answered_elsewhere() {\n .args([\n \"run\",\n \"--detach\",\n- \"--sandbox\",\n+ \"--environment\",\n \"local\",\n \"--provider\",\n \"openai\",\n@@ -516,7 +516,7 @@ fn attach_before_completion_streams_to_finished_state() {\n \"--detach\",\n \"--provider\",\n \"openai\",\n- \"--sandbox\",\n+ \"--environment\",\n \"local\",\n \"slow.fabro\",\n ]);\n@@ -637,7 +637,7 @@ fn attach_json_errors_without_prompting_for_human_input() {\n .args([\n \"run\",\n \"--detach\",\n- \"--sandbox\",\n+ \"--environment\",\n \"local\",\n \"--provider\",\n \"openai\",\ndiff --git a/lib/crates/fabro-cli/tests/it/cmd/create.rs b/lib/crates/fabro-cli/tests/it/cmd/create.rs\nindex 8fb604aa6..f3eb32f87 100644\n--- a/lib/crates/fabro-cli/tests/it/cmd/create.rs\n+++ b/lib/crates/fabro-cli/tests/it/cmd/create.rs\n@@ -45,25 +45,25 @@ fn help() {\n Path to a .fabro workflow file or .toml task config\n \n Options:\n- --json Output as JSON [env: FABRO_JSON=]\n- --server Fabro server target: http(s) URL or absolute Unix socket path [env: FABRO_SERVER=]\n- --debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=]\n- -I, --input Override a workflow input value (repeatable, format: KEY=VALUE)\n- --dry-run Execute with simulated LLM backend\n- --no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true]\n- --auto-approve Auto-approve all human gates\n- --quiet Suppress non-essential output [env: FABRO_QUIET=]\n- --goal Override the workflow goal (available as {{ goal }} in prompts)\n- --goal-file Read the workflow goal from a file\n- --model Override default LLM model\n- --provider Override default LLM provider\n- -v, --verbose Enable verbose output\n- --sandbox Sandbox for agent tools [possible values: local, docker, daytona]\n- --label Attach a label to this run (repeatable, format: KEY=VALUE)\n- --parent Link this run to an existing orchestration parent run\n- --preserve-sandbox Keep the sandbox alive after the run finishes (for debugging)\n- -d, --detach Run the workflow in the background and print the run ID\n- -h, --help Print help\n+ --json Output as JSON [env: FABRO_JSON=]\n+ --server Fabro server target: http(s) URL or absolute Unix socket path [env: FABRO_SERVER=]\n+ --debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=]\n+ -I, --input Override a workflow input value (repeatable, format: KEY=VALUE)\n+ --dry-run Execute with simulated LLM backend\n+ --no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true]\n+ --auto-approve Auto-approve all human gates\n+ --quiet Suppress non-essential output [env: FABRO_QUIET=]\n+ --goal Override the workflow goal (available as {{ goal }} in prompts)\n+ --goal-file Read the workflow goal from a file\n+ --model Override default LLM model\n+ --provider Override default LLM provider\n+ -v, --verbose Enable verbose output\n+ --environment Named environment for agent tools\n+ --label Attach a label to this run (repeatable, format: KEY=VALUE)\n+ --parent Link this run to an existing orchestration parent run\n+ --preserve-sandbox Keep the sandbox alive after the run finishes (for debugging)\n+ -d, --detach Run the workflow in the background and print the run ID\n+ -h, --help Print help\n ----- stderr -----\n \");\n }\ndiff --git a/lib/crates/fabro-cli/tests/it/cmd/dump.rs b/lib/crates/fabro-cli/tests/it/cmd/dump.rs\nindex d7c26eb51..acdd34da6 100644\n--- a/lib/crates/fabro-cli/tests/it/cmd/dump.rs\n+++ b/lib/crates/fabro-cli/tests/it/cmd/dump.rs\n@@ -103,7 +103,7 @@ fn dump_exports_large_command_output_backed_by_blob_refs() {\n let mut run_cmd = context.run_cmd();\n run_cmd.current_dir(&context.temp_dir);\n run_cmd.timeout(Duration::from_secs(30));\n- run_cmd.args([\"--run-id\", run_id.as_str(), \"--sandbox\", \"local\"]);\n+ run_cmd.args([\"--run-id\", run_id.as_str(), \"--environment\", \"local\"]);\n run_cmd.arg(&workflow);\n let run_output = run_cmd.output().expect(\"command should execute\");\n assert!(\n@@ -194,7 +194,7 @@ include = [\"assets/**\"]\n run_cmd.args([\n \"--run-id\",\n run_id.as_str(),\n- \"--sandbox\",\n+ \"--environment\",\n \"local\",\n \"run.toml\",\n ]);\ndiff --git a/lib/crates/fabro-cli/tests/it/cmd/preflight.rs b/lib/crates/fabro-cli/tests/it/cmd/preflight.rs\nindex e90c1a9d6..8bb035f3a 100644\n--- a/lib/crates/fabro-cli/tests/it/cmd/preflight.rs\n+++ b/lib/crates/fabro-cli/tests/it/cmd/preflight.rs\n@@ -20,19 +20,19 @@ fn help() {\n Path to a .fabro workflow file or .toml task config\n \n Options:\n- --json Output as JSON [env: FABRO_JSON=]\n- --server Fabro server target: http(s) URL or absolute Unix socket path [env: FABRO_SERVER=]\n- --debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=]\n- -I, --input Override a workflow input value (repeatable, format: KEY=VALUE)\n- --goal Override the workflow goal (available as {{ goal }} in prompts)\n- --no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true]\n- --goal-file Read the workflow goal from a file\n- --quiet Suppress non-essential output [env: FABRO_QUIET=]\n- --model Override default LLM model\n- --provider Override default LLM provider\n- -v, --verbose Enable verbose output\n- --sandbox Sandbox for agent tools [possible values: local, docker, daytona]\n- -h, --help Print help\n+ --json Output as JSON [env: FABRO_JSON=]\n+ --server Fabro server target: http(s) URL or absolute Unix socket path [env: FABRO_SERVER=]\n+ --debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=]\n+ -I, --input Override a workflow input value (repeatable, format: KEY=VALUE)\n+ --goal Override the workflow goal (available as {{ goal }} in prompts)\n+ --no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true]\n+ --goal-file Read the workflow goal from a file\n+ --quiet Suppress non-essential output [env: FABRO_QUIET=]\n+ --model Override default LLM model\n+ --provider Override default LLM provider\n+ -v, --verbose Enable verbose output\n+ --environment Named environment for agent tools\n+ -h, --help Print help\n ----- stderr -----\n \");\n }\ndiff --git a/lib/crates/fabro-cli/tests/it/cmd/run.rs b/lib/crates/fabro-cli/tests/it/cmd/run.rs\nindex 464c7cb95..1b50a6673 100644\n--- a/lib/crates/fabro-cli/tests/it/cmd/run.rs\n+++ b/lib/crates/fabro-cli/tests/it/cmd/run.rs\n@@ -123,25 +123,25 @@ fn help() {\n Path to a .fabro workflow file or .toml task config\n \n Options:\n- --json Output as JSON [env: FABRO_JSON=]\n- --server Fabro server target: http(s) URL or absolute Unix socket path [env: FABRO_SERVER=]\n- --debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=]\n- -I, --input Override a workflow input value (repeatable, format: KEY=VALUE)\n- --dry-run Execute with simulated LLM backend\n- --no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true]\n- --auto-approve Auto-approve all human gates\n- --quiet Suppress non-essential output [env: FABRO_QUIET=]\n- --goal Override the workflow goal (available as {{ goal }} in prompts)\n- --goal-file Read the workflow goal from a file\n- --model Override default LLM model\n- --provider Override default LLM provider\n- -v, --verbose Enable verbose output\n- --sandbox Sandbox for agent tools [possible values: local, docker, daytona]\n- --label Attach a label to this run (repeatable, format: KEY=VALUE)\n- --parent Link this run to an existing orchestration parent run\n- --preserve-sandbox Keep the sandbox alive after the run finishes (for debugging)\n- -d, --detach Run the workflow in the background and print the run ID\n- -h, --help Print help\n+ --json Output as JSON [env: FABRO_JSON=]\n+ --server Fabro server target: http(s) URL or absolute Unix socket path [env: FABRO_SERVER=]\n+ --debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=]\n+ -I, --input Override a workflow input value (repeatable, format: KEY=VALUE)\n+ --dry-run Execute with simulated LLM backend\n+ --no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true]\n+ --auto-approve Auto-approve all human gates\n+ --quiet Suppress non-essential output [env: FABRO_QUIET=]\n+ --goal Override the workflow goal (available as {{ goal }} in prompts)\n+ --goal-file Read the workflow goal from a file\n+ --model Override default LLM model\n+ --provider Override default LLM provider\n+ -v, --verbose Enable verbose output\n+ --environment Named environment for agent tools\n+ --label Attach a label to this run (repeatable, format: KEY=VALUE)\n+ --parent Link this run to an existing orchestration parent run\n+ --preserve-sandbox Keep the sandbox alive after the run finishes (for debugging)\n+ -d, --detach Run the workflow in the background and print the run ID\n+ -h, --help Print help\n ----- stderr -----\n \");\n }\n@@ -412,7 +412,7 @@ digraph VaultWorkerLlm {\n \"--run-id\",\n run_id.as_str(),\n \"--auto-approve\",\n- \"--sandbox\",\n+ \"--environment\",\n \"local\",\n \"--provider\",\n \"anthropic\",\n@@ -770,7 +770,7 @@ include = [\"assets/**\"]\n \"--run-id\",\n run_id.as_str(),\n \"--auto-approve\",\n- \"--sandbox\",\n+ \"--environment\",\n \"local\",\n \"--provider\",\n \"openai\",\n@@ -899,7 +899,7 @@ fn dry_run_persists_event_history_in_store() {\n \"run\",\n \"--dry-run\",\n \"--auto-approve\",\n- \"--sandbox\",\n+ \"--environment\",\n \"local\",\n \"--run-id\",\n run_id.as_str(),\n@@ -1041,7 +1041,7 @@ fn json_run_requires_manual_input_for_human_gates_without_auto_approve() {\n .args([\n \"--json\",\n \"run\",\n- \"--sandbox\",\n+ \"--environment\",\n \"local\",\n workflow.to_str().unwrap(),\n ])\ndiff --git a/lib/crates/fabro-cli/tests/it/cmd/runner.rs b/lib/crates/fabro-cli/tests/it/cmd/runner.rs\nindex f1d52274f..d7d87fb93 100644\n--- a/lib/crates/fabro-cli/tests/it/cmd/runner.rs\n+++ b/lib/crates/fabro-cli/tests/it/cmd/runner.rs\n@@ -481,7 +481,7 @@ methods = [\"dev-token\"]\n run_id.as_str(),\n \"--detach\",\n \"--auto-approve\",\n- \"--sandbox\",\n+ \"--environment\",\n \"local\",\n workflow_path\n .to_str()\n@@ -734,7 +734,7 @@ fn detached_run_answers_pending_question_without_interview_scratch_files() {\n \"--detach\",\n \"--run-id\",\n run_id.as_str(),\n- \"--sandbox\",\n+ \"--environment\",\n \"local\",\n workflow_path.to_str().unwrap(),\n ])\n@@ -807,7 +807,7 @@ fn worker_exits_after_sigterm_cancel_even_when_stdin_stays_open() {\n .args([\n \"create\",\n \"--auto-approve\",\n- \"--sandbox\",\n+ \"--environment\",\n \"local\",\n \"--run-id\",\n run_id.as_str(),\ndiff --git a/lib/crates/fabro-cli/tests/it/cmd/server_start.rs b/lib/crates/fabro-cli/tests/it/cmd/server_start.rs\nindex 86c928f4b..11ef4e073 100644\n--- a/lib/crates/fabro-cli/tests/it/cmd/server_start.rs\n+++ b/lib/crates/fabro-cli/tests/it/cmd/server_start.rs\n@@ -208,8 +208,8 @@ fn help() {\n Override default LLM model\n --provider \n Override default LLM provider\n- --sandbox \n- Sandbox for agent tools\n+ --environment \n+ Named environment for agent tools\n --max-concurrent-runs \n Maximum number of concurrent run executions\n --config \ndiff --git a/lib/crates/fabro-cli/tests/it/cmd/start.rs b/lib/crates/fabro-cli/tests/it/cmd/start.rs\nindex d52014e48..950a708b9 100644\n--- a/lib/crates/fabro-cli/tests/it/cmd/start.rs\n+++ b/lib/crates/fabro-cli/tests/it/cmd/start.rs\n@@ -143,7 +143,7 @@ fn start_rejects_already_active_or_completed_run() {\n \"create\",\n \"--provider\",\n \"openai\",\n- \"--sandbox\",\n+ \"--environment\",\n \"local\",\n \"slow.fabro\",\n ]);\n@@ -204,7 +204,7 @@ fn start_runs_under_server_ownership_without_launcher_record() {\n \"create\",\n \"--provider\",\n \"openai\",\n- \"--sandbox\",\n+ \"--environment\",\n \"local\",\n \"owned-by-server.fabro\",\n ])\ndiff --git a/lib/crates/fabro-cli/tests/it/cmd/support.rs b/lib/crates/fabro-cli/tests/it/cmd/support.rs\nindex e2ab6ef22..072c4bdba 100644\n--- a/lib/crates/fabro-cli/tests/it/cmd/support.rs\n+++ b/lib/crates/fabro-cli/tests/it/cmd/support.rs\n@@ -245,14 +245,14 @@ fn run_completed_dry_run(context: &TestContext, workflow: &Path) -> RunSetup {\n run_id.as_str(),\n \"--dry-run\",\n \"--auto-approve\",\n- \"--sandbox\",\n+ \"--environment\",\n \"local\",\n ]);\n cmd.arg(workflow);\n let output = cmd.output().expect(\"command should execute\");\n if !output.status.success() {\n panic!(\n- \"command failed: fabro run --dry-run --auto-approve --sandbox local {}\\nstdout:\\n{}\\nstderr:\\n{}\",\n+ \"command failed: fabro run --dry-run --auto-approve --environment local {}\\nstdout:\\n{}\\nstderr:\\n{}\",\n workflow.display(),\n stdout(&output),\n stderr(&output)\n@@ -308,14 +308,14 @@ pub(crate) fn setup_detached_dry_run(context: &TestContext) -> RunSetup {\n \"--detach\",\n \"--dry-run\",\n \"--auto-approve\",\n- \"--sandbox\",\n+ \"--environment\",\n \"local\",\n ]);\n cmd.arg(workflow);\n let output = cmd.output().expect(\"command should execute\");\n if !output.status.success() {\n panic!(\n- \"command failed: fabro run --detach --dry-run --auto-approve --sandbox local {}\\nstdout:\\n{}\\nstderr:\\n{}\",\n+ \"command failed: fabro run --detach --dry-run --auto-approve --environment local {}\\nstdout:\\n{}\\nstderr:\\n{}\",\n fixture(\"simple.fabro\").display(),\n stdout(&output),\n stderr(&output)\n@@ -413,7 +413,7 @@ fn run_local_workflow(context: &TestContext, workspace_dir: &Path, workflow: &st\n \"--run-id\",\n run_id.as_str(),\n \"--auto-approve\",\n- \"--sandbox\",\n+ \"--environment\",\n \"local\",\n \"--provider\",\n \"openai\",\n@@ -422,7 +422,7 @@ fn run_local_workflow(context: &TestContext, workspace_dir: &Path, workflow: &st\n let output = cmd.output().expect(\"command should execute\");\n if !output.status.success() {\n panic!(\n- \"command failed: fabro run --auto-approve --sandbox local --provider openai {workflow}\\nstdout:\\n{}\\nstderr:\\n{}\",\n+ \"command failed: fabro run --auto-approve --environment local --provider openai {workflow}\\nstdout:\\n{}\\nstderr:\\n{}\",\n stdout(&output),\n stderr(&output)\n );\ndiff --git a/lib/crates/fabro-cli/tests/it/cmd/worker_auth.rs b/lib/crates/fabro-cli/tests/it/cmd/worker_auth.rs\nindex 0793153d5..9f9d43921 100644\n--- a/lib/crates/fabro-cli/tests/it/cmd/worker_auth.rs\n+++ b/lib/crates/fabro-cli/tests/it/cmd/worker_auth.rs\n@@ -302,7 +302,7 @@ async fn github_only_server_dispatched_worker_succeeds_without_worker_auth_store\n \"--detach\",\n \"--dry-run\",\n \"--auto-approve\",\n- \"--sandbox\",\n+ \"--environment\",\n \"local\",\n workflow.to_str().unwrap(),\n ])\n@@ -358,7 +358,7 @@ fn runner_rejects_bogus_worker_token_against_github_only_server() {\n &run_id,\n \"--dry-run\",\n \"--auto-approve\",\n- \"--sandbox\",\n+ \"--environment\",\n \"local\",\n workflow.to_str().unwrap(),\n ])\ndiff --git a/lib/crates/fabro-cli/tests/it/scenario/lifecycle.rs b/lib/crates/fabro-cli/tests/it/scenario/lifecycle.rs\nindex ee26e2fbf..846bcab2b 100644\n--- a/lib/crates/fabro-cli/tests/it/scenario/lifecycle.rs\n+++ b/lib/crates/fabro-cli/tests/it/scenario/lifecycle.rs\n@@ -30,7 +30,7 @@ fn local_run_lifecycle() {\n .run_cmd()\n .args([\n \"--auto-approve\",\n- \"--sandbox\",\n+ \"--environment\",\n \"local\",\n fixture(\"command_pipeline.fabro\").to_str().unwrap(),\n ])\ndiff --git a/lib/crates/fabro-cli/tests/it/workflow/acp.rs b/lib/crates/fabro-cli/tests/it/workflow/acp.rs\nindex c24af9499..bf841a557 100644\n--- a/lib/crates/fabro-cli/tests/it/workflow/acp.rs\n+++ b/lib/crates/fabro-cli/tests/it/workflow/acp.rs\n@@ -40,7 +40,7 @@ fn acp_backend_workflow() {\n \n context\n .run_cmd()\n- .args([\"--auto-approve\", \"--sandbox\", \"local\"])\n+ .args([\"--auto-approve\", \"--environment\", \"local\"])\n .arg(&workflow)\n .assert()\n .success();\n@@ -119,7 +119,7 @@ fn acp_backend_does_not_inject_registered_provider_credentials() {\n .env_remove(\"ANTHROPIC_API_KEY\")\n .env_remove(\"OPENAI_API_KEY\")\n .env_remove(\"GEMINI_API_KEY\")\n- .args([\"--auto-approve\", \"--sandbox\", \"local\"])\n+ .args([\"--auto-approve\", \"--environment\", \"local\"])\n .arg(&workflow)\n .assert()\n .success();\n@@ -193,7 +193,7 @@ include = [\"verification-artifacts/**\"]\n \n context\n .run_cmd()\n- .args([\"--auto-approve\", \"--sandbox\", \"local\"])\n+ .args([\"--auto-approve\", \"--environment\", \"local\"])\n .arg(context.temp_dir.join(\"run.toml\"))\n .assert()\n .success();\ndiff --git a/lib/crates/fabro-cli/tests/it/workflow/agent_linear.rs b/lib/crates/fabro-cli/tests/it/workflow/agent_linear.rs\nindex 175445f91..031d437a7 100644\n--- a/lib/crates/fabro-cli/tests/it/workflow/agent_linear.rs\n+++ b/lib/crates/fabro-cli/tests/it/workflow/agent_linear.rs\n@@ -13,7 +13,7 @@ fn scenario_agent_linear(sandbox: &str) {\n .run_cmd()\n .args([\n \"--auto-approve\",\n- \"--sandbox\",\n+ \"--environment\",\n sandbox,\n \"--model\",\n \"claude-haiku-4-5\",\ndiff --git a/lib/crates/fabro-cli/tests/it/workflow/artifacts.rs b/lib/crates/fabro-cli/tests/it/workflow/artifacts.rs\nindex 45aabca92..71f57ade6 100644\n--- a/lib/crates/fabro-cli/tests/it/workflow/artifacts.rs\n+++ b/lib/crates/fabro-cli/tests/it/workflow/artifacts.rs\n@@ -49,7 +49,7 @@ include = [\"assets/**\"]\n \n context\n .run_cmd()\n- .args([\"--auto-approve\", \"--sandbox\", \"local\"])\n+ .args([\"--auto-approve\", \"--environment\", \"local\"])\n .arg(context.temp_dir.join(\"run.toml\"))\n .assert()\n .success();\ndiff --git a/lib/crates/fabro-cli/tests/it/workflow/command_agent_mixed.rs b/lib/crates/fabro-cli/tests/it/workflow/command_agent_mixed.rs\nindex 447ef05b4..b25b5b12d 100644\n--- a/lib/crates/fabro-cli/tests/it/workflow/command_agent_mixed.rs\n+++ b/lib/crates/fabro-cli/tests/it/workflow/command_agent_mixed.rs\n@@ -19,7 +19,7 @@ fn scenario_command_agent_mixed(sandbox: &str) {\n .run_cmd()\n .args([\n \"--auto-approve\",\n- \"--sandbox\",\n+ \"--environment\",\n sandbox,\n \"--model\",\n \"claude-haiku-4-5\",\ndiff --git a/lib/crates/fabro-cli/tests/it/workflow/command_pipeline.rs b/lib/crates/fabro-cli/tests/it/workflow/command_pipeline.rs\nindex 2f0d4c94e..21a7a7da2 100644\n--- a/lib/crates/fabro-cli/tests/it/workflow/command_pipeline.rs\n+++ b/lib/crates/fabro-cli/tests/it/workflow/command_pipeline.rs\n@@ -23,7 +23,7 @@ fn scenario_command_pipeline(sandbox: &str) {\n \n context\n .run_cmd()\n- .args([\"--auto-approve\", \"--sandbox\", sandbox])\n+ .args([\"--auto-approve\", \"--environment\", sandbox])\n .arg(fixture(\"command_pipeline.fabro\"))\n .timeout(timeout_for(sandbox))\n .assert()\ndiff --git a/lib/crates/fabro-cli/tests/it/workflow/conditional_branching.rs b/lib/crates/fabro-cli/tests/it/workflow/conditional_branching.rs\nindex 288c2ca6b..c8f703edf 100644\n--- a/lib/crates/fabro-cli/tests/it/workflow/conditional_branching.rs\n+++ b/lib/crates/fabro-cli/tests/it/workflow/conditional_branching.rs\n@@ -9,7 +9,7 @@ fn scenario_conditional_branching(sandbox: &str) {\n \n context\n .run_cmd()\n- .args([\"--auto-approve\", \"--sandbox\", sandbox])\n+ .args([\"--auto-approve\", \"--environment\", sandbox])\n .arg(fixture(\"conditional_branching.fabro\"))\n .timeout(timeout_for(sandbox))\n .assert()\ndiff --git a/lib/crates/fabro-cli/tests/it/workflow/full_stack.rs b/lib/crates/fabro-cli/tests/it/workflow/full_stack.rs\nindex e70dea83a..60a95f5f1 100644\n--- a/lib/crates/fabro-cli/tests/it/workflow/full_stack.rs\n+++ b/lib/crates/fabro-cli/tests/it/workflow/full_stack.rs\n@@ -19,7 +19,7 @@ fn scenario_full_stack(sandbox: &str) {\n .run_cmd()\n .args([\n \"--auto-approve\",\n- \"--sandbox\",\n+ \"--environment\",\n sandbox,\n \"--model\",\n \"claude-haiku-4-5\",\ndiff --git a/lib/crates/fabro-cli/tests/it/workflow/hooks.rs b/lib/crates/fabro-cli/tests/it/workflow/hooks.rs\nindex 3561b254c..03e71e29d 100644\n--- a/lib/crates/fabro-cli/tests/it/workflow/hooks.rs\n+++ b/lib/crates/fabro-cli/tests/it/workflow/hooks.rs\n@@ -120,7 +120,7 @@ fn configure_hook_env(cmd: &mut assert_cmd::Command, hook_model: &str) {\n if TestMode::from_env().is_twin() {\n cmd.env_remove(\"ANTHROPIC_API_KEY\");\n }\n- cmd.arg(\"--sandbox\").arg(\"local\");\n+ cmd.arg(\"--environment\").arg(\"local\");\n cmd.arg(\"--auto-approve\");\n cmd.arg(\"--provider\").arg(stage_provider());\n cmd.arg(\"--model\").arg(hook_model);\ndiff --git a/lib/crates/fabro-cli/tests/it/workflow/human_gate.rs b/lib/crates/fabro-cli/tests/it/workflow/human_gate.rs\nindex 48f649cbf..0f83199e9 100644\n--- a/lib/crates/fabro-cli/tests/it/workflow/human_gate.rs\n+++ b/lib/crates/fabro-cli/tests/it/workflow/human_gate.rs\n@@ -11,7 +11,7 @@ fn scenario_human_gate(sandbox: &str) {\n .run_cmd()\n .args([\n \"--auto-approve\",\n- \"--sandbox\",\n+ \"--environment\",\n sandbox,\n \"--model\",\n \"claude-haiku-4-5\",\ndiff --git a/lib/crates/fabro-config/src/builders.rs b/lib/crates/fabro-config/src/builders.rs\nindex d85a16bb1..80017af26 100644\n--- a/lib/crates/fabro-config/src/builders.rs\n+++ b/lib/crates/fabro-config/src/builders.rs\n@@ -10,8 +10,7 @@ use fabro_util::error::SharedError;\n use crate::defaults::DEFAULTS_LAYER;\n use crate::load::load_settings_path;\n use crate::resolve::{\n- ResolveError, resolve_cli, resolve_project, resolve_run, resolve_server,\n- resolve_workflow,\n+ ResolveError, resolve_cli, resolve_project, resolve_run, resolve_server, resolve_workflow,\n };\n use crate::user::load_settings_config;\n use crate::{\ndiff --git a/lib/crates/fabro-config/src/resolve/environment.rs b/lib/crates/fabro-config/src/resolve/environment.rs\nindex 250f4f3ff..7706254d8 100644\n--- a/lib/crates/fabro-config/src/resolve/environment.rs\n+++ b/lib/crates/fabro-config/src/resolve/environment.rs\n@@ -33,7 +33,10 @@ pub(crate) fn resolve_run_environment(\n return RunEnvironmentSettings::from_environment(id, EnvironmentSettings::default());\n };\n \n- let merged = layer.clone().into_environment_override().combine(base.clone());\n+ let merged = layer\n+ .clone()\n+ .into_environment_override()\n+ .combine(base.clone());\n let environment = resolve_environment_layer(&merged, \"run.environment\", errors);\n validate_provider_capabilities(&environment, \"run.environment\", errors);\n RunEnvironmentSettings::from_environment(id, environment)\ndiff --git a/lib/crates/fabro-sandbox/src/from_environment.rs b/lib/crates/fabro-sandbox/src/from_environment.rs\nindex c59fb5044..811ff620c 100644\n--- a/lib/crates/fabro-sandbox/src/from_environment.rs\n+++ b/lib/crates/fabro-sandbox/src/from_environment.rs\n@@ -3,8 +3,6 @@\n //! These mappings are consumed by both the workflow run-start path and the\n //! server preflight path, so they live here next to their destination types.\n \n-#[cfg(feature = \"docker\")]\n-use fabro_types::settings::interp::InterpString;\n #[cfg(feature = \"daytona\")]\n use fabro_types::settings::run::DockerfileSource as ResolvedDockerfileSource;\n use fabro_types::settings::run::{EnvironmentNetworkMode, RunEnvironmentSettings};\n@@ -55,16 +53,18 @@ pub fn daytona_config_from_environment(\n .resources\n .disk\n .map(|size| size_to_gb_i32(size.as_bytes())),\n- dockerfile: settings.image.dockerfile.as_ref().map(|dockerfile| {\n- match dockerfile {\n+ dockerfile: settings\n+ .image\n+ .dockerfile\n+ .as_ref()\n+ .map(|dockerfile| match dockerfile {\n ResolvedDockerfileSource::Inline(text) => {\n SandboxDockerfileSource::Inline(text.clone())\n }\n ResolvedDockerfileSource::Path { path } => {\n SandboxDockerfileSource::Path { path: path.clone() }\n }\n- }\n- }),\n+ }),\n }),\n network: Some(match settings.network.mode {\n EnvironmentNetworkMode::Block => DaytonaNetwork::Block,\n@@ -84,9 +84,9 @@ pub fn docker_config_from_environment(\n skip_clone: bool,\n ) -> DockerSandboxOptions {\n let mut env_vars = settings\n- .env\n- .iter()\n- .map(|(key, value)| format!(\"{key}={}\", resolve_interp(value)))\n+ .resolve_env(process_env_var)\n+ .into_iter()\n+ .map(|(key, value)| format!(\"{key}={value}\"))\n .collect::>();\n env_vars.sort();\n let default_options = DockerSandboxOptions::default();\n@@ -117,13 +117,6 @@ pub fn docker_config_from_environment(\n }\n }\n \n-#[cfg(feature = \"docker\")]\n-fn resolve_interp(value: &InterpString) -> String {\n- value\n- .resolve(process_env_var)\n- .map_or_else(|_| value.as_source(), |resolved| resolved.value)\n-}\n-\n #[cfg(feature = \"docker\")]\n #[expect(\n clippy::disallowed_methods,\ndiff --git a/lib/crates/fabro-types/src/settings/run.rs b/lib/crates/fabro-types/src/settings/run.rs\nindex 7b3c3450c..b9d0e8611 100644\n--- a/lib/crates/fabro-types/src/settings/run.rs\n+++ b/lib/crates/fabro-types/src/settings/run.rs\n@@ -2,7 +2,7 @@\n //!\n //! `[run]` is the shared execution domain. It may appear in all three config\n //! files and layer normally. Subdomains cover model selection, git author,\n-//! prepare steps, execution posture, checkpoint policy, sandbox selection,\n+//! prepare steps, execution posture, checkpoint policy, environment selection,\n //! notifications, interviews, agent knobs, hooks, SCM targeting, pull-request\n //! behavior, and artifact collection.\n \n@@ -468,6 +468,24 @@ impl RunEnvironmentSettings {\n env: environment.env,\n }\n }\n+\n+ /// Resolve every environment value's `{{ env.* }}` tokens via `lookup`,\n+ /// falling back to the original source string when resolution fails.\n+ #[must_use]\n+ pub fn resolve_env(&self, mut lookup: F) -> HashMap\n+ where\n+ F: FnMut(&str) -> Option,\n+ {\n+ self.env\n+ .iter()\n+ .map(|(name, value)| {\n+ let resolved = value\n+ .resolve(&mut lookup)\n+ .map_or_else(|_| value.as_source(), |resolved| resolved.value);\n+ (name.clone(), resolved)\n+ })\n+ .collect()\n+ }\n }\n \n impl Default for RunEnvironmentSettings {\n@@ -476,6 +494,50 @@ impl Default for RunEnvironmentSettings {\n }\n }\n \n+#[cfg(test)]\n+mod run_environment_settings_tests {\n+ use super::{HashMap, InterpString, RunEnvironmentSettings};\n+\n+ fn settings(env: &[(&str, &str)]) -> RunEnvironmentSettings {\n+ RunEnvironmentSettings {\n+ env: env\n+ .iter()\n+ .map(|(k, v)| ((*k).to_string(), InterpString::parse(v)))\n+ .collect(),\n+ ..RunEnvironmentSettings::default()\n+ }\n+ }\n+\n+ #[test]\n+ fn resolve_env_substitutes_env_tokens_via_lookup() {\n+ let s = settings(&[(\"NODE_ENV\", \"{{ env.NODE_ENV }}\"), (\"STATIC\", \"value\")]);\n+ let resolved = s.resolve_env(|name| match name {\n+ \"NODE_ENV\" => Some(\"test\".to_string()),\n+ _ => None,\n+ });\n+\n+ assert_eq!(resolved.get(\"NODE_ENV\"), Some(&\"test\".to_string()));\n+ assert_eq!(resolved.get(\"STATIC\"), Some(&\"value\".to_string()));\n+ }\n+\n+ #[test]\n+ fn resolve_env_falls_back_to_source_when_lookup_fails() {\n+ let s = settings(&[(\"NODE_ENV\", \"{{ env.MISSING_NODE_ENV }}\")]);\n+ let resolved = s.resolve_env(|_| None);\n+\n+ assert_eq!(\n+ resolved.get(\"NODE_ENV\"),\n+ Some(&\"{{ env.MISSING_NODE_ENV }}\".to_string())\n+ );\n+ }\n+\n+ #[test]\n+ fn resolve_env_is_empty_for_empty_settings() {\n+ let s: HashMap = settings(&[]).resolve_env(|_| None);\n+ assert!(s.is_empty());\n+ }\n+}\n+\n #[derive(Debug, Clone, PartialEq)]\n pub enum DockerfileSource {\n Inline(String),\ndiff --git a/lib/crates/fabro-workflow/src/operations/start.rs b/lib/crates/fabro-workflow/src/operations/start.rs\nindex f99f317ec..f8d53e85f 100644\n--- a/lib/crates/fabro-workflow/src/operations/start.rs\n+++ b/lib/crates/fabro-workflow/src/operations/start.rs\n@@ -363,12 +363,7 @@ impl RunSession {\n }\n };\n \n- let toml_env: HashMap = resolved\n- .environment\n- .env\n- .iter()\n- .map(|(k, v)| (k.clone(), resolve_interp(v)))\n- .collect();\n+ let toml_env = resolved.environment.resolve_env(process_env_var);\n let github_permissions: Option> =\n (!services.github_permissions.is_empty()).then(|| services.github_permissions.clone());\n let sandbox_env = SandboxEnvSpec {\n@@ -459,12 +454,6 @@ async fn configured_providers_for_start(\n }\n }\n \n-fn resolve_interp(value: &InterpString) -> String {\n- value\n- .resolve(process_env_var)\n- .map_or_else(|_| value.as_source(), |resolved| resolved.value)\n-}\n-\n fn git_checkpoint_options_from_start(\n settings: &fabro_types::WorkflowSettings,\n run_id: &RunId,\n", + "summary": { + "files_changed": 97, + "additions": 2506, + "deletions": 1555 + } + } + }, + { + "seq": 0, + "checkpoint": { + "timestamp": "2026-05-23T05:43:25.432668Z", + "current_node": "verify", + "completed_nodes": [ + "start", + "toolchain", + "preflight_compile", + "preflight_lint", + "fix_lints", + "preflight_lint", + "implement", + "simplify_opus", + "simplify_gpt", + "verify" + ], + "node_retries": {}, "context_values": { "response.simplify_opus": "Done. Here's a summary of what was fixed:\n\n## High-impact refactors\n\n1. **Extracted duplicated `runtime_*_config` helpers** (~180 LOC dedup): moved `runtime_daytona_config`, `runtime_docker_config`, `duration_to_minutes_i32`, `size_to_gb_i32`, `resolve_interp` from both `fabro-workflow/operations/start.rs` and `fabro-server/run_manifest.rs` into a new `fabro-sandbox::from_environment` module. Both callers now share one implementation.\n\n2. **Replaced `apply_*_override` with `Combine`-based merge** (~95 LOC removed): wired up the previously-dead `RunEnvironmentLayer::into_environment_override` so `run_layer.into_environment_override().combine(base.clone())` replaces all five `apply_*_override` functions and their hand-rolled CIDR/extend logic.\n\n3. **Dropped duplicate `validate_daytona_snapshot_name` call**: the snapshot check was running twice for the run-selected environment. Now runs once.\n\n4. **Dropped dead `WorkflowSettings.environments` dense field**: only one test assertion read it; also removed `resolve_environments` and `Combine`-resolution plumbing.\n\n5. **Replaced stringly-typed `clone_sandbox_*_github_credentials(&str)` with `EnvironmentProvider::is_clone_based()`**: the enum already had this method.\n\n6. **Deduplicated demo `EnvironmentSettings` literal**: bound to a local in `demo/mod.rs::runs::settings()`.\n\n## Skipped (after weighing trade-offs)\n\n- **Flatten `RunEnvironmentSettings → { id, environment: EnvironmentSettings }`**: saves ~30 lines but adds an access-path hop on every field read.\n- **Type provider/mode as enums in sparse layers**: would break `workflow_settings_accumulate_multiple_run_errors`, which depends on resolve-time error accumulation across the provider field and `run.prepare`.\n- **Derive `Combine` on `EnvironmentNetworkLayer`**: would require touching the resolver in 3 places for negligible savings.\n\n## Verification\n\n- `cargo check --workspace --tests` — clean.\n- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings` — clean.\n- `cargo nextest run` on affected crates — 2194/2195 pass, 1 fail (pre-existing `[run.sandbox]` test fixture that the original implement stage left broken).\n- Compared full workspace failures vs origin: same 30 distinct test names fail on both branches (all `--sandbox`/old-fixture pre-existing failures); **0 net regressions** from the refactor.\n\nNet diff: **~360 lines removed** across 14 files.", "graph.goal": "# Named Environments Implementation Plan\n\n> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task.\n\n**Goal:** Replace run-scoped sandbox configuration with named, provider-explicit environments that runs can select by slug.\n\n**Architecture:** Add a shared top-level environment catalog, resolve a selected environment into the run's dense settings, validate provider capabilities, and convert the resolved environment into the existing sandbox runtime specs. Keep \"environment\" as reusable desired configuration and \"sandbox\" as the concrete runtime instance created for a run.\n\n**Tech Stack:** Rust config/types crates, TOML settings layers, Fabro workflow sandbox providers, OpenAPI-generated clients, public docs.\n\n---\n\n## Summary\n\nReplace run-scoped sandbox configuration with named, provider-explicit environments. A run selects an environment by slug via `[run.environment] id = \"...\"`; Fabro resolves the environment catalog through normal config precedence, applies run-level environment overrides, validates provider capabilities, freezes the resolved environment into the run settings, and creates a concrete sandbox instance from it.\n\nThis is a greenfield break: no `[run.sandbox]` compatibility layer, no server policy layer, and no required/optional volume semantics.\n\n## Key Interface Changes\n\n- Add top-level `[environments.]` to the shared settings schema. It is valid in `settings.toml`, `.fabro/project.toml`, and `workflow.toml`.\n- Replace sandbox selection with:\n\n```toml\n[run.environment]\nid = \"fabro-dev\"\n```\n\n- Allow sparse run-level overrides under the same table:\n\n```toml\n[run.environment.resources]\nmemory = \"32GB\"\n\n[run.environment.lifecycle]\npreserve = true\n```\n\n- Environment shape:\n\n```toml\n[environments.fabro-dev]\nprovider = \"daytona\" # local | docker | daytona\n\n[environments.fabro-dev.image]\nref = \"fabro-v11\" # Docker image or Daytona snapshot name\ndockerfile = { path = \"Dockerfile\" }\n\n[environments.fabro-dev.resources]\ncpu = 8\nmemory = \"16GB\"\ndisk = \"20GB\"\n\n[environments.fabro-dev.network]\nmode = \"block\" # allow_all | block | cidr_allow_list\nallow = [\"10.0.0.0/8\"]\n\n[environments.fabro-dev.lifecycle]\npreserve = false\nstop_on_terminal = true\nauto_stop = \"30m\"\n\n[environments.fabro-dev.labels]\nrepo = \"fabro-sh/fabro\"\n\n[[environments.fabro-dev.volumes]]\nid = \"vol-agent-state\"\nmount_path = \"/home/daytona/agent-state\"\nsubpath = \"auth\"\n\n[environments.fabro-dev.env]\nNODE_ENV = \"development\"\n```\n\n- Built-in default becomes:\n\n```toml\n[run.environment]\nid = \"default\"\n\n[environments.default]\nprovider = \"docker\"\n\n[environments.default.image]\nref = \"buildpack-deps:noble\"\n\n[environments.default.resources]\ncpu = 2\nmemory = \"4GB\"\n\n[environments.default.lifecycle]\npreserve = false\nstop_on_terminal = true\n```\n\n## Implementation Changes\n\n- Add environment sparse and dense types:\n - Sparse layer in `fabro-config` for `EnvironmentLayer`, `RunEnvironmentLayer`, image/resources/network/lifecycle/volume sublayers, and `[environments]` as a `MergeMap`.\n - Dense types in `fabro-types` for `EnvironmentSettings`, `RunEnvironmentSettings`, `EnvironmentProvider`, `EnvironmentNetworkMode`, and related subsettings.\n - Add `environments` to the top-level `SettingsLayer` and resolved `WorkflowSettings`; add selected `environment` to `RunNamespace`.\n- Resolve environments before run consumers use sandbox data:\n - Merge environment definitions by slug.\n - Resolve `[run.environment].id`; error if the slug is missing.\n - Overlay sparse `[run.environment.*]` fields onto the selected environment.\n - Validate provider is `local`, `docker`, or `daytona`.\n - Validate CIDRs with existing `ipnet`.\n - Store the selected resolved environment in `RunNamespace.environment`.\n- Replace sandbox runtime mapping:\n - Convert `RunNamespace.environment` to `SandboxSpec` in workflow start and server preflight paths.\n - Daytona: `image.ref` maps to snapshot name, `dockerfile` to snapshot Dockerfile, resources to snapshot sizing, network to Daytona policy, labels/volumes/env/lifecycle to existing provider fields.\n - Docker: `image.ref` maps to Docker image, `cpu` maps to `cpu_quota = cpu * 100000`, memory maps to memory limit, `network.mode = block` maps to `network_mode = none`, `allow_all` maps to default/bridge.\n - Local: use resolved working directory; env overlays process env as today.\n- Capability diagnostics:\n - Hard error for explicit security/isolation properties a provider cannot enforce:\n - local with `network.mode = block` or `cidr_allow_list`\n - docker with `network.mode = cidr_allow_list`\n - Warnings only for unsupported resource limits, volumes, labels, `auto_stop`, and Docker `image.dockerfile`.\n - If Daytona has `image.dockerfile` without `image.ref`, error because snapshot creation needs a name.\n- Remove old sandbox config surface:\n - Delete `[run.sandbox]` parsing/resolution/types from user-facing config.\n - Replace CLI/API/tool manifest args named `sandbox` with `environment` where they select execution profile.\n - Keep runtime/public \"sandbox\" terminology only for concrete instances, e.g. `fabro sandbox ssh`, `RunSandbox`, sandbox details.\n- Update docs and generated clients:\n - Update run configuration, environments, Daytona, server configuration, CLI reference, and OpenAPI spec.\n - Regenerate Rust API types/client and TypeScript API client after OpenAPI changes.\n\n## Test Plan\n\n- Config tests:\n - default resolves to `run.environment.id = \"default\"` and Docker environment settings.\n - project/workflow/run layers merge environment catalog by slug.\n - `[run.environment]` overrides selected environment fields.\n - `env` and `labels` merge by key; `volumes` replace wholesale.\n - missing environment slug errors.\n - old `[run.sandbox]` is rejected as an unknown field.\n- Provider mapping tests:\n - Daytona environment maps to snapshot/resources/network/labels/volumes/env.\n - Docker environment maps image, CPU, memory, network block, and env.\n - Local environment ignores non-security unsupported fields with warnings.\n- Validation tests:\n - docker plus CIDR allow-list errors.\n - local plus blocked network errors.\n - resource limits unsupported by provider produce warnings, not errors.\n - volumes unsupported by provider produce warnings, not errors.\n - Daytona dockerfile without image ref errors.\n- Integration/API tests:\n - run manifest with `[environments.]` and `[run.environment]` starts with the selected provider.\n - Dockerfile path bundling works from environment image config.\n - preflight reports capability warnings and security errors.\n - CLI/API `environment` override wins over config selection.\n\n## Assumptions\n\n- No compatibility behavior is required for `[run.sandbox]` or `--sandbox`.\n- No server-side environment policy or quota enforcement is in scope.\n- Volumes are simple provider hints; unsupported volume config warns and continues.\n- Resource limits are best-effort hints; unsupported resource fields warn and continue.\n- Provider names remain explicit for now: `local`, `docker`, and `daytona`.\n", @@ -1453,6 +1713,7 @@ "internal.retry_count.simplify_gpt": 0, "graph.rankdir": "LR", "internal.retry_count.implement": 0, + "thread.simplify_gpt.current_node": "verify", "thread.toolchain.current_node": "preflight_compile", "last_stage": "simplify_gpt", "internal.retry_count.preflight_lint": 0, @@ -1462,9 +1723,10 @@ "thread.simplify_opus.current_node": "simplify_gpt", "thread.fix_lints.current_node": "preflight_lint", "thread.implement.current_node": "simplify_opus", - "current_node": "simplify_gpt", + "internal.retry_count.verify": 0, + "current_node": "verify", "internal.run_id": "01KS9BXFGAZ32SGNRE4YJV1354", - "outcome": "succeeded", + "outcome": "failed", "internal.retry_count.fix_lints": 0, "response.implement": "Implemented named environments across the Rust config/types/runtime stack, CLI/API/tool manifest surfaces, OpenAPI, TypeScript client generation, and public docs.\n\n### What changed\n\n- Added top-level `[environments.]` catalog support and `[run.environment] id = \"...\"`.\n- Added sparse config layers and dense settings types for:\n - `EnvironmentSettings`\n - `RunEnvironmentSettings`\n - image/resources/network/lifecycle/labels/volumes/env\n - `EnvironmentProvider`\n - `EnvironmentNetworkMode`\n- Removed user-facing `[run.sandbox]` parsing/resolution/types.\n- Added defaults:\n - selected `run.environment.id = \"default\"`\n - Docker `default` environment\n - convenience built-in `local`, `docker`, and `daytona` named environments.\n- Implemented environment resolution:\n - slug lookup and missing slug errors\n - sparse run-level overrides\n - `env`/`labels` merge by key\n - `volumes` replace wholesale\n - CIDR validation\n - hard security capability errors\n- Replaced runtime mapping:\n - Docker image/CPU/memory/network/env mapping\n - Daytona snapshot/resources/network/labels/volumes/env/lifecycle mapping\n - Local environment env handling\n- Added preflight capability warnings for unsupported best-effort hints.\n- Renamed selection surfaces from `sandbox` to `environment` where they select an execution profile:\n - CLI `--environment`\n - server `--environment`\n - `ManifestArgs.environment`\n - Fabro tool create spec `environment`\n- Preserved runtime “sandbox” terminology for concrete instances and commands.\n- Updated OpenAPI schema and regenerated the TypeScript Axios client.\n- Updated core environment/run/server/Daytona/CLI docs and repo `.fabro` configs.\n\n### Validation run\n\nPassed:\n\n- `cargo check -q --workspace`\n- `cargo test -q --workspace --no-run`\n- `cargo test -q -p fabro-config`\n- `cargo test -q -p fabro-workflow runtime_docker_config_maps_environment_hints`\n- `cargo test -q -p fabro-server prepare_manifest_inlines_project_config_daytona_dockerfile_from_bundle`\n- `cargo test -q -p fabro-server prepare_manifest_errors_when_project_config_dockerfile_bundle_is_missing`\n- `cargo test -q -p fabro-tool`\n- `cargo test -q -p fabro-api`\n- `cargo +nightly-2026-04-14 fmt --check --all`\n- `cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings`\n- `cd lib/packages/fabro-api-client && bun run typecheck`\n\nNote: full `cargo test -q -p fabro-server` still has 3 order-dependent storage-related failures unrelated to this change; each of those failing tests passes when run individually.", "internal.retry_count.simplify_opus": 0, @@ -1472,14 +1734,14 @@ "internal.retry_count.start": 0, "last_response": "Implemented cleanup after the review pass.\n\nFixed:\n- Centralized environment env-var interpolation in `RunEnvironmentSettings::resolve_env(...)`.\n - Reused by Docker sandbox mapping and workflow star", "internal.fidelity": "compact", - "failure_class": "", + "failure_class": "transient_infra", "internal.work_dir": "/home/daytona/workspace/fabro", "internal.node_visit_count": 1, - "failure_signature": "", + "failure_signature": "verify|transient_infra|script failed with exit code: ## output image\": { │+ \"dockerfile\": null, │+ \"ref\": null │+ }, │+ \"labels\": {}, │+ \"lifecycle\": { │+ \"auto_stop\": null, │+ \"preserve\": false, │+ \"stop_on_terminal", "response.fix_lints": "Clippy passes cleanly now. Summary of fixes:\n\n1. **`lib/crates/fabro-sandbox/src/docker.rs`** — moved `use std::io::Read as _;` (with its `#[expect(clippy::disallowed_types, ...)]` attribute) to the top of `download_file_bytes`, before any statements, to satisfy `clippy::items_after_statements`.\n\n2. **`lib/crates/fabro-agent/src/apply_patch.rs`** — replaced four `std::fs::{write, read_to_string}` test calls (disallowed in Tokio paths) with `tokio::fs` equivalents, added `use tokio::fs;` to the test module, and rewrote the calls as `fs::…` to also satisfy `clippy::absolute_paths`.", - "internal.thread_id": "simplify_opus", + "internal.thread_id": "simplify_gpt", "internal.retry_count.preflight_compile": 0, - "command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126" + "command.output": "blob://sha256/5cb927d30e99b6b94025676483f2db91b0cc96126b3f0b4d92f806d37c2f938d" }, "node_outcomes": { "fix_lints": { @@ -1583,6 +1845,17 @@ "/home/daytona/workspace/fabro/lib/crates/fabro-workflow/src/operations/start.rs" ] }, + "verify": { + "status": "failed", + "context_updates": { + "command.output": "blob://sha256/5cb927d30e99b6b94025676483f2db91b0cc96126b3f0b4d92f806d37c2f938d" + }, + "failure": { + "message": "Script failed with exit code: 100\n\n## output\nimage\": {\n 162 │+ \"dockerfile\": null,\n 163 │+ \"ref\": null\n 164 │+ },\n 165 │+ \"labels\": {},\n 166 │+ \"lifecycle\": {\n 167 │+ \"auto_stop\": null,\n 168 │+ \"preserve\": false,\n 169 │+ \"stop_on_terminal\": true\n 170 │+ },\n 171 │+ \"network\": {\n 172 │+ \"allow\": [],\n 173 │+ \"mode\": \"allow_all\"\n 174 │+ },\n 175 │+ \"provider\": \"local\",\n 176 │+ \"resources\": {\n 177 │+ \"cpu\": null,\n 178 │+ \"disk\": null,\n 179 │+ \"memory\": null\n 180 │+ },\n 181 │+ \"volumes\": []\n 182 │+ },\n 158 183 │ \"execution\": {\n 159 184 │ \"approval\": \"prompt\",\n 160 185 │ \"mode\": \"normal\"\n 161 186 │ },\n ┈┈┈┈┈┈┈┈┈┈┈┈┼┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈\n 200 225 │ \"run_branch\": {\n 201 226 │ \"enabled\": true,\n 202 227 │ \"push\": true\n 203 228 │ },\n 204 │- \"sandbox\": {\n 205 │- \"daytona\": null,\n 206 │- \"devcontainer\": false,\n 207 │- \"docker\": {\n 208 │- \"cpu_quota\": 200000,\n 209 │- \"env_vars\": {},\n 210 │- \"image\": \"buildpack-deps:noble\",\n 211 │- \"memory_limit\": 4000000000,\n 212 │- \"network_mode\": null\n 213 │- },\n 214 │- \"env\": {},\n 215 │- \"preserve\": false,\n 216 │- \"provider\": \"local\",\n 217 │- \"stop_on_terminal\": true\n 218 │- },\n 219 229 │ \"scm\": {\n 220 230 │ \"github\": null,\n 221 231 │ \"owner\": null,\n 222 232 │ \"provider\": null,\n ────────────┴───────────────────────────────────────────────────────────────────\n To update snapshots run `cargo insta review`\n Stopped on the first failure. Run `cargo insta test` to run all snapshots.\n test cmd::attach::attach_json_errors_without_prompting_for_human_input ... FAILED\n\n failures:\n\n failures:\n cmd::attach::attach_json_errors_without_prompting_for_human_input\n\n test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 507 filtered out; finished in 2.79s\n\n stderr ───\n\n thread 'cmd::attach::attach_json_errors_without_prompting_for_human_input' (342202) panicked at /root/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/insta-1.46.3/src/runtime.rs:719:13:\n snapshot assertion for 'attach_json_errors_without_prompting_for_human_input' failed in line 776\n note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace\n\n────────────\n Summary [ 9.762s] 721/6133 tests run: 719 passed, 2 failed, 180 skipped\n FAIL [ 0.223s] ( 715/6133) fabro-cli::it cmd::config::create_explicit_workflow_path_uses_project_config_relative_to_workflow\n FAIL [ 2.836s] ( 718/6133) fabro-cli::it cmd::attach::attach_json_errors_without_prompting_for_human_input\nwarning: 5412/6133 tests were not run due to test failure (run with --no-fail-fast to run all tests, or run with --max-fail)\nerror: test run failed\n", + "category": "transient_infra" + }, + "usage": null + }, "preflight_compile": { "status": "succeeded", "context_updates": { @@ -1662,7 +1935,7 @@ ] } }, - "next_node_id": "verify", + "next_node_id": "fixup", "node_visits": { "start": 1, "simplify_gpt": 1, @@ -1671,7 +1944,8 @@ "fix_lints": 1, "implement": 1, "toolchain": 1, - "simplify_opus": 1 + "simplify_opus": 1, + "verify": 1 } }, "diff": {} @@ -1886,6 +2160,33 @@ }, "state": "succeeded" }, + "verify@1": { + "first_event_seq": 3218, + "prompt": null, + "response": null, + "completion": null, + "provider_used": null, + "diff": null, + "script_invocation": { + "script": "cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --cargo-quiet --workspace --status-level fail 2>&1 && cargo dev docs refresh 2>&1 && cargo dev docs check 2>&1", + "command": "exec 2>&1\ncargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --cargo-quiet --workspace --status-level fail 2>&1 && cargo dev docs refresh 2>&1 && cargo dev docs check 2>&1", + "language": "shell" + }, + "script_timing": null, + "parallel_results": null, + "output": null, + "started_at": "2026-05-23T05:40:58.128110Z", + "handler": "command", + "usage": { + "input_tokens": 0, + "output_tokens": 0, + "total_tokens": 0, + "reasoning_tokens": 0, + "cache_read_tokens": 0, + "cache_write_tokens": 0 + }, + "state": "running" + }, "toolchain@1": { "first_event_seq": 19, "prompt": null, @@ -2197,7 +2498,12 @@ "first_event_seq": 2947, "prompt": null, "response": null, - "completion": null, + "completion": { + "outcome": "succeeded", + "notes": "Stage completed: simplify_gpt", + "failure_reason": null, + "timestamp": "2026-05-23T05:40:53.443489Z" + }, "provider_used": { "mode": "agent", "provider": "openai", @@ -2210,6 +2516,12 @@ "output": null, "started_at": "2026-05-23T05:32:25.683142Z", "handler": "agent", + "timing": { + "wall_time_ms": 507760, + "inference_time_ms": 0, + "tool_time_ms": 0, + "active_time_ms": 0 + }, "usage": { "input_tokens": 93881, "output_tokens": 9043, @@ -2223,7 +2535,7 @@ "provider": "openai", "model_id": "gpt-5.5" }, - "state": "running" + "state": "succeeded" } } } \ No newline at end of file diff --git a/stages/009-simplify_gpt@1/diff.patch b/stages/009-simplify_gpt@1/diff.patch new file mode 100644 index 000000000..f69e40678 --- /dev/null +++ b/stages/009-simplify_gpt@1/diff.patch @@ -0,0 +1,740 @@ +diff --git a/lib/crates/fabro-cli/tests/it/cmd/attach.rs b/lib/crates/fabro-cli/tests/it/cmd/attach.rs +index 3c948c7e6..60e4e0e4b 100644 +--- a/lib/crates/fabro-cli/tests/it/cmd/attach.rs ++++ b/lib/crates/fabro-cli/tests/it/cmd/attach.rs +@@ -161,7 +161,7 @@ fn start_detached_human_run( + .args([ + "run", + "--detach", +- "--sandbox", ++ "--environment", + "local", + "--provider", + "openai", +@@ -391,7 +391,7 @@ fn attach_advances_when_pending_question_is_answered_elsewhere() { + .args([ + "run", + "--detach", +- "--sandbox", ++ "--environment", + "local", + "--provider", + "openai", +@@ -516,7 +516,7 @@ fn attach_before_completion_streams_to_finished_state() { + "--detach", + "--provider", + "openai", +- "--sandbox", ++ "--environment", + "local", + "slow.fabro", + ]); +@@ -637,7 +637,7 @@ fn attach_json_errors_without_prompting_for_human_input() { + .args([ + "run", + "--detach", +- "--sandbox", ++ "--environment", + "local", + "--provider", + "openai", +diff --git a/lib/crates/fabro-cli/tests/it/cmd/create.rs b/lib/crates/fabro-cli/tests/it/cmd/create.rs +index 8fb604aa6..f3eb32f87 100644 +--- a/lib/crates/fabro-cli/tests/it/cmd/create.rs ++++ b/lib/crates/fabro-cli/tests/it/cmd/create.rs +@@ -45,25 +45,25 @@ fn help() { + Path to a .fabro workflow file or .toml task config + + Options: +- --json Output as JSON [env: FABRO_JSON=] +- --server Fabro server target: http(s) URL or absolute Unix socket path [env: FABRO_SERVER=] +- --debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=] +- -I, --input Override a workflow input value (repeatable, format: KEY=VALUE) +- --dry-run Execute with simulated LLM backend +- --no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true] +- --auto-approve Auto-approve all human gates +- --quiet Suppress non-essential output [env: FABRO_QUIET=] +- --goal Override the workflow goal (available as {{ goal }} in prompts) +- --goal-file Read the workflow goal from a file +- --model Override default LLM model +- --provider Override default LLM provider +- -v, --verbose Enable verbose output +- --sandbox Sandbox for agent tools [possible values: local, docker, daytona] +- --label Attach a label to this run (repeatable, format: KEY=VALUE) +- --parent Link this run to an existing orchestration parent 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 ++ --json Output as JSON [env: FABRO_JSON=] ++ --server Fabro server target: http(s) URL or absolute Unix socket path [env: FABRO_SERVER=] ++ --debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=] ++ -I, --input Override a workflow input value (repeatable, format: KEY=VALUE) ++ --dry-run Execute with simulated LLM backend ++ --no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true] ++ --auto-approve Auto-approve all human gates ++ --quiet Suppress non-essential output [env: FABRO_QUIET=] ++ --goal Override the workflow goal (available as {{ goal }} in prompts) ++ --goal-file Read the workflow goal from a file ++ --model Override default LLM model ++ --provider Override default LLM provider ++ -v, --verbose Enable verbose output ++ --environment Named environment for agent tools ++ --label Attach a label to this run (repeatable, format: KEY=VALUE) ++ --parent Link this run to an existing orchestration parent 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 + ----- stderr ----- + "); + } +diff --git a/lib/crates/fabro-cli/tests/it/cmd/dump.rs b/lib/crates/fabro-cli/tests/it/cmd/dump.rs +index d7c26eb51..acdd34da6 100644 +--- a/lib/crates/fabro-cli/tests/it/cmd/dump.rs ++++ b/lib/crates/fabro-cli/tests/it/cmd/dump.rs +@@ -103,7 +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(), "--sandbox", "local"]); ++ run_cmd.args(["--run-id", run_id.as_str(), "--environment", "local"]); + run_cmd.arg(&workflow); + let run_output = run_cmd.output().expect("command should execute"); + assert!( +@@ -194,7 +194,7 @@ include = ["assets/**"] + run_cmd.args([ + "--run-id", + run_id.as_str(), +- "--sandbox", ++ "--environment", + "local", + "run.toml", + ]); +diff --git a/lib/crates/fabro-cli/tests/it/cmd/preflight.rs b/lib/crates/fabro-cli/tests/it/cmd/preflight.rs +index e90c1a9d6..8bb035f3a 100644 +--- a/lib/crates/fabro-cli/tests/it/cmd/preflight.rs ++++ b/lib/crates/fabro-cli/tests/it/cmd/preflight.rs +@@ -20,19 +20,19 @@ fn help() { + Path to a .fabro workflow file or .toml task config + + Options: +- --json Output as JSON [env: FABRO_JSON=] +- --server Fabro server target: http(s) URL or absolute Unix socket path [env: FABRO_SERVER=] +- --debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=] +- -I, --input Override a workflow input value (repeatable, format: KEY=VALUE) +- --goal Override the workflow goal (available as {{ goal }} in prompts) +- --no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true] +- --goal-file Read the workflow goal from a file +- --quiet Suppress non-essential output [env: FABRO_QUIET=] +- --model Override default LLM model +- --provider Override default LLM provider +- -v, --verbose Enable verbose output +- --sandbox Sandbox for agent tools [possible values: local, docker, daytona] +- -h, --help Print help ++ --json Output as JSON [env: FABRO_JSON=] ++ --server Fabro server target: http(s) URL or absolute Unix socket path [env: FABRO_SERVER=] ++ --debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=] ++ -I, --input Override a workflow input value (repeatable, format: KEY=VALUE) ++ --goal Override the workflow goal (available as {{ goal }} in prompts) ++ --no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true] ++ --goal-file Read the workflow goal from a file ++ --quiet Suppress non-essential output [env: FABRO_QUIET=] ++ --model Override default LLM model ++ --provider Override default LLM provider ++ -v, --verbose Enable verbose output ++ --environment Named environment for agent tools ++ -h, --help Print help + ----- stderr ----- + "); + } +diff --git a/lib/crates/fabro-cli/tests/it/cmd/run.rs b/lib/crates/fabro-cli/tests/it/cmd/run.rs +index 464c7cb95..1b50a6673 100644 +--- a/lib/crates/fabro-cli/tests/it/cmd/run.rs ++++ b/lib/crates/fabro-cli/tests/it/cmd/run.rs +@@ -123,25 +123,25 @@ fn help() { + Path to a .fabro workflow file or .toml task config + + Options: +- --json Output as JSON [env: FABRO_JSON=] +- --server Fabro server target: http(s) URL or absolute Unix socket path [env: FABRO_SERVER=] +- --debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=] +- -I, --input Override a workflow input value (repeatable, format: KEY=VALUE) +- --dry-run Execute with simulated LLM backend +- --no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true] +- --auto-approve Auto-approve all human gates +- --quiet Suppress non-essential output [env: FABRO_QUIET=] +- --goal Override the workflow goal (available as {{ goal }} in prompts) +- --goal-file Read the workflow goal from a file +- --model Override default LLM model +- --provider Override default LLM provider +- -v, --verbose Enable verbose output +- --sandbox Sandbox for agent tools [possible values: local, docker, daytona] +- --label Attach a label to this run (repeatable, format: KEY=VALUE) +- --parent Link this run to an existing orchestration parent 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 ++ --json Output as JSON [env: FABRO_JSON=] ++ --server Fabro server target: http(s) URL or absolute Unix socket path [env: FABRO_SERVER=] ++ --debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=] ++ -I, --input Override a workflow input value (repeatable, format: KEY=VALUE) ++ --dry-run Execute with simulated LLM backend ++ --no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true] ++ --auto-approve Auto-approve all human gates ++ --quiet Suppress non-essential output [env: FABRO_QUIET=] ++ --goal Override the workflow goal (available as {{ goal }} in prompts) ++ --goal-file Read the workflow goal from a file ++ --model Override default LLM model ++ --provider Override default LLM provider ++ -v, --verbose Enable verbose output ++ --environment Named environment for agent tools ++ --label Attach a label to this run (repeatable, format: KEY=VALUE) ++ --parent Link this run to an existing orchestration parent 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 + ----- stderr ----- + "); + } +@@ -412,7 +412,7 @@ digraph VaultWorkerLlm { + "--run-id", + run_id.as_str(), + "--auto-approve", +- "--sandbox", ++ "--environment", + "local", + "--provider", + "anthropic", +@@ -770,7 +770,7 @@ include = ["assets/**"] + "--run-id", + run_id.as_str(), + "--auto-approve", +- "--sandbox", ++ "--environment", + "local", + "--provider", + "openai", +@@ -899,7 +899,7 @@ fn dry_run_persists_event_history_in_store() { + "run", + "--dry-run", + "--auto-approve", +- "--sandbox", ++ "--environment", + "local", + "--run-id", + run_id.as_str(), +@@ -1041,7 +1041,7 @@ fn json_run_requires_manual_input_for_human_gates_without_auto_approve() { + .args([ + "--json", + "run", +- "--sandbox", ++ "--environment", + "local", + workflow.to_str().unwrap(), + ]) +diff --git a/lib/crates/fabro-cli/tests/it/cmd/runner.rs b/lib/crates/fabro-cli/tests/it/cmd/runner.rs +index f1d52274f..d7d87fb93 100644 +--- a/lib/crates/fabro-cli/tests/it/cmd/runner.rs ++++ b/lib/crates/fabro-cli/tests/it/cmd/runner.rs +@@ -481,7 +481,7 @@ methods = ["dev-token"] + run_id.as_str(), + "--detach", + "--auto-approve", +- "--sandbox", ++ "--environment", + "local", + workflow_path + .to_str() +@@ -734,7 +734,7 @@ fn detached_run_answers_pending_question_without_interview_scratch_files() { + "--detach", + "--run-id", + run_id.as_str(), +- "--sandbox", ++ "--environment", + "local", + workflow_path.to_str().unwrap(), + ]) +@@ -807,7 +807,7 @@ fn worker_exits_after_sigterm_cancel_even_when_stdin_stays_open() { + .args([ + "create", + "--auto-approve", +- "--sandbox", ++ "--environment", + "local", + "--run-id", + run_id.as_str(), +diff --git a/lib/crates/fabro-cli/tests/it/cmd/server_start.rs b/lib/crates/fabro-cli/tests/it/cmd/server_start.rs +index 86c928f4b..11ef4e073 100644 +--- a/lib/crates/fabro-cli/tests/it/cmd/server_start.rs ++++ b/lib/crates/fabro-cli/tests/it/cmd/server_start.rs +@@ -208,8 +208,8 @@ fn help() { + Override default LLM model + --provider + Override default LLM provider +- --sandbox +- Sandbox for agent tools ++ --environment ++ Named environment for agent tools + --max-concurrent-runs + Maximum number of concurrent run executions + --config +diff --git a/lib/crates/fabro-cli/tests/it/cmd/start.rs b/lib/crates/fabro-cli/tests/it/cmd/start.rs +index d52014e48..950a708b9 100644 +--- a/lib/crates/fabro-cli/tests/it/cmd/start.rs ++++ b/lib/crates/fabro-cli/tests/it/cmd/start.rs +@@ -143,7 +143,7 @@ fn start_rejects_already_active_or_completed_run() { + "create", + "--provider", + "openai", +- "--sandbox", ++ "--environment", + "local", + "slow.fabro", + ]); +@@ -204,7 +204,7 @@ fn start_runs_under_server_ownership_without_launcher_record() { + "create", + "--provider", + "openai", +- "--sandbox", ++ "--environment", + "local", + "owned-by-server.fabro", + ]) +diff --git a/lib/crates/fabro-cli/tests/it/cmd/support.rs b/lib/crates/fabro-cli/tests/it/cmd/support.rs +index e2ab6ef22..072c4bdba 100644 +--- a/lib/crates/fabro-cli/tests/it/cmd/support.rs ++++ b/lib/crates/fabro-cli/tests/it/cmd/support.rs +@@ -245,14 +245,14 @@ fn run_completed_dry_run(context: &TestContext, workflow: &Path) -> RunSetup { + run_id.as_str(), + "--dry-run", + "--auto-approve", +- "--sandbox", ++ "--environment", + "local", + ]); + cmd.arg(workflow); + let output = cmd.output().expect("command should execute"); + if !output.status.success() { + panic!( +- "command failed: fabro run --dry-run --auto-approve --sandbox local {}\nstdout:\n{}\nstderr:\n{}", ++ "command failed: fabro run --dry-run --auto-approve --environment local {}\nstdout:\n{}\nstderr:\n{}", + workflow.display(), + stdout(&output), + stderr(&output) +@@ -308,14 +308,14 @@ pub(crate) fn setup_detached_dry_run(context: &TestContext) -> RunSetup { + "--detach", + "--dry-run", + "--auto-approve", +- "--sandbox", ++ "--environment", + "local", + ]); + cmd.arg(workflow); + let output = cmd.output().expect("command should execute"); + if !output.status.success() { + panic!( +- "command failed: fabro run --detach --dry-run --auto-approve --sandbox local {}\nstdout:\n{}\nstderr:\n{}", ++ "command failed: fabro run --detach --dry-run --auto-approve --environment local {}\nstdout:\n{}\nstderr:\n{}", + fixture("simple.fabro").display(), + stdout(&output), + stderr(&output) +@@ -413,7 +413,7 @@ fn run_local_workflow(context: &TestContext, workspace_dir: &Path, workflow: &st + "--run-id", + run_id.as_str(), + "--auto-approve", +- "--sandbox", ++ "--environment", + "local", + "--provider", + "openai", +@@ -422,7 +422,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 --sandbox local --provider openai {workflow}\nstdout:\n{}\nstderr:\n{}", ++ "command failed: fabro run --auto-approve --environment local --provider openai {workflow}\nstdout:\n{}\nstderr:\n{}", + stdout(&output), + stderr(&output) + ); +diff --git a/lib/crates/fabro-cli/tests/it/cmd/worker_auth.rs b/lib/crates/fabro-cli/tests/it/cmd/worker_auth.rs +index 0793153d5..9f9d43921 100644 +--- a/lib/crates/fabro-cli/tests/it/cmd/worker_auth.rs ++++ b/lib/crates/fabro-cli/tests/it/cmd/worker_auth.rs +@@ -302,7 +302,7 @@ async fn github_only_server_dispatched_worker_succeeds_without_worker_auth_store + "--detach", + "--dry-run", + "--auto-approve", +- "--sandbox", ++ "--environment", + "local", + workflow.to_str().unwrap(), + ]) +@@ -358,7 +358,7 @@ fn runner_rejects_bogus_worker_token_against_github_only_server() { + &run_id, + "--dry-run", + "--auto-approve", +- "--sandbox", ++ "--environment", + "local", + workflow.to_str().unwrap(), + ]) +diff --git a/lib/crates/fabro-cli/tests/it/scenario/lifecycle.rs b/lib/crates/fabro-cli/tests/it/scenario/lifecycle.rs +index ee26e2fbf..846bcab2b 100644 +--- a/lib/crates/fabro-cli/tests/it/scenario/lifecycle.rs ++++ b/lib/crates/fabro-cli/tests/it/scenario/lifecycle.rs +@@ -30,7 +30,7 @@ fn local_run_lifecycle() { + .run_cmd() + .args([ + "--auto-approve", +- "--sandbox", ++ "--environment", + "local", + fixture("command_pipeline.fabro").to_str().unwrap(), + ]) +diff --git a/lib/crates/fabro-cli/tests/it/workflow/acp.rs b/lib/crates/fabro-cli/tests/it/workflow/acp.rs +index c24af9499..bf841a557 100644 +--- a/lib/crates/fabro-cli/tests/it/workflow/acp.rs ++++ b/lib/crates/fabro-cli/tests/it/workflow/acp.rs +@@ -40,7 +40,7 @@ fn acp_backend_workflow() { + + context + .run_cmd() +- .args(["--auto-approve", "--sandbox", "local"]) ++ .args(["--auto-approve", "--environment", "local"]) + .arg(&workflow) + .assert() + .success(); +@@ -119,7 +119,7 @@ fn acp_backend_does_not_inject_registered_provider_credentials() { + .env_remove("ANTHROPIC_API_KEY") + .env_remove("OPENAI_API_KEY") + .env_remove("GEMINI_API_KEY") +- .args(["--auto-approve", "--sandbox", "local"]) ++ .args(["--auto-approve", "--environment", "local"]) + .arg(&workflow) + .assert() + .success(); +@@ -193,7 +193,7 @@ include = ["verification-artifacts/**"] + + context + .run_cmd() +- .args(["--auto-approve", "--sandbox", "local"]) ++ .args(["--auto-approve", "--environment", "local"]) + .arg(context.temp_dir.join("run.toml")) + .assert() + .success(); +diff --git a/lib/crates/fabro-cli/tests/it/workflow/agent_linear.rs b/lib/crates/fabro-cli/tests/it/workflow/agent_linear.rs +index 175445f91..031d437a7 100644 +--- a/lib/crates/fabro-cli/tests/it/workflow/agent_linear.rs ++++ b/lib/crates/fabro-cli/tests/it/workflow/agent_linear.rs +@@ -13,7 +13,7 @@ fn scenario_agent_linear(sandbox: &str) { + .run_cmd() + .args([ + "--auto-approve", +- "--sandbox", ++ "--environment", + sandbox, + "--model", + "claude-haiku-4-5", +diff --git a/lib/crates/fabro-cli/tests/it/workflow/artifacts.rs b/lib/crates/fabro-cli/tests/it/workflow/artifacts.rs +index 45aabca92..71f57ade6 100644 +--- a/lib/crates/fabro-cli/tests/it/workflow/artifacts.rs ++++ b/lib/crates/fabro-cli/tests/it/workflow/artifacts.rs +@@ -49,7 +49,7 @@ include = ["assets/**"] + + context + .run_cmd() +- .args(["--auto-approve", "--sandbox", "local"]) ++ .args(["--auto-approve", "--environment", "local"]) + .arg(context.temp_dir.join("run.toml")) + .assert() + .success(); +diff --git a/lib/crates/fabro-cli/tests/it/workflow/command_agent_mixed.rs b/lib/crates/fabro-cli/tests/it/workflow/command_agent_mixed.rs +index 447ef05b4..b25b5b12d 100644 +--- a/lib/crates/fabro-cli/tests/it/workflow/command_agent_mixed.rs ++++ b/lib/crates/fabro-cli/tests/it/workflow/command_agent_mixed.rs +@@ -19,7 +19,7 @@ fn scenario_command_agent_mixed(sandbox: &str) { + .run_cmd() + .args([ + "--auto-approve", +- "--sandbox", ++ "--environment", + sandbox, + "--model", + "claude-haiku-4-5", +diff --git a/lib/crates/fabro-cli/tests/it/workflow/command_pipeline.rs b/lib/crates/fabro-cli/tests/it/workflow/command_pipeline.rs +index 2f0d4c94e..21a7a7da2 100644 +--- a/lib/crates/fabro-cli/tests/it/workflow/command_pipeline.rs ++++ b/lib/crates/fabro-cli/tests/it/workflow/command_pipeline.rs +@@ -23,7 +23,7 @@ fn scenario_command_pipeline(sandbox: &str) { + + context + .run_cmd() +- .args(["--auto-approve", "--sandbox", sandbox]) ++ .args(["--auto-approve", "--environment", sandbox]) + .arg(fixture("command_pipeline.fabro")) + .timeout(timeout_for(sandbox)) + .assert() +diff --git a/lib/crates/fabro-cli/tests/it/workflow/conditional_branching.rs b/lib/crates/fabro-cli/tests/it/workflow/conditional_branching.rs +index 288c2ca6b..c8f703edf 100644 +--- a/lib/crates/fabro-cli/tests/it/workflow/conditional_branching.rs ++++ b/lib/crates/fabro-cli/tests/it/workflow/conditional_branching.rs +@@ -9,7 +9,7 @@ fn scenario_conditional_branching(sandbox: &str) { + + context + .run_cmd() +- .args(["--auto-approve", "--sandbox", sandbox]) ++ .args(["--auto-approve", "--environment", sandbox]) + .arg(fixture("conditional_branching.fabro")) + .timeout(timeout_for(sandbox)) + .assert() +diff --git a/lib/crates/fabro-cli/tests/it/workflow/full_stack.rs b/lib/crates/fabro-cli/tests/it/workflow/full_stack.rs +index e70dea83a..60a95f5f1 100644 +--- a/lib/crates/fabro-cli/tests/it/workflow/full_stack.rs ++++ b/lib/crates/fabro-cli/tests/it/workflow/full_stack.rs +@@ -19,7 +19,7 @@ fn scenario_full_stack(sandbox: &str) { + .run_cmd() + .args([ + "--auto-approve", +- "--sandbox", ++ "--environment", + sandbox, + "--model", + "claude-haiku-4-5", +diff --git a/lib/crates/fabro-cli/tests/it/workflow/hooks.rs b/lib/crates/fabro-cli/tests/it/workflow/hooks.rs +index 3561b254c..03e71e29d 100644 +--- a/lib/crates/fabro-cli/tests/it/workflow/hooks.rs ++++ b/lib/crates/fabro-cli/tests/it/workflow/hooks.rs +@@ -120,7 +120,7 @@ fn configure_hook_env(cmd: &mut assert_cmd::Command, hook_model: &str) { + if TestMode::from_env().is_twin() { + cmd.env_remove("ANTHROPIC_API_KEY"); + } +- cmd.arg("--sandbox").arg("local"); ++ cmd.arg("--environment").arg("local"); + cmd.arg("--auto-approve"); + cmd.arg("--provider").arg(stage_provider()); + cmd.arg("--model").arg(hook_model); +diff --git a/lib/crates/fabro-cli/tests/it/workflow/human_gate.rs b/lib/crates/fabro-cli/tests/it/workflow/human_gate.rs +index 48f649cbf..0f83199e9 100644 +--- a/lib/crates/fabro-cli/tests/it/workflow/human_gate.rs ++++ b/lib/crates/fabro-cli/tests/it/workflow/human_gate.rs +@@ -11,7 +11,7 @@ fn scenario_human_gate(sandbox: &str) { + .run_cmd() + .args([ + "--auto-approve", +- "--sandbox", ++ "--environment", + sandbox, + "--model", + "claude-haiku-4-5", +diff --git a/lib/crates/fabro-config/src/builders.rs b/lib/crates/fabro-config/src/builders.rs +index d85a16bb1..80017af26 100644 +--- a/lib/crates/fabro-config/src/builders.rs ++++ b/lib/crates/fabro-config/src/builders.rs +@@ -10,8 +10,7 @@ use fabro_util::error::SharedError; + use crate::defaults::DEFAULTS_LAYER; + use crate::load::load_settings_path; + use crate::resolve::{ +- ResolveError, resolve_cli, resolve_project, resolve_run, resolve_server, +- resolve_workflow, ++ ResolveError, resolve_cli, resolve_project, resolve_run, resolve_server, resolve_workflow, + }; + use crate::user::load_settings_config; + use crate::{ +diff --git a/lib/crates/fabro-config/src/resolve/environment.rs b/lib/crates/fabro-config/src/resolve/environment.rs +index 250f4f3ff..7706254d8 100644 +--- a/lib/crates/fabro-config/src/resolve/environment.rs ++++ b/lib/crates/fabro-config/src/resolve/environment.rs +@@ -33,7 +33,10 @@ pub(crate) fn resolve_run_environment( + return RunEnvironmentSettings::from_environment(id, EnvironmentSettings::default()); + }; + +- let merged = layer.clone().into_environment_override().combine(base.clone()); ++ let merged = layer ++ .clone() ++ .into_environment_override() ++ .combine(base.clone()); + let environment = resolve_environment_layer(&merged, "run.environment", errors); + validate_provider_capabilities(&environment, "run.environment", errors); + RunEnvironmentSettings::from_environment(id, environment) +diff --git a/lib/crates/fabro-sandbox/src/from_environment.rs b/lib/crates/fabro-sandbox/src/from_environment.rs +index c59fb5044..811ff620c 100644 +--- a/lib/crates/fabro-sandbox/src/from_environment.rs ++++ b/lib/crates/fabro-sandbox/src/from_environment.rs +@@ -3,8 +3,6 @@ + //! These mappings are consumed by both the workflow run-start path and the + //! server preflight path, so they live here next to their destination types. + +-#[cfg(feature = "docker")] +-use fabro_types::settings::interp::InterpString; + #[cfg(feature = "daytona")] + use fabro_types::settings::run::DockerfileSource as ResolvedDockerfileSource; + use fabro_types::settings::run::{EnvironmentNetworkMode, RunEnvironmentSettings}; +@@ -55,16 +53,18 @@ pub fn daytona_config_from_environment( + .resources + .disk + .map(|size| size_to_gb_i32(size.as_bytes())), +- dockerfile: settings.image.dockerfile.as_ref().map(|dockerfile| { +- match dockerfile { ++ dockerfile: settings ++ .image ++ .dockerfile ++ .as_ref() ++ .map(|dockerfile| match dockerfile { + ResolvedDockerfileSource::Inline(text) => { + SandboxDockerfileSource::Inline(text.clone()) + } + ResolvedDockerfileSource::Path { path } => { + SandboxDockerfileSource::Path { path: path.clone() } + } +- } +- }), ++ }), + }), + network: Some(match settings.network.mode { + EnvironmentNetworkMode::Block => DaytonaNetwork::Block, +@@ -84,9 +84,9 @@ pub fn docker_config_from_environment( + skip_clone: bool, + ) -> DockerSandboxOptions { + let mut env_vars = settings +- .env +- .iter() +- .map(|(key, value)| format!("{key}={}", resolve_interp(value))) ++ .resolve_env(process_env_var) ++ .into_iter() ++ .map(|(key, value)| format!("{key}={value}")) + .collect::>(); + env_vars.sort(); + let default_options = DockerSandboxOptions::default(); +@@ -117,13 +117,6 @@ pub fn docker_config_from_environment( + } + } + +-#[cfg(feature = "docker")] +-fn resolve_interp(value: &InterpString) -> String { +- value +- .resolve(process_env_var) +- .map_or_else(|_| value.as_source(), |resolved| resolved.value) +-} +- + #[cfg(feature = "docker")] + #[expect( + clippy::disallowed_methods, +diff --git a/lib/crates/fabro-types/src/settings/run.rs b/lib/crates/fabro-types/src/settings/run.rs +index 7b3c3450c..b9d0e8611 100644 +--- a/lib/crates/fabro-types/src/settings/run.rs ++++ b/lib/crates/fabro-types/src/settings/run.rs +@@ -2,7 +2,7 @@ + //! + //! `[run]` is the shared execution domain. It may appear in all three config + //! files and layer normally. Subdomains cover model selection, git author, +-//! prepare steps, execution posture, checkpoint policy, sandbox selection, ++//! prepare steps, execution posture, checkpoint policy, environment selection, + //! notifications, interviews, agent knobs, hooks, SCM targeting, pull-request + //! behavior, and artifact collection. + +@@ -468,6 +468,24 @@ impl RunEnvironmentSettings { + env: environment.env, + } + } ++ ++ /// Resolve every environment value's `{{ env.* }}` tokens via `lookup`, ++ /// falling back to the original source string when resolution fails. ++ #[must_use] ++ pub fn resolve_env(&self, mut lookup: F) -> HashMap ++ where ++ F: FnMut(&str) -> Option, ++ { ++ self.env ++ .iter() ++ .map(|(name, value)| { ++ let resolved = value ++ .resolve(&mut lookup) ++ .map_or_else(|_| value.as_source(), |resolved| resolved.value); ++ (name.clone(), resolved) ++ }) ++ .collect() ++ } + } + + impl Default for RunEnvironmentSettings { +@@ -476,6 +494,50 @@ impl Default for RunEnvironmentSettings { + } + } + ++#[cfg(test)] ++mod run_environment_settings_tests { ++ use super::{HashMap, InterpString, RunEnvironmentSettings}; ++ ++ fn settings(env: &[(&str, &str)]) -> RunEnvironmentSettings { ++ RunEnvironmentSettings { ++ env: env ++ .iter() ++ .map(|(k, v)| ((*k).to_string(), InterpString::parse(v))) ++ .collect(), ++ ..RunEnvironmentSettings::default() ++ } ++ } ++ ++ #[test] ++ fn resolve_env_substitutes_env_tokens_via_lookup() { ++ let s = settings(&[("NODE_ENV", "{{ env.NODE_ENV }}"), ("STATIC", "value")]); ++ let resolved = s.resolve_env(|name| match name { ++ "NODE_ENV" => Some("test".to_string()), ++ _ => None, ++ }); ++ ++ assert_eq!(resolved.get("NODE_ENV"), Some(&"test".to_string())); ++ assert_eq!(resolved.get("STATIC"), Some(&"value".to_string())); ++ } ++ ++ #[test] ++ fn resolve_env_falls_back_to_source_when_lookup_fails() { ++ let s = settings(&[("NODE_ENV", "{{ env.MISSING_NODE_ENV }}")]); ++ let resolved = s.resolve_env(|_| None); ++ ++ assert_eq!( ++ resolved.get("NODE_ENV"), ++ Some(&"{{ env.MISSING_NODE_ENV }}".to_string()) ++ ); ++ } ++ ++ #[test] ++ fn resolve_env_is_empty_for_empty_settings() { ++ let s: HashMap = settings(&[]).resolve_env(|_| None); ++ assert!(s.is_empty()); ++ } ++} ++ + #[derive(Debug, Clone, PartialEq)] + pub enum DockerfileSource { + Inline(String), +diff --git a/lib/crates/fabro-workflow/src/operations/start.rs b/lib/crates/fabro-workflow/src/operations/start.rs +index f99f317ec..f8d53e85f 100644 +--- a/lib/crates/fabro-workflow/src/operations/start.rs ++++ b/lib/crates/fabro-workflow/src/operations/start.rs +@@ -363,12 +363,7 @@ impl RunSession { + } + }; + +- let toml_env: HashMap = resolved +- .environment +- .env +- .iter() +- .map(|(k, v)| (k.clone(), resolve_interp(v))) +- .collect(); ++ let toml_env = resolved.environment.resolve_env(process_env_var); + let github_permissions: Option> = + (!services.github_permissions.is_empty()).then(|| services.github_permissions.clone()); + let sandbox_env = SandboxEnvSpec { +@@ -459,12 +454,6 @@ async fn configured_providers_for_start( + } + } + +-fn resolve_interp(value: &InterpString) -> String { +- value +- .resolve(process_env_var) +- .map_or_else(|_| value.as_source(), |resolved| resolved.value) +-} +- + fn git_checkpoint_options_from_start( + settings: &fabro_types::WorkflowSettings, + run_id: &RunId, diff --git a/stages/009-simplify_gpt@1/status.json b/stages/009-simplify_gpt@1/status.json new file mode 100644 index 000000000..7bcf0bf6f --- /dev/null +++ b/stages/009-simplify_gpt@1/status.json @@ -0,0 +1,6 @@ +{ + "outcome": "succeeded", + "notes": "Stage completed: simplify_gpt", + "failure_reason": null, + "timestamp": "2026-05-23T05:40:53.443489Z" +} \ No newline at end of file diff --git a/stages/010-verify@1/script_invocation.json b/stages/010-verify@1/script_invocation.json new file mode 100644 index 000000000..9eb3c36be --- /dev/null +++ b/stages/010-verify@1/script_invocation.json @@ -0,0 +1,5 @@ +{ + "script": "cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --cargo-quiet --workspace --status-level fail 2>&1 && cargo dev docs refresh 2>&1 && cargo dev docs check 2>&1", + "command": "exec 2>&1\ncargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --cargo-quiet --workspace --status-level fail 2>&1 && cargo dev docs refresh 2>&1 && cargo dev docs check 2>&1", + "language": "shell" +} \ No newline at end of file