Commit graph

178 commits

Author SHA1 Message Date
Bryan Helmkamp
61ed9a6bdb Add HTML-to-markdown conversion and prompt summarization to web_fetch
web_fetch now converts HTML responses to clean markdown using the htmd
crate (stripping script/style tags), and supports an optional prompt
parameter that makes a secondary LLM call to answer questions about the
fetched content. Each provider profile picks a cheap/fast summarizer
model (Haiku, gpt-4o-mini, gemini-2.0-flash).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 22:26:32 -05:00
Bryan Helmkamp
35005ca0fe Add use_skill tool for agent-initiated skill loading
Let the agent autonomously load skill templates when it recognizes a
matching task, instead of requiring users to type /skill-name. The
system prompt now instructs the agent to call `use_skill` and skill
names use backtick formatting.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 22:03:16 -05:00
Bryan Helmkamp
f6c711fdd9 Add --output-format json flag for NDJSON event streaming
Enables machine-readable output from the agent CLI by streaming
SessionEvent objects as newline-delimited JSON to stdout. This
unlocks scripting, integration testing, and UI integration.

- Make SessionEvent serializable with ISO-8601 timestamps via chrono
- Add OutputFormat enum (text/json) and --output-format CLI flag
- JSON mode: each event is one JSON line to stdout, flushed per line
- JSON mode: skip print_output/print_summary (all info in event stream)
- Text mode (default): behavior unchanged

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 21:51:41 -05:00
Bryan Helmkamp
2750da01c6 Add gpt-5.3-codex model and make it the default codex alias
- Add gpt-5.3-codex to catalog (API model, 1047576 context, 128K output)
- Move "codex" alias from gpt-5.2-codex to gpt-5.3-codex
- Add e2e integration test for gpt-5.3-codex via OpenAI API
- Remove gpt-5.3-codex-spark (not yet available)
- Empty CLI_ONLY_MODELS list and update related tests

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 21:40:01 -05:00
Bryan Helmkamp
bb5f9899ed Add CLI backend support for Claude, Codex, and Gemini CLIs
Route codergen nodes to external CLI tools (claude, codex, gemini) via
exec_command() based on node backend="cli" attribute, stylesheet rules,
or CLI-only model detection.

Key components:
- CliBackend: writes prompt to temp file, shells out to CLI tool,
  parses NDJSON/JSON response, detects file changes via git diff
- BackendRouter: wraps AgentBackend + CliBackend, routes per-node
- Parsers matched to real CLI output formats (Claude stream-json,
  Codex NDJSON, Gemini JSON)
- Permission flags for non-interactive use (--dangerously-skip-permissions,
  --full-auto, --yolo)
- Node::backend() accessor and "backend" stylesheet property

Tested against real Claude, Codex, and Gemini CLIs locally.
Daytona integration tests added (ignored, require DAYTONA_API_KEY).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-27 21:08:19 -05:00
Bryan Helmkamp
8c90945ab6 Add e2e parity tests for web_fetch and web_search tools
Test both tools across all three providers (Anthropic, OpenAI, Gemini).
web_fetch fetches example.com and asserts content is written to a file.
web_search searches for "Rust programming language" and asserts results
are saved.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 20:09:07 -05:00
Bryan Helmkamp
6338f3ec57 Implement web_fetch tool via exec_command curl
Replace the placeholder web_fetch tool with a working implementation
that executes curl within the execution environment, respecting
Docker/Daytona network sandboxing.

- Build curl command with shell-escaped URL, follow redirects, custom
  user agent, and configurable timeout (default 30s, max 60s)
