Skip to content

Latest commit

 

History

History
3290 lines (3072 loc) · 209 KB

File metadata and controls

3290 lines (3072 loc) · 209 KB

Changelog

All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

Added

  • Direct MCP workflow steps (type: mcp) (#392): calls a tool on a configured runtime.mcp_servers stdio server directly without an LLM. Arguments are rendered recursively with Jinja2 and auto-coerced to JSON-native types; the result envelope (content, structured, is_error) merges structured keys directly onto the output dict so routes and downstream steps can branch on output.is_error or individual fields. Calls serialize per server process to maintain stdio stream integrity while distinct servers execute concurrently in parallel groups. Output text payload is bounded by runtime.tool_output with spill-to-file support while structured data is preserved intact. Step and result values are excluded from all lifecycle events (mcp_started, mcp_completed, mcp_failed), with failure messages redacted to a safe category and full exception traces written only to a private per-run *.mcp-diagnostics.log file (named by the redacted message). See docs/workflow-syntax.md and examples/mcp-step.yaml.
  • OpenTelemetry spans for direct MCP workflow steps: each type: mcp execution is exported as an execute_tool span under its workflow, parallel group, or for-each item. Spans include bounded server, tool, result-size, and truncation metadata without recording arguments, result contents, or spill paths, and preserve routed tool errors, execution failures, and interrupted attempts as distinct outcomes.

Fixed

  • Pydantic AI structured-output agents explicitly require final_result — the generated output tool now tells models that they must call it before finishing and that plain-text responses are not accepted. This improves adherence for local models behind OpenAI- or Anthropic-compatible endpoints without replacing tool-based output, weakening schema validation, or changing authored system prompts.

0.1.37 - 2026-09-09

Added

  • Always-on client-side context compaction for the claude and openai providers (#503) — an agent whose conversation outgrows the model's context window no longer fails the run. Conductor condenses the history once it crosses a calculated trigger threshold and continues. The trigger is computed from an additive reserve formula — context window minus output limit minus an effective tool-output-derived buffer (clamped to at most 25% of the window) — and compaction is disabled, with a disabled_reason on the agent_compaction_config event, when the remaining trigger would fall below 4096 tokens. Condensing runs a three-stage strategy: clearing old tool results first, summarizing older messages with a nested model call, and sliding the window as a deterministic fallback. It is client-side only, so behaviour is identical behind an API proxy, always on with no new YAML surface, and fail-open — a compaction failure never aborts a run, and an unrecovered one disables compaction for the rest of that agent execution. Every compaction is surfaced in the console, the JSONL event log, and the web dashboard.

  • Provider-advertised model token-limit metadata (#503) for compaction sizing. The claude and openai providers read per-model input/output token limits from their SDK model listings (with full pagination for the Anthropic SDK, and a vendor-field parser for OpenAI-compatible endpoints), the copilot provider implements the get_max_output_tokens hook, and resolution falls back through the genai-prices registry to a conservative default.

  • Opt-in runtime.provider.setting_sources on claude-agent-sdk (#501) — selects which Claude Code settings tiers (user / project / local) a session may load. It is empty by default, so behaviour is unchanged unless a workflow asks: the provider still sends an explicit [], which is load-bearing because the SDK re-defaults an unset value to ["user", "project"] whenever skills is set. The case it exists for is an agent whose working_dir is a target repository shipping its own .claude/skills — the SDK has --plugin-dir but no --skill-dir, so that repo otherwise has to package its skills as a Claude Code plugin; [project] reads them, and the repo's CLAUDE.md / .claude/rules with them. An enabled tier brings that tier's hooks, so it is only for repositories trusted as much as the workflow itself; the field is rejected on every other provider name rather than accepted and silently ignored, counts as structured config (so --provider overrides warn before discarding it and -v shows it), and a per-agent skills: [] opts that agent out of the tiers entirely. See examples/claude-agent-sdk-setting-sources.yaml.

  • OpenTelemetry tracing, new opt-in OpenTelemetry tracing feature activated by OTEL_EXPORTER_OTLP_ENDPOINT. It instruments workflow orchestration to emit spans for workflows, agents, parallel/for-each groups, steps, and tool executions. For claude and openai providers, it also registers and enables native Pydantic AI instrumentation; for copilot, it captures native spans from the Copilot CLI child process over OTLP HTTP. In each case Conductor unifies orchestrator spans and LLM/tool calls into a single trace tree. Tracing is enabled by configuring standard OTEL_* environment variables for an OTLP collector. See docs/telemetry.md and examples/telemetry.yaml.

    • Added native Copilot CLI spans over OTLP HTTP using W3C trace-context propagation, backed by a per-run protocol and endpoint latch that logs a warning if gRPC is used.
  • Per-agent settings_dir on claude-agent-sdk (#513) — selects which directory's project settings tier supplies an agent's skills, independently of working_dir. The CLI advertises exactly one MCP root — its cwd — and a filesystem MCP server that sees a Roots-capable client discards the directories in its own argv, so pointing working_dir at a target repository to pick up its skills also narrowed the agent's only MCP root onto it. settings_dir splits the two, letting cwd stay wide enough for every path the agent must read. It carries a second, unconditional effect: add_dirs widens the model's built-in Read/Edit/Bash to that tree regardless of any settings tier, though no Conductor configuration reaches a permission mode where that is observable today. Only the skills of that directory travel — not CLAUDE.md, .claude/rules/*.md, .claude/settings.json or .claude/agents, all measured. Refused at conductor validate and at run time on a provider that cannot apply it; a settings_dir whose project tier is not enabled warns in both places too, since the filesystem grant applies even when the skills half no-ops. Reported on the agent lifecycle events so the grant is auditable. See examples/claude-agent-sdk-settings-dir.yaml.

Changed

  • Claude default max_tokens raised from 8192 to 16384 when unset (#503). This doubles the worst-case output cost per call for users who never set it; set runtime.max_tokens explicitly to keep the former behavior. For Claude thinking agents, low or medium effort levels without an explicit max_tokens limit now send 16384 tokens instead of the former 8192 or 12288 tokens.

  • The openai provider honors vendor-advertised token limits (#503) from the models listing when available, using them to size the compaction output reserve.

  • A working_dir or settings_dir template that renders empty is now an error (#513). Previously an empty render resolved to the workflow file's own directory — Path("") is Path("."), which is not absolute, so it was joined onto that directory and passed the existence check — and the agent ran there. A value meaning "nothing" silently becoming something real is the defect; for settings_dir it would also have granted the model access to the workflow's own tree. Both fields now fail before the provider call, naming the field and the template it came from.

Fixed

  • openai: retry transient errors delivered inside an SSE stream (#506) — the OpenAI SDK raises a bare openai.APIError (no HTTP status) for an error object embedded in a stream, which pydantic-ai does not translate, so a configured retry: policy was skipped and the run failed after the first attempt. Now retried: OpenAI mid-stream 5xx (server_error / internal_server_error), OpenAI rate limits (type requests / tokens with code rate_limit_exceeded), Anthropic-shaped gateway errors proxied unchanged (rate_limit_error / overloaded_error / api_error), and stream errors with no parseable payload type (a non-object error value from an Ollama/vLLM gateway, or an Azure-style {"code": ...} shape), which are treated like broken streams. Still fatal: recognized client-side payload types (e.g. invalid_request_error) and every HTTP 4xx. Errors a narrowed retry_on: declines are now wrapped in ProviderError naming the declined category instead of escaping as raw SDK exceptions, a declined retry is logged at warning level (a taken one already was), and a fatal bare APIError's message now carries the payload type/code the SDK leaves out of str(e).

  • Context compaction window guard against token-dense drift (#507) — the claude / openai providers' compaction trigger anchors on provider-reported token usage and estimates everything after the anchor with a ~4-characters-per-token heuristic, which undercounts token-dense content (CJK and other non-Latin scripts, base64, hex, minified data) by 2-4x. A dense suffix could therefore grow the real request past a known context window while the trigger estimate stayed below the threshold, and the provider rejected the request with context_length_exceeded. A second, density-calibrated estimate now guards the hard window: it matches the primary heuristic on ordinary prose, counts text with a substantial non-ASCII share at ~1 token per character, and whitespace-poor ASCII blobs at ~2 characters per token, so it fires only on genuinely dense content — never on a history that is merely large. When it fires, the tier chain is driven directly against that measurement (the inner strategy's own gate would re-measure with the same heuristic that under-counted the content and no-op), until the estimate is back under the target. Telemetry stays on the token scale: agent_compaction_start gains trigger_reason ("trigger" / "window_guard") and a separate density_tokens field instead of overloading tokens_before, and agent_compaction_complete gains degraded_estimators and still_over_window so a guard compaction that could not get back under the window reads as degraded, not as false success. A failed primary measurement falls back to an independent density-calibrated estimate that shares no code with it, and a double failure is reported as a new agent_compaction_skipped event (reason: "estimate_unavailable") rather than vanishing into stderr. See Workflow Syntax → Context Compaction.

  • Validator retries preserve the primary agent conversation (#511) — when a semantic validator rejects output from the Claude, OpenAI, or Hermes provider, the correction now continues the completed agent conversation — the Pydantic AI message history for Claude/OpenAI, the run's own message list for Hermes — and sends validation feedback as the next user turn. This preserves prior reasoning and tool exchanges without repeating the original prompt, workspace instructions, or injected skills. A failed re-run now reports its cause on the agent_validation_failed event and in the console log, and the dashboard keeps the agent's original prompt visible instead of replacing it with the feedback-only turn.

  • Concurrent agents no longer build duplicate provider instances (#512) — resolving one provider type from two agents at once (a parallel group, or a for_each with max_concurrent > 1) was a check-then-act with an await between the cache check and the cache write, so each could construct its own instance and the second write replaced the first, leaving the two agents holding different objects for the same provider type. A waiter could also observe a provider before its restored resume-session state had been applied. Construction, resume-session wiring, and cache publication are now one operation under a registry-local lock that re-checks the cache; cached reads stay lock-free, a failed construction caches nothing and strands no waiter, and distinct provider types remain independent.

  • A multi-line reply to a terminal dialog is now one turn (#509) — dialog mode was the only free-text human-input surface that could not accept a multi-line answer (QuestionDef.multiline defaults to True and GateOption.multiline opts in, both served by one reader in gates/human.py). The dialog gate read a reply with single-line Prompt.ask, so pasting a block of text into an interactive terminal dispatched each line as its own turn: a three-line paste became three separate questions to the model, each answered against a fragment, and the paste's trailing newline added a fourth turn with empty content. Terminal turns now read through the multi-line reader already behind the human gate's . sentinel, submitted with /send on its own line, so internal newlines survive and a paste is a single message. An empty or whitespace-only submission is no longer dispatched as a turn. A dismiss keyword is recognised only once a turn is submitted, so on a tty done now needs /send after it, and both the opening banner and the failure-recovery notice say so rather than naming a keystroke that does nothing there.

    Ctrl-D at the start of a line (Ctrl-Z then Enter on Windows) also submits the lines entered so far, or dismisses the dialog when there are none. Because a terminal's EOF does not persist, abandoning a part-written reply that way now sends what was already entered and a second Ctrl-D is needed to leave, where one used to exit; on an empty prompt it still exits in one keystroke.

    The dialog uses /send where the human gate keeps ., since a lone . is likelier to be prose in a conversational reply. Off a tty — a pipe or CI — replies are still read one line at a time and /send has no effect; the one change on that path is that a blank line is now skipped instead of dispatched as an empty turn. The web dashboard is unaffected: it takes a separate path that already delivered each message whole.

0.1.36 - 2026-09-02

Added

  • conductor mcp serve (#432) — exposes your registered Conductor workflows as MCP tools to any MCP-compatible host (Claude Code, VS Code, Cursor, etc.) over stdio, with no workflow edits required: every workflow in every configured registry is exposed by default, with a typed inputSchema derived from its own input: block. A tool call always forks a real detached conductor run — the server never executes a workflow in-process — and by default returns immediately with a run handle carrying the run_id, dashboard url and port, the captured log paths, and the conductor fleet / conductor status commands for watching it from a terminal, so the caller can report the run back and move on while it keeps going. A caller may opt into a bounded wait per call (_wait_seconds, capped by --max-wait-seconds), which changes only whether that call blocks — never how the workflow runs; start the server with --max-wait-seconds 0 to make every invocation non-blocking regardless of what a caller requests. A run that has not completed (immediate, at-gate, failed, or timed-out) returns the handle; a run that completes within a bounded wait returns its output inline, or — once serialized output: exceeds 50 KB — spilled to a file with a resource_link and no dashboard url. New conductor_run_status / conductor_await_run / conductor_cancel_run / conductor_list_runs tools answer for a run_id before, during, at a human gate, and after a run has finished — the human gate is never auto-skipped; a run that reaches one parks and reports its dashboard approval URL until a person resolves it. Optional introspect/diagnose toolsets (off by default; enable with --toolsets) add event-query, per-step detail, conductor doctor/conductor validate equivalents, and links (never file contents) to a run's raw logs. --allow/--deny narrow or force the exposed set; a registry above --max-direct-tools (default 25) degrades to a two-tool discovery pair instead of failing or overflowing a host's tool-count limit. See docs/mcp-server.md for the full guide, including a dedicated Limits section for what this release deliberately does not do (no outputSchema, no Streamable HTTP transport, tool call payloads withheld unless --introspect-full).
  • workflow.mcp: block — per-workflow configuration read by conductor mcp serve to decide how a workflow is exposed as an MCP tool: expose (default true), mode (async/sync/auto), read_only, destructive, and estimated_minutes. Every field defaults to the value that keeps an existing workflow with no mcp: block at all exposed identically to one that declares the defaults explicitly, so no existing workflow needs editing. An unknown key inside the block is a conductor validate schema error, not a silently ignored typo. See examples/mcp-serve.yaml.

Changed

  • conductor status and conductor fleet list now also list recently-completed runs, not just currently-running ones — a contract change to what these commands mean. Previously both meant "runs alive right now"; a finished run disappeared the moment its process exited. Each command now renders (or, for status --json, returns in an additive completed array) a bounded set of recently-completed runs with their terminal status (completed/failed), when they ended, duration, tokens/cost, and error type for a failure — sourced from the terminal run record every run now writes on exit. conductor fleet list's completed rows are bounded by [fleet.retention].keep_last. Both commands remain read-only: listing a completed run never removes its terminal record. Pass --live to either command to restore the exact previous scope (status --json --live also drops the completed key from the payload entirely, so an existing scripted consumer of payload["running"] is otherwise unaffected by this change). The Fleet Manager TUI's History screen also gained a failed run's error message and a completed run's rendered output, reachable by selecting a row, without adding a new column to its table.

Fixed

  • The mcp SDK dependency is now bounded below its breaking 2.0 release (mcp>=1.28.1,<2). mcp 2.0.0 renamed the camelCase attributes the existing MCP client reads (Tool.inputSchema -> input_schema), so an installation whose lock had already floated to mcp 2.x had a client that connected to a server and then raised AttributeError on every tool listing — MCP tools were silently non-functional. Re-locking with this bound restores them; the pin does not change behavior for anyone already on mcp 1.x.

0.1.35 - 2026-08-28

Removed

  • Dead workflow lifecycle hooks (on_start / on_complete / on_error) (#476). The hooks: block was parsed and its templates rendered, but the result was discarded — never emitted as an event, logged, shown, stored, or used for any side effect — so the feature was entirely unobservable, including when a hook failed. HooksConfig, WorkflowDef.hooks, the engine's _execute_hook / LifecycleHookResult and all call sites, and the Hooks section of docs/workflow-syntax.md have been removed. A workflow that still declares hooks: now fails validation with a clear error rather than silently ignoring the block. Lifecycle hooks may return later, but the right syntax will be designed against a concrete requirement (emitting an event, invoking a type: script step, or calling a webhook) rather than rendering a template and throwing it away.

Fixed

  • conductor doctor's table output no longer dies part-written on a cp1252 console (#401). The Installed/Credentials/Connection/Models columns hardcoded ///, none of which cp1252 can encode, so a run on a legacy Windows console raised UnicodeEncodeError mid-table, after the Environment section had already printed. conductor doctor now resolves each glyph once per invocation against the output console's stream encoding, falling back to OK/X/o/! when the Unicode glyphs cannot be encoded; the --json path was already safe and is unchanged.
  • Plugin flavor resolution (Claude vs. Copilot builds) (#497). A Claude-built plugin's agents/*.md subagents (no .agent.md suffix) were silently never loaded — the candidate-file rule was hardcoded to the Copilot build's convention. Flavor is now read off the manifest that actually matched and threaded as a tie-break-only axis through plugin resolution, so provider: copilot agents using a Claude-built plugin now get its subagents too. Also adds ~/.copilot/settings.json marketplace resolution as a fallback for plugin@marketplace references, and several new non-fatal warnings when a build cannot be determined unambiguously.

0.1.34 - 2026-08-24

Added

  • Native OpenAI provider — new stable openai provider built on the shared Pydantic AI runtime. Works with the real OpenAI API and any OpenAI-compatible Chat Completions endpoint (Ollama, vLLM, LM Studio, OpenRouter, corporate proxies). Supports MCP tools, structured output, interrupts, reasoning effort (low/medium/high), and the full 0.02.0 temperature range, which is now expressed as ProviderCapabilities.max_temperature and enforced in create_provider so conductor run and conductor resume are covered rather than only conductor validate. See docs/providers/openai.md and examples/openai-compatible.yaml.

    A custom base_url requires an explicit api_key: an ambient OPENAI_API_KEY is never forwarded to a non-OpenAI endpoint.

  • Fleet TUI launch directory (#477) — conductor fleet can now start runs in a directory other than the one it was itself launched from. d on the Runs screen (or ctrl+d on New run) opens a directory picker; the chosen directory is the base a relative workflow reference on the New Run screen resolves against, and the working directory that a launched or resumed run's detached child inherits. It is process-lifetime only — there is no config.toml key and no state file, and it resets when conductor fleet exits. It does not affect runtime.working_dir / agent.working_dir, and it is not a filter: Runs and History still show the whole fleet. See docs/fleet.md.

  • runtime.idle_timeout_seconds / runtime.max_idle_recovery_attempts (#488) — Copilot-only knobs to tune the idle watchdog for workflows with legitimately long tool calls. idle_timeout_seconds sets the time without SDK events before a session is treated as idle (default 90s); max_idle_recovery_attempts caps the number of "please continue" prompts sent before failing (default 5; 0 fails on the first genuine idle without ever injecting a prompt). See docs/configuration.md and docs/workflow-syntax.md.

Changed

  • The Pydantic AI dependency was narrowed from the full pydantic-ai package to pydantic-ai-slim[anthropic,openai]. This drops the bundled pydantic_ai.mcp module, which Conductor replaces with its own toolset bridge, so the change is transparent to users.
  • The previously reserved openai-agents provider name has been removed from the schema, factory, registry and diagnostics. Workflows that named it now fail at schema load time rather than at the first agent execution.

Fixed

  • The Copilot idle watchdog no longer fires during long-running tool calls (#488). The SDK does not guarantee any events during a tool call — tool.execution_progress / tool.execution_partial_result exist in the SDK schema but are opt-in per tool, so for most tool calls nothing arrives between tool.execution_start and tool.execution_complete — so a stale idle clock while a tool was still executing was previously indistinguishable from a genuinely stuck session — triggering a spurious "please continue" recovery prompt mid tool-call. That prompt's conversational reply then overwrote the agent's eventual structured output (response_content is last-message-wins), turning a healthy run into a non-retryable failure. In-flight tool calls (tracked by tool_call_id) now suppress idle recovery entirely while any remain outstanding; max_session_seconds is the sole backstop for a genuinely wedged tool. Recovery-prompt and stuck-session messages also no longer misattribute the failure to a tool that has already completed — last_activity_ref's tool name is now cleared (or rolled to another still-in-flight tool) on tool.execution_complete instead of only ever being set. The first occurrence of extended suppression during a session is logged at warning level (naming the in-flight tools and the max_session_seconds backstop); further occurrences in the same session are debug-only.
  • Retry classification now covers the ModelHTTPError and ModelAPIError types pydantic-ai actually raises, so 408, 429 and 5xx responses are retried on the Claude provider as well as the new OpenAI one. Previously they were treated as fatal.
  • runtime.default_reasoning_effort was silently dropped at run time for every provider and is now forwarded through ProviderRegistry.
  • MCP tool discovery and structured tool results no longer break with MCP 2.0 (#419). MCP 2.0 renamed the Python field on mcp.types.Tool from inputSchema to input_schema and on mcp.types.CallToolResult from structuredContent to structured_content, retaining the camelCase name as the serialization alias in both cases. The second rename failed quietly: a tool returning only structured content raised AttributeError, which was wrapped into a RuntimeError the model read as an ordinary tool failure. Conductor now reads both fields through a shared helper that tries the 2.x name and falls back to the 1.x one, preserving compatibility with both MCP 1.x and 2.x.
  • The Fleet Manager TUI's launch-directory picker no longer clobbers its own prefill (#486). Textual posts a NodeHighlighted event for the directory tree's own root at mount, from reactive initialisation, with no user interaction involved; DirectoryPickerModal mirrored every such event into the input, silently replacing the prefilled launch directory with its parent before the user ever touched the tree. The mirror now fires only while the tree actually has focus.
  • The Fleet Manager no longer loses data from large event logs (#485). The Runs, History, and run-detail screens read a run's JSONL event log through three separate bounded windows — a 512 KiB tail, a 512 KiB head recovery read, and an 8 MiB whole-log cap — that a long-lived or resumed run outgrows. On a real 9.72 MB / 20,361-line log this lost the current step and the token/cost totals, and a resumed run's second workflow_started fell outside the head window, so the wrong workflow topology was reported. All three windows are replaced by one streamed reader bounded only by the longest individual line, and a resumed run's totals now accumulate across generations while its status, gate, and topology reflect the current attempt. The Runs screen's ~2s poll prefilters lines by event type before parsing them, so the uncapped read is also faster than the capped one it replaces.
  • A run record could silently fail to be removed on Windows (#486). remove_run_record deleted a record with a single unretried unlink, and remove_run_record_for_current_process renamed it into a quarantine path with a single unretried rename; on Windows, a concurrent reader can make either fail with a sharing violation, leaving a stale record behind. Both paths now use the same bounded retry that write_run_record already used for its own os.replace.
  • The Copilot provider now recovers automatically when its nested runtime process dies (#483), instead of retrying against a dead process with a misleading "Check that copilot CLI is installed and authenticated" error. A dead spawned runtime is now detected via subprocess.Popen.poll() on the SDK's own child handle and via explicit recognition of BrokenPipeError / ConnectionResetError at the agent-execution SDK boundary (including during idle-recovery "continue" prompts, which previously burned every recovery attempt and were reported as a stuck agent rather than a dead process). Recovery rebuilds the SDK client the next time it is needed, so the existing retry loop lands its next attempt on a fresh runtime with no change to retry-loop shape; a runtime that keeps dying without a single successful call in between fails fast after 2 consecutive restarts (a fixed, non-configurable cap — with the default max_attempts of 3, a single agent execution can only trigger 2 restarts on its own, so the cap mainly bites across agents in the same workflow) rather than looping forever, while a long-running, otherwise-healthy workflow can restart it as many times as needed. A broken connection to an externally-owned runtime (runtime_url / COPILOT_PROVIDER_RUNTIME_URL) is treated differently: it is never retried or respawned, since the orchestrator that owns that runtime is responsible for its health checks and restarts.

0.1.33 - 2026-08-18

Added

  • The Fleet Manager TUI's History screen can now resume a run by pressing r on a row that correlates to an on-disk checkpoint, launching conductor resume --web-bg in the background the same way the New Run screen launches a fresh workflow. Gating is checkpoint-driven, never derived from the row's outcome — an unknown row (no terminal event) offers Resume exactly like a failed one when a checkpoint exists for it, though this only applies when the workflow opted into periodic checkpoints (runtime.checkpoint) or failed and left a failure checkpoint behind. A currently-live run is always excluded, regardless of outcome or checkpoint — resuming a run that is still executing would make the new process adopt the original run_id, overwrite its run record, and interleave two processes' events into one log. See docs/fleet.md.
  • Session continuity for the claude-agent-sdk provider via a per-agent session_key — executions tagged with the same key now continue one Claude session instead of each starting cold, so an investigate → check → retry loop keeps what it already read, and a later agent can inherit an earlier one's conversation by declaring the same key. The key is a static, unrendered label; sessions are scoped per working directory, since that is how the claude CLI stores transcripts. The map is persisted in checkpoints, so continuity survives conductor resume, and the new session_continuity capability turns session_key against a provider that cannot honor it into a conductor validate error rather than a silently dropped setting. A session the provider cannot confirm on disk logs a warning and starts fresh rather than failing the run, and conductor validate refuses a key shared across concurrent executions. See docs/workflow-syntax.md and examples/claude-agent-sdk-session-key.yaml.
  • Checkpoints now persist every active provider's session map, rather than stopping at the first provider that exposes one. A workflow mixing providers previously kept only one map, silently dropping the others' sessions depending on which agent happened to run first. claude-agent-sdk namespaces its own entries, so they cannot collide with Copilot's agent-name keys in the merged map.
  • Fleet Manager TUI: RDP session detection turns animation off automatically (issue #462). An RDP session (SESSIONNAME starting RDP-Tcp) now disables the ~10fps animation clock by default — the same repaint that made the TUI feel laggy over that transport. SSH is deliberately not detected: it ships the ANSI byte stream for the local terminal to render (a few hundred bytes per frame), where RDP renders remotely and ships changed pixel regions, so only the latter is costly in practice. CONDUCTOR_FLEET_NO_ANIM remains the remedy for a genuinely slow SSH link and for transports with no reliable signal (VNC, Citrix, xrdp). The existing CONDUCTOR_FLEET_NO_ANIM force-off switch still wins over detection, and a new CONDUCTOR_FLEET_ANIM force-on switch overrides detection when the operator knows the link can take it. Any path that disables animation — explicit CONDUCTOR_FLEET_NO_ANIM or detection — now also sets Textual's own App.animation_level to none, which additionally stops Textual's built-in widget animations (e.g. the tables' smooth-scroll easing); this is a behavior change for existing CONDUCTOR_FLEET_NO_ANIM users, not only for the new detection path. See docs/fleet.md.

Changed

  • runtime.skill_injection.max_bytes now defaults to 160KB, up from 128KB. The bundled conductor skill has grown to ~132KB, so the old ceiling no longer sat above it: a claude or hermes agent enabling the shipped skill would have failed outright instead of warning, which is the opposite of what the two defaults are for. The 64KB warn_bytes default is unchanged, so that combination still warns. Workflows that set max_bytes explicitly are unaffected.
  • examples/wait-smoke.yaml now caps itself at timeout_seconds: 15, up from 3. It doubles as CI's --web-bg launcher smoke fixture, and a cold Windows runner spends seconds of that budget on process and step overhead — so a cap sized for the ~1s the workflow actually waits reported a slow runner as a launcher failure. The timeout path it demonstrates is unchanged; drive it with a larger --input middle_duration_ms.

Fixed

  • Fleet Manager TUI: the ~10fps animation tick no longer repaints the preview pane and footer (issue #462). RunsScreen._tick used to end by calling _update_gate_detail(), rebuilding the whole preview Text and re-evaluating the footer's key bindings ten times a second for the sake of one spinner glyph — over RDP this made the whole TUI feel laggy. The preview pane is now split into #run-preview (the gate section and progress header, rebuilt on data/selection changes only) and #run-preview-score (the flowed step chips, the only part that actually animates); the frame tick now only repaints the latter, alongside the animated table cells it already updated. See docs/fleet.md.
  • claude provider: validate_connection() no longer fails startup when an Anthropic-compatible endpoint doesn't implement models.list() (issue #455). Azure AI Foundry's Anthropic endpoint, and some LiteLLM/Databricks AI Gateway configurations, answer /v1/models with a 404 while /v1/messages (what agents actually call) works fine — previously this made every workflow using such an endpoint fail before running a single agent. The startup probe now only fails on positive evidence of a broken setup: an unreachable host, rejected credentials (401/403), or a non-HTTP error. Any other HTTP status logs a warning naming the status code and continues, deferring credential verification to the first agent execution — the same posture the hermes provider already documents. See docs/providers/claude.md.
  • A step with no model no longer constructs a provider just to report a context window. Every step type emitted agent_started with a context_window_max resolved through the provider, and the registry builds providers lazily — so a wait, set, script, terminate, or human_gate step built an SDK client whose only possible answer was None. That construction runs inside the engine's timed loop, so it was charged to limits.timeout_seconds: a provider-free wait workflow paid ~0.4s of it locally and enough on a cold Windows CI runner to time the workflow out and fail the --web-bg launcher smoke job. Provider-backed agents are unaffected — they still report the window on both agent_started and agent_completed.
  • Fleet Manager TUI: the footer now says what enter does on each screen (issue #459). Every drill-down screen bound enter but left it unlabeled, so the one key that navigates the TUI was the one key the footer never advertised — Runs opens the run detail, Run detail opens the step detail, History surfaces the conductor replay command, Providers expands or collapses a provider, and Registries opens that registry's workflows. The binding is also hidden whenever it would do nothing: an empty, failed, or still-loading table, or a Providers sub-row that is not a provider. Expanding a provider a second time now collapses the provider you were actually on rather than whichever row the rebuild left under the cursor. See docs/fleet.md.
  • The Pydantic AI provider (claude) never retried on HTTP 429/5xx or transport errors (#454). pydantic-ai's Anthropic model translates the SDK's exceptions before Conductor ever sees them (a private helper, _map_api_errors in pydantic-ai 2.x, written inline at the 1.44.0 floor) into ModelHTTPError (for an HTTP error response) and ModelAPIError (for a connection/timeout failure), so neither the SDK class names nor the anthropic.APIStatusError check that _is_retryable_error relied on ever matched — every attempt failed fast as a non-retryable error regardless of retry: configuration. Both translated types are now classified directly, matching the existing 429/5xx retryable set, and a server's retry-after value is recovered from __cause__ (the translation drops response headers, but preserves the original SDK exception there) or from the response body.
  • A per-agent retry.delay_seconds larger than the 30s provider default was silently clamped back down to 30s on both the Pydantic AI (claude) and Copilot providers, so delay_seconds: 60 produced 30s waits instead of the stated 60s. The internal backoff cap is now max(default_max_delay, delay_seconds), so a larger stated delay raises the cap instead of being clamped by it; existing configurations with delay_seconds below the default are unaffected.

0.1.32 - 2026-08-16

Fixed

  • The --web-bg launch gate now terminates the whole workflow process tree, not just the pid it spawned, closing the orphan a trampoline sys.executable could leave behind (#447). Issue #444 fixed the launch gate's false-positive port conflicts but left its four failure paths terminating only subprocess.Popen.pid — under a trampoline sys.executable (e.g. a Windows uv tool install, the documented install path), that pid is a re-exec shim, not the process actually running the workflow, so a launch-gate failure could kill the shim and leave the real workflow running, undiscoverable, and still burning tokens. On Windows the child is now created suspended and assigned to a fresh job object before it can run (so it cannot re-exec out of reach), with TerminateJobObject reaching the whole tree regardless of exec depth; the job deliberately has no JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE so the tree still survives the launcher exiting, which is the entire point of --web-bg. On POSIX, os.killpg now reaches the process group the detached child already leads. After the tree kill, a final liveness sweep independently confirms every pid the gate knew about is actually dead rather than assuming it — a survivor is now named explicitly in the error message (with a conductor status / conductor stop --port pointer) instead of the message unconditionally claiming "The background process was terminated." A run record is only removed once its pid is confirmed dead by that sweep, so a surviving orphan keeps the record that is conductor stop's only remaining handle on it.

  • --web-bg no longer fails on every port with a false "Port already in use", killing a healthy run (#444). The launch gate's two identity checks both compared against the spawned process's pid (subprocess.Popen.pid), which is not always the pid of the process that ends up running the workflow: on a trampoline sys.executable (e.g. a uv tool install on Windows, the documented install path) the spawned process re-execs into a different one. That made the run-record poll (stage one-and-a-half) never see its own child's record — surfacing as "did not report a run record within 15 seconds, but is still running" — and then made stage two's /api/info probe report every port as held by a foreign process, terminating the healthy child. The run-record poll now also accepts a record whose pid differs from Popen.pid when the record is fresh (written at or after this launch spawned its child), and carries the record's real pid forward as the confirmed identity for stage two. A PORT_CONFLICT is now only raised when that identity was confirmed; an unconfirmed mismatch degrades to the existing non-fatal "still initializing" note instead of ever being fatal. The PID unknown wording seen alongside the conflict is fixed too — the foreign pid is now captured before the child is terminated instead of probed after, when it can no longer answer.

  • The Fleet Manager TUI no longer appears to freeze while a modal is open (#448). Opening the gate options modal (g) left the Runs screen animating underneath it — a covered screen is still composited, so its ~10fps repaints kept re-blending the modal sitting on top. Measured on one 160x45 terminal at an open gate, that was roughly 2.5x the escape sequences and ~40% more CPU than the same screen with no modal up. On a terminal that cannot absorb that stream — over SSH, in a multiplexer, on a slow emulator — keystrokes queued behind the redraw and the modal appeared frozen. The animation is now suppressed while a screen is covered and its timer paused, which also stops the Runs screen animating under the splash, run-detail, history, providers, registries, and new-run screens. The ~2s data poll is deliberately left running, so gate-entry and run-failure notifications still fire while a modal is up. The empty-fleet state also no longer pairs "no runs" with a preview pane still offering g for a run that had gone.

  • The Fleet Manager TUI's kill confirmation prompt is no longer an empty red box (#449). #confirm-dialog had width: auto while both of its children fell back to Textual's base 1fr, and an auto-width container whose children are all 1fr resolves to zero — so the dialog collapsed to 0x0 and painted nothing but its border, leaving k looking like a broken no-op with no way to see what was about to be killed. The dialog now has a fixed width (capped at 90% of the terminal so it still fits a narrow one), its message scrolls instead of overflowing or being silently truncated, and the confirm/cancel hint is docked to the bottom so a long message cannot push it off screen.

  • Fleet Manager TUI no longer blocks the Textual event loop on the Runs screen's ~2s poll, the run-detail screen's poll, History's initial load, or opening a run's dashboard (#437). Each screen's data load now runs in a worker thread (asyncio.to_thread), with rendering back on the event loop, so the UI stays responsive on a large fleet or a slow filesystem (e.g. a WSL dashboard-open call that can take up to 15s). A tick arriving while the previous scan is still running is skipped rather than started alongside it, and each screen shows a brief "Loading…" line while its first result is in flight. An explicit refresh — after a kill, or after a gate is resolved — is coalesced rather than skipped, so those actions still update the table without waiting out a poll interval.

  • The Fleet Manager TUI now tells you when it cannot read a run, instead of showing something that looks like success (#437 review). A run-record directory it cannot read is reported on the Runs screen rather than leaving a "Loading…" line that never resolves or a table silently frozen at its last good contents; a fleet whose summaries all fail to derive is reported as an error rather than as the "no runs — launch one" empty state, which invited launching a duplicate of a workflow that was still running; and a History read failure is reported rather than rendered as "No run history yet.", which claimed absence and, since that screen loads once, never corrected itself. A run whose summary fails to derive on one poll tick also no longer loses its notification history, which had made it re-fire its gate/failure terminal notification on the next successful tick.

0.1.31 - 2026-08-15

Added

  • Install scripts now diagnose a blocked package index instead of retrying into a generic failure. On networks that block direct access to the public Python package index — increasingly common on managed corporate devices — uv tool install fails with a fetch/403/DNS-shaped error. uv has already exhausted its own retries by that point, so both scripts spent their 2s/5s/10s backoff on a failure that cannot heal, and install.ps1 then printed file-lock advice (including a Windows Defender exclusion suggestion) that is both useless and misleading for a network-policy block. Both scripts now classify the failure, stop after the attempt that hit it, and explain the actual remedy: point uv at your organization's index with UV_DEFAULT_INDEX. They also echo the active index at install time — with any credentials in the URL redacted — so "did my override apply?" is answerable from the install log. On the blocked-index path install.ps1 no longer reaches its Defender advice.
  • Two neighbouring failures are told apart rather than blamed on the index. uv words an unreachable git remote exactly as it words an unreachable index, and the installer fetches Conductor itself from git+https://github.com/... — so a blocked github.com was being reported as a blocked package index, sending users to configure something that could not help. It now gets its own message. Separately, connection-level blips (connection reset and friends) still get the full retry schedule, since unlike a policy block they can genuinely heal; only a definitive block short-circuits the retries.
  • README: "Installing behind a proxy or private package index" — how to install through a mirrored/proxied index with uv (UV_DEFAULT_INDEX, uv.toml, named-index credentials, TLS inspection, proxies) and with pip/pipx, including the trap that uv does not read pip's configuration, so pip config set global.index-url alone has no effect on the install scripts, uv tool install, or conductor update. Conductor ships no default mirror and never redirects package resolution on its own; the index is always user-supplied configuration.

Fixed

  • --web-bg/resume --web-bg: a failed run-record write no longer kills a healthy background workflow (#435). The launch gate's run-record poll (Fleet Manager D2) used to terminate the child and fail the launch if it couldn't confirm the run record within 15 seconds, even when the child was alive and its dashboard was still reachable — treating a bookkeeping failure as a workflow failure. It now downgrades to a warning (BackgroundLaunch.run_record_written=False, surfaced via a new note pointing at the captured stderr log, and via a TUI notification from the Fleet Manager's New Run screen) and lets the launch proceed; only a child that is actually dead, or whose dashboard has gone unreachable, still fails the launch.
  • The run_id format is now defined once, in a new leaf module conductor.run_id (#435). Previously fleet/records.py enforced a broad path-safe pattern while engine/event_log.py independently enforced a narrower hex-only pattern and lowercased its input; a resumed --web-bg run whose checkpoint run_id contained uppercase characters could be silently folded to a different value by the event log, causing the parent's launch-gate poll to look for a key the child never wrote and kill the resumed run 15 seconds after a successful start. fleet/history.py and fleet/retention.py's filename parsers, and fleet/records.py's own timestamp parser, now derive their run-id-matching regexes from the same shared pattern.
  • Install hints for optional extras now print a command that works, and upgrades stop uninstalling the extras you have (#441). Every hint pointing at an optional extra hardcoded pip install 'conductor-cli[<extra>]', which cannot work on the documented install path: install.sh/install.ps1 create a uv tool venv, which is not pip-managed, and conductor-cli is not published to PyPI so pip has nothing to resolve against there. conductor fleet without the tui extra, and the aca / claude-agent-sdk provider errors, now resolve the command from the detected install context — uv tool install --force '<spec>' for an install-script install, uv sync --inexact --extra <extra> for a source checkout, and pip install as the fallback, carrying the git URL you installed from when there is one so a pip/pipx-from-git install resolves too. The suggested command reuses the install source recorded for your install (so a fork or a local build is not redirected upstream) and carries the extras you already have, because uv tool install --force replaces the tool's entire requirement set and uv sync is exact by default. A receipt that cannot be read is reported rather than treated as "no extras" — in the hint, and in both install scripts, which warn and carry on rather than either dropping the extras silently or refusing to run. For the same reason, install.sh and install.ps1 now read the existing install's uv-receipt.toml and rebuild the source as conductor-cli[<extras>] @ <source>, so conductor update (which drives them) no longer silently uninstalls [tui] or [aca] on upgrade — it also names the extras it found before you commit. New --extras <a,b> / CONDUCTOR_INSTALL_EXTRAS adds an extra during an install or upgrade (rejecting one this package does not declare, which uv would otherwise accept with a warning and a zero exit status), and --no-preserve-extras / CONDUCTOR_INSTALL_NO_PRESERVE_EXTRAS drops back to a bare install.
  • Fleet Manager History no longer accumulates an entire retained event log into memory to build one entry (#436). _read_full_log now streams parsed events one at a time instead of materializing them into a list before scanning, so building a History entry from a large *.events.jsonl file no longer holds the whole parsed log in memory at once.

0.1.30 - 2026-08-14

Added

  • Fleet Manager: conductor stop, conductor fleet list, and a new interactive conductor fleet TUI now discover every run, not just --web-bg ones (#431). Previously only --web-bg wrote a discoverable (port-keyed .pid) record, so a plain conductor run or conductor run --web process was invisible to conductor stop and had to be killed by hand. Every run path now writes a run_id-keyed JSON record to ~/.conductor/runs/<run_id>.json describing its mode (fg/fg-web/bg), PID, workflow path, and dashboard port (when it has one); stop, fleet list, and the TUI all read from this same store. The legacy port-keyed .pid file is still read (and cleaned up) for a still-running pre-upgrade process, but is no longer written by any current code path. Behavior change: stopping a foreground run (mode fg/fg-web — anything holding a terminal) now requires interactive confirmation, since a plain SIGTERM discards in-flight progress unless periodic checkpoints are enabled for that run; a background-only fleet is unaffected. Use --yes/-y to skip the prompt (e.g. scripts, CI); a non-interactive stdin without --yes refuses to proceed rather than silently defaulting to "yes". stop also gained --run-id, the only selector that can target a foreground run with no dashboard port to match on. See docs/cli-reference.md.
  • conductor fleet (#431) — an optional interactive Textual TUI (pip install 'conductor-cli[tui]') for monitoring, managing, and launching Conductor runs across dedicated screens: Runs (home, ~2s-polled, sorted by recency), Run detail (per-agent topology and timings, not a DAG), Providers (collapsed-by-default provider/model diagnostics, reusing providers/diagnostics.py), Registries (registries → workflows → inputs), New Run (form generated from a workflow's declared input:, launches via the same conductor run --web-bg path the CLI uses), and History (every retained run regardless of outcome, bounded by retention plus an independent 200-entry display cap, delegating replay to conductor replay <log> rather than re-implementing it). A human gate is displayed as a persistent badge for every run mode; it can additionally be resolved from the TUI (g) for any run with a dashboard port (fg-web/bg) via the existing conductor gate respond HTTP path — a plain foreground run's gate is display-only (its PID is shown) since its blocking prompt thread cannot be reached remotely. A terminal bell / OSC 9 notification fires once per transition into at-gate or a failure. conductor fleet list and conductor fleet prune need no optional dependency; only the bare, no-subcommand conductor fleet (which launches the TUI) requires the tui extra. See docs/fleet.md.
  • ~/.conductor/config.toml (#431) — a new machine-wide, read-only-in-v1 settings file (src/conductor/settings.py), read with stdlib tomllib and honoring $CONDUCTOR_HOME the same way registries.toml does. Currently controls [fleet.retention]: an opportunistic sweep (enabled = true by default, keep_last = 200) that bounds the otherwise-unbounded $TMPDIR/conductor/ directory of event logs at the start of every conductor run/resume. Never deletes the checkpoints/ subdirectory or an event log a live/resuming run still references. conductor fleet prune is the explicit manual entry point (with --keep-last/--dry-run) and always works regardless of the enabled setting. A missing file is normal (every setting defaults cleanly); a malformed file only breaks an explicit reader (fleet prune with no --keep-last override) — never conductor run/resume, which swallow a settings load failure and just skip the feature it configures. See docs/configuration.md.

Changed

  • workflow_started now records the run's resolved inputs (#431). Two runs of the same workflow are otherwise indistinguishable in a listing. The values are written to the run's JSONL event log, which is also read by conductor replay and the dashboard.

Fixed

  • conductor stop against a foreground run is no longer a silent no-op (#431). With the interactive keyboard listener active, the SIGTERM handler delegated to the previous disposition only when it was callable — and in an unmodified process signal.getsignal(SIGTERM) returns SIG_DFL, an IntEnum member that is not callable, so the signal fell through and was swallowed entirely: the process survived and kept running. The handler now restores the default disposition and re-raises against itself. An inherited SIG_IGN is honoured rather than converted into a termination.
  • A questions node no longer leaves the run parked at an already-answered gate (#431). A questions node reuses gate_presented but never emitted the matching gate_resolved, so every consumer of the event stream — the web dashboard as well as the Fleet Manager — held a gate that never closed for the remainder of the run.

0.1.29 - 2026-08-13

Security

  • Hardened the web dashboard's HTTP/WebSocket surface (#397). Every mutating route (POST /api/stop, /api/kill, /api/resume, /api/gate-respond, /api/guidance) and the /ws handshake now require a per-run token by default — previously the only protection was the optional CONDUCTOR_GATE_TOKEN env var, and requests were unauthenticated when it was unset. The token is minted automatically per run and discoverable by conductor gate respond / guide / stop via a new 0600 file (POSIX; on Windows the mode bits are not honoured and the file relies on the user-profile ACL instead — see #425) at ~/.conductor/runs/dashboard-<port>.token; CONDUCTOR_GATE_TOKEN still overrides it when set. A new pure-ASGI OriginHostGuard middleware also validates the Host and (when present) Origin headers on every HTTP and WebSocket request, closing the DNS-rebinding and CSRF-from-another-open-page angles; CONDUCTOR_WEB_ALLOW_ORIGINS (comma-separated origins) extends the allowlist for local dev servers. Breaking for external API callers: every mutating route now also requires Content-Type: application/json (415 otherwise), including the previously bodyless control POSTs, and a request whose Host/Origin doesn't match the bound dashboard is rejected with 403 regardless of token. Read-only routes (/api/state, /api/info, /api/logs, /api/gate-status, /api/files/*, and the replay dashboard) remain unauthenticated, protected by Origin/Host only.
  • Hardened the ACA agent runner's transport surface (#396). The experimental aca provider's in-container runner previously relied entirely on the Azure session-gateway network boundary; it now adds four independent layers, none individually load-bearing. The runner binds 127.0.0.1 by default (the shipped container image sets ACA_RUNNER_HOST=0.0.0.0 explicitly, so a deployed pool is unaffected — only a runner started by hand changes behaviour). An opt-in transport token, ACA_RUNNER_AUTH_TOKEN, makes /execute require a matching X-Conductor-Runner-Token header — checked before the inner Copilot provider is constructed — and 401 otherwise; the host sends the header automatically when the same value is set on its side. GET /health stays unauthenticated (the image's own HEALTHCHECK sends no header) but now reports auth_required and auth_token_present, letting the host warn when a gateway is silently stripping the header or when only one side has a token configured. The runner also rejects any inner_provider_settings key outside base_url / api_key / bearer_token / github_token, closing off runtime_url and headers injection, and ACA_RUNNER_ALLOWED_BASE_URLS optionally restricts which BYOK base_url values are accepted. See docs/providers/aca.md#security.

Fixed

  • A pathological gate or dialog prompt no longer stalls the event loop (#395). linkify_markdown — which runs on human-gate prompts, dialog turns, and rendered agent prompts — degraded to quadratic time on inputs containing long unterminated runs of backticks/tildes or [ characters, so agent-generated text could freeze every concurrent agent sharing the loop. The fenced-code opener no longer backtracks character-by-character, and existing-markdown-link detection is now a linear single-pass scanner (fuzz-verified equivalent to the regex it replaces). A defensive 256K character cap skips linkification entirely on anything larger — whitespace is still normalized — so a future pathological shape degrades gracefully rather than hanging.

  • Token cost is no longer massively overstated for cached, tool-calling agents. AgentOutput.input_tokens is the whole prompt and already contains cache_read_tokens / cache_write_tokens, but calculate_cost billed all four buckets additively — charging every cached token at the full input rate and again at the cache rate (11x on claude-sonnet-5). Because a long agentic loop re-reads almost its entire prompt from cache on every turn, the error compounded across turns: a real run reporting $51.08 actually cost about $8. A cached bucket is now subtracted from the input bucket before the input rate is applied, so each physical token is priced exactly once — the same treatment genai-prices uses, including its rule that a bucket is only subtracted when a rate exists to charge it at (a 0.0 cache rate in the table means "no published rate", not "free"). Cost figures on the dashboard, the CLI summary, agent_completed events and the JSONL event log all drop accordingly; no workflow config changes. The Claude Agent SDK provider, whose Anthropic-shaped usage dict reports cached tokens outside input_tokens, now folds them in and reports both cache buckets, so cached tokens there are billed at the cache rate instead of not being billed at all — note this also makes that provider's reported input_tokens / tokens_used counts cache-inclusive, matching Copilot, so token totals rise even as cost falls.

    If you set limits.budget_usd: existing values were calibrated against the inflated figures and now permit correspondingly more real spend. Review them, particularly under budget_mode: enforce.

  • claude-opus-5 and the dotted Claude 4.5 names are no longer unpriced. DEFAULT_PRICING had no claude-opus-5 entry, and get_pricing's versioned-suffix fallback only extends a key with a - delimiter — so the SDK-advertised claude-haiku-4.5 never matched the dashed claude-haiku-4-5 entry either. Those models fell back to None and were reported as unpriced whenever the provider's live pricing hook was unavailable (an older Copilot SDK, or a non-Copilot provider). Added claude-opus-5, claude-opus-4.5, claude-sonnet-4.5 and claude-haiku-4.5 at the published Anthropic rates. Both spellings are kept: the dashed keys are what price the date-suffixed Anthropic ids (claude-haiku-4-5-20251001).

  • gpt-5.6-sol, gpt-5.6-terra, and gpt-5.6-luna are no longer unpriced (#386). Added to DEFAULT_PRICING as exact keys at the existing GPT-5.x family rate ($2.00 in / $8.00 out per million tokens) — exact keys rather than a gpt-5.6 prefix key so the three resolve silently instead of through get_pricing's fuzzy-match warning path. grok-4.5, gemini-3.6-flash, mai-code-1.1-flash, and mai-code-1-flash-picker remain deliberately unpriced in the static table pending a published rate; an invented rate would print as a confident cost, which is worse than the honest (N unpriced) marker. The live provider-pricing hook prices any model whose SDK metadata carries billing.token_prices (verified for claude-opus-5 in #418, on Copilot SDK >=1.0.9 — already the hard floor pinned in pyproject.toml) — the static-table gap only matters when that hook is unavailable or the model's metadata lacks a rate.

Added

  • conductor doctor --models now shows per-model pricing (#386). The Models detail table gained Input $/Mtok, Output $/Mtok, and Pricing columns — the last distinguishing provider (live get_model_pricing hook), table (static DEFAULT_PRICING fallback), and none (genuinely unpriced) so "why is my run unpriced" is answerable with one read-only command (pricing resolution adds no new network round-trip — the Copilot SDK memoizes list_models() for the process). --json gains matching input_per_mtok / output_per_mtok / pricing_source fields on each model object.

0.1.28 - 2026-08-12

Added

  • Mid-run guidance for --web and --web-bg runs (#400). The dashboard previously offered only Stop, Resume, and Kill — there was no way to correct a run's course without stopping it first. conductor guide --text "..." (auto-discovering the dashboard port) and a dashboard Guide button both POST to a new POST /api/guidance endpoint, which feeds a GuidanceChannel the engine drains at the next step boundary (agents, parallel groups, for-each groups, scripts, sets, and waits alike) or immediately if an agent is currently paused, in which case it resumes with the guidance applied — reusing a Copilot follow-up on the same session when one is available. The TTY Esc/Ctrl+G interrupt path now goes through the same add_user_guidance entry point, so that guidance is visible in the dashboard and JSONL log too, and parallel/for-each group members now receive the current guidance section (previously always omitted). resume --guidance "..." (repeatable) applies guidance to the restored context before the resumed agent runs. Protected by the same CONDUCTOR_GATE_TOKEN as conductor gate respond when configured.

  • conductor status — see what is running without stopping it (#384). conductor stop with no arguments lists background workflows, but stops one when exactly one is running, so the natural "what's running?" reflex was destructive precisely when there was a single run to lose. status never terminates anything and never removes a PID file, so a run stays discoverable even when its liveness cannot be confirmed. It prints each run's dashboard URL, which is otherwise unrecoverable once the launching terminal is gone, and --json makes it scriptable. A malformed PID file is skipped with a warning rather than taking down the listing.

  • Git-backed plugin sources (#380). runtime.plugins alone resolves against machine state — an installed plugin name, or a path — so a workflow shared with a teammate still needed "first install these plugins" in a README, and a teammate who skipped that step got a hard error rather than a working run. runtime.plugin_sources maps a marketplace name to where it comes from (owner/repo#v1.4.0, any http/https/ssh or git@host:path remote, or a local path), and plugins: entries reference it as prs@acme. The split follows the Copilot CLI's own settings, which separate extraKnownMarketplaces from enabledPlugins — eleven plugins commonly come from one repository, so inlining a URL per entry would either clone it eleven times or silently pick one of eleven refs. The load-bearing property is that prs@acme means the same thing whether the marketplace was declared, installed via a CLI, or is a local directory: a declared source registers its name into the same resolution table the installed roots populate, so git feeds resolution rather than adding a second code path. It also gives the ambiguity error added in #378 a second remedy — qualify git as git@acme instead of falling back to a path. Both repository shapes are handled: a marketplace.json catalog or a plugin.json single plugin, with a plugin: key for a repository that is both. There is no lockfile — the YAML is the lock. A ref that is a full 40-character SHA is pinned and fetched once; a tag, branch, or absent ref floats and is re-resolved every run, matching how workflow registries already behave. conductor run acquires sources up front and in parallel; conductor plugin fetch primes the cache as its own step, which is what keeps conductor validate off the network entirely; conductor plugin list reports what a run would load, including the component counts that make a change in what a plugin ships visible. An unreachable remote with a warm cache warns and reuses the checkout, so offline runs keep working. Checkouts are cached under $CONDUCTOR_HOME/cache/plugins/, keyed by resolved commit. Cloning shells out to git, so existing SSH keys and credential helpers apply and self-hosted forges work.

    Sources are resolved one at a time, so a source that is unfetched or broken costs its own diagnostic rather than the report for every healthy source beside it. The two are distinguished: an unfetched source is a warning naming conductor plugin fetch, since conductor run heals it, while a source that is itself wrong — a path that does not exist, a path: that escapes the checkout, an unparseable catalog — is an error, because no amount of fetching fixes it. A declared source that shadows a same-named installed marketplace is reported, since the two can ship different subagents or a different MCP server. See examples/plugin-sources.yaml and the Plugins section of docs/workflow-syntax.md.

  • Output field constraints — enum, pattern, minimum/maximum, minLength/maxLength, required, nullable (#372). An output: field could declare a type and nothing more, so "verdict is one of three values" or "score is 0-100" lived in the prompt, where it was a suggestion rather than a contract. The eight new keywords are emitted into the schema each provider shows its model and enforced when the response comes back, and because a violation raises the same ValidationError a type mismatch does, it lands inside the existing in-session recovery loop — the model gets a chance to correct itself before the workflow fails. Constraints are checked recursively, so they hold inside object properties and array items too.

    Illegal combinations are rejected at load time rather than at run time: pattern on a number, an enum whose members do not match the declared type, minLength above maxLength, a regex that does not compile. Unknown keys are rejected as well, so a misspelled minlength fails validation instead of quietly leaving the field unconstrained. required: false is allowed only inside object properties — a root-level output field cannot be optional.

    Two things worth knowing when using them. pattern runs under a one-second deadline on a re-compatible engine, because model output is untrusted input and a backtracking pattern would otherwise stall the event loop and every agent sharing it; an exceeded deadline is a validation failure, not a hang. And templates render with StrictUndefined, so a nullable field that came back null renders as None and an omitted optional property raises — guard both with is not none / is defined, as examples/output-constraints.yaml shows. See docs/workflow-syntax.md (Field Constraints).

  • Plugins as the unit of opt-in (#378). Conductor loaded a plugin's skills/ and dropped everything else it shipped. That is a problem because a plugin's parts are written to work together: its SKILL.md routinely tells the agent to hand work to prs:code-reviewer, or to call an ado MCP tool. The skill loaded, the agent read those instructions, reached for a subagent that was never registered — and said nothing. runtime.plugins (and per-agent plugins:) now opts into the whole unit: skills, agents/*.agent.md subagents, and declared MCP servers. Entries take a string shorthand or an object with per-component switches (skills, agents, mcp), all defaulting on, because defaulting one off would recreate the partial load the feature exists to fix. An entry is an installed plugin name or a path, classified by the same syntactic rule skills: uses; an uninstalled name errors naming where it looked, and an ambiguous one errors rather than picking a winner. Conductor deconstructs a plugin rather than handing its root to the SDK: both SDKs' whole-plugin surfaces are all-or-nothing, and on Copilot hiding an MCP tool from the model does not stop its server subprocess launching with the user's credentials — so mcp: false built that way would be a guarantee that isn't one. Deconstructed, a plugin's MCP servers also pick up the same runtime.tool_output limits, dashboard tool events, and credential/${VAR} resolution as a workflow-declared server. Supported on copilot and claude-agent-sdk; claude, hermes and aca reject plugins: at validation time, since injecting text into a prompt cannot produce a subagent or an MCP server. conductor validate prints what each plugin contributes, including every subagent by name, so a change in what a plugin ships is visible before the run rather than during it. See examples/plugins.yaml and the Plugins section of docs/workflow-syntax.md.

  • Copilot-convention plugin manifests are recognised (#378). .github/plugin/plugin.json now resolves alongside .claude-plugin/plugin.json. Both have always worked at runtime, so recognising only the latter was Conductor's own gap — on an ordinary machine it stranded 12 of 13 installed plugins, which on claude-agent-sdk meant a packaged skill was rejected outright and on copilot meant it was silently demoted to a bare directory.

  • type: questions — ask a human a set of questions in one step (#376). human_gate handles a single decision; asking N questions previously meant hand-rolling a gate that loops back through a set step accumulating a string transcript. That loop cannot support going back — a workflow step cannot be un-executed, and a concatenated transcript has no addressable per-question answer to overwrite — and it costs two engine iterations per question. A questions node holds the cursor and answers internally, so the whole set costs one iteration and answers land in a keyed dict where revisiting question 3 overwrites answers.q3. Questions come from an inline questions: list or a source: dotted path; entries may be plain strings or objects with choices, so an agent already emitting array of string migrates unchanged while gaining candidate answers is a backward-compatible upgrade. Supports back/skip/skip-all/abort, required, per-question defaults, a closing review, and partial answers that survive a checkpoint. --skip-gates never selects a suggested answer — those come from the agent, so recording one would feed invented input back as though a human gave it. See examples/questions.yaml and the Questions section of docs/workflow-syntax.md.

  • Opt-in multi-line text for human gatesGateOption.multiline (default false, so existing gates are unchanged). The terminal reads until a lone . or EOF; the dashboard renders a textarea where Enter inserts a newline and Ctrl/Cmd+Enter submits. Previously a multi-paragraph answer to a prompt_for input was silently truncated at the first newline.

Removed

  • skill_discovery.sources: [plugins] (#378). The source scanned every installed plugin's skills/ directory — reaching into a plugin and taking exactly one of the three things it ships, which is the bug runtime.plugins fixes rather than a feature with a gap. It was also wrong more often than it looked: of 13 plugins on an ordinary machine, 3 loaded their instructions without the subagents those instructions dispatch to, and the 3 most plugin-like — MCP and subagent toolkits with no skills/ at all — were never discovered by it. Nothing distinguished the working set from the broken set at authoring time, on a machine the workflow author may not even be using. personal and project are unchanged. Replace sources: [plugins] with runtime.plugins, which brings the whole plugin and, unlike a scan, reproduces on another machine.

Fixed

  • --web-bg no longer reports success and prints a URL for workflows that never actually started (#410). The launcher's readiness check used to trust a bare TCP connect: the moment anything accepted a connection on the dashboard port, it wrote the PID file, printed the URL, and exited 0 — even for a workflow that failed load_config moments later. Two changes close this: (1) WebDashboard.start() (which binds the port) now runs after load_config succeeds in run_workflow_async, so a ConfigurationError from a broken workflow never binds a port in the first place; (2) _finalize_background_launch now confirms the workflow actually started, not just that a socket answered. _wait_for_server checks the child's exit status on every iteration of its connect loop, so a dead child is detected in well under a second instead of after the full 15s timeout; a stage-two probe then polls GET /api/info (the same identity endpoint conductor stop already uses) for up to 30s (CONDUCTOR_WEB_BG_START_TIMEOUT, 0 disables it) until it reports a workflow_started event, exiting 1 with the exit code and a tail of the captured stderr log if the child dies first, or naming the conflicting PID if the port turns out to be held by an unrelated process. The PID file is written as soon as the port opens — before this second wait — so a slow-starting run stays visible to conductor status/stop throughout; if the child then dies, the entry is removed. Passing the 30s deadline with the child still alive is not treated as a failure — the URL is still printed, alongside a note that the workflow hasn't reported starting yet.

  • Live provider pricing works again (#386). Every Copilot model was being costed from the static DEFAULT_PRICING table instead of the live rates the SDK reports, and models absent from that table reported no cost at all. CopilotProvider.get_model_pricing reads billing.token_prices, and github-copilot-sdk 1.0.1 — the version the lock pinned — parsed the models.list response with a hand-written client.ModelBilling that declared only multiplier and discarded the tokenPrices wire field. The field never left the API and is still modelled in the SDK's generated types; only the client dataclass dropped it. The hook therefore returned None for every model, so the resolution chain #265 built (workflow override → provider hook → static table → unpriced) ran permanently on its fallback. No per-token rates were invented for the missing models; with the hook alive they price from the SDK.

    The field was restored in SDK 1.0.7; the floor moves to >=1.0.9, the version tested here. Moving the floor rather than only the lock is the point — the old >=1.0.0 was satisfied by 1.0.1, so an existing environment kept dead pricing while reporting a healthy dependency. 1.0.9 also splits the cached-token rate into separate read and write prices and deprecates the single cache_price the hook read, which would have silently priced cache reads at $0.00, and it ships a pure-Python wheel that fetches the CLI binary on first use instead of bundling it per platform.

    _default_permission_handler no longer forwards approve_all's result blindly. That helper stopped being unconditional: it abstains with PermissionNoResult when the runtime marks a request managed_approval_required, and raises when managed settings are enabled. Conductor is the only connected client, so an abstention is never answered — the CLI blocks on a pending permission request until idle recovery gives up minutes later and reports a timeout that blames the network. Both cases now decline explicitly and say why. Declining rather than approving is deliberate: managed approval is a policy control, and overriding it would turn a hang into a bypass.

    The existing hook tests built their models from SimpleNamespace, so they asserted what Conductor does with a billing object rather than whether the SDK still supplies one, and stayed green throughout. Tests now build the model through the SDK's own ModelInfo.from_dict, so the next SDK release that stops carrying the field fails the build instead of quietly reverting every run to static pricing, and the permission handler has behavioural coverage for the first time.

  • Plugin checkouts from a file:// source no longer land outside the plugin cache on Windows. The cache key is derived from the URL's path segments, but the splitter only knew /, so a native Windows path arrived as a single segment with its backslashes intact — and the key kept them, putting the checkout at a drive-absolute location rather than under the cache root, which is the same escape the .. check exists to prevent. Two further problems sat behind it: a drive colon made an owner of C:_src read as a drive (or, in the middle of a name, as an NTFS alternate data stream), and flattening a deep path into one segment produced a directory name long enough that git refused to create .git inside it. Separators are now folded, the characters that change a path's meaning on Windows are substituted, and an over-long segment is replaced by a digest of itself — on every platform, so one workflow file resolves to the same cache layout wherever it runs.

  • Two sources resolving to the same commit no longer fail the whole fetch on Windows. Publishing a completed checkout tolerates losing the race to a concurrent fetch, but recognised only the POSIX errnos for "destination already exists"; Windows reports that as ERROR_ACCESS_DENIED, so the tolerance never applied and the second source raised. Safe to accept because the readiness sentinel is written after publishing: a winner that died mid-clone leaves no sentinel, so the tree is re-fetched rather than read half-written.

  • A local path is recognised the same way on every platform_is_local_path asked pathlib.Path, which is the running platform's flavour, so a POSIX absolute path such as /srv/plugins was refused as an unrecognised source on Windows. Both conventions are now consulted.

  • Registry names are validated before they can corrupt the config — a name containing a quote, a space, = or # was accepted, written into registries.toml as an unescaped table key, and then failed to parse. Since registry add, remove and get all load the config first, the user could not remove the entry that broke it and every unrelated registry went down with it. Names are now restricted to letters, digits, ., _ and -, which also keeps them legal as cache directory names on Windows, and the table key is quoted so a dotted name stays one registry instead of becoming a nested table.

  • conductor doctor no longer reports a missing Claude CLI on Windows — the CLI probe dropped five ~-anchored fallback locations on Windows, including ~/.claude/local/claude where Claude Code's own installer puts it, so validate_connection() returned False for a CLI the SDK would find and run. Only /usr/local/bin/claude is now skipped there: it is rooted but driveless, so it resolves against the current drive, which any unprivileged local user can write to.

  • Registry TOML values are escaped — a registry whose source or type contained a quote or a backslash produced a file that could not be re-read.

  • Dashboard context-window bar no longer reports cumulative input tokens as a false red at >100% of the cap (#412). The bar reused AgentOutput.input_tokens — a billing total summed across every API call in an agent's execution — as a context measurement, so a multi-turn tool-calling agent (or a Copilot parse-recovery retry) could report more tokens than the model's context window physically allows. A new AgentOutput.last_call_input_tokens field carries only the prompt size of the most recent single API call, populated by every provider, and the engine now sources context_window_used from it instead. When a provider cannot isolate one call's prompt size, the field is None and the dashboard hides the bar rather than showing a misleading number; a used > max pair (impossible for one real API call) is dropped to None entirely, logged at debug on every occurrence and at warning once per run (nothing reads debug logs in production), since either the usage figure or the looked-up cap is untrustworthy. Riding along: copilot.py's assistant.usage handler previously overwrote its running token counts on every event instead of summing them, so a 20-turn tool-calling agent's cost was billed only for its final API call — this under-report is fixed at the same time, since fixing it alone (without the new field) would have made the context bar report the sum of every turn rather than just the last, making the original defect worse. The event's dedup guard (which prevents a repeated assistant.usage event from double-billing the same API call) keys on api_call_id, falling back to provider_call_id then service_request_id when the SDK omits it, since all three are independently optional. Cost figures for multi-turn Copilot agents will rise as a result; a cost.budget_usd tuned against the old under-reported total may now trip its limit sooner.

  • conductor stop no longer kills the run it is executing inside (#399). An agent smoke-testing conductor stop from its own workflow's bash tool inherited that workflow's background environment and terminated itself — the process printed "Stopped" and was killed by what it printed. stop now identifies the run it is executing inside via CONDUCTOR_RUN_ID (set on every --web-bg child and inherited by descendants), the legacy CONDUCTOR_WEB_BG/CONDUCTOR_WEB_PORT pair (for PID files predating #411's run_id field), and POSIX process ancestry, and excludes it from targeting by default: --all now means "stop all other runs", the no-flag auto-stop skips it, and --port <your own port> is refused (exit 1, naming --allow-self as the remedy). If only your own run is alive, stop/stop --all print a refusal and exit 0 rather than erroring, since nothing named was declined. Pass --allow-self to restore the previous targeting exactly; a yellow warning is printed whenever it actually causes your own run to be signalled. Process-ancestry detection is POSIX-only — Windows relies on the env-var signals alone.

  • A pricing hook that silently prices nothing is now reported (#386). #265 warns when the provider pricing hook raises; the companion case — a hook that never raises and returns None for everything — looked identical to "these models are simply unpriced", so live pricing could be dead for a whole run with no symptom beyond newer models showing up as unpriced. The verdict is drawn once when the run ends — however it ends, so a run that dies part way still reports it, which is when a partial cost total most needs the caveat — and is emitted as a pricing_hook_silent event as well as a log line, so it reaches the event log and the console rather than only unattributed stderr. The run summary gains usage.live_pricing_degraded and the cost breakdown prints a matching caveat, because a model priced from the static table still reports a confident cost and would otherwise carry no qualification. Providers that do not implement the hook are excluded: returning None is the documented default, so counting them accused four of the five providers of a broken SDK for behaving correctly.

  • conductor stop now confirms the process actually stopped, and never stops the wrong one (#344). stop sent one signal and reported success without checking, so a workflow that ignored it was reported as stopped and its PID file deleted — leaving a live run untracked, invisible to stop, and holding its port. Termination is now a ladder (ask the dashboard to cancel, then signal, then force-terminate), each rung confirmed before the next, and the PID file is removed only once the process is confirmed gone. Every PID-directed rung is gated on the dashboard confirming its own PID, because between a PID file being written and stop reading it the OS may have recycled that PID onto an unrelated process. --force overrides uncertainty only: a positive identity mismatch blocks every rung, force included. PID files are written atomically, so a concurrent stop can no longer read a half-written file and deregister a live run, and the reader logs before deleting anything it cannot parse. --force can clear an entry whose liveness cannot be probed, which would otherwise wedge stop --all at exit 2 permanently (#166).

  • Bracketed text no longer crashes or corrupts CLI output (#406). The same defect as #382, which #387 fixed only in cli/run.py. conductor validate died with an unhandled MarkupError traceback on a workflow whose name: contained [/bold], and silently deleted the token when it contained [dim]. The quiet half is the more damaging one: a listing that drops part of a name looks like it worked. Rich treats a bracketed token as a style tag when its first character is lowercase, #, / or @, so [0] is fine, [task1] disappears, and [/etc/x] raises — and style= does not turn parsing off, which is what made the earlier fix look complete.

    Two consequences shipped unnoticed. Every for-each iteration's verbose panel read the same, because the engine qualifies a member's name as <agent>[<key>] so interleaved output can be attributed to one iteration, and a key_by: key of task1 erased exactly that identity — while a key starting with /, which key_by: over paths or URLs produces, killed the run from a logging call. This needed no flags: verbose and full mode both default on. Separately, conductor status (#389) and conductor plugin list (#398) were written against the unfixed pattern in files #387 never touched, and #398 made these strings third-party rather than the author's own YAML, since plugin, marketplace, skill and subagent names are now read out of git-cloned repositories.

    Rather than escape ~450 call sites, the default is inverted: every console is built by the new conductor.console.make_console() with markup=False, so a plain string is literal unless it asks to be styled, and conductor's own styling goes through styled("<template>", value), which parses the template but inserts values verbatim and byte-exact. Panel titles and Prompt prompts are handled separately because rich parses those regardless of the console setting — that is the trap that left #387 incomplete one line from the code it changed. rich.markup.escape is no longer used anywhere: it cannot round-trip a value containing a backslash before a bracket, so an ordinary regex came out mangled. Eight static guards now read the source and fail with file:line if a new call site reintroduces any of these shapes — including a Text flattened back into an f-string, which is how the defect kept coming back, and unescaped brackets in typer help text, which had silently cost conductor run --help the whole [@registry][@version] syntax.

  • conductor status --json no longer ships two permanently dead fields (#404). --web-bg's launcher wrote every PID file's run_id empty and its log_file was a promise it could never keep — the JSONL path is derived inside the child by EventLogSubscriber, after the PID file is already written. write_pid_file now records the launch's actual run_id and stderr_log/stdout_log (replacing log_file, which the parent legitimately knows) — the same three artefacts _finalize_background_launch already had in scope but never threaded through. run_id is the join key to the run's conductor-<name>-<ts>-<run_id>.events.jsonl, so a populated value makes that file findable by glob without storing a path the parent would otherwise have to guess. conductor resume --web-bg goes further: it now resolves the checkpoint exactly as the resumed child does (--from first, else the latest checkpoint for the workflow) and adopts its run_id for the whole launch, rather than minting a fresh one that matches neither the child's EventLogSubscriber (which reuses the checkpoint's id whenever the original JSONL still exists) nor the events log filename. A checkpoint with a missing or malformed run_id falls back to a fresh id rather than failing the launch. conductor status --json also now emits null for an absent/empty run_id/stderr_log/stdout_log instead of "", so a PID file predating this fix is distinguishable from one that legitimately has no run id. conductor status is unreleased, so the log_filestderr_log/stdout_log rename costs no released contract.

  • conductor status's dashboard URL no longer gets cropped at a default 80-column terminal (#405). At that width, the Dashboard column — the one field the command exists to surface — was the one rich elided, leaving http://127.0.0.1:… reconstructable only by hand from the Port column. Both the Started and Dashboard columns now fold onto a second line instead of cropping — a folded value is complete and readable, a cropped one is unrecoverable from the output. Started also renders to minute precision in UTC (2026-08-11 12:48Z, down from a 32-character microsecond-precision timestamp), leaving more room for Workflow before folding is ever needed. The table is a glance-at listing, not an audit log, so --json keeps reporting the exact recorded timestamp untouched — only the human-readable rendering changed. _print_running_list is shared with conductor stop, so its listing gets the shorter timestamp too. The test fixture that let this through built its own PID-file JSON by hand with a 19-character naive started_at, well short of production's 32-character value — it now goes through the real write_pid_file, so the widths under test match the widths production writes.

  • JSON result output no longer crashes on a legacy Windows stdout (#342). On a cp1252 console, conductor run exited non-zero with UnicodeEncodeError after the workflow had already succeeded, having written a truncated document callers could not parse. json.dumps emits ASCII by default, but rich's print_json re-parses and re-serialises with ensure_ascii=False immediately before the write, restoring the character it had escaped. Every JSON sink now passes ensure_ascii=True. Results carry \uXXXX escapes on all platforms as a result, which is valid JSON and decodes identically. conductor doctor's default table output is unaffected by this change and still fails on such a console (#401).

  • Agent text containing bracketed tokens no longer kills a run (#382). A step whose output contained ordinary technical prose such as {provider}/{type}[/{nestedType}...]/read was parsed by rich as a closing markup tag, raising MarkupError and ending the workflow — unresumably, since the crash happened while rendering rather than while running. Every console sink that renders agent-supplied text now passes it as rich.text.Text rather than interpolating it into markup, and the file-log console disables markup entirely. style= does not turn markup parsing off, which is what hid three of the five sinks; two of those were reachable on a bare conductor run with no flags. Opening tags such as [bold] were the quieter half of the same bug: rich consumed them without raising and the text simply disappeared.

  • Non-ASCII workflow inputs are shown literally in verbose output (#391). The verbose "Workflow Inputs" panel serialised inputs with json.dumps' default ensure_ascii=True, so a Cyrillic, CJK or emoji input was displayed as \uXXXX escapes rather than the text the user typed. Every other JSON display path in the repo already passed ensure_ascii=False (#356); this was the last one that did not. Machine-readable JSON results are unaffected and still ASCII-escaped, which is what keeps them safe on a legacy Windows console (#342).

  • Structured runtime.provider for name: claude no longer drops a YAML-declared api_key — the schema accepted api_key (alongside base_url and auth_token) but the provider factory silently discarded it, so only the ANTHROPIC_API_KEY env var ever reached the Anthropic client. The factory now forwards it. Two credential-semantics fixes ride along so the documented behavior is what the code does: the Claude provider's model path now resolves credentials as a unit like the Anthropic SDK does — setting either credential in YAML suppresses both ANTHROPIC_API_KEY and ANTHROPIC_AUTH_TOKEN, where previously a YAML auth_token still let an ambient ANTHROPIC_API_KEY ride along and the SDK sent both X-Api-Key and Authorization: Bearer headers to whatever base_url pointed at (a credential leak against gateway endpoints). And when both credentials are set, Conductor now logs a warning naming that behavior instead of shipping both headers silently — the same parity warning the Copilot provider already logs for api_key + bearer_token. New example: examples/claude-custom-endpoint.yaml; see docs/providers/claude.md (Custom Endpoints and Gateways).

  • conductor resume --web no longer shows a running workflow as stopped — a workflow that was paused from the dashboard (Stop, then Kill) recorded an agent_paused event in its event log with no agent_resumed counterpart. On resume the CLI seeds the dashboard from that log, so the pause replayed and latched the dashboard's global paused state for the entire resumed run: the header showed Resume/Kill instead of Stop for a pause that never happened, hiding the only graceful stop behind a Kill that would hard-stop the healthy resumed run. Pause, iteration-limit-gate, and dialog events are now dropped on replay at every workflow depth, alongside the root lifecycle events already filtered; a gate the resumed run genuinely re-enters emits its own fresh event. Prior agent output and messages are still replayed. The dashboard's live-control buttons are also hidden in conductor replay mode, where the recorded-log server serves no /api/stop, /api/resume, or /api/kill endpoint.

  • Dashboard Stop/Resume/Kill no longer hang on a failed requestfetch resolves rather than rejects on a 4xx/5xx response, so a non-2xx reply left the button disabled and reading "Stopping…" indefinitely, with nothing logged and no way back except reloading the page. The response status is now checked explicitly and surfaced next to the controls.

  • Dashboard: expanding a subworkflow no longer slides the graph out from under you (#375) — the graph layout normalizes its bounding box to the origin on every rebuild, so growing one container repositioned every node while the camera stayed where it was. Expanding or collapsing a subworkflow now pans the viewport by the same amount the toggled container moved, so that container stays pinned under the cursor and the surrounding nodes visibly move out of its way instead. Collapsing that same subworkflow returns you to the view you started from. Expand-all, and any other change that toggles several subworkflows at once, anchors on whatever sits nearest the center of the pane; the view is not refit in either case. The same compensation steadies the graph when a running workflow's topology grows — a for_each fanning out or a subworkflow's DAG arriving.

0.1.27 - 2026-08-04

Added

  • Skills: runtime.skills and per-agent skills: give agents opt-in knowledge bases (#180) — runtime.skills enables a list of skills for every provider-backed agent in the workflow, and any agent can override it with its own skills: (omitted = inherit, [] = explicit opt-out, a list = explicit set). Conductor ships a built-in conductor skill covering its own YAML schema, execution model, authoring patterns, and CLI commands, so an agent that writes or reviews workflows can be made Conductor-aware without pasting documentation into its prompt. The observable contract is the same on every provider — the agent has access to the named skill — but the mechanism differs: copilot registers the skill directory on the SDK session, so only the frontmatter is read up front and the body is loaded on demand, while providers with no native skill surface receive the skill content injected into the prompt. Providers declare whether they can load skills at all, and conductor validate rejects a workflow that enables skills on one that cannot, rather than letting the content be silently dropped at run time. Skills are rejected on step types with no model behind them (script, set, wait, terminate, workflow, human_gate). See examples/skills-self-improving-workflow.yaml and docs/workflow-syntax.md (Skills).

  • Skill discovery: runtime.skill_discovery picks up skills already installed on the machine (issue #362) — sources: [personal, project, plugins] scans ~/.copilot/skills and ~/.claude/skills, .github/skills and .claude/skills from the workflow file's directory up to the repository root, and every installed plugin's skills/ directory, so a workflow can use a personal or team skill library without enumerating it. Off by default. exclude: drops individual skills by name. Conductor scans the union of both CLIs' locations itself rather than enabling each provider's own discovery: locations are provider-specific, so a per-provider flag would give a copilot agent and a claude-agent-sdk agent different skill sets inside a single run. Scanning centrally also keeps Copilot's enable_config_discovery off, which would otherwise auto-load MCP servers from any .mcp.json in the working directory. Discovered skills join runtime.skills, so an agent declaring its own skills: (including skills: []) still overrides them; a skill named in skills: beats a discovered one of the same name. Discovered content is held to a laxer standard than declared content — broken frontmatter, a taken name, an unreadable directory, or a skill claude-agent-sdk cannot load are errors for a declared skill and warning-plus-skip for a discovered one (a provider with no native skill surface at all is the exception, and errors either way). Rejected at validation time on claude and hermes, which inject every skill body into every prompt and cannot bound a machine-dependent set; on claude-agent-sdk only discovered skills inside a Claude Code plugin are loaded and the rest are skipped with a warning. conductor validate lists what was found, where each skill came from, and the total size if eagerly injected. See examples/skills-discovery.yaml and docs/workflow-syntax.md (Discovering installed skills).

  • skills: now accepts filesystem paths, not just built-in names (issue #350) — an entry is treated as a path when it starts with . or ~, or contains / or \; everything else must still be a registered built-in, so a bare conductor can never be shadowed by a same-named local directory. A path may point at a single skill directory (one holding SKILL.md) or at a root of them, which expands to every immediate child that holds one. Relative paths resolve against the workflow file's directory — the same rule working_dir uses — so a team can version a skill alongside the workflow that uses it with no per-developer install step, and the workflow resolves identically from any working directory. Conductor expands roots itself rather than handing them to a provider, because eager injection needs a name per skill and claude-agent-sdk needs a <plugin>:<skill> name; doing it centrally keeps every provider seeing the same set. Skill paths are trusted input by design: the same workflow file can already declare type: script steps running arbitrary shell, so no additional allowlist applies.

  • runtime.skill_injection bounds eagerly injected skill content (issue #350) — warn_bytes (default 64KB) logs a warning and reports from conductor validate; max_bytes (default 128KB) fails the agent. Either can be set to null to disable it. Providers without a native skill surface (claude, hermes) have no progressive disclosure: AgentExecutor prepends each enabled skill's SKILL.md plus its entire references/ tree on every call and every retry, and there was previously no ceiling at all. The bundled conductor skill alone is ~117KB (~29K tokens), so the defaults deliberately straddle it — enabling it on claude now warns instead of breaking, while accumulating several large skills errors. Both limits are measured against the exact string being prepended and report a per-skill breakdown naming the offender. Providers with progressive disclosure (copilot, claude-agent-sdk) are unaffected.

  • hermes declares skills=True (issue #350) — the provider omitted skills from its CAPABILITIES, which defaults to False, so conductor validate rejected skills: on it while its own execute() docstring described eager injection working. Injection happens in AgentExecutor, upstream of every provider, so the path was always reachable and the declaration was simply inaccurate. Now bounded by runtime.skill_injection like claude.

  • claude-agent-sdk provider now honors working_dir — the directory resolved from agent.working_dir / runtime.working_dir is forwarded to ClaudeAgentOptions.cwd, so the claude CLI runs there and every stdio MCP server it spawns inherits the same directory. Previously the provider declared working_dir=False and conductor validate rejected any workflow that set it, which was accurate but left the provider out of step with copilot and claude. There is no per-server stamping as there is for Copilot, because the SDK's stdio server config has no working-directory field — inheritance from the CLI subprocess covers it. A missing directory still fails before the provider is reached, and strict_mcp_config remains enabled so a .mcp.json sitting in the new directory cannot inject undeclared servers. The claude CLI would also read CLAUDE.md and .claude/settings*.json from its working directory, but the same release pins setting_sources to an empty list (see the skills entry below), so those are no longer loaded from wherever the agent happens to run. Launch failures caused by a bad working directory are now reported as such rather than as connection problems, and are no longer treated as retryable. See docs/workflow-syntax.md and docs/providers/experimental.md. (#348)

  • claude-agent-sdk provider now supports MCP servers — workflow-level runtime.mcp_servers are translated to the SDK's own stdio / http / sse config shapes and passed through ClaudeAgentOptions, so an agent can use custom MCP tool servers and the built-in claude_code tool preset at the same time. Previously the two were mutually exclusive: the provider declared mcp_tools=False and the factory rejected any workflow declaring MCP servers. The generated config is written to a 0600 temp file and passed by path so resolved env values and Authorization headers never reach the claude CLI's command line, and strict_mcp_config is always enabled so ambient project/user MCP config cannot inject undeclared servers. A narrowing per-server tools: filter has no SDK equivalent and is refused (when the first agent on this provider runs) rather than silently ignored. See examples/claude-agent-sdk-mcp.yaml and docs/mcp-tools.md. (#335)

  • Conservative unwrapping of wrapper-shaped scalars. When a scalar output field receives an object holding exactly one value of the expected type under either the field's own name or a generic value/result key, providers unwrap it and log a warning instead of spending a recovery round-trip. Ambiguous shapes (two matching candidates) and any other key shape are left alone and re-prompted, so an object like {"error": "I could not complete the task"} is never laundered into an answer. Validation itself stays strict, so set and script step output is unaffected. (#343)

  • Non-object JSON responses are recoverable. A response parsing to a bare scalar, null, or an array is now re-prompted as a shape failure rather than producing an unhelpful terminal error. On Claude this previously reached validate_output inside the API error handler and surfaced as "check API key, model name, and request parameters"; on Copilot it reached the executor backstop and was reported as a missing required field. (#343)

  • agent_parse_recovery event. Recovery attempts were previously visible only under verbose console logging, so a run could burn its entire recovery budget without leaving a trace. All three providers now emit an event carrying the attempt number, the budget, whether the cause was schema or syntax, and the error. Rendered in the dashboard activity stream, the console, and the structured event log. (#343)

  • Output validation errors describe the offending value. Container values are rendered by shape (object with keys ['a', 'b']) rather than dumped, since validate_output also runs on set and script step output that may carry secrets. (#343)

  • Sub-workflow nodes in the dashboard are expandable before they run. A type: workflow node previously became expandable only once the engine actually reached that step, so the inner DAG of a sub-workflow that had not started yet was invisible. Conductor now resolves each sub-workflow's static topology up front and attaches it to the graph, so the real inner DAG can be expanded — recursively, for nested sub-workflows — from the moment the run starts. Resolution is best-effort: a missing file, registry error, cycle, or depth limit simply leaves the node collapsed until the engine reaches it. (#360)

Fixed

  • A malformed SKILL.md no longer fails silently (issue #350) — both the Copilot CLI and Claude Code skip a skill whose YAML frontmatter cannot be parsed, with no warning and no error, leaving an agent running without the knowledge its author asked for. The trap is ordinary: a description containing Triggers: ... as an unquoted plain scalar is invalid YAML. Conductor now parses the frontmatter itself, requires a non-empty name and description, and reports the underlying YAML error along with the description: | block-scalar fix. Enforced during resolution rather than only in conductor validate, because conductor run never invokes the static validator.

  • conductor validate rejects a claude-agent-sdk skill outside a plugin (issue #350) — that SDK exposes no bare skill-directory option, only plugin roots plus skill names, so such a skill is unreachable there even though copilot loads it fine. It previously surfaced as a runtime ProviderError on first execution; it is now reported before the run starts, naming the directory and offering both remedies (package it as a plugin, or run the agent on copilot).

  • skills: [] is now a real opt-out on claude-agent-sdk, and agents no longer inherit ambient skills from the machine. The provider left the SDK's setting_sources unset, so the claude CLI discovered and enabled skills from ~/.claude/skills/, every .claude/skills/ up the directory tree, and enabled plugins — none of which the workflow declared, and all of which varied by developer machine and launch directory. Conductor documents skills: [] as an explicit opt-out; on this provider it silently opted out of nothing. Two options now carry that fix together and neither is redundant: setting_sources is always [] (the same unconditional isolation strict_mcp_config already applies to MCP servers), and skills is always passed explicitly, because the SDK treats an omitted list as "CLI defaults apply" and re-defaults setting_sources to ["user", "project"] whenever skills is set without it. Behavior change: agents on this provider also stop picking up ambient CLAUDE.md, .claude/rules/*.md, user/project/local settings.json (including env and apiKeyHelper), and hooks. Instruction files can be supplied explicitly with --workspace-instructions (or --instructions); settings and hooks have no equivalent, so move anything load-bearing there into the environment. Note the SDK's skill list is a context filter, not a sandbox — undeclared skills are hidden from the model's listing, but their files stay readable on disk. (#352)

  • tools: [] no longer fails validation when no MCP servers are declared — the capability cross-check rejected an explicit empty allowlist against any provider with mcp_tools=True and workflow_tools_passthrough=False (such as aca), even when the workflow declared no mcp_servers and therefore had nothing to forward. The check is now gated on MCP servers actually being configured. (#335)

  • Schema-shape failures now go through the parse-recovery loop instead of killing the workflow. When an agent returned syntactically valid JSON whose fields had the wrong shape (for example an object where type: string was declared), the Copilot and Claude providers failed the run immediately with zero recovery attempts, even though max_parse_recovery_attempts exists for exactly this class of contract violation. Schema validation ran one layer up in executor/agent.py, after the provider had already returned and — for Copilot — after its SDK session had been disconnected, so the loop that could have re-prompted never saw the error. Both providers now validate inside the recovery loop, matching Hermes. (#343)

  • Exhausted recovery keeps the specific validation error. A schema-shape failure that survives every recovery attempt now re-raises the original ValidationError naming the offending field and its expected type, rather than collapsing into a generic "failed to parse structured output" provider error. Syntax failures keep raising ProviderError as before. This also fixes Hermes, which previously discarded the field detail. (#343)

  • Hermes now honors retry.max_parse_recovery_attempts. It used a hardcoded module constant of 3 and silently ignored the YAML value that Copilot and Claude both respect. (#343)

  • Nested array item schemas in output: are now enforced. validate_output type-checked only the top level of an array, so an array<object> output whose items were missing declared fields — or had the wrong types — passed validation silently, even though the full nested schema had been sent to the model. Validation now recurses through both object.properties and array.items at every depth, for LLM agents, set steps, and script steps alike. Behavior change: a workflow that was quietly emitting output violating its own declared nested schema will now fail with ValidationError instead of passing. Flat schemas and object nesting are unaffected, and arrays declared without items keep their existing passthrough. (#337)

  • MCP server connections are now closed by the task that opened them. Each stdio/session lifecycle is held in its own owner task and signalled at shutdown, so AnyIO cancel scopes always exit in the task that entered them. Previously a connection opened in a worker task and closed from the root task could raise a cancel-scope error during teardown, surfacing as a spurious failure at the end of an otherwise successful run. Cleanup failures are logged rather than masking the caller's own cancellation, and registering a duplicate server name is now rejected instead of orphaning the existing connection. (#353)

  • Non-ASCII agent output is no longer truncated ~6x too aggressively before semantic validation. The validator: grader and the dialog-trigger evaluator serialized the agent output with ASCII escaping before applying their fixed character budget, so every Cyrillic or CJK code point cost 6 characters and every emoji 12. Non-English output reached the grader with a fraction of the source material an equivalent English output would get, and the cut could land mid-escape, leaving malformed JSON in the prompt. Both now serialize without escaping, so the budget is measured in real characters for every language. (#356)

  • An inline-expanded sub-workflow now follows the live run after a loop-back. When a loop-back route re-invoked the same sequential sub-workflow, the inline graph expansion stayed pinned to the first, already-completed invocation, even though double-click navigation and the Activity tab correctly tracked the live one. Inline expansion now resolves newest-first, matching the rest of the dashboard. (#361)

  • Every historical sub-workflow iteration is now reachable from the dashboard. In the "Subworkflow Runs (N)" list, each row resolved by slot rather than by position, and every re-invocation of a sequential sub-workflow shares a slot — so clicking an older, completed run always landed on the most recent one. Rows now navigate to the exact run clicked, are labelled Iteration N once a slot repeats, and the breadcrumb trail says which iteration is being viewed. Following the live run (double-click, inline expansion, deep links) is deliberately unchanged. (#365)

  • Handled fail-open paths no longer print a full traceback at WARNING level. When a semantic validator rejected an output and the retry itself failed, Conductor correctly kept the original output and carried on — but logged the warning with a complete Python traceback, making a recovered run look like a crash. The warning is now a single line naming the exception type and message, with the traceback kept at DEBUG. The same treatment was applied to the sibling best-effort paths: validator call failures and timeouts, provider session-ID collection for checkpoints, checkpoint save and rotation failures, and event-callback errors in hermes. (#357)

Changed

  • The claude provider now runs its agentic loop through Pydantic AI. The hand-written inner loop was replaced with a Pydantic AI runtime while keeping Conductor's provider contract at the boundary — the same AgentOutput, event vocabulary, retry and interrupt semantics, usage accounting, MCP policy, and output validation. The visible gain is streaming: claude now emits model, reasoning, and tool events as they happen rather than only at completion, so the dashboard, console, and event log follow a Claude agent live the way they already followed Copilot. Structured output is produced natively by the runtime and still re-validated against the declared output: schema. This adds pydantic-ai>=1.44.0 as a required runtime dependency, so a conductor upgrade pulls in a larger dependency set than before. (#355)

  • claude-agent-sdk now loads skills natively instead of injecting them into every prompt. The provider previously took the eager preamble path on the grounds that the SDK had no skill surface — out of date, and expensive: the full SKILL.md plus the entire references/ tree was prepended to every call and every retry (~29K tokens for the bundled conductor skill). The owning Claude Code plugin is now registered on the session and the skill enabled by its <plugin>:<skill> name, so the CLI reads only the frontmatter up front and loads the body on demand. An agent with an explicit tools: [] is granted back the single Skill tool when it has skills enabled, since an empty base tool set would otherwise leave the declared skill unreachable. Wheels now also ship plugins/conductor/.claude-plugin/; without the manifest no plugin root resolves at all, so a non-editable install would fail every skills-enabled agent on this provider. (#352)

  • The claude-agent-sdk optional dependency floor is now claude-agent-sdk>=0.2.82 — the 0.2.x line is what Conductor tests against. (#335)

  • claude-agent-sdk agents no longer inherit ambient MCP configuration. Conductor now always sets strict_mcp_config, so a project .mcp.json, user-global settings, or plugin-provided servers are ignored and only servers declared in runtime.mcp_servers attach. Workflows that relied on Claude Code's own MCP settings must declare those servers in the workflow. (#335)

0.1.26 - 2026-07-27

Added

  • New experimental aca provider (Azure Container Apps) — delegates an agent's entire agentic loop, tools, and MCP calls to a remote ACA dynamic-sessions sandbox instead of running it on the host, so untrusted or isolation-sensitive agents execute in a disposable container. Includes provisioning tooling (scripts/aca/provision-pool.sh), an in-package runner image, an end-to-end example (examples/aca-coding-agent.yaml), and automatic inner-Copilot credential resolution — falling back through COPILOT_PROVIDER_BASE_URL (BYOK) → COPILOT_GITHUB_TOKEN/GH_TOKEN/GITHUB_TOKENgh auth token — so an operator already signed in with the GitHub CLI needs no ACA-specific credential setup. See docs/providers/aca.md for architecture, setup, and the declared experimental-tier capability carve-outs. (#284)

Fixed

  • Copilot and Hermes prompt schemas no longer fall back to a synthetic "The {field} field" description, matching Claude's existing behavior — fields without an explicit description now produce a shorter JSON Schema object instead of a made-up one. Hermes also now shares the same recursive prompt-schema builder as Copilot, fixing cases its old hand-rolled builder got wrong: array<object> items now include required alongside properties, and array<array> nesting recurses correctly instead of collapsing. (#317)

0.1.25 - 2026-07-21

Fixed

  • Web dashboard warns when stuck reconnecting after a silent crash — a new amber banner appears if the dashboard's WebSocket client has been disconnected and retrying for more than 60 seconds while a workflow is still marked "running", pointing at the best available log (--web-bg's captured stderr/stdout log, the --log-file debug log, or the launching terminal) so a silently crashed --web-bg process is no longer indistinguishable from a healthy, still-running workflow. (#332)

0.1.24 - 2026-07-21

Added

  • Grouped CLI command surface — related subcommands are now organised under noun groups: conductor checkpoint list (was conductor checkpoints) and conductor gate respond (was conductor gate-respond), alongside the existing registry group. The root --help groups commands into Run & Recover, Author & Inspect, Environment, Interact, and State panels, while the hot-path verbs (run, resume, validate, show, stop, replay, update, doctor) stay flat. (#275)

Deprecated

  • conductor checkpoints and conductor gate-respond — replaced by conductor checkpoint list and conductor gate respond respectively. The old names still work and forward to the new commands, but print a one-line stderr deprecation warning, are hidden from --help, and will be removed in a future release. (#275)

Fixed

  • --web-bg no longer aborts when a workflow contains a human_gate — the dashboard already supported resolving gates (modal, WebSocket gate_response, POST /api/gate-respond, conductor gate-respond), but the detached background process raced a CLI prompt against it and crashed with EOFError on its closed stdin. The gate now waits web-only when running in --web-bg (or any non-TTY context), and the CLI prints a notice pointing at the dashboard URL and conductor gate-respond instead of aborting the launch. (#286)
  • --web-bg could hang forever if no dashboard client ever connected — the auto-shutdown grace timer was only armed from WebSocket-disconnect code paths, so an unwatched run that finished before anyone opened the dashboard never started its grace countdown and the detached process became a zombie holding its port and PID file. The timer now also arms on the workflow's root completion event. (#318)
  • conductor doctor showed the copilot provider as red/unconfigured even when fully authenticated — credential env vars are now modeled as optional per provider; an absent optional credential (e.g. copilot's GitHub/Copilot CLI login) renders as a neutral with an explanatory note instead of a red , while genuinely required credentials (e.g. claude's ANTHROPIC_API_KEY) still flag as missing. (#319)
  • Web dashboard could keep showing a stale UI after conductor updateindex.html is now served with Cache-Control: no-cache so the browser revalidates it on every load and always picks up the current build's version-hashed asset bundle. CI also now fails the Frontend job if the committed static/ bundle doesn't match a fresh make build-frontend, so a frontend change can no longer merge without its built assets. (#321)

0.1.23 - 2026-07-20

Added

  • working_dir for LLM agents and their MCP servers — agents gain an optional working_dir (with a workflow-wide runtime.working_dir default) so an agent and its MCP servers run in a chosen directory. It is resolved with precedence agent > runtime > os.getcwd() and accepts static values or Jinja templates. The Copilot and Claude providers stamp the resolved directory onto the agent session and each MCP server; conductor validate rejects working_dir on providers that don't support it (hermes, claude-agent-sdk) and on wait / set / terminate / human_gate / workflow step types. See the "Working Directory" section of docs/mcp-tools.md. (#297)
  • runtime.tool_output limits for oversized MCP tool results — a configurable per-result cap stops a single large MCP tool result from overflowing the model's context window with a fatal token-limit error. Oversized results are truncated (max_chars, default 50000) and the full text is spilled to a temp file the agent can page through, with a notice surfaced in the console, event log, and dashboard. Claude truncates conductor-side; Copilot forwards the limit to its native SDK spill feature; the setting is ignored by claude-agent-sdk (managed via the native MAX_MCP_OUTPUT_TOKENS) and is N/A for hermes. See examples/tool-output-limits.yaml and the "Tool output limits" section of docs/mcp-tools.md. (#313)
  • Inline expand/collapse for subworkflows in the dashboard graph — subworkflow nodes now expand inline (collapsed by default) to reveal their internal DAG without leaving the current view, alongside the existing double-click drill-down "focus mode." Adds an Expand/Collapse-all toolbar control and an E keyboard shortcut, and expands for_each-of-workflow groups into inline sub-containers. (#316)

Fixed

  • Subworkflow-adjacent agent node could appear stuck "running" in the dashboard — the graph store now clones the subworkflowContexts tree on every event, so selectors keyed on it reliably observe nested status updates. An agent node next to a type: workflow step no longer renders as "running" after its underlying data is already completed (a pure rendering desync, not an engine data bug). (#308)

0.1.22 - 2026-07-15

Added

  • conductor doctor --models surfaces per-model reasoning-effort support and context-window limits — a new optional AgentProvider.get_model_capabilities hook (alongside the existing get_max_prompt_tokens / get_model_pricing hooks) reports, per model: which reasoning.effort levels it accepts, its default effort, and its prompt/output/context-window token limits. --models now renders a separate per-provider Models detail table with this data (the Providers table's Models column shows a count); the JSON models field is now a list of capability objects rather than plain id strings. The Copilot provider implements the hook fully via client.list_models(); the Claude provider derives reasoning-effort support from the existing thinking-model heuristic and reports prompt tokens only (the Anthropic SDK exposes no output/total-context split); other providers (claude-agent-sdk, hermes, openai-agents) don't implement model enumeration at all, so they show n/a in the Providers table and get no Models detail table. See the "Per-model capabilities" section in docs/cli-reference.md. (#301)

  • max reasoning-effort level — the unified reasoning scale is now low | medium | high | xhigh | max, unifying it with the GitHub Copilot CLI. On the Copilot provider max is forwarded to the SDK and still validated per-model against the model's advertised supported_reasoning_efforts. On the Claude provider max maps to a 59904-token extended-thinking budget (64000 − 4096, the largest budget that keeps the default answer headroom under the 64000-token output cap); both the main agentic-loop and dialog-turn code paths share the same clamping helper, so the cap is enforced consistently. The experimental Hermes provider keeps the original four levels — max is rejected both statically (conductor validate) and at execute time (including when a Jinja-templated reasoning.effort only resolves to max after rendering). (#299)

Fixed

  • Copilot per-model reasoning_effort validation was a silent no-op_validate_reasoning_effort_for_model read capabilities.supported_reasoning_efforts, but the installed github-copilot-sdk (>=1.0.0) exposes that field (and default_reasoning_effort) at the top level of the Model object, not nested under capabilities. The lookup always returned None, so the per-model check (including the max-rejection behavior from #299) never fired against the real SDK — a model without max support would only be caught by the backend, not by Conductor's own validation. Fixed to read the correct field; discovered and corrected while implementing #301, which needed the same field for doctor --models.

  • Install-script tests no longer pollute the developer's shell profile — the install scripts ran uv tool update-shell unconditionally, so the -m install_scripts integration tests appended each run's throwaway UV_TOOL_BIN_DIR to the real ~/.zshenv (and shell equivalents). Those stale entries shadowed the user's actual conductor install with an old v0.0.2 test fixture, so conductor reported the wrong version and conductor update appeared to do nothing. install.sh / install.ps1 now honor CONDUCTOR_INSTALL_SKIP_PATH_UPDATE=1 (and a matching --skip-path-update / -SkipPathUpdate flag) — set by default in the test harness — and a regression test asserts the scripts never touch shell profiles.

Documentation

  • README provider comparison table corrected — set Context Window to "Per-model" across all providers, set Pricing to "Subscription" for Copilot and Claude Agent SDK, labeled Claude Agent SDK as experimental in the top-level Features list (matching the Providers table's Tier column), and added a "Using Copilot" section with a config example and auth notes. (#296)

0.1.21 - 2026-07-13

Added

  • Connect the Copilot provider to an existing runtimeruntime.provider gains a Copilot-only runtime_url (plus optional runtime_token) that points Conductor at an already-running copilot --headless process instead of spawning its own nested one. Agents share the authenticated runtime process while retaining separate SDK sessions. Both fields also resolve from the namespaced COPILOT_PROVIDER_RUNTIME_URL / COPILOT_PROVIDER_RUNTIME_TOKEN environment variables, which activate the connection with no YAML — the zero-config path for external orchestrators that already own an authenticated Copilot process. Runtime transport can be combined with custom model-provider routing. See examples/copilot-existing-runtime.yaml and the "Connecting to an Existing Copilot Runtime" section of docs/configuration.md.
  • Provider-supplied model pricing — cost reporting now resolves pricing via a new AgentProvider.get_model_pricing hook before falling back to the static table. Resolution order is workflow cost.pricing → provider hook → built-in DEFAULT_PRICING → unpriced. The Copilot provider derives live rates from its SDK billing metadata (AI Credits → USD), so newly-released models are priced without waiting for a table refresh; providers whose SDK exposes no pricing (e.g. the Anthropic API) fall back to the table. (#265)
  • conductor doctor — provider & environment diagnostics — a safe, read-only command that reports which providers are installed, their capability tier (stable / experimental), which credential environment variables are detected (presence only — values are never printed), plus Conductor version / update status and configured registries. Offline by default; --check tests provider connections, --models lists available models, --provider NAME scopes to one provider, and --json emits machine-readable output for CI. Exit code is 1 only when --check is set and the scoped provider (default copilot) fails to connect. Also adds a public list_models() method to the provider interface (implemented for Copilot and Claude). (#274)
  • Jinja include/import/extends in !file-loaded prompts — prompt templates loaded via prompt: !file ... (and system_prompt: !file ...) now support loader-dependent Jinja constructs, resolved relative to the prompt file's own directory, enabling reusable prompt partials across workflows. Inline prompts that attempt these constructs get a clear error instead of a confusing failure. (#291, closes #287)

Fixed

  • Cost summary silently undercounted unpriced models — the run summary summed only the priced subset of agents and presented it as the complete total, so spend on models without available pricing vanished with no signal. Unpriced agents are now surfaced in the CLI summary and the web dashboard status bar (~$X (N agents unpriced: model-a, model-b)), so a partial total is never shown as a clean, complete number. (#265)
  • Missing pricing for current models costed them at $0 — several current models had no DEFAULT_PRICING entry, so get_pricing returned None and they were silently costed at $0 in the Token Usage Summary / cost breakdown (dotted version suffixes like claude-opus-4.8 are not bridged by the --delimited fuzzy fallback). Added entries for claude-opus-4.7, claude-opus-4.8, claude-sonnet-5, gpt-5.3-codex, gpt-5.4, gpt-5.5, gpt-5-mini, gpt-5.4-mini, and gemini-3.5-flash. (The related sub-workflow usage under-reporting in the same issue was already fixed in #212.) (#266)
  • for_each inline agents skipped most provider-capability checksconductor validate's provider-capability cross-check applied the full per-agent matrix (reasoning effort, structured output, per-agent MCP provider override, explicit max_session_seconds) only to top-level agents:. A for_each group's inline agent — which runs at runtime exactly like a top-level agent — was checked for tool allowlists only, so it could request a capability its provider doesn't support (e.g. reasoning.effort: high on the claude-agent-sdk provider), pass validation, then fail or silently degrade mid-iteration. The per-agent checks are now shared and run over inline agents too, and the workflow-level mcp_servers / max_session_seconds inheritance checks now also account for inline agents on the default provider. (#270)
  • For-each dive-in worked only for finished items — in the web dashboard's for-each group detail panel, the per-item "Dive into subworkflow" control was nested inside the row's expand/collapse <button>, which is disabled while an item has no expandable details (a running workflow-type iteration that has not yet produced a prompt/output/activity/error). A disabled button suppresses clicks across its whole subtree, so dive-in only fired once the item had failed or completed. The toggle and the dive-in control are now siblings, so dive-in stays clickable for running items too. (#273)
  • Custom default Jinja filter rejected the standard boolean argument — Conductor's override of Jinja2's built-in default filter only accepted two parameters, so valid templates using the standard third boolean argument raised a TypeError. The filter now matches Jinja's built-in signature (default(value, default_value="", boolean=False)) while preserving Conductor's existing two-argument behavior when boolean is omitted. (#292, closes #288)
  • Claude provider silently dropped agent.system_prompt — the native claude provider (Anthropic Messages API) ignored system_prompt instead of sending it. It is now passed as the native top-level system parameter on every API call in the agent execution path (main loop, tool-use iterations, parse recovery, interrupt partial output, retries), and the validator rubric's system prompt now reaches the model the same way. (#293, closes #289)

Documentation

  • --quiet / --silent documented as run options — both flags are root-level options defined on the app callback, so they must appear before the subcommand (conductor --quiet run workflow.yaml); placing them after it is rejected with "No such option". docs/cli-reference.md and the README listed them in the conductor run options table, while docs/cli-reference.md also showed post-subcommand examples (conductor run workflow.yaml --quiet) that fail as written — as did the conductor run --help epilog. Moved the flags to a new Root-Level Options section, corrected the examples (docs and --help text), and fixed the same mis-ordering in the conductor skill's quick reference.

0.1.20 - 2026-06-26

Added

  • Hermes provider (experimental) — optional third provider built on the NousResearch hermes-agent library, which manages its own tool ecosystem (no MCP configuration). Install separately with pip install hermes-agent; Conductor works without it. Declared in the experimental provider tier with documented capability carve-outs (no MCP servers, no per-agent workflow tools: allowlist, structured output via prompt injection). Supports custom endpoints via structured runtime.provider (base_url / api_key), hermes_home profiles, hermes_toolsets, streaming + reasoning event callbacks, cooperative interrupt, session-history resume, and max_session_seconds. See docs/providers/hermes.md. (#235)
  • Cost budget enforcement — set a USD ceiling for a run with runtime.budget_usd and choose how the engine reacts via runtime.budget_mode: audit (default — track spend and warn as the cap is approached) or enforce (stop the run once the projected cost would exceed the cap). Off by default — no budget tracking happens unless budget_usd is set. (#212)
  • External-workflow friction improvements — four knobs that surfaced while running real-world workflows: per-agent output_mode (raw | envelope) for cross-provider output-shape parity; retry.max_parse_recovery_attempts to cap the in-session JSON-correction prompts sent when an agent's output fails to parse; a new conductor gate-respond --port <port> --choice <value> command to resolve a human gate from the CLI (handy for --web / --web-bg runs); and more robust Windows path handling. (#234)
  • Templated reasoning.effort and context_tier — both per-agent fields now accept Jinja2 templates (e.g. effort: "{{ workflow.input.eff }}") resolved at runtime against the workflow context, instead of being rejected at YAML load as invalid enum literals. The rendered value is re-validated against the allowed set. (#263, closes #262)
  • Scoped applyTo instruction loading.github/instructions/*.md files with a scoped applyTo glob (e.g. **/*.cs, services/foo/**) are now loaded when their scope overlaps the run's working directory, instead of being silently dropped unless applyTo: "**". Multi-glob values separated by ; or , are supported, and the closest-owning convention directory wins for nested instruction files. (#238, closes #231)

Fixed

  • Copilot model attribution for auto-routed runs — when an agent uses model: auto, AgentOutput.model now records the concrete model the Copilot SDK resolved to (captured from the assistant.usage event) instead of the literal string "auto", so token usage and cost are attributed to the real model. (#268)
  • claude-agent-sdk default tool preset — an agent that omits tools: on the claude-agent-sdk provider again receives the full claude_code preset instead of zero tools. The omitted-vs-empty distinction (tools: absent means "all tools"; tools: [] means "none") was being lost at the executor→provider boundary whenever the workflow declared no MCP tools. (#269)

0.1.19 - 2026-06-16

Added

  • Context tier — new context_tier knob (default | long_context) to select a model's long-context (e.g. 1M-token) window on the Copilot provider. Set per agent via context_tier: (sibling to model) or workflow-wide via runtime.default_context_tier; the per-agent value wins. It composes independently with reasoning.effort (the two map to separate create_session kwargs). The Copilot provider forwards the resolved value as context_tier to create_session; other providers ignore it. Only valid on standard agent-type agents (rejected on script, human_gate, and workflow agents). See examples/context-tier.yaml and Context Tier. (#251)
  • Validator block — an optional validator: block on provider-backed agents that runs a second LLM call to grade the agent's output against a user-defined rubric. Distinct from retry: (transient failures, same prompt) and output: (shape/type): it catches structurally valid but semantically wrong, incomplete, or off-rubric output. Fields: criteria (required), model (defaults to the agent's model), and max_retries (0 or 1, default 1, hard-capped at 1). On passed: false the agent re-runs once with the validator's issues appended under a ## Validation feedback section; the second output is final (no second validation loop). Validation is fail-open — grader errors or unparseable responses are logged and treated as a pass, so a hung or broken grader can't block the workflow. Wired into the main loop, parallel groups, and for-each loops; emits agent_validator_start / agent_validator_complete / agent_validation_failed events surfaced in --verbose console output and the web dashboard, and records the grading call (plus any discarded first attempt) as a separate <agent> (validator) usage row. Rejected on script / human_gate / workflow / wait / set / terminate steps. See examples/validator.yaml and the Validator section of the workflow syntax docs. (#256, closes #220)
  • Periodic / milestone checkpoints — opt-in runtime.checkpoint block that saves a resumable checkpoint at step boundaries so a long run that stalls or is hard-killed stays recoverable (previously Conductor only checkpointed on failure). Two OR-combined triggers — every_agent: true (save at every step boundary) and every_seconds: N (a throttle: save at the first boundary once N seconds have elapsed since the last checkpoint) — plus keep_last (default 5) to rotate older periodic checkpoints per run. Off by default, so failure-only behaviour is preserved. Periodic checkpoints are scoped to their own run and trigger, so failure checkpoints and other runs' files are never rotated away, and they are cleaned up automatically on clean completion or an explicit terminate. A failed periodic save emits a checkpoint_save_failed event (console + JSONL + dashboard) so a recovery-reliant run is never left silently without checkpoints, and conductor checkpoints gains a Trigger column. See examples/periodic-checkpoints.yaml and the Periodic Checkpoints section of the workflow syntax docs. (#255, closes #244)
  • Script stdin: payload transporttype: script steps accept a new stdin: field: a Jinja2 string template rendered against the workflow context and piped to the child process on stdin as UTF-8. Workflows can now hand large or structured payloads to scripts without hitting command-line length limits (notably Windows, which caps the command line at ~32 KB), removing argument-name-specific temp-file workarounds. stdin and args are orthogonal; an explicit empty string pipes an immediate EOF, and omitting it preserves the legacy behaviour of inheriting the parent's stdin. The payload is streamed in the background so multi-MB inputs can't deadlock, the submitted byte count is surfaced as stdin_bytes on the script_completed event, and an unencodable payload raises a named ExecutionError. Rejected on every non-script step type. See examples/script-stdin.yaml. (#253, refs #18)
  • Experimental provider tier — providers that delegate part of the agentic loop to an upstream SDK can now declare an experimental stability tier with explicit, allowed capability carve-outs instead of silently eroding provider parity. Every provider declares a class-level CAPABILITIES (ProviderCapabilities) descriptor, and conductor validate cross-checks workflow features against each agent's resolved provider — surfacing silent mismatches (unsupported runtime.mcp_servers, non-empty per-agent tools: allowlists, reasoning.effort, structured output:, concurrent use in parallel/for-each groups, max_session_seconds) at validate time rather than at runtime. The workflow_started event gains a providers block (tier, upstream pin, maintainer, full capability dump) plus a provider_name per agent; the CLI prints a one-time banner per experimental provider per run, and the web dashboard renders a yellow "exp" badge on affected agent nodes. See Experimental Providers. (#242, closes #241)
  • claude-agent-sdk provider — a new, experimental provider that delegates the agentic loop, tool execution, and structured-output extraction to the Claude Code CLI via the claude-agent-sdk package. Unlike the raw claude provider it does not manage its own retry logic, MCP servers, or tool wiring — the SDK runtime owns those. It achieves event and output parity (agent_turn_start, agent_message, agent_reasoning, agent_tool_start / agent_tool_complete, and the standard AgentOutput shape), pairing real ToolResultBlock results rather than emitting nulls. Workflow-level runtime.mcp_servers, non-empty per-agent tools: lists, and temperature / max_tokens are rejected at the factory (the CLI controls these). Install with the optional extra (pip install conductor[claude-agent-sdk]) plus the claude CLI. See examples/experimental-claude-agent-sdk.yaml. (#104)

Fixed

  • Web dashboard Stop/Kill now always writes a checkpoint (or clearly explains why it couldn't). Previously, killing a run while an agent was actively executing — or clicking Stop during the brief startup window before the engine bound its interrupt event — cancelled the engine task from the CLI wrapper, bypassing the engine's failure handling so no checkpoint and no workflow_failed event were produced and progress was silently lost. Now:
    • A dashboard stop that cancels the engine routes through a best-effort checkpoint + workflow_failed/checkpoint_saved emit (WorkflowEngine.handle_dashboard_stop), so the run is resumable with conductor resume.
    • POST /api/stop during startup is queued until the interrupt event is bound (graceful pause path) instead of falling back to a hard cancel.
    • The dashboard shows a dedicated, calm "Workflow Stopped" banner with Checkpoint saved: <path> — or No checkpoint could be saved — <reason> when one genuinely couldn't be written — instead of an alarming red "Workflow Failed". (#245)
  • human_gate agents: the dict returned by prompt_for text-collection fields is no longer spread into the gate's output root, where it could silently overwrite the reserved selected key (e.g. an option declaring prompt_for: selected would clobber the chosen option value with whatever the user typed). Collected values are now nested under an explicit additional_input key, matching the shape the gate_resolved event already used. (#237)
  • context: explicit mode now supports nested output projection in input: declarations. References of the form agent_name.output.field.subfield... (and the agent_name.field.subfield... shorthand) project arbitrarily deep into a prior step's structured output, where previously only a single level (agent_name.output.field) resolved and deeper paths silently failed. Optional refs (?) skip missing intermediate paths, and projected leaves are deep-copied to avoid mutation aliasing. Static validation in conductor validate was aligned with the runtime so valid nested references no longer fail validation. (#239)

Changed

  • BREAKING (templates)human_gate output shape changed.
    • Before: {{ <gate>.output.<prompt_for_field> }} (root-level).
    • After: {{ <gate>.output.additional_input.<prompt_for_field> }} (nested).
    • Gates without any prompt_for now produce additional_input: {} rather than just {"selected": ...} — the key is always present.
    • <gate>.output.selected is unchanged.
    • Templates that referenced the old flat path now raise TemplateError (StrictUndefined), so the migration fails loudly rather than rendering to empty strings.
    • In context: explicit mode, input: declarations can reference either <gate>.output.additional_input (the whole dict) or an individual <gate>.output.additional_input.<field>. Nested explicit-input projection landed in this same release (#239), so the dotted field path now resolves directly; you can also still declare the parent and read individual fields via Jinja2 in the consuming agent's prompt or output template.

0.1.18 - 2026-05-28

Added

  • New type: set workflow step that evaluates Jinja2 expressions and binds the results into the workflow context — no LLM call, no subprocess, no I/O. Two surface forms: value: (single expression bound as <step>.output, scalar / list / dict by auto-detection or explicit output_type:) and values: (named bindings rendered in one pass against the pre-step context and bound as <step>.output.<key>). Type detection defaults to YAML auto-parsing with a JSON-safety pass that converts datetime/date/time to ISO 8601 strings and raises ExecutionError on other non-JSON-safe values (including non-string dict keys) so checkpoint round-trips stay stable. Explicit output_type: (single-value only) supports string, number, integer, boolean, list, dict. The engine dispatches set steps in the main loop, parallel groups, and for-each groups via the shared _run_set_step helper, emitting set_started / set_completed / set_failed and enforcing the output: schema (rejected for scalar outputs with a friendly suggestion). WorkflowContext.store was widened to accept any JSON-safe value; _add_agent_input returns scalars verbatim for step.output and raises a clear KeyError for step.output.field shorthand on non-dict outputs. The web dashboard adds a dedicated SetNode (variable icon, key count / value preview) and SetDetail panel showing output type, bindings, and rendered value. New examples/set-step.yaml demonstrates single + multi binding plus a boolean route on the derived flag (#226, closes #221).
  • New type: wait workflow step that pauses execution for a parsed duration via in-process asyncio.sleep. Cross-platform — no shell sleep dependency. Use for rate-limit cooldowns, polling intervals, external-system catch-up, and demos. The duration: field accepts plain numbers (seconds), suffixed strings ("500ms", "60s", "2.5m", "1h"), or a Jinja2 template that renders to one of those (e.g. "{{ workflow.input.poll_interval }}s"). Schema enforces 0 < duration <= 24h and rejects boolean values pre-coercion. Esc / Ctrl+G cancels in-progress waits immediately (the engine races the sleep against the interrupt event), and the workflow-level limits.timeout_seconds also cancels them. Wait steps emit wait_started / wait_completed / wait_failed events alongside the generic agent_started (with agent_type: "wait"), so existing dashboards keyed on agent lifecycle pick them up automatically. The dashboard adds a dedicated WaitNode (clock icon) and WaitDetail panel that show the requested duration, actual elapsed time, reason, and an "interrupted" indicator. The public output contract is strict — only {"waited_seconds": float} is exposed to workflow context; extra metadata lives in event payloads. Wait steps count toward limits.max_iterations (each pause is one step) but are not subject to max_agent_iterations (per-LLM-agent tool counter). Wait cannot be used inside parallel or for_each groups. New examples/wait-step.yaml demonstrates a polling pattern with a templated poll interval and route loop-back (#224, closes #218).
  • New type: terminate workflow step that explicitly ends the workflow with a structured status (success | failed) and Jinja2-rendered reason, plus an optional output_template (dict[str, str]) that replaces the workflow-level output: mapping for that termination path. Reaching a terminate step ends the workflow immediately (no routes evaluated after). status: success returns the rendered output cleanly (CLI exit 0, dashboard ✅, emits workflow_completed { termination_reason, terminated_by, is_explicit: true, status: "success" }); status: failed raises a new WorkflowTerminated exception (ExecutionError subclass), gives the CLI a non-zero exit code while still printing the rendered output JSON to stdout for downstream tooling, and intentionally skips the on-failure checkpoint save because explicit termination is not a resumable transient failure. Inside a sub-workflow, a failed terminate is downgraded at the parent boundary to a new SubworkflowTerminatedError (also an ExecutionError) preserving the child's rendered terminated_output / terminated_reason / terminated_by as structured attributes, so the parent treats it as a normal sub-workflow failure (its own workflow_failed does NOT inherit is_explicit: true) while debugging surfaces can still inspect what the child intended to emit. Schema validation rejects routes, tools, output, prompt, model, provider, and the other agent-only fields on terminate steps, and conversely rejects status / reason / output_template on every other step type so authors who forget type: terminate get a clear error instead of silently dropped fields. Terminate cannot be used as a parallel-group member or as a for_each inline agent — route to one from those groups' routes: instead. The example workflow lives at examples/terminate.yaml (#219).
  • runtime.provider now accepts either the bare string shorthand (provider: copilot) or a structured ProviderSettings object that forwards a ProviderConfig to the Copilot SDK's create_session(provider=…) parameter. This lets workflows route the Copilot SDK at OpenAI-compatible / Azure / Anthropic endpoints — Ollama, vLLM, LM Studio, Azure OpenAI, llamafile, or any other OpenAI-compatible REST endpoint — instead of being locked to the GitHub Copilot service. The structured form supports name, type (openai|azure|anthropic), wire_api (completions|responses), base_url, api_key, bearer_token, headers, and azure.api_version. api_key and bearer_token are Pydantic SecretStr (redacted in model_dump, dashboard payloads, event logs, and checkpoints). Custom routing activates only when YAML sets at least one non-name field — ambient OPENAI_* env vars never divert default routing on their own. Once activated, missing fields fall back from COPILOT_PROVIDER_BASE_URLOPENAI_BASE_URL for base_url, COPILOT_PROVIDER_API_KEY for api_key, and COPILOT_PROVIDER_BEARER_TOKEN for bearer_token. Ambient OPENAI_API_KEY is intentionally NOT consulted as an implicit fallback (credential-leak risk); use api_key: ${OPENAI_API_KEY} YAML interpolation for explicit opt-in. The schema rejects every non-name field when name != "copilot" (structured config for Claude / openai-agents is a follow-up), and rejects anchorless or empty combinations (wire_api / type / headers / azure alone, empty headers, empty SecretStr, empty azure block) so silent no-ops cannot reach the SDK. Custom routing applies to both agent execution and dialog turns so all sessions hit the same endpoint. See examples/copilot-local-llm.yaml and Configuration → Custom Provider Routing (#225, #136).

Added

  • New output_mode field on AgentDef (raw | envelope). Setting output_mode: raw bypasses JSON schema injection and parse-recovery entirely, wrapping the model's response as {"result": "<text>"}. Useful for agents that produce large Markdown, prose, or code output that should not be JSON-extracted. output_mode: raw is incompatible with output: — declaring both raises a ValidationError at config load time.
  • New max_parse_recovery_attempts field on RetryPolicy (YAML retry: block, per-agent or workflow-level). Overrides the provider default (Copilot: 5, Claude: 2) for agents that need tighter or looser in-session parse-recovery budgets. Accepts integer 0–10; 0 disables all recovery attempts and lets the first parse failure propagate immediately. Threaded through both the Copilot and Claude providers.
  • New POST /api/gate-respond and GET /api/gate-status HTTP API endpoints on the web dashboard server. GET /api/gate-status returns whether a human_gate agent is currently waiting, and which agent name it is. POST /api/gate-respond resolves the parked gate by injecting a GateResponse into the engine's queue. When the optional CONDUCTOR_GATE_TOKEN secret is configured on the server, POST /api/gate-respond requires an Authorization: Bearer <token> header matching it (compared in constant time) — requests with a missing or mismatched token are rejected with HTTP 403. GET /api/gate-status is unauthenticated. The matching WebSocket gate_response path enforces the same token and waiting-state checks so it cannot be used to bypass auth.
  • New conductor gate-respond CLI command for resolving a parked human gate from the command line without opening a browser. Accepts --port, --choice, --agent (auto-discovered via /api/gate-status when omitted), --input, and --token / CONDUCTOR_GATE_TOKEN env var. Designed for SSH or headless environments where the web dashboard UI is unreachable.
  • script steps now resolve a bare command name (e.g. python) or an extension-less path against the executable search path before launching, so the binary the shell would pick is the one that runs (and a Windows path missing its .exe/.cmd suffix resolves correctly). Resolution uses the subprocess's own PATH — including any env.PATH override on the step — so the resolved binary matches what the child process would execute. Relative paths containing a separator are left untouched so they keep resolving against working_dir, and an unresolvable command falls back to the rendered value so the existing not-found error still fires.

Changed

  • Breaking (Claude provider): ClaudeProvider._extract_text_content now returns {"result": "<text>"} instead of {"text": "<text>"}. This aligns the Claude provider with the Copilot provider (cross-provider parity). Any existing Claude workflow that references {{ <agent>.output.text }} must be updated to {{ <agent>.output.result }}. Workflows that declare an output: schema are unaffected (the schema fields take precedence). See the new output_mode: raw feature if you need to consume unstructured text output reliably across both providers.

Fixed

  • _verbose_console is now silent-aware at the source: a _SilentAwareConsole subclass no-ops every .print(...) when is_verbose() is False, so the remaining conductor --silent stderr leaks (dashboard-failed-to-start and log-file-open warnings, workflow-hash mismatch, "Press Esc to interrupt", "Event log written to…", "Log written to…", _print_resume_instructions, and the replay command's "Press Ctrl+C to exit" / "Replay stopped" banners) no longer reach stderr. The app-wide console remains unchanged because it carries real error messages; the two replay prints are gated per-call. conductor --silent replay <log> now produces zero bytes on stderr (#223, closes #209).
  • Parse-exhaustion ProviderError (after all in-session recovery attempts are spent) is now marked is_retryable=False in both Copilot and Claude providers. Previously Copilot marked it is_retryable=True, causing the outer retry loop to re-run the entire agent up to 3× on deterministic parse failures — burning tokens with no chance of success.
  • Parse-exhaustion error messages now include the first 500 characters of the model's response (up from 200) and suggest output_mode: raw as a fix.
  • parse_json_output and the Copilot provider's _extract_json now use a two-stage fenced-block extraction (non-greedy re.findall + per-candidate try-parse, then a greedy single-capture fallback) so JSON whose string fields contain triple-backtick substrings no longer matches prematurely and falls into parse-recovery loops, while responses with multiple fenced JSON blocks still pick the first valid one. Resolves a recurring failure mode for agents emitting Markdown-bearing JSON (external-workflow-friction Issue #1) (#232).
  • conductor run --web-bg and conductor resume --web-bg now abort before forking when the workflow contains a human_gate agent (including gates nested in for_each.agent) and --skip-gates is not set, with a message listing the four supported options. resume --web-bg also recovers the workflow path from the checkpoint when invoked without an explicit workflow argument so the guard still fires. Previously the detached child crashed with EOFError and the parent only reported "Background process exited immediately with code 1" (Issue #8).

Documentation

  • New "Choosing whether to declare output:" section in docs/workflow-syntax.md describing when to declare a schema versus consuming raw <agent>.output.result for prose or large JSON. Closes a documentation gap that contributed to misconfiguration of agents emitting large payloads (Issue #2).
  • docs/cli-reference.md --web-bg section now documents the human_gate incompatibility and the new pre-fork validation behavior.

Added

  • Workflow limits.budget_usd and limits.budget_mode (audit | enforce) cap cumulative LLM cost across a run. audit (default) emits a budget_exceeded event and continues so users can profile costs before enforcing; enforce saves a checkpoint and stops with BudgetExceededError. Resuming with conductor resume starts a fresh budget window (cumulative spend resets to $0), so the remaining work runs under a full budget — raising budget_usd first is optional. Sub-workflow spend is merged into the parent so a parent budget accounts for delegated cost. Schema, engine enforcement at all five existing limit-check points, resume parity for restored budget state, and the new BudgetExceededError type are wired end-to-end. See docs/workflow-syntax.md and docs/configuration.md for the graduation path.

0.1.17 - 2026-05-21

Added

  • Script agents can now declare an output: schema using the same OutputField syntax as LLM agents. When declared, the engine parses stdout as JSON and validates it against the schema before emitting script_completed; missing fields, wrong types, non-JSON stdout, empty stdout, and JSON arrays/scalars all raise ValidationError and emit script_failed (with stdout/stderr/exit_code) instead of completing. Validation runs on the merged output dict so declared stdout / stderr / exit_code fields validate the value downstream actually sees (matching the PR #122 shadowing contract). An explicit output: {} opts into strict JSON-object mode with zero required fields. Without a declared schema, the legacy best-effort JSON-stdout auto-merge from PR #122 is fully preserved, so this is purely additive. Routing conditions can now reference declared fields (e.g. when: "phase == 'planning'") rather than opaque exit codes (#206, #118).
  • conductor validate now warns on undeclared agent.output references and field-level mismatches in explicit context mode, closing two follow-up gaps left by PR #125 that still produced the runtime TemplateError: 'dict object' has no attribute 'X' from issue #105. The validator now tracks declared fields per agent root (a.output.foo vs a.output.bar), so a prompt that references an undeclared field on an otherwise-declared agent surfaces a warning instead of a runtime failure; the same logic applies to static parallel groups (pg.outputs.member.field). Output-vs-error namespaces are tracked independently so input: ["pg.errors"] no longer silently suppresses warnings for {{ pg.outputs.* }} references, and the AST walker now filters inner-link Getattr nodes (no more spurious whole-output refs from {{ a.output.bar }} chains), detects method-call nodes ({% for k,v in a.output.items() %} registers as a whole-output ref), and degrades gracefully on TemplateAssertionError. For-each groups remain skipped (whole-member copy makes field precision a false positive); human_gate is now correctly excluded from agent.output warnings since the engine renders gate prompts in accumulate mode (#208, refs #105).

Changed

  • Copilot provider verbose log lines (tool calls, reasoning, processing indicators, idle/parse recovery) are now prefixed with the originating agent name in parallel and for-each runs, eliminating the un-attributable interleaved output that made the for-each case unreadable (every iteration previously shared the same agent name). An optional agent_name parameter is plumbed through _execute_sdk_call_send_and_wait_log_event_verbose and rendered as a magenta [agent_name] tag between the tree icon and event content (continuation lines tagged too). For-each iterations additionally get a model_copy() of the per-iteration agent with name = f"{name}[{key}]" so each iteration produces a distinct tag; the original AgentDef is untouched and context lookups still use the unqualified name. Static parallel groups are unaffected — each agent already has a unique name. The _item_callback merge order is flipped so the wrapper's agent_name/item_key win over any qualified name the provider emits, preserving the dashboard/JSONL event contract (agent_name = for-each group name; item_key disambiguates iterations). Backward compatible: agent_name defaults to None for sequential agents (#207, closes #16).

Fixed

  • conductor resume … --web and --web-bg no longer open an empty dashboard. Checkpoints now record the original run_id and JSONL event_log_path. On resume the dashboard's history is seeded BEFORE it accepts clients: the CLI prepends a fresh workflow_started event built from the current YAML (so historical events apply to the correct topology), then replays the original JSONL log line-by-line (or, when no log file is available, synthesises minimal *_started/*_completed pairs from the restored execution history). The resumed engine's own workflow_started emit is suppressed so the dashboard sees exactly one root start — no wfDepth double-counting. Root-level workflow_completed / workflow_failed / checkpoint_saved events from the original run are filtered out on replay; subworkflow lifecycle events are preserved so the frontend's context tracking stays balanced. The resumed EventLogSubscriber appends to the original log, preserving run_id across resume generations so log/timeline correlation tools see one continuous run (#167).

  • --web-bg startup crashes on Windows are no longer silent (#116). Three changes work together to make any crash forensically traceable:

    • conductor.cli.bg_runner now captures the detached child's stdout and stderr to log files in $TMPDIR/conductor/ (named to match the existing .events.jsonl filename) instead of discarding them with subprocess.DEVNULL. A Python traceback or faulthandler dump from the child now survives the parent's exit. The captured stderr path is printed alongside the dashboard URL and is included in every background-launch failure message so users always know where to look.
    • conductor/__init__.py enables faulthandler at import time (writing to sys.__stderr__), so a native crash — segfault, abort, fatal Python error — dumps a Python-level stack trace into the captured stderr log.
    • WorkflowEngine._execute_loop now catches BaseException (in addition to the existing KeyboardInterrupt / ConductorError / Exception arms) and emits a workflow_failed event with is_base_exception: true before re-raising. A bare SystemExit or other non-Exception failure between agent_started and agent_prompt_rendered now leaves a structured failure event in the JSONL log instead of an unexplained two-event truncation. An explicit except asyncio.CancelledError: raise arm sits in front of it so a normal dashboard-stop or parent cancellation is not mis-reported as an unexpected failure. Two new env vars (CONDUCTOR_RUN_ID, CONDUCTOR_BG_STDERR_LOG, CONDUCTOR_BG_STDOUT_LOG) propagate the parent-chosen run id and log paths to the child so the bg log files and the child's events JSONL share an 8-hex run id in their filenames, and workflow_started system metadata surfaces both bg log paths to the dashboard. The root cause of the underlying intermittent Windows crash is still pending — this change makes it diagnosable rather than invisible.
  • Workflows that configure reasoning.effort (or workflow-wide runtime.default_reasoning_effort) on the Copilot provider were broken for every named Copilot model when running against github-copilot-sdk 0.3.0. The SDK's models.list response includes a billing object on every model, but none of them currently ship the multiplier field that the SDK's ModelBilling.from_dict parser treats as required — so every model in the response triggers ValueError("Missing required field 'multiplier' in ModelBilling"), which kills the entire list_models() call. The error then leaked through the narrow except tuple in _validate_reasoning_effort_for_model (and get_max_prompt_tokens), poisoned the retry loop, and surfaced as Dialog turn failed: … after three wasted attempts. (get_max_prompt_tokens was rescued by the engine's outer except Exception, so context-window metadata was silently unavailable rather than fatal.) Both metadata methods now catch any Exception raised at the SDK boundary and treat the failure as "metadata unavailable" — validation is skipped permissively and the configured reasoning_effort is forwarded to create_session as before. asyncio.CancelledError/KeyboardInterrupt/SystemExit (all BaseException subclasses) still propagate.

  • conductor resume --web-bg (and --web) no longer exit silently when a workflow exceeds max_iterations. The bg child was forked with --no-interactive and stdin=subprocess.DEVNULL, so when the engine hit the limit, IntPrompt.ask raised EOFError, got coerced to 0 (stop), and the workflow ended with no way to recover. The max-iterations gate can now be resolved from the dashboard. New resolution policy: skip_gates auto-stops (unchanged); no web dashboard uses the legacy CLI prompt (unchanged); web dashboard + bg/non-TTY stdin uses a web-only wait (the CLI prompt is deliberately NOT raced because it would synchronously EOFError and win every dashboard click), with dashboard.wait_for_stop() racing so POST /api/stop can terminate the wait when no dashboard tab is open; web dashboard + TTY foreground races CLI vs web. Each iteration_limit_reached payload carries a uuid4 gate_id that the dashboard must echo back in iteration_limit_response, and the server matches/discards stale responses so a delayed double-click cannot be misapplied to a later gate. iteration_limit_resolved includes the same gate_id so subscribers can correlate the pair. New top-level IterationLimitModal (parallel-group gates can't attach to a per-agent panel) shows iteration count, recent agent history, and number input; it is hidden when skip_gates is true and does not close on Escape so the workflow can't be accidentally orphaned (#202, fixes #198).

  • conductor run --web-bg and conductor resume --web-bg no longer get killed within ~10 seconds when launched from a shell wrapper that runs commands inside a Windows job object with JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE (GitHub Actions runners, VS Code integrated terminal, JetBrains IDE terminals, GitHub Copilot CLI shell tool). The detached child previously inherited the parent's job and died with it; users saw a dashboard URL but the workflow never made progress. The Popen call now requests CREATE_BREAKAWAY_FROM_JOB in addition to CREATE_NEW_PROCESS_GROUP so the child fully detaches. In hardened CI environments that clear JOB_OBJECT_LIMIT_BREAKAWAY_OK, CreateProcess raises ERROR_ACCESS_DENIED; in that case a visible stderr warning is emitted (so the user understands bg mode may not survive shell exit) and the spawn is retried without the breakaway flag. Other OSErrors propagate unchanged so the existing RuntimeError wrapper still surfaces them cleanly. Refactors the two near-identical detachment+Popen blocks in launch_background and launch_background_resume into a single _spawn_detached helper; constants are resolved via getattr so the module remains importable on POSIX hosts and tests can patch sys.platform to "win32" from Linux/macOS (#200).

  • conductor run --web-bg --log-file auto now produces a log file with a real provider-side trace. bg_runner.launch_background() / launch_background_resume() already redirect the child's stdout/stderr/stdin to subprocess.DEVNULL, so silence is enforced at the OS level — but they also passed --silent to the child, which flipped verbose_mode=False and gated more than console prints (the Copilot provider's _log_event_verbose(), _log_parse_recovery(), and _log_recovery_attempt() all became no-ops, dropping events from the log file too). Both synthesized commands now omit --silent; console output still goes to DEVNULL via the Popen kwargs. Side benefit: the synthesized command is now reproducible by hand without learning that --silent was being injected behind the scenes (#199, #196).

  • --web-bg and other --silent invocations no longer leak the dashboard URL banner to stdout. Several console.print / typer.echo calls in cli/run.py were unconditionally writing the bg-launch URL, stderr log path, and conductor stop hint even with --silent / is_verbose() == False. Remaining unguarded URL prints are now gated behind is_verbose() so --silent is honored end to end (#203, #211).

  • conductor validate <registry-workflow> now succeeds for workflows that conductor run already executed successfully. The validator's _resolve_subworkflow_ref_for_validation was missing the step that the engine's _resolve_subworkflow_path already had: when a parent workflow lives inside a registry SHA cache and references a sibling via a relative path (e.g. ../document-review/workflow.yaml), the engine auto-fetches the sibling from the same registry+SHA cache via auto_fetch_relative_workflow. The validator only checked the filesystem and reported "sub-workflow file not found". Validation and execution now agree on which refs are resolvable (#197).

  • Registry cache now mirrors the source repository layout so repo-relative references between workflows in the same registry repo resolve correctly. Previously each workflow was isolated under <base>/<registry>/<workflow_name>/<sha[:12]>/<filename>, so sdd-plan/plan.yaml referencing ../document-review/workflow.yaml resolved to a path that never existed in the cache and forced manual workarounds. The cache now stores workflows from the same registry+SHA under a shared per-SHA root (<base>/<registry>/<sha[:12]>/<repo_path>); metadata lives in a sibling _meta/<sha[:12]>/ tree so it can never collide with real repo paths (e.g. a repo's own .conductor/ directory). Per-workflow readiness sentinels are written last so readers never observe a partially populated workflow; per-file os.replace() stays intra-filesystem for atomic promotion; _safe_repo_path() rejects .., absolute paths, NUL bytes, and empty paths from any index/sibling entry; _resolve_within() adds defense-in-depth that resolved targets stay under the SHA root; source.json carries cache_layout_version, registry_type, source, and full_sha so cache hits require all four to match (stale metadata triggers re-fetch); the registry index is cached on disk so cache hits avoid a network round-trip. Sub-workflow refs from the same registry are auto-fetched when not yet present (gated to file-path-looking candidates with no @). add_registry() now rejects names containing /, \, the empty string, or the reserved _adhoc / _meta namespaces (#194).

0.1.16 - 2026-05-14

Added

  • type: workflow agents now accept registry references (workflow[@registry][#ref]) in the workflow: field, not just local file paths. Resolution prefers a local file when one exists relative to the parent workflow directory (preserves backward compatibility for extensionless local refs); otherwise the value is parsed as a registry reference, fetched via the registry cache, and executed from the cached location. conductor validate now recursively validates fetched sub-workflows with cycle detection (inode-based identity, so case-variant paths on macOS/Windows collapse correctly) and a depth cap of 10 — when the cap is hit a warning surfaces so users know validation was truncated rather than silently clean. Mutable registry refs (name@registry#main, or no #ref) may resolve to a different commit on conductor resume if the upstream branch has moved; pinned tags or commit SHAs guarantee deterministic resume (#188).
  • Conductor now ships as a Claude Code plugin marketplace at the repo root. Users can install the conductor skill directly from microsoft/conductor with /plugin marketplace add microsoft/conductor followed by /plugin install conductor@conductor. The plugin ships markdown only (no bin/, hooks, MCP servers, or executables), keeping the trust surface minimal. The same SKILL.md remains usable via gh skill install microsoft/conductor conductor for Copilot CLI users. The previous .claude/skills/conductor location was removed — the plugin is now the single home for the skill; for local development on the skill itself, use claude --plugin-dir plugins/conductor (#186).

Changed

  • The bundled Conductor skill (SKILL.md + references) was refreshed to reflect the current CLI, schema, and feature set: show / replay / --metadata / --workspace-instructions quick-reference entries; new type: workflow, dialog, retry, hooks, metadata, instructions, timeout_seconds, and openai-agents provider concepts; corrected update behavior (default prints the install-script one-liner, --apply launches the installer); CONDUCTOR_NO_UPDATE_CHECK; registry latest = branch HEAD and #ref syntax; sub-workflow agents and dialog mode authoring guidance; script JSON-stdout auto-merge; workflow.dir / workflow.file template variables; and unknown-fields rejection in schema validation (#187).
  • README "Why Conductor?" rewritten around three pillars — repeatable execution, deterministic routing, and version-controlled YAML workflows — and now leads with the real differentiator (zero-token orchestration) using concrete use-case examples (#185).

0.1.15 - 2026-05-13

Added

  • Per-agent timeout_seconds field for hard wall-clock timeouts on agent execution. Wraps execution in asyncio.wait_for() at the engine level so a slow agent no longer blocks the rest of the workflow. Effective timeout is min(agent.timeout_seconds, remaining_workflow_timeout) — when the workflow timeout is stricter it owns the error so attribution is never mislabeled. Raises a new AgentTimeoutError (subclass of TimeoutError) honored by existing fail_fast / continue_on_error semantics in parallel and for-each groups, and emits an agent_timeout event (with elapsed time and limit) for console + dashboard subscribers. Scoped to provider-backed agents; rejected on script, human_gate, and workflow types (#150).
  • Auto-discovery of .github/instructions/**/*.instructions.md workspace conventions, matching GitHub Copilot's documented semantics. Files marked applyTo: "**" in their frontmatter are loaded into the workspace preamble alongside AGENTS.md / CLAUDE.md / .github/copilot-instructions.md; scoped (applyTo: "<glob>") and absent-applyTo files are skipped per the convention's manual-attach default. The internal CONVENTION_FILES: list[str] table is refactored to a polymorphic CONVENTIONS: list[Convention] (ConventionFile | ConventionDirectory) so adding new conventions (Cursor rules, Cline rules, etc.) becomes one filter function plus one list entry; a CONVENTION_FILES module-level alias preserves backward compatibility for downstream imports (#169).

Fixed

  • agent.system_prompt is now rendered and forwarded to providers. The executor was rendering agent.system_prompt only to discard the result (_ = self.renderer.render(...)), so providers that forward system_prompt — notably the Copilot provider, which concatenates it into the prompt — received the un-rendered Jinja template. Agents whose instructions lived in system_prompt sent literal {{ ... }} placeholders to the model and got back "the prompt template contains unfilled variables" refusals. Also adds a conductor validate warning for agents that define system_prompt but no prompt: (a portability hazard since the Claude provider drops system_prompt entirely, and almost always a missing-prompt: typo) (#179).
  • conductor update on Windows no longer attempts an in-process self-upgrade. The previous flow tried to re-install into the same venv the running python.exe lives in, producing "Access is denied" failures that earlier mitigations only papered over. conductor update now checks for a newer version and prints the OS-appropriate install.ps1 / install.sh one-liner, and the install scripts become the single upgrade path: they detect other running conductor processes (auto-stopping under -Yes), sweep stale *.exe.old files, retry with backoff (2s / 5s / 10s), and — when uv can't remove the conductor-cli tool dir because of file locks — rename the whole dir aside and retry. install.sh reaches parity with --yes / --force / --source flags, retry-with-backoff, running-process detection, and a post-install conductor --version verify (#171).
  • install.ps1 is now stored without a UTF-8 BOM. The documented one-liner irm https://aka.ms/conductor/install.ps1 | iex returns the script body as a single string with the BOM surviving as U+FEFF at index 0; PowerShell's in-memory iex parser then trips on the [CmdletBinding()] attribute with Unexpected attribute 'CmdletBinding'. Both fresh installs via irm | iex and conductor update --apply (which re-runs the same command in a spawned console) now succeed. Direct powershell.exe -File install.ps1 invocations were unaffected, which is why prior file-based integration tests didn't catch it (#178).
  • conductor stop (including --all and --port) no longer crashes on Windows when a PID file exists in ~/.conductor/runs/. The Unix idiom os.kill(pid, 0) for liveness probing is not a no-op on Windows — any signal other than CTRL_C_EVENT / CTRL_BREAK_EVENT routes through TerminateProcess and can raise OSError subclasses outside ProcessLookupError / PermissionError (e.g. WinError 11 / ERROR_BAD_FORMAT), and even "successful" calls would actually terminate the target with exit code 0. _is_process_alive() now dispatches to a Windows-specific implementation using OpenProcess + GetExitCodeProcess for a truly non-destructive liveness check (#176).

0.1.14 - 2026-05-06

Fixed

  • conductor update no longer reports its own launching shim as another running Conductor process. On Windows the conductor.exe shim is a separate process from the Python interpreter that runs the update command, so excluding only os.getpid() caused a false "1 other Conductor process is running" warning. The check now walks the full ancestor PID chain (via wmic on Windows, ps elsewhere) and excludes every process along the way, falling back to {getpid(), getppid()} if the parent map cannot be built. #164

0.1.13 - 2026-05-06

Added

  • conductor resume is now at flag parity with conductor run. New flags: --provider / -p (runtime provider override), --metadata / -m (CLI metadata merged on top of YAML metadata), --web (real-time dashboard for the resumed run), --web-port, and --web-bg (fork a detached resume + dashboard process). --web and --web-bg are mutually exclusive, matching run. The dashboard only shows events from the resumed agent forward — agent runs that completed before the checkpoint were emitted in the original process and are not replayed. --input, --workspace-instructions, --instructions, and --dry-run are intentionally not mirrored (#158).
  • Reasoning effort (low / medium / high / xhigh) is now displayed in the web dashboard under each agent's metadata, right after Model. Effective value is per-agent reasoning.effort if set, otherwise runtime.default_reasoning_effort, otherwise omitted. Backed by a new reasoning_effort field on the workflow_started event payload, so older event log JSONL files replay gracefully (the row simply doesn't render) (#160).
  • New iteration_limit_reached and iteration_limit_resolved events are emitted when a workflow hits its max_iterations cap. Previously the console showed an interactive IntPrompt while the web dashboard went silently dark; the dashboard now renders the prompt state and the chosen resolution. The iteration_limit_reached payload includes a possible_loop heuristic flag (set when the last 3 history entries are the same agent) so subscribers can call out stuck review loops (#162).

Changed

  • Workflow registry references now resolve latest (and bare name@registry refs) to the default branch HEAD instead of the newest git tag. Previously, the moment a registry repo got its first tag, bare references silently froze at that tag and stopped picking up commits to main. Tags remain first-class — pin explicitly via workflow#v1.2.3 for releases. Also saves one GitHub API call on the hot path of bare-name fetches (#157).

Fixed

  • Schema validation now rejects unknown fields on AgentDef, ParallelGroup, ForEachDef, and WorkflowConfig instead of silently dropping them. Misnesting parallel: or for_each: inside an agents: item — or typos like prmpt: — used to fall through to a runtime Model "gpt-4o" is not available error three layers downstream. They now fail at parse time with a clear Pydantic error pointing at the offending location. conductor validate also gained "Parallel Groups" and "For-each Groups" rows in its summary table so missing groups are immediately visible (#159).
  • Tool arguments and results are now pretty-printed in dashboard / JSONL / verbose-console events. Copilot tool results no longer leak the full Result(content=..., contents=None, detailed_content=..., kind=None) repr with literal \\n escapes and doubled \\\\ Windows paths, and tool arguments render as JSON ({"k": "v"}) instead of Python dict repr ({'k': 'v'}). Both providers share a new src/conductor/providers/_event_format.py helper for parity (#161).
  • install.ps1 on Windows now captures full uv tool install stdout AND stderr via Start-Process -RedirectStandardOutput -RedirectStandardError to temp files. Previously, with $ErrorActionPreference = 'Stop', PowerShell treated uv's stderr as a terminating error and threw before the assignment completed, so install failures showed (no output captured) with no way to diagnose them (#156).

0.1.12 - 2026-05-05

Added

  • Unified reasoning.effort configuration for per-agent and workflow-wide control of model reasoning / extended-thinking effort. Set runtime.default_reasoning_effort (low | medium | high | xhigh) for a workflow-wide default, or override per agent with a reasoning.effort block. Translates to reasoning_effort on the Copilot session and to extended thinking budget on Claude (low=2048, medium=8192, high=16384, xhigh=32768 tokens, with temperature coerced to 1.0 and max_tokens bumped to fit). Validates against each model's supported efforts/capabilities and surfaces thinking content via agent_reasoning events. See examples/reasoning-effort.yaml (#152).
  • Tag-based versioning for the workflow registry. Versions are now auto-discovered from git tags instead of being explicitly listed in registry.yaml, and refs accept any tag, branch, or SHA via the new workflow#ref syntax (e.g. sdd/plan#v3.0.0, sdd/plan#main, sdd/plan#abc1234). Stale CDN content is bypassed via cache-busting query parameters so registry updates are visible immediately (#151).

Fixed

  • conductor update reliability on Windows. Adds a pre-flight check for other running Conductor processes (which hold file locks on %LOCALAPPDATA%\uv\tools\conductor-cli\ and cause uv tool install --force to fail with "Access is denied"), retries the install up to 3 times to absorb transient Windows Defender failures, surfaces full uv stdout AND stderr on failure with Defender-exclusion guidance, broadens the Windows entrypoint rename to cover the uv tool venv Scripts/ directory in %LOCALAPPDATA% and %APPDATA%, and adds a new conductor update --force flag to skip the pre-flight check (#155).
  • Dashboard layout for workflows with human_gate options or multiple loop-back routes (e.g. revision loops). The workflow_started event now emits routes from human_gate options[].route so gate edges aren't silently dropped, and the frontend pre-classifies back-edges via DFS from $start and feeds them to Dagre in reversed direction so cycles no longer scramble rank assignment. Workflows like sdd/plan-v3.yaml now render as a coherent top-to-bottom DAG instead of disconnected columns with long diagonal edges (#153).
  • Windows install failures now surface useful diagnostics. install.ps1 prints captured uv stdout/stderr on failure instead of swallowing it, and uses the correct Microsoft Defender cmdlet so the install path is exclusion-friendly (#149).

0.1.11 - 2026-05-04

Added

  • metadata dict on workflow definitions, settable statically in YAML or dynamically via --metadata / -m CLI flags. Merged metadata is included in the workflow_started event for downstream consumers (#107).
  • input_mapping field on type: workflow agents, enabling Jinja2-templated per-call inputs to sub-workflows evaluated against the parent context. When omitted, the parent's workflow.input.* is forwarded as before (#109).
  • type: workflow agents are now allowed inside for_each groups, enabling dynamic fan-out to sub-workflows with per-iteration input_mapping. Each iteration emits its own subworkflow_started / subworkflow_completed events (#110).
  • Self-referential sub-workflows are now allowed; depth is bounded by the global MAX_SUBWORKFLOW_DEPTH plus an optional per-agent max_depth field on AgentDef (#111).
  • workflow.dir, workflow.file, and workflow.name template variables are now available in all agent contexts (regardless of context mode). Lets registry-hosted workflows reference co-located scripts and assets without depending on the caller's working directory (#121).
  • Script agent stdout that is valid JSON is auto-parsed and merged into the agent's output dict alongside stdout, stderr, and exit_code, enabling field-based when: route conditions instead of opaque exit-code matching (#122).
  • conductor validate now performs semantic validation in addition to YAML schema checks, catching stale agent references, missing workflow inputs, and undeclared explicit-mode dependencies before runtime in prompt, system_prompt, command, args, working_dir, input_mapping, parallel-group inputs, and workflow output: templates (#125).
  • Web dashboard: breadcrumb navigation, double-click dive-in to sub-workflow graphs, isolated subworkflow contexts (no node-status bleed across repeated runs), and reliable Stop button during subworkflows (#113, follow-up fixes in #146).
  • Dialog mode for agents: multi-turn conversational interactions driven by a dialog gate with conditional transitions, full Copilot and Claude provider support, and dedicated dashboard UI (DialogDetail, DialogEngagementPrompt, DialogOverlay) (#130).
  • Markdown rendering and auto-linkification in human gate prompts. Gate prompts render through Rich Markdown in the terminal and as GitHub-Flavored Markdown in the dashboard. Bare file paths and URLs in gate prompts are converted to clickable links; relative paths open a sandboxed FileViewer modal served via a path-traversal-safe GET /api/files/{path} endpoint (#131).
  • Workspace instructions support: --workspace-instructions and --instructions CLI flags plus a YAML-level instructions: field on the workflow. Auto-discovers AGENTS.md, CLAUDE.md, and .github/copilot-instructions.md by walking from CWD to the git root, prepends them to every agent's prompt, inherits into sub-workflows, and persists in checkpoints (#141).

Changed

  • The dashboard's "context window remaining" bar now sources context_window_max from each provider's SDK at runtime instead of a hand-maintained static table. Values now reflect the actual cap the SDK enforces (e.g. claude-opus-4.6 reports 200K rather than the theoretical 1M; gpt-5.x reports 128K rather than 400K). The context_window field on ModelPricing has been removed; pricing data continues to be hand-maintained for cost calculation only (#144).

Fixed

  • Pass streaming=True to the Copilot SDK's create_session to prevent silent truncation of large tool-call arguments. In non-streaming mode the model's per-turn output budget is exhausted mid-JSON for large arguments (e.g., create with multi-KB file_text), the CLI executes the partial tool call, and the agent loops on the broken call until the wall-clock session limit fires (#129).
  • Build the Copilot prompt schema recursively from nested output: definitions instead of flattening to top-level fields only. Nested object properties, required keys, and array item schemas are now included in the prompt-facing schema used for initial guidance and parse recovery (#100).
  • Coerce Python literal "True" / "False" / "None" strings produced by Jinja's default str(bool) rendering into native Python types when building workflow output. Previously, output: { matched: "{{ a == b }}" } produced the string "False" (truthy), causing downstream when: comparisons against false to silently misbehave (#139).
  • Pricing fuzzy match no longer silently inherits values across model families. Names sharing a textual prefix with a known key (e.g. claude-opus-4.7 previously matched claude-opus-4) now require a - delimiter; non-matching names return None and the dashboard hides the cost field. A one-time warning is emitted per requested name on any non-exact match (#143).
  • Run uv tool update-shell after uv tool install in both install.ps1 and install.sh so conductor is available on PATH in new shells, CI agents, and IDE extensions after a fresh install (#142).
  • In explicit context mode, workflow.input is now always available to script and type: workflow agent templates regardless of the agent's declared input: list. The explicit-mode contract still applies to LLM agents (no undeclared inputs in prompts to control token cost) (#119).
  • Optional workflow inputs without an explicit default: now resolve to type-appropriate zero values ("", 0, false, [], {}) instead of Python None, so templates like {{ workflow.input.optional | default("fallback") }} render the fallback rather than the literal string "None" (#123).
  • Web dashboard: events without an engine-supplied subworkflow_path stamp (e.g., for_each_item_started for a parent for_each over type: workflow agents) now route strictly to the root context instead of falling back to the user's currently-viewed path. This fixes two related symptoms: dashboards opened during a run with sub-workflows no longer auto-land inside an iteration, and a parent for_each panel now displays every iteration rather than silently dropping the middle ones into a sibling sub-workflow's context (#148).

0.1.10 - 2026-04-30

Added

  • Sub-workflow composition support: workflow-type agents can now be used inside for_each groups, with dynamic per-iteration input_mapping (#101, #102).

Changed

  • Bumped github-copilot-sdk to >=0.3.0. The SDK ships a bundled copilot CLI binary used for JSON-RPC session.create calls; 0.2.2 bundled CLI 1.0.21, which rejected newer model IDs locally with JSON-RPC -32603: Model "<id>" is not available. 0.3.0 bundles CLI 1.0.36-0, which accepts the current Copilot model catalog (including claude-opus-4.7* variants).

Fixed

  • Suppressed noisy PowerShell stderr output from uv tool install during Windows self-update (#99).