diff --git a/.claude/skills/changelog/watermark b/.claude/skills/changelog/watermark index 20393229f..3d0a9c1ae 100644 --- a/.claude/skills/changelog/watermark +++ b/.claude/skills/changelog/watermark @@ -1 +1 @@ -485f2a5718b6d983520aa5cccd39128d99bad7ae +15a56263f52992592d97b904914626a366eee0d8 diff --git a/.claude/skills/docs/watermark b/.claude/skills/docs/watermark index 20393229f..3d0a9c1ae 100644 --- a/.claude/skills/docs/watermark +++ b/.claude/skills/docs/watermark @@ -1 +1 @@ -485f2a5718b6d983520aa5cccd39128d99bad7ae +15a56263f52992592d97b904914626a366eee0d8 diff --git a/docs/changelog/2026-03-10.mdx b/docs/changelog/2026-03-10.mdx index ecedc3770..cb848b290 100644 --- a/docs/changelog/2026-03-10.mdx +++ b/docs/changelog/2026-03-10.mdx @@ -1,8 +1,19 @@ --- -title: "MCP servers in workflows, arc init, and arc diff" +title: "One-line installer, MCP servers, and devcontainer support" date: "2026-03-10" --- +## One-line installer and guided setup wizard + +Getting Arc installed and configured previously required multiple manual steps — downloading binaries, setting API keys, and configuring GitHub integration individually. Now, a single command installs Arc and walks you through everything: + +```bash +curl -fsSL https://raw.githubusercontent.com/brynary/arc/main/install.sh | bash +arc install +``` + +The installer detects your platform, installs to `~/.arc/bin`, and auto-configures your shell PATH. The `arc install` wizard then guides you through LLM provider setup with immediate API key validation, OpenAI OAuth for Codex compatibility, and GitHub App creation via manifest flow — all with styled terminal output showing progress at each step. + ## MCP servers in workflows Agents running inside sandboxes can now access MCP tools during workflow runs. A new `sandbox` transport type starts an MCP server inside the Daytona sandbox, waits for it to listen, and connects automatically — no host-side setup required. This means agents can use tools like Playwright for browser automation, or any other MCP server, directly from within their sandbox environment. @@ -14,39 +25,50 @@ command = ["npx", "@playwright/mcp@latest", "--port", "3100", "--headless"] port = 3100 ``` -## `arc init` — initialize a new project +## Devcontainer support in sandboxes -Getting started with Arc previously meant creating `arc.toml` and workflow files by hand. Now, `arc init` scaffolds a project with a generated `arc.toml` that includes comments and a link to the docs. +Workflows can now use your project's `devcontainer.json` to configure sandbox environments. When `devcontainer = true` is set in the sandbox config, Arc resolves the devcontainer from the repo, uses its Dockerfile for the Daytona snapshot, runs lifecycle hooks (`onCreateCommand`, `postCreateCommand`, `postStartCommand`), and merges devcontainer environment variables into the sandbox. Unsupported `COPY`/`ADD` instructions in base Dockerfiles are detected and reported. -```bash -arc init +```toml title="workflow.toml" +[sandbox] +devcontainer = true ``` -## `arc diff` — view workflow run diffs - -You can now inspect the code changes from any workflow run without digging through log files. `arc diff` surfaces the final patch and per-node diffs from completed runs. For in-progress runs, it reconnects to the sandbox to show a live diff. - -```bash -arc diff -``` + +**`--no-dotenv` flag removed.** Arc now always loads `~/.arc/.env` and never loads a local `./.env` file. If you were relying on project-local `.env` files, move those values to `~/.arc/.env`. + ## More + +- New signoff resource in the verification system — stamps of approval for (control, repository, commit SHA) tuples via `POST /verification/signoffs` + + +- Added `arc pr list/view/merge/close` subcommands for managing pull requests created by workflow runs +- Added `arc init` command to scaffold new projects with a generated `arc.toml` +- Added `arc diff` command to view code changes from workflow runs, with live diffs for in-progress runs - `arc setup` renamed to `arc install` for clarity - Added `arc ssh` command for direct SSH access to Daytona sandboxes - Added `arc preview` command to get preview URLs for ports exposed by Daytona sandboxes - `arc doctor` now runs live service probes by default; use `--dry-run` to skip - `arc doctor` now validates GitHub App configuration and private key +- `arc doctor` now hides unconfigured LLM providers for a cleaner output +- `arc doctor` sections reordered: Config, LLM, GitHub App, Cloud sandbox, Brave Search +- `arc run --preflight` now shows a Repository section, merged LLM check, and truncated output - Added `[pull_request]` support to project config (`arc.toml`) for project-level PR defaults - Per-run data moved from `~/.arc/logs/` to `~/.arc/runs/`; daily CLI logs stay in `~/.arc/logs/` - Per-run tracing logs now written to `cli.log` inside each run directory - Asset paths now display full `~/...` paths after workflow run output - Hidden `arc ps`, `llm`, `exec`, and `parse` from CLI help output - PR body now includes Arc Details section even when retro is skipped +- Bumped version to 0.2.0 +- Fixed Daytona sandbox clone failing when the worktree branch hadn't been pushed to the remote +- Fixed "Git clean: false" showing incorrectly for remote sandboxes by checking both clean and pushed status - Fixed LLM provider resolution to check the model catalog before falling back to the default provider -- Fixed verbose mode breaking terminal rendering after many tool calls +- Fixed `install.sh` prompt not showing when piped via stdin +- Fixed race condition in git checkpoint push operations diff --git a/docs/changelog/2026-03-11.mdx b/docs/changelog/2026-03-11.mdx new file mode 100644 index 000000000..93dc2f9d0 --- /dev/null +++ b/docs/changelog/2026-03-11.mdx @@ -0,0 +1,27 @@ +--- +title: "Shadow branch run records and arc diff improvements" +date: "2026-03-11" +--- + +## Shadow branch as a complete run record + +The shadow branch now stores the full execution trace for each workflow run — prompts, responses, status files, diffs, and retro data per node — alongside the existing checkpoint and manifest data. This makes the shadow branch a self-contained, git-native record of everything that happened during a run, browsable with standard git tools. + +## More + + +- Added `devcontainer` field to `SandboxConfiguration` OpenAPI schema + + + +- Added `--shortstat` flag to `arc diff` for compact summary output +- `arc diff` now includes uncommitted changes for in-progress runs, not just committed ones + + + +- Added `gemini-3.1-pro-preview-customtools` to model catalog +- Parallel fan-out branches now render like subagent info lines in the progress UI +- Tool call paths now display relative to the sandbox working directory +- Remote sandbox runs skip unnecessary local worktree creation +- Added `last_file_touched` as a third fallback for status extraction + diff --git a/docs/core-concepts/models.mdx b/docs/core-concepts/models.mdx index 38485da65..75fed73cb 100644 --- a/docs/core-concepts/models.mdx +++ b/docs/core-concepts/models.mdx @@ -24,6 +24,7 @@ No single model is best at everything. Arc lets you assign the right model to ea | `gpt-5.4` | openai | `gpt54` | 1M | $2.50 / $15.00 | 70 tok/s | | `gpt-5.4-pro` | openai | `gpt54-pro` | 1M | $30.00 / $180.00 | 20 tok/s | | `gemini-3.1-pro-preview` | gemini | `gemini-pro` | 1M | $2.00 / $12.00 | 85 tok/s | +| `gemini-3.1-pro-preview-customtools` | gemini | `gemini-customtools` | 1M | $2.00 / $12.00 | 85 tok/s | | `gemini-3-flash-preview` | gemini | `gemini-flash` | 1M | $0.50 / $3.00 | 150 tok/s | | `gemini-3.1-flash-lite-preview` | gemini | `gemini-flash-lite` | 1M | $0.20 / $1.50 | 200 tok/s | | `kimi-k2.5` | kimi | `kimi` | 262K | $0.60 / $3.00 | 50 tok/s | diff --git a/docs/docs.json b/docs/docs.json index 26495145b..fe4ce4a6f 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -268,6 +268,7 @@ "group": "March 2026", "icon": "clock-rotate-left", "pages": [ + "changelog/2026-03-11", "changelog/2026-03-10", "changelog/2026-03-09", "changelog/2026-03-08", diff --git a/docs/execution/checkpoints.mdx b/docs/execution/checkpoints.mdx index ee7cd8716..a83c9a930 100644 --- a/docs/execution/checkpoints.mdx +++ b/docs/execution/checkpoints.mdx @@ -50,6 +50,7 @@ After each node, the metadata branch is updated with: - **`checkpoint.json`** — Full execution state (see below) - **`artifacts/*.json`** — Any offloaded artifact data (large context values over 100KB) +- **`nodes/{node_id}/`** — Per-node execution trace files (prompts, responses, status, diffs — files under 512KB from an allowlist) ## What's in a checkpoint diff --git a/docs/execution/devcontainers.mdx b/docs/execution/devcontainers.mdx index 6b40cc874..c9de9f3e0 100644 --- a/docs/execution/devcontainers.mdx +++ b/docs/execution/devcontainers.mdx @@ -3,6 +3,41 @@ title: "Devcontainers" description: "Run Arc workflows inside development containers" --- - -Devcontainer support is **under development** and not yet available for use. This page will be updated when the feature ships. - +Arc can use your project's [devcontainer](https://containers.dev/) configuration to set up sandbox environments. When enabled, Arc resolves `devcontainer.json` from the repository, uses its Dockerfile to build the Daytona sandbox snapshot, runs lifecycle hooks inside the sandbox, and merges devcontainer environment variables into the sandbox environment. + +## Enabling devcontainer support + +Set `devcontainer = true` in the `[sandbox]` section of your run config: + +```toml title="run.toml" +version = 1 +graph = "workflow.dot" + +[sandbox] +provider = "daytona" +devcontainer = true +``` + +Arc looks for `.devcontainer/devcontainer.json` in the repository root. If found, it extracts the Dockerfile, lifecycle commands, and environment variables from the configuration. + +## What Arc uses from devcontainer.json + +| Field | How Arc uses it | +|---|---| +| `build.dockerfile` | Used as the Dockerfile for the Daytona sandbox snapshot. A deterministic snapshot name is generated from a hash of the Dockerfile content. | +| `onCreateCommand` | Runs inside the sandbox after it's created | +| `postCreateCommand` | Runs after `onCreateCommand` completes | +| `postStartCommand` | Runs after the sandbox starts | +| `containerEnv` | Merged into sandbox environment variables (TOML `[sandbox.env]` values take precedence on key collisions) | + +Lifecycle commands (`onCreateCommand`, `postCreateCommand`, `postStartCommand`) execute sequentially inside the sandbox. If any command fails, the run aborts before the workflow starts. + +## Dockerfile limitations + +Arc detects and reports unsupported `COPY` and `ADD` instructions in devcontainer base Dockerfiles. These instructions reference files from the build context, which isn't available when building Daytona snapshots. If your Dockerfile uses `COPY` or `ADD`, you'll need to restructure it to use `RUN` commands that fetch files at build time (e.g., via `curl` or `wget`). + +## Interaction with other sandbox settings + +When `devcontainer = true`, the devcontainer Dockerfile overrides any `snapshot.dockerfile` setting in `[sandbox.daytona]`. Other Daytona settings (`cpu`, `memory`, `disk`, `auto_stop_interval`, `labels`) still apply. + +Environment variables from `containerEnv` in the devcontainer config are merged with `[sandbox.env]` from the TOML config. On key collisions, the TOML config wins. diff --git a/docs/execution/run-configuration.mdx b/docs/execution/run-configuration.mdx index bb8e84c1a..9674626cb 100644 --- a/docs/execution/run-configuration.mdx +++ b/docs/execution/run-configuration.mdx @@ -157,6 +157,7 @@ preserve = true |---|---| | `provider` | Sandbox mode: `local` (default), `docker`, `daytona`, or `exe`. | | `preserve` | When `true`, keep the sandbox alive after the run finishes. Useful for debugging. | +| `devcontainer` | When `true`, use the repo's `devcontainer.json` to configure the sandbox. See [Devcontainers](/execution/devcontainers). | #### `[sandbox.daytona]` diff --git a/docs/reference/cli.mdx b/docs/reference/cli.mdx index 8e0540a68..2e7337437 100644 --- a/docs/reference/cli.mdx +++ b/docs/reference/cli.mdx @@ -252,9 +252,13 @@ arc cp -r :/output ./results # recursive download Run IDs support prefix matching — you can use the first few characters instead of the full ID. -## `arc pr create` +## `arc pr` -Create a GitHub pull request from a completed workflow run. Uses the run's persisted manifest, conclusion, and diff. Requires a [GitHub App](/integrations/github) to be configured. +Manage GitHub pull requests created by workflow runs. Requires a [GitHub App](/integrations/github) to be configured. + +### `arc pr create` + +Create a GitHub pull request from a completed workflow run. Uses the run's persisted manifest, conclusion, and diff. ```bash arc pr create @@ -268,6 +272,57 @@ arc pr create --model claude-opus-4-6 The run must have completed successfully (or with partial success) and have a `final.patch` with changes. +### `arc pr list` + +List all pull requests created by workflow runs. Shows PR number, title, status, and the associated run ID. + +```bash +arc pr list +arc pr list --all +``` + +| Flag | Description | +|---|---| +| `--all` | Include closed and merged PRs (default: open only) | + +### `arc pr view` + +Show details of a pull request from a specific run. + +```bash +arc pr view +``` + +| Argument | Description | +|---|---| +| `` | Run ID or prefix (required) | + +### `arc pr merge` + +Merge a pull request from a specific run. + +```bash +arc pr merge +arc pr merge --method rebase +``` + +| Argument / Flag | Description | +|---|---| +| `` | Run ID or prefix (required) | +| `--method ` | Merge method: `squash` (default), `merge`, or `rebase` | + +### `arc pr close` + +Close a pull request from a specific run without merging. + +```bash +arc pr close +``` + +| Argument | Description | +|---|---| +| `` | Run ID or prefix (required) | + --- ## `arc system df` @@ -338,6 +393,7 @@ arc diff --stat | `` | Run ID or prefix (required) | | `--node ` | Show diff for a specific node instead of the full run | | `--stat` | Show diffstat instead of full patch (live diffs only) | +| `--shortstat` | Show only files-changed/insertions/deletions summary (live diffs only) | Output is colorized when writing to a terminal. diff --git a/lib/crates/arc-workflows/src/cli/run.rs b/lib/crates/arc-workflows/src/cli/run.rs index 75286fe23..db5ef75c6 100644 --- a/lib/crates/arc-workflows/src/cli/run.rs +++ b/lib/crates/arc-workflows/src/cli/run.rs @@ -1911,11 +1911,6 @@ async fn run_preflight( CheckDetail::new(format!("Nodes: {}", graph.nodes.len())), CheckDetail::new(format!("Edges: {}", graph.edges.len())), CheckDetail::new(format!("Goal: {}", graph.goal())), - CheckDetail::new(format!("Model: {model}")), - CheckDetail::new(format!( - "Provider: {}", - provider.as_deref().unwrap_or("anthropic") - )), ], remediation: None, }); @@ -2006,62 +2001,85 @@ async fn run_preflight( }); } - // 4. LLM check (merged providers + provider parse) - let resolved_provider = provider.as_deref().unwrap_or("anthropic"); + // 4. Per-model LLM checks + let default_provider = provider.as_deref().unwrap_or("anthropic"); let llm_ok = match arc_llm::client::Client::from_env().await { Ok(c) => { - let names: Vec = c.provider_names().iter().map(|s| s.to_string()).collect(); - if names.is_empty() { - checks.push(CheckResult { - name: "LLM".into(), - status: CheckStatus::Error, - summary: "no API keys".into(), - details: vec![], - remediation: Some("Set at least one LLM provider API key".into()), - }); - false - } else { - match resolved_provider.parse::() { + let configured: Vec = + c.provider_names().iter().map(|s| s.to_string()).collect(); + + // Collect all distinct (model, provider) pairs from LLM nodes + let mut model_providers = std::collections::BTreeSet::new(); + for node in graph.nodes.values() { + if !crate::graph::types::is_llm_handler_type(node.handler_type()) { + continue; + } + let node_model = node.llm_model().unwrap_or(&model); + let node_provider = node.llm_provider().unwrap_or(default_provider); + + // Resolve through catalog to get canonical model ID and provider + let (resolved_model, resolved_provider) = + if let Some(info) = arc_llm::catalog::get_model_info(node_model) { + (info.id, info.provider) + } else { + (node_model.to_string(), node_provider.to_string()) + }; + + // Use node-level provider override if explicitly set, otherwise catalog provider + let final_provider = if node.llm_provider().is_some() { + node_provider.to_string() + } else { + resolved_provider + }; + + model_providers.insert((resolved_model, final_provider)); + } + + // If no LLM nodes found, fall back to the default model/provider + if model_providers.is_empty() { + let (resolved_model, resolved_provider) = + if let Some(info) = arc_llm::catalog::get_model_info(&model) { + (info.id, info.provider) + } else { + (model.clone(), default_provider.to_string()) + }; + model_providers.insert((resolved_model, resolved_provider)); + } + + let mut all_ok = true; + for (model_id, provider_name) in &model_providers { + match provider_name.parse::() { Ok(_) => { let mut status = CheckStatus::Pass; - if !names.iter().any(|n| n == resolved_provider) { + if !configured.iter().any(|n| n == provider_name) { status = CheckStatus::Warning; + all_ok = false; } checks.push(CheckResult { name: "LLM".into(), status, - summary: resolved_provider.to_string(), - details: vec![CheckDetail::new(format!( - "Configured: {}", - names.join(", ") - ))], + summary: model_id.clone(), + details: vec![CheckDetail::new(format!("Provider: {provider_name}"))], remediation: if status == CheckStatus::Warning { - Some(format!( - "Provider \"{resolved_provider}\" not in configured providers" - )) + Some(format!("Provider \"{provider_name}\" is not configured")) } else { None }, }); - status == CheckStatus::Pass } Err(e) => { checks.push(CheckResult { name: "LLM".into(), status: CheckStatus::Error, - summary: resolved_provider.to_string(), - details: vec![CheckDetail::new(format!( - "Configured: {}", - names.join(", ") - ))], - remediation: Some(format!( - "Invalid provider \"{resolved_provider}\": {e}" - )), + summary: model_id.clone(), + details: vec![CheckDetail::new(format!("Provider: {provider_name}"))], + remediation: Some(format!("Invalid provider \"{provider_name}\": {e}")), }); - false + all_ok = false; } } } + all_ok } Err(e) => { checks.push(CheckResult {