- Validate URL scheme (http/https only) to prevent misuse
- Truncate responses exceeding 100KB to protect context window
- Register web_fetch in Anthropic and OpenAI profiles (was Gemini-only)
- Add web_fetch guidance to all three profile system prompts
- Add captured_command to MockExecutionEnvironment for test assertions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 20:07:28 -05:00
Bryan Helmkamp
a93ca70fb4 Emit subagent lifecycle events and forward child session events
Add SubAgentSpawned/Completed/Failed/Closed/Event variants to AgentEvent
with a deferred callback mechanism on SubAgentManager. Child session
events are subscribed to at spawn time and forwarded as wrapped
SubAgentEvent (skipping streaming noise). Wired in both the standalone
CLI and the pipeline AgentBackend.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 19:59:56 -05:00
Bryan Helmkamp
f211ec86d9 Cache ripgrep availability check in all execution environments
The rg availability probe (rg --version / which rg) was running on every
grep() call. Cache the result in a OnceLock/OnceCell per environment so
the probe runs at most once. Also add grep -rn fallback to Daytona env,
matching the pattern already used by local and Docker envs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 19:47:07 -05:00
Bryan Helmkamp
915cdd2a1f Build system prompt once in initialize() instead of per-input
Moves build_system_prompt() from run_single_input() to initialize(),
storing the result as a Session field. This makes the prompt's
static-ness explicit and guarantees Anthropic cache breakpoint 1
always hits. Internal methods (build_request, check_context_usage,
compact_context, estimate_token_count) no longer take a system_prompt
parameter.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 19:40:48 -05:00
Bryan Helmkamp
29b08fc6c5 Implement web_search tool using Brave Search API
Replace the placeholder web_search tool with a real implementation backed
by the Brave Search API. Register it in all three profiles (Anthropic,
OpenAI, Gemini) so every provider has web search capability.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 19:36:38 -05:00
Bryan Helmkamp
3253589193 Simplify LLM crate: constructors, shared HTTP/SSE infra, Deref delegation
- Add ToolResult::success()/error() constructors, replacing 16 manual
  construction sites across tools.rs, session.rs, history.rs, generate.rs
- Collapse ContentPart::RedactedThinking into Thinking (use redacted field)
- Extract HttpApi base struct shared by all 4 provider adapters
- Extract LineReader into common.rs for shared SSE byte buffering and
  timeout handling; convert Anthropic/OpenAI from BoxStream to Response
- Replace manual delegation methods on GenerateResult/StepResult with
  Deref<Target=Response>

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 19:12:33 -05:00
Bryan Helmkamp
0756a42ce8 Consolidate redact and terminal crates into util
Merges two small utility crates into a single `util` crate to reduce
workspace clutter. No behavioral changes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 18:44:19 -05:00
Bryan Helmkamp
b6d285a79c Remove dead LLM crate code and add --schema to ullm prompt
Remove unused items that have no callers outside their own tests:
middleware wrap_stream_with_middleware/process_stream_event,
common ApiMessage/send_and_read_body, types ProviderEvent variant,
lib CancellationToken re-export, tools execute_all_tools, and
catalog get_latest_model.

Add --schema/-S flag to `ullm prompt` so generate_object() and
stream_object() are exercisable end-to-end. Includes unit test for
invalid JSON rejection and two ignored integration tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 18:31:11 -05:00
Bryan Helmkamp
ab3b3bb050 Wire subagent tools into production session creation
Register spawn_agent, send_input, wait, and close_agent tools in both
AgentBackend::create_session and the agent CLI run() so subagents are
available outside of tests. Child sessions inherit the parent's
tool_approval callback but omit subagent tools to prevent recursive
spawning.

Also classifies subagent tools as auto-approved at all permission levels
and removes unused id/depth fields from SubAgent.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 18:23:09 -05:00
Bryan Helmkamp
6c43c8b34b Lazy-compile gitleaks regexes to eliminate startup cost
Replace eager Regex compilation in GitleaksEngine::build() with
OnceLock-based lazy compilation. Individual regexes (rule patterns,
allowlist patterns, global allowlist patterns) are now compiled on
first use rather than at startup. This drops dry-run time from ~1.4s
to ~0.5s since the common no-match path never compiles any regexes.

Also removes the warm_up() pre-loading function and its callers since
lazy compilation makes it unnecessary.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 18:02:37 -05:00
Bryan Helmkamp
030086a110 Disable empty binary test suites for agent and attractor
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 17:41:17 -05:00
Bryan Helmkamp
f714e300a5 Append -attempt_{n} to node directories on revisits
When a pipeline revisits a node (goal gate retries, loops), each visit
now gets a distinct stage directory instead of silently overwriting the
previous one. First visit keeps the clean `nodes/{id}/` path; visit 2+
produces `nodes/{id}-attempt_{n}/`.

The engine always tracks visit counts and sets
`internal.node_visit_count` in context. Handlers read the count via
`visit_from_context()` and pass it to the updated `node_dir()`.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 17:39:24 -05:00
Bryan Helmkamp
8b7f4599b6 Add full pipeline E2E test for artifact sync on Daytona
Runs the pipeline engine with a live Daytona sandbox: handler produces
150KB output, engine offloads to local artifact store, syncs to the
sandbox, and the checkpoint pointer references the remote path. Verifies
the file is readable in the sandbox.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 17:06:58 -05:00
Bryan Helmkamp
7e4fda2a87 Add Daytona E2E test for artifact sync and update SDK
Update daytona-sdk-rust to 5d370099 which fixes multipart file upload
(upload_file was sending an empty form). Add E2E test that verifies
artifact pointers are rewritten and files uploaded to a live Daytona
sandbox.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 16:53:59 -05:00
Bryan Helmkamp
648418a1b0 Sync artifact files to remote execution environments
After offloading large context values to local disk, check whether each
artifact file is accessible in the execution environment. When it isn't
(Docker/Daytona), read the local file and upload it via write_file,
rewriting the pointer to the remote path. Local envs skip the sync
since file_exists returns true for local paths.

Also switches the pointer prefix from artifact:// to file://.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 15:56:29 -05:00
Bryan Helmkamp
8e6acde1b3 Move node directories under logs_root/nodes/
Node stage directories ({logs_root}/{node_id}/) could collide with
{logs_root}/artifacts/ if a node has id="artifacts". Fix by nesting
all node directories under a nodes/ subdirectory so the layout becomes:

  {logs_root}/manifest.json
  {logs_root}/checkpoint.json
  {logs_root}/nodes/{node_id}/status.json, prompt.md, response.md, ...
  {logs_root}/artifacts/{key}.json

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 15:32:38 -05:00
Bryan Helmkamp
28d668b5a3 Auto-offload large context values to ArtifactStore
After a handler returns, the engine automatically offloads context values
exceeding 100KB into the ArtifactStore, replacing them with artifact://
pointer strings. This prevents bloating Context, Checkpoints, and
status.json files. The preamble renders "See: {path}" for artifact
pointers instead of inlining large content.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 14:56:57 -05:00
Bryan Helmkamp
f16eb49d5b Remove legacy tool.output key and preserve stdout on script failure
Drop the `tool.output` context key (unused alias from tool→script rename)
and rewrite the failure branch to include stdout in both `failure_reason`
and `script.output` context, so build/test output isn't lost on failure.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 14:46:36 -05:00
Bryan Helmkamp
7709d0a01c Improve summary:medium and summary:low preamble rendering
summary:medium now uses compact handler-specific rendering (script
command/stdout/stderr, codergen model/files) instead of dumping raw
context_updates. summary:low adds minimal handler info (type, script
command, or model name) to each stage line.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 14:39:10 -05:00
Bryan Helmkamp
28f75f75dc Fix pre-existing clippy warnings
- Use ? operator instead of if-let-err pattern in backend.rs
- Collapse nested if blocks in preamble.rs summary:medium path
- Allow too_many_arguments on CodergenBackend trait and llm_evaluate fn

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 14:28:15 -05:00
Bryan Helmkamp
b11c92bf66 Redesign compact and summary:high preamble rendering
Compact mode now renders nested-bullet summaries with handler-specific
sub-items (script: command/stdout/stderr; codergen: model/tokens/files)
under a ## Completed stages heading. Summary:high renders per-stage
## Stage sections with full detail and a ## Current context table.

Also captures script.stderr in context_updates for both success and
failure branches, and improves context filtering across all modes to
exclude engine keys (graph.*, thread.*, response.*, last_stage, etc.).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 14:26:07 -05:00
Bryan Helmkamp
92a4c5e506 Inject fidelity preamble into LLM prompts and reuse sessions for full fidelity
CodergenHandler now reads `current.preamble` from context and prepends it
to the prompt before dispatching to backends. For `full` fidelity the engine
sets preamble to empty, so the prepend is a no-op. This ensures downstream
LLM nodes see prior node outputs (e.g. script stdout) without any trait
changes.

AgentBackend gains a `sessions` cache keyed by thread ID. When fidelity is
`full` and a thread_id is present, sessions are reused across nodes so the
LLM sees the full conversation history. Usage aggregation only counts turns
added after the reuse point to prevent double-counting. Failed sessions are
dropped rather than cached.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 12:27:36 -05:00
Bryan Helmkamp
38caf9166f Fix pre-existing lint warnings
Remove unused set_event_callback on MockExecutionEnvironment and
replace always-true u64 >= 0 comparisons with is_u64() checks.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 12:09:48 -05:00
Bryan Helmkamp
ae236a383a Remove unused codergen_mode attribute from daytona-check report node
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 12:09:10 -05:00
Bryan Helmkamp
379f92c110 Add legacy_tool.dot workflow and CLI tests for backwards compat
Exercises the old tool_command attribute through validate and
dry-run to catch regressions in the tool → script compatibility.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 12:08:04 -05:00
Bryan Helmkamp
f81113d71c Add backwards compatibility for tool → script rename
Register "tool" as an alias for ScriptHandler, fall back to
tool_command attribute, dual-write tool.output context key, and
add "tool" to known handler types for validation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 12:07:20 -05:00
Bryan Helmkamp
7ad750c5b6 Rename tool handler to script handler and add language attribute
Rename the entire "tool" concept to "script": ToolHandler → ScriptHandler,
tool_command attribute → script, tool.output → script.output, and all
related artifact filenames. Add a language attribute (shell | python,
default shell) so script nodes can run Python via python3 -c natively.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 09:50:39 -05:00
Bryan Helmkamp
cde7f5dd78 Use tool node for test execution and one-shot LLM for summary in daytona-check
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 00:42:01 -05:00
Bryan Helmkamp
b51e387885 Fix Daytona sandbox: rewrite SSH URLs to HTTPS and generate unique names
SSH git URLs (git@github.com:...) cause "invalid auth method" errors
since Daytona uses HTTPS token auth. Added ssh_url_to_https() to
convert them before cloning. Also removed the static sandbox name
config field and auto-generate unique names with timestamps to prevent
"already exists" errors on repeated runs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 00:23:54 -05:00
Bryan Helmkamp
d1e232262d daytona_build.py 2026-02-26 22:10:52 -05:00
Bryan Helmkamp
5504660379 Add more tests for daytona 2026-02-26 22:10:43 -05:00
Bryan Helmkamp
05903892f6 Add execution environment and setup command observability events
Introduces ExecutionEnvEvent enum (15 variants covering lifecycle, Docker
image pull, Daytona snapshot, and git clone operations) with callback
injection on all execution environment implementations. Adds
PipelineEvent::ExecutionEnv wrapper and Setup* variants for setup command
tracking. Wires callbacks in run.rs so exec env and setup events flow
through the pipeline event emitter to stderr and progress.ndjson.

Also changes PipelineEngine::new to accept Arc<EventEmitter> so the
emitter can be shared with exec env callbacks before engine construction.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 22:09:41 -05:00
Bryan Helmkamp
278bb323fa daytona-check pipeline 2026-02-26 21:52:33 -05:00
Bryan Helmkamp
35bccb5d36 Share one execution environment per pipeline run
Instead of creating a separate Docker/Daytona sandbox per stage inside
AgentBackend::run(), create the execution environment once in
run_command(), initialize it, run setup commands, and pass it through
EngineServices to all handlers. This lets multi-stage pipelines share
file changes across nodes and avoids redundant container/sandbox creation.

Key changes:
- Add execution_env field to EngineServices
- Add execution_env param to CodergenBackend::run() trait method
- Remove env creation, setup_commands, daytona_config from AgentBackend
- Lift env lifecycle (create, initialize, setup, cleanup) to run_command()
- Use scopeguard for cleanup on error/panic paths
- Update PipelineEngine constructors to accept execution_env

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Entire-Checkpoint: 92cbe0d3c133
2026-02-26 21:27:42 -05:00
Bryan Helmkamp
2253259ec5 Add [execution] section to TOML task config for execution environment
Allow specifying execution environment (local/docker/daytona) in TOML
task configs so pipelines are self-contained. CLI flag takes precedence
over TOML, which takes precedence over the default (local).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Entire-Checkpoint: 600b7720bbda
2026-02-26 18:53:51 -05:00
Bryan Helmkamp
8d593ff68e Add Daytona execution environment and replace --docker with --execution-env
Replace the `--docker` boolean flag with `--execution-env <local|docker|daytona>`
to support three execution environments. The new `DaytonaExecutionEnvironment`
uses the Daytona cloud sandbox SDK to run agent tools remotely, auto-cloning the
current git repo into the sandbox via `gh auth token` credentials.

Setup commands from TOML task configs now run inside the execution environment
(via `exec_command` after `initialize()`) rather than locally, so they work
correctly for both Docker and Daytona sandboxes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Entire-Checkpoint: 306da8ff6de1
2026-02-26 18:35:33 -05:00
Bryan Helmkamp
93ab7b8dbd Add [vars] expansion in TOML task configs for DOT source parameterization
Allows users to define variables in their TOML task config that get
expanded as $name placeholders in the DOT file before parsing. This
enables parameterized pipelines without duplicating DOT files.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Entire-Checkpoint: 293af13a0e54
2026-02-26 13:58:27 -05:00
Bryan Helmkamp
0f9e31204f Add TOML-based task config for attractor run
Support `attractor run task.toml` as an alternative to `.dot` files.
Auto-detects format by file extension. TOML bundles pipeline config
(graph path, model, setup commands, working directory) into a single
file with precedence: CLI flag > TOML > DOT graph attrs > defaults.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Entire-Checkpoint: 8744d57d6053
2026-02-26 13:37:08 -05:00
Bryan Helmkamp
5198a29a62 Close logging gaps vs Kilroy: panic.txt, graph.dot, run.pid, final.json, parallel_results.json, provider_used.json, api_request.json, api_response.json
Adds 8 log file outputs to reach parity with Kilroy's per-node and
run-level logging:

- panic.txt: written on handler panic with the panic message
- graph.dot: DOT source saved to logs dir at run start
- run.pid: process ID saved to logs dir at run start
- final.json: run outcome (status, duration, failure_reason) written
  after engine completes, before error propagation
- parallel_results.json: branch results written per parallel node
- provider_used.json: mode/provider/model written by AgentBackend
- api_request.json: serialized LLM request in one_shot mode
- api_response.json: serialized LLM response in one_shot mode

Adds stage_dir parameter to CodergenBackend trait methods so backends
can write artifacts to the correct per-node log directory.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Entire-Checkpoint: a3934db0f826
2026-02-25 19:44:18 -05:00
Bryan Helmkamp
db85f7b0bd Add per-node log files to ToolHandler for Kilroy parity
ToolHandler now writes 4 files to {logs_root}/{node_id}/:
- tool_invocation.json (command, timeout_ms) before execution
- stdout.log and stderr.log after command completes
- tool_timing.json (duration_ms, exit_code, timed_out) after execution

The timeout branch also writes tool_timing.json with timed_out: true
and exit_code: null. Inlined process_output since stdout/stderr are
now needed in the surrounding scope for file writes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Entire-Checkpoint: 6c9fde8bdc72
2026-02-25 19:19:39 -05:00
Bryan Helmkamp
650567a1cf Add codergen_mode (one_shot | agent_loop) to support single-call LLM nodes
Enables simple generation nodes (summarization, classification, routing)
without the overhead of a full agent session. Nodes default to agent_loop
when the attribute is absent.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Entire-Checkpoint: 1574218eca96
2026-02-25 19:12:57 -05:00
Bryan Helmkamp
1e3726b7f3 Add missing data to agent events and cost tracking in pipeline logs
Enrich three AgentEvent variants with payload data (UserInput.text,
TurnLimitReached.max_turns, SteeringInjected.text) and forward
UserInput/SteeringInjected to the pipeline event stream. Add cost
field to StageUsage (computed from model pricing catalog) and
total_cost to PipelineCompleted (summed from stage outcomes).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Entire-Checkpoint: adb2363c9ec4
2026-02-25 17:17:13 -05:00
Bryan Helmkamp
d47c8e9048 Redact secrets from pipeline event output (NDJSON, live.json, SSE)
Apply redact::redact_jsonl_line at all three serialization sites so
secrets (AWS keys, GitHub PATs, private keys, etc.) are scrubbed
before reaching disk or the network.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Entire-Checkpoint: e23c92750cd9
2026-02-25 16:02:06 -05:00
Bryan Helmkamp
5067a59f7b Add per-node visit counter to prevent infinite loops in dry-run mode
Cyclic graphs (e.g. consensus_task.dot, semport.dot) loop infinitely
under --dry-run because the mock backend always returns outcome=success,
which never matches conditional edges. Add a configurable max_node_visits
limit (graph attr, default 0=disabled) that defaults to 10 in dry-run
mode, terminating execution when any node is visited more than the limit.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Entire-Checkpoint: 633b59435252
2026-02-25 15:58:01 -05:00